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.
Files changed (39) hide show
  1. ckanext/__init__.py +9 -0
  2. ckanext/permissions/__init__.py +0 -0
  3. ckanext/permissions/cli.py +80 -0
  4. ckanext/permissions/const.py +10 -0
  5. ckanext/permissions/helpers.py +51 -0
  6. ckanext/permissions/implementation/__init__.py +5 -0
  7. ckanext/permissions/implementation/permission_labels.py +45 -0
  8. ckanext/permissions/logic/__init__.py +0 -0
  9. ckanext/permissions/logic/action.py +113 -0
  10. ckanext/permissions/logic/auth.py +82 -0
  11. ckanext/permissions/logic/schema.py +54 -0
  12. ckanext/permissions/logic/validators.py +112 -0
  13. ckanext/permissions/migration/permissions/alembic.ini +74 -0
  14. ckanext/permissions/migration/permissions/env.py +85 -0
  15. ckanext/permissions/migration/permissions/script.py.mako +24 -0
  16. ckanext/permissions/migration/permissions/versions/a849104ccfdc_init_tables.py +69 -0
  17. ckanext/permissions/model.py +148 -0
  18. ckanext/permissions/plugin.py +67 -0
  19. ckanext/permissions/tests/__init__.py +0 -0
  20. ckanext/permissions/tests/actions/test_permission.py +68 -0
  21. ckanext/permissions/tests/actions/test_role.py +110 -0
  22. ckanext/permissions/tests/conftest.py +52 -0
  23. ckanext/permissions/tests/test_autoassign.py +26 -0
  24. ckanext/permissions/tests/test_helpers.py +67 -0
  25. ckanext/permissions/tests/test_permission_labels.py +28 -0
  26. ckanext/permissions/tests/test_utils.py +275 -0
  27. ckanext/permissions/tests/test_validators.py +98 -0
  28. ckanext/permissions/types.py +34 -0
  29. ckanext/permissions/utils.py +197 -0
  30. ckanext/permissions_manager/__init__.py +0 -0
  31. ckanext/permissions_manager/helpers.py +23 -0
  32. ckanext/permissions_manager/plugin.py +48 -0
  33. ckanext/permissions_manager/views.py +351 -0
  34. ckanext_permissions-0.2.0.dist-info/METADATA +104 -0
  35. ckanext_permissions-0.2.0.dist-info/RECORD +39 -0
  36. ckanext_permissions-0.2.0.dist-info/WHEEL +5 -0
  37. ckanext_permissions-0.2.0.dist-info/entry_points.txt +6 -0
  38. ckanext_permissions-0.2.0.dist-info/licenses/LICENSE +661 -0
  39. ckanext_permissions-0.2.0.dist-info/top_level.txt +1 -0
ckanext/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ # this is a namespace package
2
+ try:
3
+ import pkg_resources
4
+
5
+ pkg_resources.declare_namespace(__name__)
6
+ except ImportError:
7
+ import pkgutil
8
+
9
+ __path__ = pkgutil.extend_path(__path__, __name__)
File without changes
@@ -0,0 +1,80 @@
1
+ import click
2
+
3
+ import ckan.model as model
4
+
5
+ import ckanext.permissions.const as perm_const
6
+ from ckanext.permissions import model as perm_model
7
+ from ckanext.permissions import utils
8
+
9
+ __all__ = ["permissions"]
10
+
11
+
12
+ @click.group()
13
+ def permissions():
14
+ """Permissions management commands."""
15
+
16
+
17
+ @permissions.command()
18
+ def init_default_roles():
19
+ """Create default roles (anonymous, authenticated, administrator) in the database"""
20
+ created_count = utils.ensure_default_roles()
21
+
22
+ if created_count > 0:
23
+ click.secho(f"{created_count} role(s) created successfully", fg="green")
24
+ else:
25
+ click.secho("All default roles already exist", fg="yellow")
26
+
27
+ return created_count
28
+
29
+
30
+ @permissions.command()
31
+ @click.argument("role", default=perm_const.Roles.Authenticated.value, required=False)
32
+ def assign_default_user_roles(role: str):
33
+ """Assign automatic roles to users (initializes default roles if needed)"""
34
+ # Ensure default roles exist
35
+ click.echo("Checking default roles...")
36
+ created_count = utils.ensure_default_roles()
37
+
38
+ if created_count > 0:
39
+ click.secho(f"{created_count} role(s) created", fg="green")
40
+ click.echo() # Empty line for readability
41
+
42
+ # Check if the specified role exists
43
+ if not perm_model.Role.get(role):
44
+ click.secho(f"Error: Role '{role}' does not exist", fg="red")
45
+ return
46
+
47
+ users = model.Session.query(model.User).filter(model.User.state == "active").all()
48
+
49
+ for user in users:
50
+ utils.assign_role_to_user(user.id, role)
51
+
52
+ click.secho(f"Role '{role}' assigned to {len(users)} active user(s)", fg="green")
53
+
54
+
55
+ @permissions.command()
56
+ @click.argument("role", default=perm_const.Roles.Authenticated.value, required=False)
57
+ @click.option(
58
+ "--user-ids",
59
+ "-u",
60
+ multiple=True,
61
+ help="User IDs to remove role from (if not specified, removes from all users)",
62
+ )
63
+ def remove_role_from_users(role: str, user_ids: tuple[str, ...]):
64
+ """Remove automatic roles from users"""
65
+ if user_ids:
66
+ users = (
67
+ model.Session.query(model.User).filter(model.User.id.in_(user_ids)).all()
68
+ )
69
+ else:
70
+ users = model.User.all()
71
+
72
+ for user in users:
73
+ utils.remove_role_from_user(user.id, role)
74
+
75
+ if user_ids:
76
+ click.secho(
77
+ f"Role '{role}' removed from {len(users)} specified user(s)", fg="green"
78
+ )
79
+ else:
80
+ click.secho(f"Role '{role}' removed from all users", fg="green")
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+ ROLE_ID_MIN_LENGTH = 1
4
+ ROLE_ID_MAX_LENGTH = 50
5
+
6
+
7
+ class Roles(Enum):
8
+ Anonymous = "anonymous"
9
+ Authenticated = "authenticated"
10
+ Administrator = "administrator"
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from ckanext.permissions import const, model, utils
4
+
5
+
6
+ def get_registered_roles() -> dict[str, str]:
7
+ """Get the registered roles
8
+
9
+ Returns:
10
+ The registered roles
11
+ """
12
+ return utils.get_registered_roles()
13
+
14
+
15
+ def get_role_permissions(role_id: str, permission: str) -> bool:
16
+ """Check if a role has a permission
17
+
18
+ Args:
19
+ role_id (str): The id of the role
20
+ permission (str): The permission to check
21
+
22
+ Returns:
23
+ True if the role has the permission, False otherwise
24
+ """
25
+ return model.RolePermission.get(role_id, permission) is not None
26
+
27
+
28
+ def get_user_roles(user_id: str, scope: str = "global", scope_id: str | None = None) -> list[str]:
29
+ """Get the roles of a user
30
+
31
+ Args:
32
+ user_id (str): The id of the user
33
+ scope (str): The scope of the role
34
+ scope_id (str | None): The id of the scope
35
+
36
+ Returns:
37
+ The roles of the user
38
+ """
39
+ return [str(role.role_id) for role in model.UserRole.get(user_id, scope, scope_id)]
40
+
41
+
42
+ def is_default_role(role_id: str) -> bool:
43
+ """Check if the role is a default role
44
+
45
+ Args:
46
+ role_id (str): The id of the role to check
47
+
48
+ Returns:
49
+ True if the role is a default role, False otherwise
50
+ """
51
+ return any(role_id == role.value for role in const.Roles)
@@ -0,0 +1,5 @@
1
+ from .permission_labels import PermissionLabels
2
+
3
+ __all__ = [
4
+ "PermissionLabels",
5
+ ]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import ckan.plugins as p
4
+ from ckan import model
5
+ from ckan.lib.plugins import DefaultPermissionLabels
6
+
7
+ import ckanext.permissions.utils as perm_utils
8
+
9
+
10
+ class PermissionLabels(p.SingletonPlugin, DefaultPermissionLabels):
11
+ p.implements(p.IPermissionLabels)
12
+
13
+ def get_dataset_labels(self, dataset_obj: model.Package) -> list[str]:
14
+ labels: list[str] = super().get_dataset_labels(dataset_obj)
15
+
16
+ labels.append("permission-allowed")
17
+
18
+ if dataset_obj.owner_org:
19
+ labels.append(f"permission-allowed-org:{dataset_obj.owner_org}")
20
+
21
+ return labels
22
+
23
+ def get_user_dataset_labels(self, user_obj: model.User | None) -> list[str]:
24
+ """Get permission labels for a user.
25
+
26
+ Extends the default CKAN permission labels by checking if the user has
27
+ special read permissions (read_any_dataset or read_private_dataset).
28
+ If they do, adds a 'permission-allowed' label that grants access to
29
+ the dataset.
30
+
31
+ Args:
32
+ user_obj: The user to get labels for. Can be None for anonymous users.
33
+
34
+ Returns:
35
+ List of permission labels for this user
36
+ """
37
+ labels: list[str] = super().get_user_dataset_labels(user_obj) # type: ignore
38
+
39
+ user = user_obj or model.AnonymousUser()
40
+
41
+ for permission in ["read_any_dataset", "read_private_dataset"]:
42
+ if perm_utils.check_permission(permission, user):
43
+ labels.append("permission-allowed")
44
+
45
+ return labels
File without changes
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import cast
4
+
5
+ import ckan.plugins.toolkit as tk
6
+ from ckan import model
7
+ from ckan.logic import validate
8
+ from ckan.types import Context, DataDict
9
+
10
+ from ckanext.permissions import model as perm_model
11
+ from ckanext.permissions import types as perm_types
12
+ from ckanext.permissions import utils as perm_utils
13
+ from ckanext.permissions.logic import schema
14
+
15
+
16
+ @validate(schema.role_create)
17
+ def permission_role_create(context: Context, data_dict: DataDict) -> perm_types.Role:
18
+ tk.check_access("manage_user_roles", context, data_dict)
19
+ return perm_model.Role.create(**data_dict).dictize(context)
20
+
21
+
22
+ @validate(schema.role_delete)
23
+ def permission_role_delete(context: Context, data_dict: DataDict) -> None:
24
+ tk.check_access("manage_user_roles", context, data_dict)
25
+
26
+ if role := perm_model.Role.get(data_dict["id"]):
27
+ role.delete()
28
+
29
+
30
+ @validate(schema.role_update)
31
+ def permission_role_update(context: Context, data_dict: DataDict) -> perm_types.Role:
32
+ tk.check_access("manage_user_roles", context, data_dict)
33
+
34
+ role = cast(perm_model.Role, perm_model.Role.get(data_dict["id"]))
35
+
36
+ role.update(data_dict["description"])
37
+
38
+ return role.dictize(context)
39
+
40
+
41
+ @validate(schema.permissions_update)
42
+ def permissions_update(context: Context, data_dict: DataDict) -> DataDict:
43
+ """Update the permissions for a given permission key.
44
+
45
+ Returns:
46
+ A dictionary with the updated permissions and the missing permissions
47
+ """
48
+ tk.check_access("manage_permissions", context, data_dict)
49
+
50
+ _validate_permission_data(data_dict)
51
+ registered_permissions = perm_utils.get_permissions()
52
+
53
+ updated_permissions = {}
54
+ missing_permissions = []
55
+
56
+ for permission_key, roles_data in data_dict["permissions"].items():
57
+ if permission_key not in registered_permissions:
58
+ missing_permissions.append(permission_key)
59
+ continue
60
+
61
+ permission_data = {}
62
+
63
+ for role_id, flag in roles_data.items():
64
+ role_permission = perm_model.RolePermission.get(role_id, permission_key)
65
+
66
+ if flag and role_permission:
67
+ continue
68
+
69
+ if not flag and not role_permission:
70
+ continue
71
+
72
+ if not flag and role_permission:
73
+ role_permission.delete()
74
+ else:
75
+ perm_model.RolePermission.create(role_id, permission_key)
76
+
77
+ permission_data[role_id] = flag
78
+
79
+ updated_permissions[permission_key] = permission_data
80
+
81
+ model.Session.commit()
82
+
83
+ return {
84
+ "updated_permissions": updated_permissions,
85
+ "missing_permissions": missing_permissions,
86
+ }
87
+
88
+
89
+ def _validate_permission_data(data: DataDict) -> None:
90
+ for permission_key, roles_data in data["permissions"].items():
91
+ if not isinstance(permission_key, str):
92
+ raise tk.ValidationError("Invalid permission key")
93
+
94
+ if not isinstance(roles_data, dict):
95
+ raise tk.ValidationError("Invalid permission mapping")
96
+
97
+ for role_id, flag in roles_data.items():
98
+ if not isinstance(flag, bool):
99
+ raise tk.ValidationError("Invalid permission value")
100
+
101
+ data, errors = tk.navl_validate(
102
+ {"id": role_id},
103
+ {
104
+ "id": [
105
+ tk.get_validator("not_empty"),
106
+ tk.get_validator("unicode_safe"),
107
+ tk.get_validator("permission_role_exists"),
108
+ ],
109
+ },
110
+ )
111
+
112
+ if errors:
113
+ raise tk.ValidationError(errors)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import ckan.plugins.toolkit as tk
4
+ from ckan import model, types
5
+
6
+ import ckanext.permissions.utils as perm_utils
7
+
8
+
9
+ @tk.chained_auth_function
10
+ @tk.auth_allow_anonymous_access
11
+ def package_show(
12
+ next_: types.AuthFunction,
13
+ context: types.Context,
14
+ data_dict: types.DataDict | None,
15
+ ) -> types.AuthResult:
16
+ user = model.User.get(context.get("user")) or model.AnonymousUser()
17
+ package = context.get("package") # type: ignore
18
+
19
+ if not package:
20
+ return next_(context, data_dict or {})
21
+
22
+ # Check permissions in order of precedence
23
+ permission_checks = [
24
+ ("read_any_dataset", None),
25
+ ("read_private_dataset", lambda: package.private),
26
+ ]
27
+
28
+ for permission, condition in permission_checks:
29
+ if condition is not None and not condition():
30
+ continue
31
+
32
+ if perm_utils.check_permission(permission, user):
33
+ return {"success": True}
34
+
35
+ return next_(context, data_dict or {})
36
+
37
+
38
+ @tk.chained_auth_function
39
+ @tk.auth_allow_anonymous_access
40
+ def package_update(
41
+ next_: types.AuthFunction, context: types.Context, data_dict: types.DataDict | None
42
+ ) -> types.AuthResult:
43
+ user = model.User.get(context.get("user")) or model.AnonymousUser()
44
+
45
+ if perm_utils.check_permission("update_any_dataset", user):
46
+ return {"success": True}
47
+
48
+ return next_(context, data_dict or {})
49
+
50
+
51
+ @tk.chained_auth_function
52
+ @tk.auth_allow_anonymous_access
53
+ def package_delete(
54
+ next_: types.AuthFunction, context: types.Context, data_dict: types.DataDict | None
55
+ ) -> types.AuthResult:
56
+ user = model.User.get(context.get("user")) or model.AnonymousUser()
57
+
58
+ if perm_utils.check_permission("delete_any_dataset", user):
59
+ return {"success": True}
60
+
61
+ return next_(context, data_dict or {})
62
+
63
+
64
+ @tk.chained_auth_function
65
+ @tk.auth_allow_anonymous_access
66
+ def resource_delete(
67
+ next_: types.AuthFunction, context: types.Context, data_dict: types.DataDict | None
68
+ ) -> types.AuthResult:
69
+ user = model.User.get(context.get("user")) or model.AnonymousUser()
70
+
71
+ if perm_utils.check_permission("delete_any_resource", user):
72
+ return {"success": True}
73
+
74
+ return next_(context, data_dict or {})
75
+
76
+
77
+ def manage_user_roles(context: types.Context, data_dict: types.DataDict) -> types.AuthResult:
78
+ return {"success": False}
79
+
80
+
81
+ def manage_permissions(context: types.Context, data_dict: types.DataDict) -> types.AuthResult:
82
+ return {"success": False}
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from ckan import types
4
+ from ckan.logic.schema import validator_args
5
+
6
+
7
+ @validator_args
8
+ def role_create(not_empty, unicode_safe, role_id_validator, role_doesnt_exists, ignore) -> types.Schema:
9
+ return {
10
+ "id": [not_empty, unicode_safe, role_id_validator, role_doesnt_exists],
11
+ "label": [not_empty, unicode_safe],
12
+ "description": [not_empty, unicode_safe],
13
+ "__extras": [ignore],
14
+ }
15
+
16
+
17
+ @validator_args
18
+ def role_delete(not_empty, unicode_safe, permission_role_exists, not_default_role) -> types.Schema:
19
+ return {
20
+ "id": [not_empty, unicode_safe, permission_role_exists, not_default_role],
21
+ }
22
+
23
+
24
+ @validator_args
25
+ def role_update(not_empty, unicode_safe, permission_role_exists) -> types.Schema:
26
+ return {
27
+ "id": [not_empty, unicode_safe, permission_role_exists],
28
+ "description": [not_empty, unicode_safe],
29
+ }
30
+
31
+
32
+ @validator_args
33
+ def permissions_update(default, convert_to_json_if_string, dict_only) -> types.Schema:
34
+ return {
35
+ "permissions": [default("{}"), convert_to_json_if_string, dict_only],
36
+ }
37
+
38
+
39
+ @validator_args
40
+ def permission_group_schema(not_empty, unicode_safe) -> types.Schema:
41
+ return {
42
+ "name": [not_empty, unicode_safe],
43
+ "description": [not_empty, unicode_safe],
44
+ "permissions": permission_schema(),
45
+ }
46
+
47
+
48
+ @validator_args
49
+ def permission_schema(not_empty, unicode_safe, ignore_missing) -> types.Schema:
50
+ return {
51
+ "key": [not_empty, unicode_safe],
52
+ "label": [not_empty, unicode_safe],
53
+ "description": [ignore_missing, unicode_safe],
54
+ }
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ import ckan.plugins.toolkit as tk
6
+
7
+ import ckanext.permissions.const as perm_const
8
+ import ckanext.permissions.model as perm_model
9
+
10
+
11
+ def role_doesnt_exists(role: str) -> str:
12
+ """Ensure that a role doesn't exists.
13
+
14
+ Args:
15
+ role (str): role name
16
+
17
+ Raises:
18
+ tk.Invalid: if the role exists
19
+
20
+ Returns:
21
+ role name
22
+ """
23
+ if perm_model.Role.get(role) is not None:
24
+ raise tk.Invalid(f"Role {role} is already exists")
25
+
26
+ return role
27
+
28
+
29
+ def permission_role_exists(role: str) -> str:
30
+ """Ensure that a role exists.
31
+
32
+ Args:
33
+ role (str): role name
34
+
35
+ Raises:
36
+ tk.Invalid: if the role doesn't exists
37
+
38
+ Returns:
39
+ role name
40
+ """
41
+ if perm_model.Role.get(role) is None:
42
+ raise tk.Invalid(f"Role {role} doesn't exists")
43
+
44
+ return role
45
+
46
+
47
+ def roles_exists(roles: list[str]) -> list[str]:
48
+ """Ensure that all roles exists.
49
+
50
+ Args:
51
+ roles (list[str]): list of roles
52
+
53
+ Raises:
54
+ tk.Invalid: if a role doesn't exists
55
+
56
+ Returns:
57
+ list of roles
58
+ """
59
+ for role in roles:
60
+ permission_role_exists(role)
61
+
62
+ return roles
63
+
64
+
65
+ def role_id_validator(value: str) -> str:
66
+ """Validate a role ID.
67
+
68
+ Ensures that:
69
+ - the role ID is a string
70
+ - is at least N characters long
71
+ - is at most N characters long
72
+ - contains only lowercase alpha (ascii) characters and these symbols: -_
73
+
74
+ Args:
75
+ value (str): role ID
76
+
77
+ Raises:
78
+ tk.Invalid: if the role ID is invalid
79
+
80
+ Returns:
81
+ role ID
82
+ """
83
+ name_match = re.compile(r"[a-z_\-]*$")
84
+
85
+ if len(value) < perm_const.ROLE_ID_MIN_LENGTH:
86
+ raise tk.Invalid(f"Role ID must be at least {perm_const.ROLE_ID_MIN_LENGTH} characters long.")
87
+
88
+ if len(value) > perm_const.ROLE_ID_MAX_LENGTH:
89
+ raise tk.Invalid(f"Role ID must be a maximum of {perm_const.ROLE_ID_MAX_LENGTH} characters long.")
90
+
91
+ if not name_match.match(value):
92
+ raise tk.Invalid("Role ID must be purely lowercase alpha(ascii) characters and these symbols: -_")
93
+
94
+ return value
95
+
96
+
97
+ def not_default_role(role_id: str) -> str:
98
+ """Ensure that the role is not a default role.
99
+
100
+ Args:
101
+ role_id (str): role ID
102
+
103
+ Raises:
104
+ tk.Invalid: if the role is a default role
105
+
106
+ Returns:
107
+ role ID
108
+ """
109
+ if role_id in [role.value for role in perm_const.Roles]:
110
+ raise tk.Invalid(f"Role {role_id} is a default role.")
111
+
112
+ return role_id
@@ -0,0 +1,74 @@
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = %(here)s
6
+
7
+ # template used to generate migration files
8
+ # file_template = %%(rev)s_%%(slug)s
9
+
10
+ # timezone to use when rendering the date
11
+ # within the migration file as well as the filename.
12
+ # string value is passed to dateutil.tz.gettz()
13
+ # leave blank for localtime
14
+ # timezone =
15
+
16
+ # max length of characters to apply to the
17
+ # "slug" field
18
+ #truncate_slug_length = 40
19
+
20
+ # set to 'true' to run the environment during
21
+ # the 'revision' command, regardless of autogenerate
22
+ # revision_environment = false
23
+
24
+ # set to 'true' to allow .pyc and .pyo files without
25
+ # a source .py file to be detected as revisions in the
26
+ # versions/ directory
27
+ # sourceless = false
28
+
29
+ # version location specification; this defaults
30
+ # to /home/berry/projects/master/ckanext-permissions/ckanext/permissions/migration/permissions/versions. When using multiple version
31
+ # directories, initial revisions must be specified with --version-path
32
+ # version_locations = %(here)s/bar %(here)s/bat /home/berry/projects/master/ckanext-permissions/ckanext/permissions/migration/permissions/versions
33
+
34
+ # the output encoding used when revision files
35
+ # are written from script.py.mako
36
+ # output_encoding = utf-8
37
+
38
+ sqlalchemy.url = driver://user:pass@localhost/dbname
39
+
40
+
41
+ # Logging configuration
42
+ [loggers]
43
+ keys = root,sqlalchemy,alembic
44
+
45
+ [handlers]
46
+ keys = console
47
+
48
+ [formatters]
49
+ keys = generic
50
+
51
+ [logger_root]
52
+ level = WARN
53
+ handlers = console
54
+ qualname =
55
+
56
+ [logger_sqlalchemy]
57
+ level = WARN
58
+ handlers =
59
+ qualname = sqlalchemy.engine
60
+
61
+ [logger_alembic]
62
+ level = INFO
63
+ handlers =
64
+ qualname = alembic
65
+
66
+ [handler_console]
67
+ class = StreamHandler
68
+ args = (sys.stderr,)
69
+ level = NOTSET
70
+ formatter = generic
71
+
72
+ [formatter_generic]
73
+ format = %(levelname)-5.5s [%(name)s] %(message)s
74
+ datefmt = %H:%M:%S