Beispiel #1
0
async def get_role_resource_permissions(
        request: Request,
        role: Role,
        resource: Resource,
        nocache: bool = False) -> typing.List[int]:
    """
    Get the permission granted on a resource for a specified role.
    Bypassing the cache can have a significant performance and should only be
    used in exceptional circumstances.

    :param request: The request associated with this call
    :param role: The role
    :param resource: The resource
    :param nocache: Bypass the cache if True
    :return: A list of permission ids
    """
    role_id = role if type(role) is int else Mappings.role_id_for(role)
    resource_id = resource if type(resource) is int else \
        Mappings.resource_id_for(resource)

    key = bytes(f"{MKP_ROLE_RESOURCE_PERMISSION}:{role_id}:{resource_id}",
                encoding="utf8")
    with client_exception_handler():
        role_resource_permissions = None if nocache else await request.app[
            "redis"].get(key)

    if not role_resource_permissions:
        # The API call returns a List[RoleResourcePermission]
        with client_exception_handler():
            role_resource_permissions = await request.app[
                "access_control_api"].roleresourcepermission_list(
                    role_id=role_id, resource_id=resource_id)

        # Internally we use only the permission ids, i.e. List[int]
        role_resource_permissions = [
            entry.permission_id for entry in role_resource_permissions
        ]

        with client_exception_handler():
            await request.app["redis"].set(
                key,
                json.dumps(role_resource_permissions).encode("utf8"),
                expire=CACHE_TIME)
    else:
        role_resource_permissions = json.loads(role_resource_permissions,
                                               encoding="utf8")

    return role_resource_permissions
Beispiel #2
0
async def role_has_permission(request: Request,
                              role: Role,
                              permission: Permission,
                              resource: Resource,
                              nocache: bool = False) -> bool:
    """
    Check if a role has a specified resource permission.

    Bypassing the cache can have a significant performance impact and should
    only be used in exceptional circumstances.

    :param request: The request associated with this call
    :param role: The role to check
    :param permission: The required permission
    :param resource: The resource
    :param nocache: Bypass the cache if True
    :return: True if the role has the permission on the resource, else False
    """
    result = False
    permission_id = permission if type(permission) is int else \
        Mappings.permission_id_for(permission)

    permission_ids = await get_role_resource_permissions(request,
                                                         role,
                                                         resource,
                                                         nocache=nocache)
    if permission_ids:
        result = permission_id in permission_ids

    return result
    async def test_empty_resource_permissions_lists(self, mocked_function):
        """
        Check the expected behaviour when an empty resource permission list
        is used with the any or all operation.
        We mock a response for the get_user_roles_for_site() function in
        order to test the case where the specified required resource
        permission list is empty.
        """
        # require_permissions(all, []) always succeeds, regardless of which
        # roles the user has.
        @require_permissions(all, [], nocache=True)
        def empty_all(_request):
            return True

        # require_permissions(any, []) always fails, regardless of which roles
        # the user has.
        @require_permissions(any, [], nocache=True)
        def empty_any(_request):
            return "You must be a tech admin"

        mocked_function.side_effect = make_coroutine_returning(
            {0})  # An arbitrary id

        # Always gets allowed, regardless of the user's roles
        self.assertTrue(await empty_all(self.mocked_request))

        # Never gets allowed unless the user has the TECH_ADMIN role
        with self.assertRaises(JSONForbidden):
            await empty_any(self.mocked_request)

        mocked_function.side_effect = make_coroutine_returning(
            {Mappings.role_id_for(TECH_ADMIN_ROLE_LABEL)})
        self.assertEqual(await empty_any(self.mocked_request),
                         "You must be a tech admin")
def get_user_and_site(request):
    """
    A request contains the JWT token payload from which we get
    * the user id from the "sub" (subscriber) field
    * the client id from the "aud" (audience) field
    :param request: An HttpRequest
    :return: A (user_id, site_id) tuple.
    :raises: HTTPForbidden if the token's client id does not map to a site id.
    """
    # Extract the user's UUID from the request
    user_id = uuid.UUID(request["token"]["sub"])
    # Extract the client id from the request
    client_id = request["token"]["aud"]

    try:
        site_id = Mappings.site_id_for_token_client_id(client_id)
    except KeyError:
        raise JSONForbidden(
            message=f"No site linked to the client '{client_id}'")

    return user_id, site_id
Beispiel #5
0
async def get_user_roles_for_site(request: Request,
                                  user: UserId,
                                  site: SiteId,
                                  nocache: bool = False) -> typing.Set[int]:
    """
    Get the roles that a user has for a specified site.
    Bypassing the cache can have a significant performance and should only be
    used in exceptional circumstances.
    :param request: The request associated with this call
    :param user: The user
    :param site: The site
    :param nocache: Bypass the cache if True
    :return: A set of role ids
    """
    site_id = site if type(
        site) is int else Mappings.site_id_for_token_client_id(site)

    user_roles = await get_all_user_roles(request, user, nocache)
    # Look up the list of role ids associated with the site key. Return an
    # empty set of it does not exist.
    return set(user_roles.get(f"s:{site_id}", []))
Beispiel #6
0
async def get_user_roles_for_domain(request: Request,
                                    user: UserId,
                                    domain: Domain,
                                    nocache: bool = False) -> typing.Set[int]:
    """
    Get the roles that a user has for a specified domain.
    Bypassing the cache can have a significant performance and should only be
    used in exceptional circumstances.
    :param request: The request associated with this call
    :param user: The user
    :param domain: The site
    :param nocache: Bypass the cache if True
    :return: A set of role ids
    """
    domain_id = domain if type(domain) is int else Mappings.domain_id_for(
        domain)

    user_roles = await get_all_user_roles(request, user, nocache)
    # Look up the list of role ids associated with the domain key. Return an
    # empty set of it does not exist.
    return set(user_roles.get(f"d:{domain_id}", []))
        async def wrapped_f(*args, **kwargs):
            """
            :param args: A list of positional arguments
            :param kwargs: A dictionary of keyword arguments
            :return: Whatever the wrapped function
            """
            # Extract the request from the function arguments
            request = get_value_from_args_or_kwargs(request_field, args,
                                                    kwargs)

            user_id, token_site_id = get_user_and_site(request)

            # The decorator need to get the values for the following variables from the
            # decorated function.
            role_id = None
            target_user_id = None
            target_invitation_id = None
            domain_id = None
            site_id = None

            if body_field is not None:
                body = get_value_from_args_or_kwargs(body_field, args, kwargs)

                # When body_field is specified, the xxx_field arguments must be the names of the
                # fields in the body dictionary or None, in which case the defaults will be used.
                role_id = body[role_id_field or "role_id"]
                target_user_id = body.get(target_user_id_field or "user_id")
                target_invitation_id = body.get(target_invitation_id
                                                or "invitation_id")
                # We expect either a domain_id or a site_id to be provided
                domain_id = body.get(domain_id_field or "domain_id")
                site_id = body.get(site_id_field or "site_id")
                if domain_id is None and site_id is None or \
                        domain_id is not None and site_id is not None:
                    raise RuntimeError(
                        "Either a domain_id or site_id needs to exist in the body"
                    )
            else:
                # If the body_field is not specified, the rest of the arguments are considered to be
                # positional arguments.
                role_id = get_value_from_args_or_kwargs(
                    role_id_field, args, kwargs)
                if domain_id_field is None and site_id_field is None or \
                        domain_id_field is not None and site_id_field is not None:
                    raise RuntimeError(
                        "Either a domain_id_field or site_id_field needs to defined"
                    )

                if domain_id_field:
                    domain_id = get_value_from_args_or_kwargs(
                        domain_id_field, args, kwargs)
                else:
                    site_id = get_value_from_args_or_kwargs(
                        site_id_field, args, kwargs)

                # Extract the user_id or invitation_id for which a role must be a assigned/revoked
                if target_user_id_field is not None:
                    target_user_id = get_value_from_args_or_kwargs(
                        target_user_id_field, args, kwargs)
                else:
                    target_invitation_id = get_value_from_args_or_kwargs(
                        target_invitation_id_field, args, kwargs)

            if domain_id:
                user_roles = await utils.get_user_roles_for_domain(
                    request, user_id, domain_id, nocache)
            else:  # Use site_id
                user_roles = await utils.get_user_roles_for_site(
                    request, user_id, site_id, nocache)

            # If the user roles contains the one we want to assign or Tech Admin, we allow
            # the call to proceed.
            if user_roles.intersection(
                [role_id, Mappings.role_id_for(TECH_ADMIN_ROLE_LABEL)]):
                if asyncio.iscoroutinefunction(f):
                    return await f(*args, **kwargs)
                else:
                    return f(*args, **kwargs)

            if target_user_id_field is not None:
                log_message = f"User {user_id} cannot assign role {Mappings.role_label_for(role_id)}" \
                              f" to target user {target_user_id} on "
            else:
                log_message = f"User {user_id} cannot assign role {Mappings.role_label_for(role_id)}" \
                              f" to target invitation {target_invitation_id} on "

            if domain_id:
                log_message += f"domain {Mappings.domain_name_for(domain_id)} "
            else:
                log_message += f"site {Mappings.site_name_for(site_id)} "

            log_message += f"because it does not have the role or {TECH_ADMIN_ROLE_LABEL} itself."

            logger.debug(log_message)

            raise JSONForbidden(message=log_message)
Beispiel #8
0
async def roles_have_permissions(request: Request,
                                 roles: typing.Set[Role],
                                 operator: Operator,
                                 resource_permissions: ResourcePermissions,
                                 nocache: bool = False) -> bool:
    """
    Check whether the specified roles has any or all (as per the operator) of
    the specified resource permissions.
    It is typically used as part of the user_has_permissions() function as the
    following simplified pseudo-code shows:

        user_has_permissions(user, operator, resource_permissions):
            roles = get_user_roles()
            return roles_have_permissions(roles, operator, resource_permissions)

    If any of the roles has the required permission, the requirement is
    satisfied.
    Bypassing the cache can have a significant performance impact and should
    only be used in exceptional circumstances.
    :param request: The request associated with this call
    :param roles: A set of role ids
    :param operator: any or all
    :param resource_permissions: The resource permissions required
    :param nocache: Bypass the cache if True
    :return: True if the user is has the required permissions on the resources
    """
    if operator not in [all, any]:
        raise RuntimeError("The operator must be all or any")

    # Normalise to ids
    roles = {
        role if type(role) is int else Mappings.role_id_for(role)
        for role in roles
    }

    if Mappings.role_id_for(TECH_ADMIN_ROLE_LABEL) in roles:
        return True

    # This code snippet was originally used before the utility functions were changed
    # into co-routines. It is elegant and efficient and left here for posterity and as
    # an example of the logic that is performed in the code below, which unfortunately
    # ended up being more verbose. - cobusc
    #
    # return operator(
    #     # Any role can provide the permission.
    #     any(role_has_permission(role, permission, resource, nocache) for role in roles)
    #     for resource, permission in resource_permissions)

    for resource, permission in resource_permissions:
        some_role_has_the_permission = False
        for role in roles:
            if await role_has_permission(request,
                                         role,
                                         permission,
                                         resource,
                                         nocache=nocache):
                some_role_has_the_permission = True
                break  # No need to look further

        # Short-circuit conditions
        if operator is any and some_role_has_the_permission:
            return True
        if operator is all and not some_role_has_the_permission:
            return False

    # If none of the short-circuit conditions have been met, it means that
    # all of the checks where True if the operator was all, or none of the checks
    # were True if the operator is any.
    return True if operator is all else False
async def auth_middleware(request, handler):
    authorization_header = request.headers.get("authorization", None)

    if request.method == "OPTIONS":
        # HTTP OPTION requests do not need a token
        pass
    elif request.path in WHITELIST_URLS:
        # URLs that do not require authorisation
        pass
    elif any(
            request.path.startswith(prefix)
            for prefix in WHITELIST_URL_PREFIXES):
        # URLs that do not require authorisation
        pass
    elif authorization_header:
        if not authorization_header.lower().startswith(TOKEN_PREFIX):
            return json_response({"message": "Malformed authorization header"},
                                 status=INVALID_TOKEN_STATUS)

        jwt_token = authorization_header[TOKEN_PREFIX_LENGTH:]
        try:
            LOGGER.debug("JWT Token: {}".format(jwt_token.encode("utf-8")))
            unverified_headers = jwt.get_unverified_header(jwt_token)
            algorithm = unverified_headers["alg"]
            if algorithm == "HS256":
                payload = jwt.decode(
                    jwt_token,
                    key=settings.JWT_SECRET,
                    algorithms=[settings.JWT_ALGORITHM],
                    audience=settings.JWT_AUDIENCE,
                    issuer=settings.JWT_ISSUER,
                    options=VERIFICATION_OPTIONS,
                )
            elif algorithm == "RS256":
                key_id = unverified_headers["kid"]
                key = Mappings.public_key_for(key_id)
                payload = jwt.decode(
                    jwt_token,
                    key=key,
                    algorithms=[algorithm],
                    audience=settings.JWT_AUDIENCE,
                    issuer=settings.JWT_ISSUER,
                    options=VERIFICATION_OPTIONS,
                )
            else:
                return json_response(
                    {
                        "message":
                        "Unsupported token algorithm: {}".format(algorithm)
                    },
                    status=INVALID_TOKEN_STATUS)

            LOGGER.debug("Token payload: {}".format(payload))
        except jwt.DecodeError as e:
            return json_response({"message": "Token is invalid: {}".format(e)},
                                 status=INVALID_TOKEN_STATUS)
        except jwt.ExpiredSignatureError:
            return json_response({"message": "Token expired"},
                                 status=INVALID_TOKEN_STATUS)
        except jwt.InvalidAudienceError:
            return json_response({"message": "Token audience is invalid"},
                                 status=INVALID_TOKEN_STATUS)
        except jwt.InvalidIssuerError:
            return json_response({"message": "Token issuer is invalid"},
                                 status=INVALID_TOKEN_STATUS)
        except Exception as e:
            return json_response(
                {"message": "Exception: {} {}".format(type(e), str(e))},
                status=INVALID_TOKEN_STATUS)

        LOGGER.debug("Token payload: {}".format(payload))
        request["token"] = payload
    else:
        return json_response(
            {"message": "An authentication token is required"},
            status=INVALID_TOKEN_STATUS)

    return await handler(request)
    async def test_roles_have_permissions(self,
                                          mocked_roleresourcepermission_list):
        """
        Test that the "roles_have_permissions" function work as intended.
        :param mocked_roleresourcepermission_list: Function mocked by @patch
            decorator
        """
        async def dummy_roleresourcepermission_list(role_id, resource_id):
            responses = {
                (1, 1): [
                    RoleResourcePermission(
                        **{
                            "role_id":
                            role_id,
                            "resource_id":
                            resource_id,
                            "permission_id":
                            TEST_PERMISSION_NAME_TO_ID_MAP["permission1"],
                            "created_at":
                            datetime.now(),
                            "updated_at":
                            datetime.now()
                        })
                ]
            }
            return responses.get((role_id, resource_id), [])

        mocked_roleresourcepermission_list.side_effect = \
            dummy_roleresourcepermission_list

        # We test both the cached and non-cached use-cases. It is important
        # that we test with nocache=True first, since this will not read from
        # the cache, but populate it for the next iteration where nocache=False.
        # Test using the 'all' operator
        for nocache in [True, False]:
            # Call function using labels
            self.assertTrue(await utils.roles_have_permissions(
                self.dummy_request, {"role1", "role2"}, all,
                [("urn:resource1", "permission1")], nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {"role1", "role2"}, all,
                [("urn:resource1", "permission2")], nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {"role1", "role2"}, all,
                [("urn:resource2", "permission1")], nocache))

            # Call function using ids
            self.assertTrue(await utils.roles_have_permissions(
                self.dummy_request, {
                    TEST_ROLE_LABEL_TO_ID_MAP["role1"],
                    TEST_ROLE_LABEL_TO_ID_MAP["role2"]
                }, all, [(TEST_RESOURCE_URN_TO_ID_MAP["urn:resource1"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission1"])],
                nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {
                    TEST_ROLE_LABEL_TO_ID_MAP["role1"],
                    TEST_ROLE_LABEL_TO_ID_MAP["role2"]
                }, all, [(TEST_RESOURCE_URN_TO_ID_MAP["urn:resource1"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission2"])],
                nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {
                    TEST_ROLE_LABEL_TO_ID_MAP["role1"],
                    TEST_ROLE_LABEL_TO_ID_MAP["role2"]
                }, all, [(TEST_RESOURCE_URN_TO_ID_MAP["urn:resource2"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission1"])],
                nocache))

        # Test using the 'any' operator
        for nocache in [True, False]:
            # Call function using labels
            self.assertTrue(await utils.roles_have_permissions(
                self.dummy_request, {"role1", "role2"}, any,
                [("urn:resource1", "permission1"),
                 ("urn:resource3", "permission3")], nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {"role1", "role2"}, any,
                [("urn:resource3", "permission3"),
                 ("urn:resource4", "permission4")], nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {"role1", "role2"}, any,
                [("urn:resource2", "permission1")], nocache))

            # Call function using ids
            self.assertTrue(await utils.roles_have_permissions(
                self.dummy_request, {
                    TEST_ROLE_LABEL_TO_ID_MAP["role1"],
                    TEST_ROLE_LABEL_TO_ID_MAP["role2"]
                }, any, [(TEST_RESOURCE_URN_TO_ID_MAP["urn:resource1"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission1"]),
                         (TEST_RESOURCE_URN_TO_ID_MAP["urn:resource3"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission3"])],
                nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {
                    TEST_ROLE_LABEL_TO_ID_MAP["role1"],
                    TEST_ROLE_LABEL_TO_ID_MAP["role2"]
                }, all, [(TEST_RESOURCE_URN_TO_ID_MAP["urn:resource3"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission3"]),
                         (TEST_RESOURCE_URN_TO_ID_MAP["urn:resource4"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission4"])],
                nocache))

            self.assertFalse(await utils.roles_have_permissions(
                self.dummy_request, {
                    TEST_ROLE_LABEL_TO_ID_MAP["role1"],
                    TEST_ROLE_LABEL_TO_ID_MAP["role2"]
                }, all, [(TEST_RESOURCE_URN_TO_ID_MAP["urn:resource1"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission1"]),
                         (TEST_RESOURCE_URN_TO_ID_MAP["urn:resource3"],
                          TEST_PERMISSION_NAME_TO_ID_MAP["permission3"])],
                nocache))

        # Check that the TECH_ADMIN role grants permission to everything
        # for 'all' and 'any' operator.
        for operator in [all, any]:
            self.assertTrue(await utils.roles_have_permissions(
                self.dummy_request,
                {Mappings.role_id_for(TECH_ADMIN_ROLE_LABEL)}, operator,
                [(f"urn:resource{i}", f"permission{i}")
                 for i in range(1, 11)], nocache))
    async def test_context_header(self, mocked_site_function,
                                  mocked_domain_function):
        """
        """
        @require_permissions(all, [("urn:resource1", "permission1")],
                             nocache=True)
        async def somecall(_request):
            return True

        mocked_site_function.side_effect = make_coroutine_returning(
            set())  # No roles
        mocked_domain_function.side_effect = make_coroutine_returning(
            set())  # No roles

        # Invalid value
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "foo"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID
        }

        with self.assertRaises(JSONBadRequest):
            await somecall(mocked_request)

        # Invalid value
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "foo:bar"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID
        }
        with self.assertRaises(JSONBadRequest):
            await somecall(mocked_request)

        # Invalid value
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "foo:123"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID
        }
        with self.assertRaises(JSONBadRequest):
            await somecall(mocked_request)

        # Valid header, but Management Portal is not the caller
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "d:123"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID + "foo"
        }
        with self.assertRaises(JSONForbidden):
            await somecall(mocked_request)

        # Valid site but not roles
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "s:123"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID
        }
        with self.assertRaises(JSONForbidden):
            await somecall(mocked_request)

        # Valid domain, but no roles
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "d:123"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID
        }
        with self.assertRaises(JSONForbidden):
            await somecall(mocked_request)

        # Roles will be returned
        mocked_site_function.side_effect = make_coroutine_returning(
            {Mappings.role_id_for(TECH_ADMIN_ROLE_LABEL)})
        mocked_domain_function.side_effect = make_coroutine_returning(
            {Mappings.role_id_for(TECH_ADMIN_ROLE_LABEL)})

        # Valid site
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "s:123"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID
        }
        self.assertTrue(await somecall(mocked_request))

        # Valid domain, but no roles
        mocked_request = make_mocked_request(
            "GET", "/", headers={PORTAL_CONTEXT_HEADER: "d:123"})
        mocked_request["token"] = {
            "sub": str(self.user),
            "aud": MANAGEMENT_PORTAL_CLIENT_ID
        }
        self.assertTrue(await somecall(mocked_request))