ckanext-permissions 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ckanext/__init__.py +9 -0
- ckanext/permissions/__init__.py +0 -0
- ckanext/permissions/cli.py +80 -0
- ckanext/permissions/const.py +10 -0
- ckanext/permissions/helpers.py +51 -0
- ckanext/permissions/implementation/__init__.py +5 -0
- ckanext/permissions/implementation/permission_labels.py +45 -0
- ckanext/permissions/logic/__init__.py +0 -0
- ckanext/permissions/logic/action.py +113 -0
- ckanext/permissions/logic/auth.py +82 -0
- ckanext/permissions/logic/schema.py +54 -0
- ckanext/permissions/logic/validators.py +112 -0
- ckanext/permissions/migration/permissions/alembic.ini +74 -0
- ckanext/permissions/migration/permissions/env.py +85 -0
- ckanext/permissions/migration/permissions/script.py.mako +24 -0
- ckanext/permissions/migration/permissions/versions/a849104ccfdc_init_tables.py +69 -0
- ckanext/permissions/model.py +148 -0
- ckanext/permissions/plugin.py +67 -0
- ckanext/permissions/tests/__init__.py +0 -0
- ckanext/permissions/tests/actions/test_permission.py +68 -0
- ckanext/permissions/tests/actions/test_role.py +110 -0
- ckanext/permissions/tests/conftest.py +52 -0
- ckanext/permissions/tests/test_autoassign.py +26 -0
- ckanext/permissions/tests/test_helpers.py +67 -0
- ckanext/permissions/tests/test_permission_labels.py +28 -0
- ckanext/permissions/tests/test_utils.py +275 -0
- ckanext/permissions/tests/test_validators.py +98 -0
- ckanext/permissions/types.py +34 -0
- ckanext/permissions/utils.py +197 -0
- ckanext/permissions_manager/__init__.py +0 -0
- ckanext/permissions_manager/helpers.py +23 -0
- ckanext/permissions_manager/plugin.py +48 -0
- ckanext/permissions_manager/views.py +351 -0
- ckanext_permissions-0.2.0.dist-info/METADATA +104 -0
- ckanext_permissions-0.2.0.dist-info/RECORD +39 -0
- ckanext_permissions-0.2.0.dist-info/WHEEL +5 -0
- ckanext_permissions-0.2.0.dist-info/entry_points.txt +6 -0
- ckanext_permissions-0.2.0.dist-info/licenses/LICENSE +661 -0
- ckanext_permissions-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ckan.tests.helpers import call_action
|
|
4
|
+
|
|
5
|
+
from ckanext.permissions import const, helpers, model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
9
|
+
class TestGetRegisteredRoles:
|
|
10
|
+
def test_get_default_roles(self):
|
|
11
|
+
result = helpers.get_registered_roles()
|
|
12
|
+
|
|
13
|
+
assert len(result) == 3
|
|
14
|
+
|
|
15
|
+
assert result[const.Roles.Anonymous.value]
|
|
16
|
+
assert result[const.Roles.Authenticated.value]
|
|
17
|
+
assert result[const.Roles.Administrator.value]
|
|
18
|
+
|
|
19
|
+
def test_with_custom_roles(self, test_role: dict[str, str]):
|
|
20
|
+
result = helpers.get_registered_roles()
|
|
21
|
+
|
|
22
|
+
assert len(result) == 4
|
|
23
|
+
assert result[test_role["id"]] == test_role["label"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
27
|
+
class TestGetRolePermissions:
|
|
28
|
+
def test_no_permission(self, test_role: dict[str, str]):
|
|
29
|
+
assert not helpers.get_role_permissions(test_role["id"], "read_any_dataset")
|
|
30
|
+
|
|
31
|
+
def test_with_permission(self, test_role: dict[str, str]):
|
|
32
|
+
call_action(
|
|
33
|
+
"permissions_update",
|
|
34
|
+
permissions={"read_any_dataset": {test_role["id"]: True}},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
assert helpers.get_role_permissions(test_role["id"], "read_any_dataset")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
41
|
+
class TestGetUserRoles:
|
|
42
|
+
def test_only_default_roles(self, user: dict[str, str]):
|
|
43
|
+
assert helpers.get_user_roles(user["id"]) == ["authenticated"]
|
|
44
|
+
|
|
45
|
+
def test_add_new_role(self, user: dict[str, str], test_role: dict[str, str]):
|
|
46
|
+
model.UserRole.create(user["id"], test_role["id"])
|
|
47
|
+
result = helpers.get_user_roles(user["id"])
|
|
48
|
+
assert "authenticated" in result
|
|
49
|
+
assert test_role["id"] in result
|
|
50
|
+
|
|
51
|
+
def test_remove_role(self, user: dict[str, str], test_role: dict[str, str]):
|
|
52
|
+
model.UserRole.create(user["id"], test_role["id"])
|
|
53
|
+
result = helpers.get_user_roles(user["id"])
|
|
54
|
+
assert "authenticated" in result
|
|
55
|
+
assert test_role["id"] in result
|
|
56
|
+
|
|
57
|
+
model.UserRole.delete(user["id"], test_role["id"])
|
|
58
|
+
assert helpers.get_user_roles(user["id"]) == ["authenticated"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
62
|
+
class TestIsDefaultRole:
|
|
63
|
+
def test_is_default_role(self):
|
|
64
|
+
assert helpers.is_default_role(const.Roles.Authenticated.value)
|
|
65
|
+
|
|
66
|
+
def test_is_not_default_role(self, test_role: dict[str, str]):
|
|
67
|
+
assert not helpers.is_default_role("test_role")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import ckan.plugins.toolkit as tk
|
|
6
|
+
from ckan.tests.helpers import call_action
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.usefixtures("with_plugins", "clean_index", "clean_db")
|
|
10
|
+
class TestPermissionLabels:
|
|
11
|
+
def test_private_dataset_is_not_visible_to_anonymous_user(
|
|
12
|
+
self, app, dataset_factory: Callable[..., dict[str, Any]]
|
|
13
|
+
):
|
|
14
|
+
dataset = dataset_factory(private=True)
|
|
15
|
+
|
|
16
|
+
app.get(tk.h.url_for("dataset.read", id=dataset["id"]), headers={}, status=404)
|
|
17
|
+
|
|
18
|
+
def test_private_dataset_make_anon_user_to_read_private_dataset(
|
|
19
|
+
self, app, dataset_factory: Callable[..., dict[str, Any]]
|
|
20
|
+
):
|
|
21
|
+
dataset = dataset_factory(private=True)
|
|
22
|
+
|
|
23
|
+
call_action(
|
|
24
|
+
"permissions_update",
|
|
25
|
+
permissions={"read_private_dataset": {"anonymous": True}},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
app.get(tk.h.url_for("dataset.read", id=dataset["id"]), headers={}, status=200)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import ckan.plugins.toolkit as tk
|
|
4
|
+
from ckan import model
|
|
5
|
+
from ckan.tests.helpers import call_action
|
|
6
|
+
|
|
7
|
+
from ckanext.permissions import const, utils
|
|
8
|
+
from ckanext.permissions.types import PermissionDefinition, PermissionGroup
|
|
9
|
+
from ckanext.permissions.utils import validate_groups
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.usefixtures("with_plugins")
|
|
13
|
+
class TestParsePermissionGroupSchemas:
|
|
14
|
+
def test_valid_schema(self):
|
|
15
|
+
assert utils.parse_permission_group_schemas()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.usefixtures("with_plugins")
|
|
19
|
+
class TestParsePermissionGroupsValidation:
|
|
20
|
+
def test_valid_group(self):
|
|
21
|
+
validate_groups(
|
|
22
|
+
{
|
|
23
|
+
"new_group": PermissionGroup(
|
|
24
|
+
name="xxx",
|
|
25
|
+
description="xxx",
|
|
26
|
+
permissions=[
|
|
27
|
+
PermissionDefinition(
|
|
28
|
+
key="xxx",
|
|
29
|
+
label="xxx",
|
|
30
|
+
description="xxx",
|
|
31
|
+
)
|
|
32
|
+
],
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def test_group_missing_name(self):
|
|
38
|
+
with pytest.raises(tk.ValidationError, match="Missing value"):
|
|
39
|
+
validate_groups(
|
|
40
|
+
{
|
|
41
|
+
"new_group": PermissionGroup(
|
|
42
|
+
name="",
|
|
43
|
+
description="xxx",
|
|
44
|
+
permissions=[],
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def test_group_missing_description(self):
|
|
50
|
+
with pytest.raises(tk.ValidationError, match="Missing value"):
|
|
51
|
+
validate_groups(
|
|
52
|
+
{
|
|
53
|
+
"new_group": PermissionGroup(
|
|
54
|
+
name="xxx",
|
|
55
|
+
description="",
|
|
56
|
+
permissions=[],
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def test_group_missing_permissions(self):
|
|
62
|
+
with pytest.raises(tk.ValidationError, match="Missing permissions"):
|
|
63
|
+
validate_groups(
|
|
64
|
+
{
|
|
65
|
+
"new_group": PermissionGroup(
|
|
66
|
+
name="xxx",
|
|
67
|
+
description="xxx",
|
|
68
|
+
permissions=[],
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def test_group_permissions_empty_list(self):
|
|
74
|
+
with pytest.raises(tk.ValidationError, match="Missing permissions"):
|
|
75
|
+
validate_groups(
|
|
76
|
+
{
|
|
77
|
+
"new_group": PermissionGroup(
|
|
78
|
+
name="xxx",
|
|
79
|
+
description="xxx",
|
|
80
|
+
permissions=[],
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def test_missing_permission_key(self):
|
|
86
|
+
with pytest.raises(tk.ValidationError, match="Missing value"):
|
|
87
|
+
validate_groups(
|
|
88
|
+
{
|
|
89
|
+
"new_group": PermissionGroup(
|
|
90
|
+
name="xxx",
|
|
91
|
+
description="xxx",
|
|
92
|
+
permissions=[
|
|
93
|
+
PermissionDefinition(
|
|
94
|
+
key="",
|
|
95
|
+
label="xxx",
|
|
96
|
+
description="xxx",
|
|
97
|
+
)
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def test_missing_permission_label(self):
|
|
104
|
+
with pytest.raises(tk.ValidationError, match="Missing value"):
|
|
105
|
+
validate_groups(
|
|
106
|
+
{
|
|
107
|
+
"new_group": PermissionGroup(
|
|
108
|
+
name="xxx",
|
|
109
|
+
description="xxx",
|
|
110
|
+
permissions=[
|
|
111
|
+
PermissionDefinition(key="xxx", label="", description="xxx")
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def test_allow_empty_permission_description(self):
|
|
118
|
+
validate_groups(
|
|
119
|
+
{
|
|
120
|
+
"new_group": PermissionGroup(
|
|
121
|
+
name="xxx",
|
|
122
|
+
description="xxx",
|
|
123
|
+
permissions=[
|
|
124
|
+
PermissionDefinition(key="xxx", label="xxx", description="")
|
|
125
|
+
],
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.usefixtures("with_plugins")
|
|
132
|
+
class TestLoadSchemas:
|
|
133
|
+
def test_valid_schemas(self):
|
|
134
|
+
assert utils._load_schemas(
|
|
135
|
+
[
|
|
136
|
+
"ckanext.permissions:tests/data/test_group.yaml",
|
|
137
|
+
"ckanext.permissions:default_group.yaml",
|
|
138
|
+
],
|
|
139
|
+
"name",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def test_nonexistent_file(self):
|
|
143
|
+
result = utils._load_schemas(["ckanext.permissions:tests:missing.yaml"], "name")
|
|
144
|
+
assert result == {}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@pytest.mark.usefixtures("with_plugins")
|
|
148
|
+
class TestLoadSchema:
|
|
149
|
+
def test_valid_schema(self):
|
|
150
|
+
assert utils._load_schema(
|
|
151
|
+
"ckanext.permissions:tests/data/test_group.yaml",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def test_nonexistent_file(self):
|
|
155
|
+
assert not utils._load_schema(
|
|
156
|
+
"ckanext.permissions:tests/data/missing.yaml",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.usefixtures("with_plugins")
|
|
161
|
+
class TestGetPermissionGroups:
|
|
162
|
+
def test_get_permission_groups(self):
|
|
163
|
+
result = utils.get_permission_groups()
|
|
164
|
+
assert isinstance(result, list)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.usefixtures("with_plugins")
|
|
168
|
+
class TestGetPermissions:
|
|
169
|
+
def test_get_permissions(self):
|
|
170
|
+
result = utils.get_permissions()
|
|
171
|
+
|
|
172
|
+
assert isinstance(result, dict)
|
|
173
|
+
assert result["perm_1"] == PermissionDefinition(
|
|
174
|
+
key="perm_1", label="Permission 1"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
179
|
+
class TestCheckPermission:
|
|
180
|
+
def test_set_permission(self):
|
|
181
|
+
anon_user = model.AnonymousUser()
|
|
182
|
+
assert not utils.check_permission("perm_1", anon_user)
|
|
183
|
+
|
|
184
|
+
call_action(
|
|
185
|
+
"permissions_update",
|
|
186
|
+
permissions={"perm_1": {const.Roles.Anonymous.value: True}},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
assert utils.check_permission("perm_1", anon_user)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
193
|
+
class TestEnsureDefaultRoles:
|
|
194
|
+
def test_creates_default_roles(self, reset_db, migrate_db_for):
|
|
195
|
+
"""Test that ensure_default_roles creates all default roles"""
|
|
196
|
+
from ckanext.permissions import model as perm_model
|
|
197
|
+
|
|
198
|
+
for role in perm_model.Role.all():
|
|
199
|
+
perm_model.Role.delete(perm_model.Role.get(role["id"]))
|
|
200
|
+
|
|
201
|
+
assert len(perm_model.Role.all()) == 0
|
|
202
|
+
|
|
203
|
+
created_count = utils.ensure_default_roles()
|
|
204
|
+
|
|
205
|
+
assert created_count == 3
|
|
206
|
+
|
|
207
|
+
assert perm_model.Role.get("anonymous") is not None
|
|
208
|
+
assert perm_model.Role.get("authenticated") is not None
|
|
209
|
+
assert perm_model.Role.get("administrator") is not None
|
|
210
|
+
|
|
211
|
+
def test_reuse(self):
|
|
212
|
+
"""Test that ensure_default_roles can be called multiple times"""
|
|
213
|
+
# Call ensure_default_roles after initial call in conftest.py
|
|
214
|
+
addiitonal_call = utils.ensure_default_roles()
|
|
215
|
+
assert addiitonal_call == 0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
219
|
+
class TestAssignRoleToUser:
|
|
220
|
+
def test_assign_role_to_user(self, user_factory):
|
|
221
|
+
"""Test assigning a role to a user"""
|
|
222
|
+
from ckanext.permissions import model as perm_model
|
|
223
|
+
|
|
224
|
+
user = user_factory()
|
|
225
|
+
|
|
226
|
+
utils.assign_role_to_user(user["id"], const.Roles.Administrator.value)
|
|
227
|
+
|
|
228
|
+
user_roles = perm_model.UserRole.get(user["id"])
|
|
229
|
+
role_ids = [role.role_id for role in user_roles]
|
|
230
|
+
assert const.Roles.Administrator.value in role_ids
|
|
231
|
+
|
|
232
|
+
def test_assign_duplicate_role(self, user_factory):
|
|
233
|
+
"""Test that assigning the same role twice doesn't create duplicates"""
|
|
234
|
+
from ckanext.permissions import model as perm_model
|
|
235
|
+
|
|
236
|
+
user = user_factory()
|
|
237
|
+
|
|
238
|
+
utils.assign_role_to_user(user["id"], const.Roles.Administrator.value)
|
|
239
|
+
utils.assign_role_to_user(user["id"], const.Roles.Administrator.value)
|
|
240
|
+
|
|
241
|
+
user_roles = perm_model.UserRole.get(user["id"])
|
|
242
|
+
role_ids = [role.role_id for role in user_roles]
|
|
243
|
+
assert len(role_ids) == 2
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
247
|
+
class TestRemoveRoleFromUser:
|
|
248
|
+
def test_remove_role_from_user(self, user_factory):
|
|
249
|
+
"""Test removing a role from a user"""
|
|
250
|
+
from ckanext.permissions import model as perm_model
|
|
251
|
+
|
|
252
|
+
user = user_factory()
|
|
253
|
+
|
|
254
|
+
utils.assign_role_to_user(user["id"], const.Roles.Administrator.value)
|
|
255
|
+
utils.remove_role_from_user(user["id"], const.Roles.Administrator.value)
|
|
256
|
+
|
|
257
|
+
user_roles = perm_model.UserRole.get(user["id"])
|
|
258
|
+
role_ids = [role.role_id for role in user_roles]
|
|
259
|
+
assert const.Roles.Administrator.value not in role_ids
|
|
260
|
+
|
|
261
|
+
def test_remove_role_doesnt_affect_other_roles(self, user_factory, test_role):
|
|
262
|
+
"""Test that removing one role doesn't affect other roles"""
|
|
263
|
+
from ckanext.permissions import model as perm_model
|
|
264
|
+
|
|
265
|
+
user = user_factory()
|
|
266
|
+
|
|
267
|
+
utils.assign_role_to_user(user["id"], const.Roles.Administrator.value)
|
|
268
|
+
|
|
269
|
+
utils.remove_role_from_user(user["id"], const.Roles.Administrator.value)
|
|
270
|
+
|
|
271
|
+
# Verify only the correct role was removed
|
|
272
|
+
user_roles = perm_model.UserRole.get(user["id"])
|
|
273
|
+
role_ids = [role.role_id for role in user_roles]
|
|
274
|
+
assert const.Roles.Administrator.value not in role_ids
|
|
275
|
+
assert const.Roles.Authenticated.value in role_ids
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import ckan.plugins.toolkit as tk
|
|
8
|
+
|
|
9
|
+
import ckanext.permissions.logic.validators as perm_validators
|
|
10
|
+
from ckanext.permissions.const import ROLE_ID_MAX_LENGTH, Roles
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
14
|
+
class TestRoleDoesntExists:
|
|
15
|
+
def test_role_doesnt_exists_valid(self):
|
|
16
|
+
"""Test role_doesnt_exists with non-existent role."""
|
|
17
|
+
assert perm_validators.role_doesnt_exists("new_role")
|
|
18
|
+
|
|
19
|
+
def test_role_doesnt_exists_invalid(self, test_role: dict[str, str]):
|
|
20
|
+
"""Test role_doesnt_exists with existing role."""
|
|
21
|
+
with pytest.raises(tk.Invalid) as e:
|
|
22
|
+
perm_validators.role_doesnt_exists(test_role["id"])
|
|
23
|
+
|
|
24
|
+
assert e.value.error == f"Role {test_role['id']} is already exists"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
28
|
+
class TestRoleExists:
|
|
29
|
+
def test_role_exists_valid(self, test_role: dict[str, str]):
|
|
30
|
+
"""Test permission_role_exists with existing role."""
|
|
31
|
+
assert (
|
|
32
|
+
perm_validators.permission_role_exists(test_role["id"]) == test_role["id"]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def test_role_exists_invalid(self):
|
|
36
|
+
"""Test permission_role_exists with non-existent role."""
|
|
37
|
+
role_name = "non_existent_role"
|
|
38
|
+
with pytest.raises(tk.Invalid) as e:
|
|
39
|
+
perm_validators.permission_role_exists(role_name)
|
|
40
|
+
|
|
41
|
+
assert e.value.error == f"Role {role_name} doesn't exists"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
|
45
|
+
class TestRolesExists:
|
|
46
|
+
def test_roles_exists_valid(self, role_factory: Callable[..., dict[str, str]]):
|
|
47
|
+
"""Test roles_exists with existing roles."""
|
|
48
|
+
role_names = ["creator", "moderator"]
|
|
49
|
+
for role in role_names:
|
|
50
|
+
role_factory(id=role)
|
|
51
|
+
|
|
52
|
+
assert perm_validators.roles_exists(role_names) == role_names
|
|
53
|
+
|
|
54
|
+
def test_roles_exists_invalid(self, role_factory: Callable[..., dict[str, str]]):
|
|
55
|
+
"""Test roles_exists with non-existent role."""
|
|
56
|
+
role_names = ["creator", "moderator"]
|
|
57
|
+
role_factory(id=role_names[0])
|
|
58
|
+
|
|
59
|
+
with pytest.raises(tk.Invalid) as e:
|
|
60
|
+
perm_validators.roles_exists(role_names)
|
|
61
|
+
|
|
62
|
+
assert e.value.error == f"Role {role_names[1]} doesn't exists"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestRoleIdValidator:
|
|
66
|
+
def test_valid_role_id(self):
|
|
67
|
+
"""Test role_id_validator with valid role ID."""
|
|
68
|
+
role_id = "valid-role-id"
|
|
69
|
+
assert perm_validators.role_id_validator(role_id) == role_id
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize(
|
|
72
|
+
"invalid_id",
|
|
73
|
+
[
|
|
74
|
+
"A", # too short
|
|
75
|
+
"a" * (ROLE_ID_MAX_LENGTH + 1), # too long
|
|
76
|
+
"Invalid", # uppercase
|
|
77
|
+
"invalid@role", # invalid character
|
|
78
|
+
"test1", # invalid character
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
def test_invalid_role_id(self, invalid_id: str):
|
|
82
|
+
"""Test role_id_validator with invalid role IDs."""
|
|
83
|
+
with pytest.raises(tk.Invalid):
|
|
84
|
+
perm_validators.role_id_validator(invalid_id)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestNotDefaultRole:
|
|
88
|
+
def test_valid_custom_role(self):
|
|
89
|
+
"""Test not_default_role with custom role."""
|
|
90
|
+
role_id = "custom-role"
|
|
91
|
+
assert perm_validators.not_default_role(role_id) == role_id
|
|
92
|
+
|
|
93
|
+
def test_invalid_default_role(self):
|
|
94
|
+
"""Test not_default_role with default role."""
|
|
95
|
+
with pytest.raises(tk.Invalid) as e:
|
|
96
|
+
perm_validators.not_default_role(Roles.Administrator.value)
|
|
97
|
+
|
|
98
|
+
assert e.value.error == f"Role {Roles.Administrator.value} is a default role."
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PermissionGroup(TypedDict):
|
|
7
|
+
name: str
|
|
8
|
+
permissions: list["PermissionDefinition"]
|
|
9
|
+
description: Optional[str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PermissionDefinition(TypedDict, total=False):
|
|
13
|
+
key: str
|
|
14
|
+
label: str
|
|
15
|
+
description: Optional[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PermissionRoleDefinition(TypedDict):
|
|
19
|
+
role: str
|
|
20
|
+
state: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PermissionRolePayload(PermissionRoleDefinition):
|
|
24
|
+
permission: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PermissionRole(PermissionRolePayload):
|
|
28
|
+
id: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Role(TypedDict):
|
|
32
|
+
id: str
|
|
33
|
+
label: str
|
|
34
|
+
description: str
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
import ckan.plugins.toolkit as tk
|
|
10
|
+
from ckan import model
|
|
11
|
+
|
|
12
|
+
import ckanext.permissions.const as perm_const
|
|
13
|
+
import ckanext.permissions.logic.schema as perm_schema
|
|
14
|
+
import ckanext.permissions.model as perm_model
|
|
15
|
+
import ckanext.permissions.types as perm_types
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_permission_group_schemas() -> dict[str, perm_types.PermissionGroup]:
|
|
19
|
+
groups = _load_schemas(
|
|
20
|
+
tk.aslist(tk.config.get("ckanext.permissions.permission_groups")), "name"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
validate_groups(groups)
|
|
24
|
+
|
|
25
|
+
return groups
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_schemas(schemas: list[str], type_field: str):
|
|
29
|
+
result = {}
|
|
30
|
+
|
|
31
|
+
for path in schemas:
|
|
32
|
+
schema = _load_schema(path)
|
|
33
|
+
|
|
34
|
+
if not schema:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
result[schema[type_field]] = schema
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_schema(path: str):
|
|
43
|
+
"""
|
|
44
|
+
Given a path like "ckanext.permissions:default_group.yaml"
|
|
45
|
+
find the second part relative to the import path of the first
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
module, file_name = path.split(":", 1)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
imp_module = __import__(module, fromlist=[""])
|
|
52
|
+
except ImportError:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
file_path = os.path.join(os.path.dirname(inspect.getfile(imp_module)), file_name)
|
|
56
|
+
|
|
57
|
+
if not os.path.exists(file_path):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
with open(file_path) as file:
|
|
61
|
+
return yaml.safe_load(file)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_groups(groups: dict[str, perm_types.PermissionGroup]) -> bool:
|
|
65
|
+
permissions = []
|
|
66
|
+
|
|
67
|
+
for group in groups.values():
|
|
68
|
+
data, errors = tk.navl_validate(
|
|
69
|
+
cast(dict, group), perm_schema.permission_group_schema()
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if errors:
|
|
73
|
+
raise tk.ValidationError(errors)
|
|
74
|
+
|
|
75
|
+
if not data.get("permissions"):
|
|
76
|
+
raise tk.ValidationError("Missing permissions")
|
|
77
|
+
|
|
78
|
+
if not isinstance(data["permissions"], list):
|
|
79
|
+
raise tk.ValidationError("Permissions must be a list")
|
|
80
|
+
|
|
81
|
+
for permission in data["permissions"]:
|
|
82
|
+
if permission["key"] in permissions:
|
|
83
|
+
raise tk.ValidationError(
|
|
84
|
+
f"Permission {permission['key']} is duplicated"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
permissions.append(permission["key"])
|
|
88
|
+
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_permission_groups() -> list[perm_types.PermissionGroup]:
|
|
93
|
+
from ckanext.permissions.plugin import PermissionsPlugin
|
|
94
|
+
|
|
95
|
+
return PermissionsPlugin._permissions_groups # type: ignore
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_permissions() -> dict[str, perm_types.PermissionDefinition]:
|
|
99
|
+
from ckanext.permissions.plugin import PermissionsPlugin
|
|
100
|
+
|
|
101
|
+
return PermissionsPlugin._permissions # type: ignore
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_registered_roles() -> dict[str, str]:
|
|
105
|
+
return {role["id"]: role["label"] for role in perm_model.Role.all()}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def check_permission(
|
|
109
|
+
permission: str, user: model.User | model.AnonymousUser, scope: str = "global"
|
|
110
|
+
) -> bool:
|
|
111
|
+
"""Check if user has the given permission through any of their roles.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
permission: The permission key to check
|
|
115
|
+
user: The user to check permissions for
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
bool: True if user has the permission, False otherwise
|
|
119
|
+
"""
|
|
120
|
+
if isinstance(user, model.AnonymousUser):
|
|
121
|
+
return (
|
|
122
|
+
perm_model.RolePermission.get(perm_const.Roles.Anonymous.value, permission)
|
|
123
|
+
is not None
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
for role in user.roles: # type: ignore
|
|
127
|
+
if scope not in role.scope:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if perm_model.RolePermission.get(str(role.role_id), permission) is not None:
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def assign_role_to_user(
|
|
137
|
+
user_id: str, role_id: str, scope: str = "global", scope_id: str | None = None
|
|
138
|
+
):
|
|
139
|
+
"""Assign role to an User.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
role_id: The role to assign
|
|
143
|
+
user_id: The user to assign the role to
|
|
144
|
+
scope: The scope of the role
|
|
145
|
+
scope_id: The scope ID of the role
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
scope_roles = perm_model.UserRole.get(user_id, scope, scope_id)
|
|
149
|
+
|
|
150
|
+
if role_id not in [role.role_id for role in scope_roles]:
|
|
151
|
+
perm_model.UserRole.create(user_id, role_id)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def remove_role_from_user(user_id: str, role_id: str):
|
|
155
|
+
"""Remove role from an User.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
role_id: The role to remove
|
|
159
|
+
user_id: The user to remove the role from
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
roles = perm_model.UserRole.get(user_id)
|
|
163
|
+
|
|
164
|
+
if role_id in [role.role_id for role in roles]:
|
|
165
|
+
perm_model.UserRole.delete(user_id, role_id)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def ensure_default_roles() -> int:
|
|
169
|
+
"""Ensure default roles exist in the database.
|
|
170
|
+
|
|
171
|
+
Creates anonymous, authenticated, and administrator roles if they don't exist.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
int: Number of roles created
|
|
175
|
+
"""
|
|
176
|
+
default_roles = [
|
|
177
|
+
("anonymous", "Anonymous", "Default role for anonymous users"),
|
|
178
|
+
(
|
|
179
|
+
"authenticated",
|
|
180
|
+
"Authenticated",
|
|
181
|
+
"Regular user that will be assigned automatically for all users on a portal",
|
|
182
|
+
),
|
|
183
|
+
(
|
|
184
|
+
"administrator",
|
|
185
|
+
"Administrator",
|
|
186
|
+
"Administrator that should have all permissions",
|
|
187
|
+
),
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
created_count = 0
|
|
191
|
+
for role_id, label, description in default_roles:
|
|
192
|
+
existing_role = perm_model.Role.get(role_id)
|
|
193
|
+
if not existing_role:
|
|
194
|
+
perm_model.Role.create(role_id, label, description)
|
|
195
|
+
created_count += 1
|
|
196
|
+
|
|
197
|
+
return created_count
|
|
File without changes
|