Exemple #1
0
def upload(request: HttpRequest) -> JsonResponse:
    logging.log_request_received(
        logger,
        request.META['PATH_INFO'] if 'PATH_INFO' in request.META.keys() else '-path to file UNAVAILABLE-',
        FileTransferToken.Operation.upload
    )

    if request.content_type in ['multipart/form-data', '', None] or request.content_type.isspace():
        return gatekeeper_access_denied_response(
            'Unsupported content type.',
            FileTransferToken.Operation.upload,
            ErrorCode.HEADER_CONTENT_TYPE_NOT_SUPPORTED,
            request.META['PATH_INFO'] if 'PATH_INFO' in request.META.keys() else 'UNAVAILABLE'
        )
    path_to_file = request.get_full_path().partition(reverse('gatekeeper:upload'))[2]

    response_or_file_info = parse_headers(request, path_to_file, FileTransferToken.Operation.upload)

    if not isinstance(response_or_file_info, FileTransferToken.FileInfo):
        assert isinstance(response_or_file_info, JsonResponse)
        return response_or_file_info

    response = JsonResponse({"message": "Request passed all upload validations."}, status = 200)
    response["Concent-File-Size"] = response_or_file_info["size"]
    response["Concent-File-Checksum"] = response_or_file_info["checksum"]

    return response
Exemple #2
0
    def wrapper(
        request: HttpRequest,
        *args: list,
        **kwargs: dict,
    ) -> JsonResponse:

        if not is_given_golem_messages_version_supported_by_concent(
                request=request):
            error_message = f"Unsupported protocol version. Client's version is {request.META['HTTP_X_GOLEM_MESSAGES']}, " \
                f"Concent's version is {settings.GOLEM_MESSAGES_VERSION}."
            return gatekeeper_access_denied_response(
                error_message,
                FileTransferToken.Operation.download,
                ErrorCode.HEADER_PROTOCOL_VERSION_UNSUPPORTED,
                request.META['PATH_INFO'] if 'PATH_INFO'
                in request.META.keys() else '-path to file UNAVAILABLE-',
            )
        return view(request, *args, *kwargs)
Exemple #3
0
    def test_gatekeeper_access_denied_response_should_return_appropriate_body_and_headers(
            self):
        response = gatekeeper_access_denied_response(
            self.message, FileTransferToken.Operation.upload,
            ErrorCode.HEADER_AUTHORIZATION_MISSING, self.path, None,
            self.client_key)
        response_body = json.loads(response.content.decode())

        self.assertIsInstance(response, JsonResponse)
        self.assertEqual(response.status_code, 401)
        self.assertTrue(response.has_header("WWW-Authenticate"))
        self.assertEqual("application/json", response["Content-Type"])
        self.assertEqual(response_body["subtask_id"], None)
        self.assertEqual(response_body["message"], self.message)
        self.assertEqual(response_body["path_to_file"], self.path)
        self.assertEqual(response_body["client_key"], self.client_key)
        self.assertEqual(response_body["error_code"],
                         ErrorCode.HEADER_AUTHORIZATION_MISSING.value)
Exemple #4
0
    def wrapper(
        request: HttpRequest,
        *args: list,
        **kwargs: dict,
    ) -> JsonResponse:

        if not is_given_golem_messages_version_supported_by_concent(
                request=request):
            log(
                logger,
                f'Wrong version of golem messages. Clients version is {request.META["HTTP_X_Golem_Messages"]}, '
                f'Concent version is {settings.GOLEM_MESSAGES_VERSION}.',
            )
            return gatekeeper_access_denied_response(
                "Protocol version in request does not match protocol version in Concent",
                FileTransferToken.Operation.download,
                ErrorCode.HEADER_PROTOCOL_VERSION_UNSUPPORTED,
                request.META['PATH_INFO'] if 'PATH_INFO'
                in request.META.keys() else '-path to file UNAVAILABLE-',
            )
        return view(request, *args, *kwargs)
Exemple #5
0
def download(request):
    logging.log_request_received(
        logger,
        request.META['PATH_INFO'] if 'PATH_INFO' in request.META.keys() else '-path to file UNAVAILABLE-',
        FileTransferToken.Operation.download
    )
    # The client should not sent Content-Type header with GET requests.
    # FIXME: When running on `manage.py runserver` in development, empty or missing Concent-Type gets replaced
    # with text/plain. gunicorn does not do this. Looks like a bug to me. We'll let it pass for now sice we ignore
    # the body anyway and the check is mostly to inform the client about its mistake.
    if request.content_type != 'text/plain' and request.content_type != '':
        return gatekeeper_access_denied_response(
            'Download request cannot have data in the body.',
            FileTransferToken.Operation.download,
            ErrorCode.REQUEST_BODY_NOT_EMPTY,
        )

    path_to_file = request.get_full_path().partition(reverse('gatekeeper:download'))[2]
    response_or_file_info = parse_headers(request, path_to_file, FileTransferToken.Operation.download)
    if not isinstance(response_or_file_info, FileTransferToken.FileInfo):
        assert isinstance(response_or_file_info, JsonResponse)
        return response_or_file_info
    return JsonResponse({"message": "Request passed all download validations."}, status = 200)
Exemple #6
0
def parse_headers(
    request: WSGIRequest,
    path_to_file: str,
    operation: FileTransferToken.Operation,
) -> Union[FileTransferToken.FileInfo, JsonResponse]:
    # Decode and check if request header contains a golem message:
    if 'HTTP_AUTHORIZATION' not in request.META:
        return gatekeeper_access_denied_response(
            "Missing 'Authorization' header.",
            operation,
            ErrorCode.HEADER_AUTHORIZATION_MISSING,
            path_to_file,
        )

    authorization_scheme_and_token = request.META['HTTP_AUTHORIZATION'].split(" ", 1)
    assert len(authorization_scheme_and_token) in [1, 2]

    if len(authorization_scheme_and_token) == 1:
        return gatekeeper_access_denied_response(
            "Missing token in the 'Authorization' header.",
            operation,
            ErrorCode.HEADER_AUTHORIZATION_MISSING_TOKEN,
            path_to_file,
        )

    (scheme, token) = authorization_scheme_and_token

    if scheme != 'Golem':
        return gatekeeper_access_denied_response(
            "Unrecognized scheme in the 'Authorization' header.",
            operation,
            ErrorCode.HEADER_AUTHORIZATION_UNRECOGNIZED_SCHEME,
            path_to_file,
        )

    try:
        decoded_auth_header_content = b64decode(token, validate = True)
    except binascii.Error:
        return gatekeeper_access_denied_response(
            "Unable to decode token in the 'Authorization' header.",
            operation,
            ErrorCode.HEADER_AUTHORIZATION_NOT_BASE64_ENCODED_VALUE,
            path_to_file,
        )

    try:
        loaded_golem_message = load(
            decoded_auth_header_content,
            settings.CONCENT_PRIVATE_KEY,
            settings.CONCENT_PUBLIC_KEY,
            check_time = False
        )
    except MessageError:
        return gatekeeper_access_denied_response(
            "Token in the 'Authorization' header is not a valid Golem message.",
            operation,
            ErrorCode.HEADER_AUTHORIZATION_TOKEN_INVALID_MESSAGE,
            path_to_file,
        )

    assert isinstance(loaded_golem_message, Message)

    # Check if request header contains Concent-Auth:
    if 'HTTP_CONCENT_AUTH' not in request.META:
        return gatekeeper_access_denied_response(
            'Missing Concent-Auth header.',
            operation,
            ErrorCode.AUTH_CLIENT_AUTH_MESSAGE_MISSING,
            path_to_file,
            loaded_golem_message.subtask_id
        )

    # Try to load in ClientAuthorization message from Concent-Auth header
    try:
        client_authorization = load(
            b64decode(request.META['HTTP_CONCENT_AUTH'], validate=True),
            settings.CONCENT_PRIVATE_KEY,
            loaded_golem_message.authorized_client_public_key,
        )
        concent_client_public_key = b64encode(client_authorization.client_public_key).decode('ascii')
    except (MessageError, binascii.Error):
        return gatekeeper_access_denied_response(
            'Cannot load ClientAuthorization message from Concent-Auth header.',
            operation,
            ErrorCode.AUTH_CLIENT_AUTH_MESSAGE_INVALID,
            path_to_file,
            loaded_golem_message.subtask_id
        )
    logging.log(
        logger,
        f"{loaded_golem_message.operation.capitalize()} request will be validated. "
        f"Message type: '{loaded_golem_message.__class__.__name__}'. File: '{path_to_file}'",
        subtask_id=loaded_golem_message.subtask_id,
        client_public_key=concent_client_public_key,
    )
    current_time = get_current_utc_timestamp()

    if current_time > loaded_golem_message.token_expiration_deadline:
        return gatekeeper_access_denied_response(
            'token_expiration_deadline has passed.',
            operation,
            ErrorCode.MESSAGE_TOKEN_EXPIRATION_DEADLINE_PASSED,
            path_to_file,
            loaded_golem_message.subtask_id,
            concent_client_public_key
        )

    try:
        validate_file_transfer_token(loaded_golem_message)
    except FileTransferTokenError as exception:
        return gatekeeper_access_denied_response(
            exception.error_message,
            operation,
            exception.error_code,
            path_to_file,
            loaded_golem_message.subtask_id,
            concent_client_public_key
        )

    authorized_client_public_key_base64 = (b64encode(loaded_golem_message.authorized_client_public_key)).decode('ascii')
    if concent_client_public_key != authorized_client_public_key_base64:
        return gatekeeper_access_denied_response(
            'You are not authorized to use this token.',
            operation,
            ErrorCode.MESSAGE_AUTHORIZED_CLIENT_PUBLIC_KEY_UNAUTHORIZED_CLIENT,
            path_to_file,
            loaded_golem_message.subtask_id,
            concent_client_public_key
        )

    # -OPERATION
    if request.method == 'POST' and loaded_golem_message.operation != FileTransferToken.Operation.upload:
        return gatekeeper_access_denied_response(
            'Upload requests must use POST method.',
            operation,
            ErrorCode.MESSAGE_OPERATION_INVALID,
            path_to_file,
            loaded_golem_message.subtask_id,
            concent_client_public_key
        )
    if request.method in ['GET', 'HEAD'] and loaded_golem_message.operation != FileTransferToken.Operation.download:
        return gatekeeper_access_denied_response(
            'Download requests must use GET or HEAD method.',
            operation,
            ErrorCode.MESSAGE_OPERATION_INVALID,
            path_to_file,
            loaded_golem_message.subtask_id,
            concent_client_public_key
        )

    matching_files = [file for file in loaded_golem_message.files if path_to_file == file['path']]

    if len(matching_files) == 1:
        logging.log(
            logger,
            f"{loaded_golem_message.operation.capitalize()} request passed all validations. "
            f"Message type: '{loaded_golem_message.__class__.__name__}'. File: '{path_to_file}'",
            subtask_id=loaded_golem_message.subtask_id,
            client_public_key=concent_client_public_key
        )
        return matching_files[0]
    else:
        assert len(matching_files) == 0
        return gatekeeper_access_denied_response(
            'Your token does not authorize you to transfer the requested file.',
            operation,
            ErrorCode.MESSAGE_FILES_PATH_NOT_LISTED_IN_FILES,
            path_to_file,
            loaded_golem_message.subtask_id,
            concent_client_public_key
        )