Exemple #1
0
async def get_tokens_data(access_token_response: dict) -> dict:
    """Get tokens data.

    :param access_token_response: Access token response
    :return: Tokens data
    :raises HTTPResponseException: If found an error in the access token response.
    """
    if "error" in access_token_response:
        raise HTTPResponseException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail={
                "error_code":
                access_token_response.get("error"),
                "error_description":
                re.split(
                    ": ",
                    re.split("\r\n",
                             access_token_response.get("error_description"),
                             1)[0], 1)[1]
            })

    return {
        "data": {
            "token_type": access_token_response.get("token_type"),
            "access_token": access_token_response.get("access_token"),
            "access_token_expiration": access_token_response.get("expires_in"),
            "scope": access_token_response.get("scope"),
            "refresh_token": access_token_response.get("refresh_token"),
        }
    }
Exemple #2
0
async def call_microsoft_graph_web_service(
        method: str,
        path: str,
        parameters: Optional[dict] = None,
        headers: Optional[dict] = None) -> dict:
    """Call a Microsoft Graph web service.

    :param method: HTTP method
    :param path: Web service path
    :param parameters: Parameters
    :param headers: Headers
    :return: Microsoft Graph web service response
    :raises HTTPResponseException: If there were some errors during the process.
    """
    try:
        async with AsyncClient(
                base_url="https://graph.microsoft.com/v1.0",
                headers={
                    "Authorization": await
                    __get_microsoft_graph_authorization_header()
                }) as client:
            response: Response = await client.request(method=method,
                                                      url=path,
                                                      params=parameters,
                                                      headers=headers)

            if response.status_code == status.HTTP_200_OK:
                return response.json()
            elif response.status_code in [
                    status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN
            ]:
                raise HTTPResponseException(status_code=response.status_code)
            else:
                response.raise_for_status()
    except ConnectTimeout as connection_timeout:
        logging.error(
            f"Timed out while requesting {connection_timeout.request.url}.")
        raise HTTPResponseException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
    except HTTPError as connection_error:
        logging.error(
            f"Found an error when requesting {connection_error.request.url}. {connection_error.__str__()}"
        )
        raise HTTPResponseException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
Exemple #3
0
    async def _handle_database_server_error(cls, database_server_error: ClassVar[Exception]) -> None:
        """Log and raise a database server error.

        :param database_server_error: Database server error
        :raises HTTPResponseException: If there were some errors during the database operation.
        """
        logging.error(database_server_error.__str__())

        raise HTTPResponseException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
    async def __decode_access_token(cls, access_token: str) -> dict:
        """Decode an access token.

        :param access_token: Access token
        :return: Access token claims
        :raises HTTPResponseException: If the specified access token was invalid.
        """
        try:
            header: dict = jwt.get_unverified_header(access_token)

            return jwt.decode(jwt=access_token,
                              key=await
                              cls.__get_public_key(header.get("kid")),
                              algorithms=[header.get("alg")],
                              audience=os.getenv("AZURE_AD_AUDIENCE"),
                              issuer=cls.__issuer,
                              options={
                                  "require_exp": True,
                                  "require_iat": True,
                                  "require_nbf": True,
                                  "verify_exp": True,
                                  "verify_iat": True,
                                  "verify_nbf": True,
                                  "verify_aud": True,
                                  "verify_iss": True,
                                  "verify_signature": True,
                              })
        except ExpiredSignatureError:
            raise HTTPResponseException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail=cls.__expired_token)
        except (MissingRequiredClaimError, ImmatureSignatureError,
                InvalidIssuedAtError, InvalidAudienceError, InvalidIssuerError,
                InvalidSignatureError, DecodeError):
            raise HTTPResponseException(
                status_code=status.HTTP_401_UNAUTHORIZED)
        except ValueError as value_error:
            logging.error(value_error.__str__())
            raise HTTPResponseException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
    async def get_user_identifier(cls,
                                  access_token: str,
                                  accepted_roles: Set[UserRole] = None) -> str:
        """Get the user identifier from the specified access token.

        :param access_token: Access token
        :param accepted_roles: Accepted user roles
        :return: User identifier
        :raises HTTPResponseException: If the specified access token was invalid.
        """
        decoded_access_token: dict = await cls.__decode_access_token(
            access_token=access_token)

        if "access_as_user" not in await cls.__get_scopes(decoded_access_token
                                                          ):
            raise HTTPResponseException(status_code=status.HTTP_403_FORBIDDEN)

        if accepted_roles is not None \
                and set(map(lambda x: x.value, accepted_roles)).isdisjoint(await cls.__get_roles(decoded_access_token)):
            raise HTTPResponseException(status_code=status.HTTP_403_FORBIDDEN)

        return decoded_access_token.get("oid")
Exemple #6
0
    def test_exception_response(self) -> None:
        """Test an exception response.
        """
        status_code: int = 902
        result: HTTPResponseException = HTTPResponseException(
            status_code=status_code)

        assert result.status_code == status_code
        assert result.detail == {
            "error_code": "item_not_found",
            "error_description": "The specified item was not found."
        }
        assert result.headers is None
Exemple #7
0
    def test_exception_response_with_status_code_401(self) -> None:
        """Test an exception response with status code 401.
        """
        status_code: int = 401
        result: HTTPResponseException = HTTPResponseException(
            status_code=status_code)

        assert result.status_code == status_code
        assert result.detail == {
            "error_code": "invalid_token",
            "error_description": "The access token was invalid."
        }
        assert result.headers == {"WWW-Authenticate": "Bearer"}
Exemple #8
0
async def __prove_owner(authorization: HTTPAuthorizationCredentials,
                        post_id: str) -> None:
    """Prove the owner of the post that is specified by post ID.

    :param authorization: Authorization header
    :param post_id: Post ID
    :raises HTTPResponseException: If the signed-in user was not the post's owner.
    """
    owner: str = await JsonWebToken.get_user_identifier(
        access_token=authorization.credentials)
    post: dict = await Mongo.get(COLLECTION, post_id, PostPreRelationships)

    if post.get("data").get("owner") != owner:
        raise HTTPResponseException(status_code=status.HTTP_403_FORBIDDEN)
Exemple #9
0
    async def delete(cls, collection: AsyncIOMotorCollection, identifier: Any) -> None:
        """Delete a document.

        :param collection: Collection reference
        :param identifier: Identifier
        :raises HTTPResponseException: If there were some errors during the database operation.
        """
        try:
            result: DeleteResult = await collection.delete_one(await cls._get_primary_key_pair(collection, identifier))

            if result.deleted_count == 0:
                raise HTTPResponseException(status_code=status.HTTP_404_NOT_FOUND)
        except PyMongoError as database_server_error:
            await cls._handle_database_server_error(database_server_error)
    async def validate_application_access_token(cls,
                                                access_token: str) -> None:
        """Validate an application access token.

        :param access_token: Access token
        :raises HTTPResponseException: If the specified access token was invalid.
        """
        decoded_access_token: dict = await cls.__decode_access_token(
            access_token=access_token)

        if decoded_access_token.get("scp") is not None:
            return

        if "access_as_application" not in await cls.__get_roles(
                decoded_access_token):
            raise HTTPResponseException(status_code=status.HTTP_403_FORBIDDEN)
Exemple #11
0
    def test_exception_response_with_custom_detail_value(self) -> None:
        """Test an exception response with a custom detail value.
        """
        status_code: int = 902
        detail: dict = {
            "error_code":
            "invalid_grant",
            "error_description":
            "The code_verifier does not match the code_challenge."
        }
        result: HTTPResponseException = HTTPResponseException(
            status_code=status_code, detail=detail)

        assert result.status_code == status_code
        assert result.detail == detail
        assert result.headers is None
Exemple #12
0
    def test_exception_response_with_invalid_custom_detail_value(self) -> None:
        """Test an exception response with an invalid custom detail value.
        """
        status_code: int = 902
        detail: dict = {
            "error":
            "invalid_grant",
            "error_description":
            "The code_verifier does not match the code_challenge."
        }

        with pytest.raises(
                ValueError,
                match=
                "The detail's keys must be error_code and error_description."):
            HTTPResponseException(status_code=status_code, detail=detail)
Exemple #13
0
    def test_exception_response_with_headers(self) -> None:
        """Test an exception response with headers.
        """
        status_code: int = 902
        headers: dict = {
            "custom-code": "item_not_found",
            "custom-description": "The specified item was not found.",
        }
        result: HTTPResponseException = HTTPResponseException(
            status_code=status_code, headers=headers)

        assert result.status_code == status_code
        assert result.detail == {
            "error_code": "item_not_found",
            "error_description": "The specified item was not found."
        }
        assert result.headers == headers
Exemple #14
0
    async def __call__(self, request: Request) -> HTTPAuthorizationCredentials:
        """Get a Bearer token.

        :param request: HTTP request
        :return: Bearer token
        :raises HTTPException: If the request's Authorization header was invalid.
        """
        try:
            return await super().__call__(request)
        except HTTPException:
            raise HTTPResponseException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail={
                    "error_code":
                    "authorization_header_not_found",
                    "error_description":
                    "An Authorization header must be included in the request."
                })
Exemple #15
0
    def test_exception_response_with_status_code_401_and_headers(self) -> None:
        """Test an exception response with status code 401 and headers.
        """
        status_code: int = 401
        headers: dict = {
            "custom-code": "item_not_found",
            "custom-description": "The specified item was not found.",
        }
        result: HTTPResponseException = HTTPResponseException(
            status_code=status_code, headers=headers)

        assert result.status_code == status_code
        assert result.detail == {
            "error_code": "invalid_token",
            "error_description": "The access token was invalid."
        }
        assert result.headers == {
            "custom-code": "item_not_found",
            "custom-description": "The specified item was not found.",
            "WWW-Authenticate": "Bearer"
        }
Exemple #16
0
    async def get(cls, collection: AsyncIOMotorCollection, identifier: Any, projection_model: Type[BaseModel]) -> Data:
        """Get a document by identifier.

        :param collection: Collection reference
        :param identifier: Identifier
        :param projection_model: Projection model
        :return: Document
        :raises HTTPResponseException: If there were some errors during the database operation
         or the specified document was not found.
        """
        try:
            document: Optional[dict] = await collection.find_one(
                await cls._get_primary_key_pair(collection, identifier),
                projection=await cls.__get_projection(projection_model)
                )

            if document is None:
                raise HTTPResponseException(status_code=status.HTTP_404_NOT_FOUND)

            return {"data": document}
        except PyMongoError as database_server_error:
            await cls._handle_database_server_error(database_server_error)
Exemple #17
0
async def __get_microsoft_graph_authorization_header() -> str:
    """Get a Microsoft graph authorization header.

    :return: Microsoft graph authorization header
    :raises HTTPResponseException: If could not get an access token for calling a Microsoft Graph web service.
    """
    azure_client: ConfidentialClientApplication = ConfidentialClientApplication(
        client_id=os.getenv("AZURE_AD_AUDIENCE"),
        client_credential=await
        get_file_environment("AZURE_AD_AUDIENCE_SECRET_FILE"),
        authority=os.getenv("AZURE_AD_AUTHORITY"))
    access_token_response: dict = azure_client.acquire_token_for_client(
        scopes=["https://graph.microsoft.com/.default"])

    try:
        tokens: dict = (await
                        get_tokens_data(access_token_response)).get("data")
        return tokens.get("token_type") + " " + tokens.get("access_token")
    except HTTPResponseException as error:
        logging.error(
            f"Could not get an access token for calling a Microsoft Graph web service.\n{error.detail}"
        )
        raise HTTPResponseException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
Exemple #18
0
    def test_exception_response_with_undefined_status_code(self) -> None:
        """Test an exception response with an undefined status code.

        """
        with pytest.raises(ValueError):
            HTTPResponseException(status_code=100)