Skip to content

Commit

Permalink
Merge pull request #511 from Ouranosinc/permissions-patch
Browse files Browse the repository at this point in the history
Permissions patch
  • Loading branch information
cwcummings authored Mar 23, 2022
2 parents 060c556 + d0b2289 commit 903e8e0
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 54 deletions.
6 changes: 5 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ Changes
`Unreleased <https://github.com/Ouranosinc/Magpie/tree/master>`_ (latest)
------------------------------------------------------------------------------------

* Nothing new for the moment.
Features / Changes
~~~~~~~~~~~~~~~~~~~~~
* Add ``PATCH /permissions`` endpoint that updates permissions and creates related resources if necessary.
* Add support of new format for ``permissions.cfg`` for the ``type`` parameter, using multiple types separated
by a slash character, matching each type with each resource found in the ``resource`` parameter.

.. _changes_3.23.0:

Expand Down
2 changes: 2 additions & 0 deletions config/permissions.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# service: service name to receive the permission (directly on it if no 'resource' mentioned, must exist)
# resource: (optional) tree path of the service's resource (ex: /res1/sub-res2/sub-sub-res3)
# type: (optional) resource type employed in case of ambiguity between multiple choices (depends on service type)
# can either be a single type, or a full path of resource types corresponding to the types of each
# resource found in the path of the 'resource' parameter (ex: /directory/directory/file)
# user: user for which to apply the modification of permission (skip if omitted or None)
# group: group for which to apply the modification of permission (skip if omitted or None)
# permission: name or object of the permission to be applied (see 'magpie.permissions' for supported values)
Expand Down
1 change: 1 addition & 0 deletions magpie/api/management/resource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
def includeme(config):
LOGGER.info("Adding API resource...")
# Add all the rest api routes
config.add_route(**s.service_api_route_info(s.PermissionsAPI))
config.add_route(**s.service_api_route_info(s.ResourcesAPI))
config.add_route(**s.service_api_route_info(s.ResourceAPI))
config.add_route(**s.service_api_route_info(s.ResourcePermissionsAPI))
Expand Down
104 changes: 103 additions & 1 deletion magpie/api/management/resource/resource_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from pyramid.httpexceptions import HTTPBadRequest, HTTPConflict, HTTPForbidden, HTTPInternalServerError, HTTPOk
from pyramid.settings import asbool
from pyramid.view import view_config
from ziggurat_foundations.models.services.group import GroupService
from ziggurat_foundations.models.services.user import UserService

from magpie import models
from magpie.api import exception as ax
Expand All @@ -12,8 +14,9 @@
from magpie.api.management.resource import resource_utils as ru
from magpie.api.management.service.service_formats import format_service_resources
from magpie.api.management.service.service_utils import get_services_by_type
from magpie.api.management.user import user_utils as uu
from magpie.permissions import PermissionType, format_permissions
from magpie.register import sync_services_phoenix
from magpie.register import magpie_register_permissions_from_config, sync_services_phoenix
from magpie.services import SERVICE_TYPE_DICT, get_resource_child_allowed

if TYPE_CHECKING:
Expand Down Expand Up @@ -212,3 +215,102 @@ def get_res_types(res):
}
return ax.valid_http(http_success=HTTPOk, content=data,
detail=s.ResourceTypes_GET_OkResponseSchema.description)


@s.PermissionsAPI.patch(schema=s.Permissions_PATCH_RequestSchema, tags=[s.PermissionTag],
response_schema=s.Permissions_PATCH_responses)
@view_config(route_name=s.PermissionsAPI.name, request_method="PATCH")
def update_permissions(request):
"""
Update the requested permissions and create missing related resources if necessary.
"""
permissions = ar.get_value_multiformat_body_checked(request, "permissions", check_type=list)
ax.verify_param(permissions, not_none=True, not_empty=True, http_error=HTTPBadRequest,
msg_on_fail="No permissions to update (empty `permissions` parameter).")

required_users = set()
required_groups = set()
has_permission_to_update = False
for entry in permissions:
ax.verify_param(entry, is_type=True, param_compare=dict, http_error=HTTPBadRequest,
msg_on_fail="Permission entry should be of `dict` type, but type `{}` was found instead".format(
type(entry)),
param_content={"value": entry})
if "permission" in entry and entry["permission"]:
user = entry.get("user")
group = entry.get("group")
ax.verify_param(bool(user or group), is_true=True, http_error=HTTPBadRequest,
msg_on_fail="No user or group defined with the permission to update.",
param_content={"value": entry})
has_permission_to_update = True
if user:
required_users.add(user)
if group:
required_groups.add(group)

for user_name in required_users:
user = UserService.by_user_name(user_name, db_session=request.db)
ax.verify_param(user, not_none=True, http_error=HTTPBadRequest,
msg_on_fail="Permission's user `{}` could not be found in the database.".format(user_name))
uu.check_user_editable(user, request)
for group_name in required_groups:
ax.verify_param(GroupService.by_group_name(group_name, db_session=request.db),
not_none=True, http_error=HTTPBadRequest,
msg_on_fail="Permission's group `{}` could not be found in the database.".format(group_name))

ax.verify_param(has_permission_to_update, is_true=True, http_error=HTTPBadRequest,
msg_on_fail="No permissions to update (none of the input entries has a defined permission).",
param_content={"value": permissions})

# Reformat permissions config
permissions_cfg = {"permissions": []}
resource_full_path = ""
resource_full_type = ""
for i, entry in enumerate(permissions):
resource_name = entry.get("resource_name")
resource_type = entry.get("resource_type")
permission = entry.get("permission")
user = entry.get("user")
group = entry.get("group")
action = entry.get("action", "create")

ax.verify_param(resource_name, not_none=True, not_empty=True, http_error=HTTPBadRequest,
msg_on_fail="Missing `resource_name` parameter for permissions update.",
param_name="resource_name", param_content={"value": entry})
ax.verify_param(resource_type, not_none=True, not_empty=True, http_error=HTTPBadRequest,
msg_on_fail="Missing `resource_type` parameter for permissions update.",
param_name="resource_type", param_content={"value": entry})
if i == 0:
ax.verify_param(resource_type, is_equal=True, param_compare="service", http_error=HTTPBadRequest,
msg_on_fail="First resource in the permissions list should have a `service` type but has "
"a `{}` type instead.".format(resource_type),
param_name="resource_type", param_content={"value": entry})
service_name = resource_name
else:
resource_full_path += "/" + resource_name
ax.verify_param(resource_type, not_equal=True, param_compare="service", http_error=HTTPBadRequest,
msg_on_fail="Only the first resource in the permissions list should be of `service` type.",
param_name="resource_type", param_content={"value": entry})
resource_full_type += "/" + resource_type
if permission:
cfg_entry = {
"service": service_name,
"resource": resource_full_path,
"type": resource_type if resource_type == "service" else resource_full_type,
"permission": permission,
"action": action
}
if user:
cfg_entry["user"] = user
if group:
cfg_entry["group"] = group

permissions_cfg["permissions"].append(cfg_entry)

# Apply permission update
ax.evaluate_call(
lambda: magpie_register_permissions_from_config(
permissions_config=permissions_cfg, db_session=request.db, raise_errors=True),
http_error=HTTPBadRequest, msg_on_fail="Failed to update requested permissions.")

return ax.valid_http(http_success=HTTPOk, detail=s.Permissions_PATCH_OkResponseSchema.description)
78 changes: 78 additions & 0 deletions magpie/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ def service_api_route_info(service_api, **kwargs):
GroupResourcePermissionAPI = Service(
path="/groups/{group_name}/resources/{resource_id}/permissions/{permission_name}",
name="GroupResourcePermission")
PermissionsAPI = Service(
path="/permissions",
name="Permissions")
RegisterGroupsAPI = Service(
path="/register/groups",
name="RegisterGroups")
Expand Down Expand Up @@ -857,6 +860,73 @@ class PermissionNameListSchema(colander.SequenceSchema):
)


class PermissionPatchObjectSchema(colander.MappingSchema):
resource_name = colander.SchemaNode(
colander.String(),
description="Name of the resource associated with the permission. This resource will be created if missing."
)
resource_type = colander.SchemaNode(
colander.String(),
description="Type of the resource. The first resource must be of the `service` type, and children "
"resources must have a relevant resource type that is not of the `service` type.",
example="service"
)

user = colander.SchemaNode(
colander.String(),
description="Registered local user associated with the permission.",
example="toto",
missing=colander.drop
)
group = colander.SchemaNode(
colander.String(),
description="Registered user group associated with the permission.",
example="users",
missing=colander.drop
)

# FIXME: support oneOf(string, object), permission can actually be either a string or a PermissionObjectSchema(dict)
# Currently not possible to have multiple type options with colander.
permission = PermissionObjectSchema(
missing=colander.drop
)

action = colander.SchemaNode(
colander.String(),
description="Action to apply on the permission.",
example="create",
default="create",
validator=colander.OneOf(["create", "remove"])
)


class PermissionPatchListSchema(colander.SequenceSchema):
permission = PermissionPatchObjectSchema()


class Permissions_PATCH_RequestBodySchema(colander.MappingSchema):
permissions = PermissionPatchListSchema()


class Permissions_PATCH_RequestSchema(BaseRequestSchemaAPI):
body = Permissions_PATCH_RequestBodySchema()


class Permissions_PATCH_OkResponseSchema(BaseResponseSchemaAPI):
description = "Update permissions successful."
body = BaseResponseBodySchema(code=HTTPOk.code, description=description)


class Permissions_PATCH_BadRequestResponseSchema(BaseResponseSchemaAPI):
description = "Missing or invalid parameters for permissions update."
body = ErrorResponseBodySchema(code=HTTPBadRequest.code, description=description)


class Permissions_PATCH_ForbiddenResponseSchema(BaseResponseSchemaAPI):
description = "Failed to update requested permissions."
body = ErrorResponseBodySchema(code=HTTPForbidden.code, description=description)


class BaseUserInfoSchema(colander.MappingSchema):
user_name = UserNameParameter
email = colander.SchemaNode(
Expand Down Expand Up @@ -3591,6 +3661,14 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema):
"422": UnprocessableEntityResponseSchema(),
"500": InternalServerErrorResponseSchema(),
}
Permissions_PATCH_responses = {
"200": Permissions_PATCH_OkResponseSchema(),
"400": Permissions_PATCH_BadRequestResponseSchema(), # FIXME: https://github.com/Ouranosinc/Magpie/issues/359
"401": UnauthorizedResponseSchema(),
"403": Permissions_PATCH_ForbiddenResponseSchema(),
"406": NotAcceptableResponseSchema(),
"500": InternalServerErrorResponseSchema(),
}
Users_GET_responses = {
"200": Users_GET_OkResponseSchema(),
"400": Users_GET_BadRequestSchema(),
Expand Down
Loading

0 comments on commit 903e8e0

Please sign in to comment.