Skip to content

Commit

Permalink
Merge branch 'master' into fix-auth-control-header
Browse files Browse the repository at this point in the history
  • Loading branch information
fmigneault authored Sep 8, 2023
2 parents c6ab947 + 157006c commit 4e6aa9c
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 66 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Bug Fixes
~~~~~~~~~~~~~~~~~~~~~
* Fix returned headers in ``401 Unauthenticated`` response which did not properly employ ``Authentication-Control``
header to return the ``location-when-unauthenticated`` parameter instead of returning it directly as invalid header.
* Add `API` endpoint ``GET /services/{service_name}/resources/{resource_id}`` similar to
existing endpoint ``GET /resources/{resource_id}`` allowing retrieval of a `Resource` details
with prior validation that it lies under the referenced `Service`
(fixes `#347 <https://github.com/Ouranosinc/Magpie/issues/347>`_).
* Improve ``JSON`` typing definitions to reduce false-positives linting errors and add missing typing definitions.

.. _changes_3.35.0:

Expand Down
63 changes: 52 additions & 11 deletions magpie/api/management/resource/resource_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from typing import TYPE_CHECKING

from pyramid.httpexceptions import HTTPBadRequest, HTTPConflict, HTTPForbidden, HTTPInternalServerError, HTTPOk
from pyramid.httpexceptions import (
HTTPBadRequest,
HTTPConflict,
HTTPForbidden,
HTTPInternalServerError,
HTTPNotFound,
HTTPOk
)
from pyramid.settings import asbool
from pyramid.view import view_config
from ziggurat_foundations.models.services.group import GroupService
Expand All @@ -12,20 +19,23 @@
from magpie.api import schemas as s
from magpie.api.management.resource import resource_formats as rf
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 import service_formats as sf
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 magpie_register_permissions_from_config, sync_services_phoenix
from magpie.services import SERVICE_TYPE_DICT, get_resource_child_allowed

if TYPE_CHECKING:
from magpie.typedefs import NestingKeyType
from typing import List

from magpie.typedefs import JSON, AnyRequestType, AnyResponseType, NestingKeyType


@s.ResourcesAPI.get(tags=[s.ResourcesTag], response_schemas=s.Resources_GET_responses)
@view_config(route_name=s.ResourcesAPI.name, request_method="GET")
def get_resources_view(request):
# type: (AnyRequestType) -> AnyResponseType
"""
List all registered resources.
"""
Expand All @@ -34,20 +44,34 @@ def get_resources_view(request):
services = get_services_by_type(svc_type, db_session=request.db)
res_json[svc_type] = {}
for svc in services:
res_json[svc_type][svc.resource_name] = format_service_resources(
res_json[svc_type][svc.resource_name] = sf.format_service_resources(
svc, request.db, show_all_children=True, show_private_url=False)
res_json = {"resources": res_json}
return ax.valid_http(http_success=HTTPOk, detail=s.Resources_GET_OkResponseSchema.description, content=res_json)


@s.ResourceAPI.get(schema=s.Resource_GET_RequestSchema, tags=[s.ResourcesTag],
response_schemas=s.Resource_GET_responses)
@view_config(route_name=s.ResourceAPI.name, request_method="GET")
def get_resource_view(request):
def get_resource_handler(request):
# type: (AnyRequestType) -> AnyResponseType
"""
Get resource information.
Obtains the resource specified by the request with all applicable parameter validation and handling.
"""
resource = ar.get_resource_matchdict_checked(request)

# additional check only for endpoint based under service, resource-based endpoint accesses the resource directly
if "service_name" in request.matchdict:
# if the requested resource ID is the service itself, 'root_service_id=None', must check 'resource_id' also
service = ar.get_service_matchdict_checked(request)
ax.verify_param(
service.resource_id, [resource.root_service_id, resource.resource_id],
is_in=True, param_name="service_name",
http_error=HTTPNotFound,
content={
"service": sf.format_service(service, basic_info=True),
"resource": rf.format_resource(resource, basic_info=True),
},
msg_on_fail="Requested resource is not located under the specified service.",
)

parents = asbool(ar.get_query_param(request, ["parents", "parent"], False))
flatten = False
if parents:
Expand Down Expand Up @@ -101,10 +125,22 @@ def get_resource_view(request):
detail=s.Resource_GET_OkResponseSchema.description)


@s.ResourceAPI.get(schema=s.Resource_GET_RequestSchema, tags=[s.ResourcesTag],
response_schemas=s.Resource_GET_responses)
@view_config(route_name=s.ResourceAPI.name, request_method="GET")
def get_resource_view(request):
# type: (AnyRequestType) -> AnyResponseType
"""
Get resource information.
"""
return get_resource_handler(request)


@s.ResourcesAPI.post(schema=s.Resources_POST_RequestSchema, tags=[s.ResourcesTag],
response_schemas=s.Resources_POST_responses)
@view_config(route_name=s.ResourcesAPI.name, request_method="POST")
def create_resource_view(request):
# type: (AnyRequestType) -> AnyResponseType
"""
Register a new resource.
"""
Expand All @@ -119,6 +155,7 @@ def create_resource_view(request):
response_schemas=s.Resources_DELETE_responses)
@view_config(route_name=s.ResourceAPI.name, request_method="DELETE")
def delete_resource_view(request):
# type: (AnyRequestType) -> AnyResponseType
"""
Unregister a resource.
"""
Expand All @@ -129,6 +166,7 @@ def delete_resource_view(request):
response_schemas=s.Resource_PATCH_responses)
@view_config(route_name=s.ResourceAPI.name, request_method="PATCH")
def update_resource(request):
# type: (AnyRequestType) -> AnyResponseType
"""
Update a resource information.
"""
Expand Down Expand Up @@ -170,6 +208,7 @@ def rename_service_magpie_and_phoenix():
response_schemas=s.ResourcePermissions_GET_responses)
@view_config(route_name=s.ResourcePermissionsAPI.name, request_method="GET")
def get_resource_permissions_view(request):
# type: (AnyRequestType) -> AnyResponseType
"""
List all applicable permissions for a resource.
"""
Expand All @@ -186,6 +225,7 @@ def get_resource_permissions_view(request):
response_schemas=s.ResourceTypes_GET_responses)
@view_config(route_name=s.ResourceTypesAPI.name, request_method="GET")
def get_resource_types_view(request):
# type: (AnyRequestType) -> AnyResponseType
"""
List all applicable children resource types under another resource within a service hierarchy.
"""
Expand Down Expand Up @@ -221,10 +261,11 @@ def get_res_types(res):
response_schema=s.Permissions_PATCH_responses)
@view_config(route_name=s.PermissionsAPI.name, request_method="PATCH")
def update_permissions(request):
# type: (AnyRequestType) -> AnyResponseType
"""
Update the requested permissions and create missing related resources if necessary.
"""
permissions = ar.get_value_multiformat_body_checked(request, "permissions", check_type=list)
permissions = ar.get_value_multiformat_body_checked(request, "permissions", check_type=list) # type: List[JSON]
ax.verify_param(permissions, not_none=True, not_empty=True, http_error=HTTPBadRequest,
msg_on_fail="No permissions to update (empty `permissions` parameter).")

Expand Down Expand Up @@ -294,7 +335,7 @@ def update_permissions(request):
resource_full_type += "/" + resource_type
if permission:
cfg_entry = {
"service": service_name,
"service": service_name, # noqa
"resource": resource_full_path,
"type": resource_type if resource_type == "service" else resource_full_type,
"permission": permission,
Expand Down
32 changes: 30 additions & 2 deletions magpie/api/management/service/service_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from magpie.api import requests as ar
from magpie.api import schemas as s
from magpie.api.management.resource import resource_utils as ru
from magpie.api.management.resource import resource_views as rv
from magpie.api.management.service import service_formats as sf
from magpie.api.management.service import service_utils as su
from magpie.permissions import Permission, PermissionType, format_permissions
Expand All @@ -26,14 +27,16 @@
if TYPE_CHECKING:
from typing import List, Optional, Union

from pyramid.request import Request
from sqlalchemy.orm.session import Session

from magpie.typedefs import JSON, Str
from magpie.typedefs import JSON, AnyResponseType, Str


@s.ServiceTypesAPI.get(tags=[s.ServicesTag], response_schemas=s.ServiceTypes_GET_responses)
@view_config(route_name=s.ServiceTypesAPI.name, request_method="GET")
def get_service_types_view(request): # noqa: F811
# type: (Request) -> AnyResponseType
"""
List all available service types.
"""
Expand All @@ -45,6 +48,7 @@ def get_service_types_view(request): # noqa: F811
response_schemas=s.ServiceType_GET_responses)
@view_config(route_name=s.ServiceTypeAPI.name, request_method="GET")
def get_services_by_type_view(request):
# type: (Request) -> AnyResponseType
"""
List all registered services from a specific type.
"""
Expand All @@ -55,13 +59,15 @@ def get_services_by_type_view(request):
response_schemas=s.Services_GET_responses)
@view_config(route_name=s.ServicesAPI.name, request_method="GET")
def get_services_view(request):
# type: (Request) -> AnyResponseType
"""
List all registered services.
"""
return get_services_runner(request)


def get_services_runner(request):
# type: (Request) -> AnyResponseType
"""
Generates services response format from request conditions.
Expand Down Expand Up @@ -111,6 +117,7 @@ def get_services_runner(request):
response_schemas=s.Services_POST_responses)
@view_config(route_name=s.ServicesAPI.name, request_method="POST")
def register_service_view(request):
# type: (Request) -> AnyResponseType
"""
Registers a new service.
"""
Expand All @@ -127,6 +134,7 @@ def register_service_view(request):
response_schemas=s.Service_PATCH_responses)
@view_config(route_name=s.ServiceAPI.name, request_method="PATCH")
def update_service_view(request):
# type: (Request) -> AnyResponseType
"""
Update service information.
"""
Expand Down Expand Up @@ -200,8 +208,9 @@ def update_service_magpie_and_phoenix(_svc, new_name, new_url, svc_push, db_sess
@s.ServiceAPI.get(tags=[s.ServicesTag], response_schemas=s.Service_GET_responses)
@view_config(route_name=s.ServiceAPI.name, request_method="GET")
def get_service_view(request):
# type: (Request) -> AnyResponseType
"""
Get a service information.
Get service information.
"""
service = ar.get_service_matchdict_checked(request)
service_info = sf.format_service(service, show_private_url=True,
Expand All @@ -214,6 +223,7 @@ def get_service_view(request):
response_schemas=s.Service_DELETE_responses)
@view_config(route_name=s.ServiceAPI.name, request_method="DELETE")
def unregister_service_view(request):
# type: (Request) -> AnyResponseType
"""
Unregister a service.
"""
Expand All @@ -227,6 +237,7 @@ def unregister_service_view(request):
msg_on_fail="Delete service from resource tree failed.", content=svc_content)

def remove_service_magpie_and_phoenix(svc, svc_push, db_session):
# type: (models.Service, bool, Session) -> None
db_session.delete(svc)
if svc_push and svc.type in SERVICES_PHOENIX_ALLOWED:
sync_services_phoenix(db_session.query(models.Service))
Expand All @@ -241,6 +252,7 @@ def remove_service_magpie_and_phoenix(svc, svc_push, db_session):
@s.ServicePermissionsAPI.get(tags=[s.ServicesTag], response_schemas=s.ServicePermissions_GET_responses)
@view_config(route_name=s.ServicePermissionsAPI.name, request_method="GET")
def get_service_permissions_view(request):
# type: (Request) -> AnyResponseType
"""
List all applicable permissions for a service.
"""
Expand All @@ -253,10 +265,22 @@ def get_service_permissions_view(request):
content=format_permissions(svc_perms, PermissionType.ALLOWED))


@s.ServiceResourceAPI.get(schema=s.ServiceResource_GET_RequestSchema, tags=[s.ServicesTag, s.ResourcesTag],
response_schemas=s.ServiceResource_GET_responses)
@view_config(route_name=s.ServiceResourceAPI.name, request_method="GET")
def get_service_resource_view(request):
# type: (Request) -> AnyResponseType
"""
Get resource information under a service.
"""
return rv.get_resource_handler(request)


@s.ServiceResourceAPI.delete(schema=s.ServiceResource_DELETE_RequestSchema, tags=[s.ServicesTag],
response_schemas=s.ServiceResource_DELETE_responses)
@view_config(route_name=s.ServiceResourceAPI.name, request_method="DELETE")
def delete_service_resource_view(request):
# type: (Request) -> AnyResponseType
"""
Unregister a resource.
"""
Expand All @@ -267,6 +291,7 @@ def delete_service_resource_view(request):
response_schemas=s.ServiceResources_GET_responses)
@view_config(route_name=s.ServiceResourcesAPI.name, request_method="GET")
def get_service_resources_view(request):
# type: (Request) -> AnyResponseType
"""
List all resources registered under a service.
"""
Expand All @@ -281,6 +306,7 @@ def get_service_resources_view(request):
response_schemas=s.ServiceResources_POST_responses)
@view_config(route_name=s.ServiceResourcesAPI.name, request_method="POST")
def create_service_resource_view(request):
# type: (Request) -> AnyResponseType
"""
Register a new resource directly under a service or under one of its children resources.
"""
Expand Down Expand Up @@ -313,6 +339,7 @@ def create_service_resource_view(request):
response_schemas=s.ServiceTypeResources_GET_responses)
@view_config(route_name=s.ServiceTypeResourcesAPI.name, request_method="GET")
def get_service_type_resources_view(request):
# type: (Request) -> AnyResponseType
"""
List details of resource types supported under a specific service type.
"""
Expand All @@ -335,6 +362,7 @@ def _get_resource_types_info(res_type_names):
response_schemas=s.ServiceTypeResourceTypes_GET_responses)
@view_config(route_name=s.ServiceTypeResourceTypesAPI.name, request_method="GET")
def get_service_type_resource_types_view(request):
# type: (Request) -> AnyResponseType
"""
List all resource types supported under a specific service type.
"""
Expand Down
15 changes: 10 additions & 5 deletions magpie/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,19 +455,19 @@ class ContentType(colander.SchemaNode):

class RequestHeaderSchemaAPI(colander.MappingSchema):
accept = AcceptType(name="Accept", validator=colander.OneOf(SUPPORTED_ACCEPT_TYPES),
description="Desired MIME type for the response body content.")
description="Desired media-type for the response body content.")
content_type = ContentType(validator=colander.OneOf(KNOWN_CONTENT_TYPES),
description="MIME content type of the request body.")
description="Media-type of the request body content.")


class RequestHeaderSchemaUI(colander.MappingSchema):
content_type = ContentType(default=CONTENT_TYPE_HTML, example=CONTENT_TYPE_HTML,
description="MIME content type of the request body.")
description="Media-type of the request body content.")


class QueryRequestSchemaAPI(colander.MappingSchema):
format = AcceptType(validator=colander.OneOf(SUPPORTED_FORMAT_TYPES),
description="Desired MIME type for the response body content. "
description="Desired media-type for the response body content. "
"This formatting alternative by query parameter overrides the Accept header.")


Expand Down Expand Up @@ -601,7 +601,7 @@ class BaseRequestSchemaAPI(colander.MappingSchema):

class HeaderResponseSchema(colander.MappingSchema):
content_type = ContentType(validator=colander.OneOf(SUPPORTED_ACCEPT_TYPES),
description="MIME content type of the response body.")
description="Media-type of the response body content.")


class BaseResponseSchemaAPI(colander.MappingSchema):
Expand Down Expand Up @@ -1789,6 +1789,10 @@ class ServiceResources_POST_UnprocessableEntityResponseSchema(BaseResponseSchema
body = ErrorResponseBodySchema(code=HTTPUnprocessableEntity.code, description=description)


class ServiceResource_GET_RequestSchema(Resource_GET_RequestSchema):
service_name = ServiceNameParameter


# delete service's resource use same method as direct resource delete
class ServiceResource_DELETE_RequestSchema(Resource_DELETE_RequestSchema):
service_name = ServiceNameParameter
Expand Down Expand Up @@ -3654,6 +3658,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema):
"422": UnprocessableEntityResponseSchema(),
"500": InternalServerErrorResponseSchema(),
}
ServiceResource_GET_responses = Resource_GET_responses
ServiceResource_DELETE_responses = {
"200": ServiceResource_DELETE_OkResponseSchema(),
"400": Resource_MatchDictCheck_BadRequestResponseSchema(), # FIXME: https://github.com/Ouranosinc/Magpie/issues/359
Expand Down
10 changes: 7 additions & 3 deletions magpie/typedefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@

AnyKey = Union[Str, int]
AnyValue = Union[Str, Number, bool, None]
_JSONType = "JSON" # type: TypeAlias # pylint: disable=C0103
BaseJSON = Union[AnyValue, List[_JSONType], Dict[AnyKey, _JSONType]]
JSON = Union[Dict[Str, Union[_JSONType]], List[_JSONType]]
_JSON = "JSON" # type: TypeAlias # pylint: disable=C0103
_JsonObjectItemAlias = "_JsonObjectItem" # type: TypeAlias # pylint: disable=C0103
_JsonListItemAlias = "_JsonListItem" # type: TypeAlias # pylint: disable=C0103
_JsonObjectItem = Dict[str, Union[AnyValue, _JSON, _JsonObjectItemAlias, _JsonListItemAlias]]
_JsonListItem = List[Union[AnyValue, _JSON, _JsonObjectItem, _JsonListItemAlias]]
_JsonItem = Union[AnyValue, _JSON, _JsonObjectItem, _JsonListItem]
JSON = Union[Dict[str, Union[_JSON, _JsonItem]], List[Union[_JSON, _JsonItem]], AnyValue]

GroupPriority = Union[int, Type[math.inf]]
UserServicesType = Union[Dict[Str, Dict[Str, Any]], List[Dict[Str, Any]]]
Expand Down
Loading

0 comments on commit 4e6aa9c

Please sign in to comment.