def validate_claim( token_info: Claim, dataset_id: DatasetId, permissions: List[DatasetPermission], organization_id: Optional[OrganizationId] = None, ) -> AuthContext: # API v1 does not specify an organization. Additionally, for v1 API # compatability, we assume the organization of context is the head # organization in the given JWT claim: if organization_id is None: if token_info.head_organization_node_id is None: raise OAuthProblem("Missing organization node id") organization_int_id: OrganizationId = OrganizationId( token_info.head_organization_id.id ) else: organization_int_id = OrganizationId(organization_id) dataset_int_id = dataset_id if token_info.is_user_claim: if token_info.content.node_id is None: raise OAuthProblem("Missing user node ID") user_node_id = UserNodeId(token_info.content.node_id) else: user_node_id = SERVICE_USER_NODE_ID auth_organization_id = RoleOrganizationId(organization_int_id) auth_dataset_id = RoleDatasetId(dataset_int_id) if not token_info.has_organization_access(auth_organization_id): raise Forbidden for permission in permissions: if not token_info.has_dataset_access(auth_dataset_id, permission): raise Forbidden # (invariant): # These roles should never be None and are assumed to be valid given the # checks above. organization_role = token_info.get_role(auth_organization_id) dataset_role = token_info.get_role(auth_dataset_id) def get_locked(): for role in token_info.content.roles: # we do not pass through the locked field for a wildcard role, # since by definition the wildcard means we don't know individual datasets are locked or not if role.id == auth_dataset_id and isinstance(role, DatasetRole): return role.locked return None return AuthContext( organization_id=organization_int_id, dataset_id=dataset_id, user_node_id=user_node_id, organization_node_id=organization_role.node_id, dataset_node_id=dataset_role.node_id, locked=get_locked(), )
def test_permission_requires_an_organization_role_specifier( app_context, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ DatasetRole(id=dataset_id, node_id=dataset_node_id, role=RoleType.OWNER) ], ), TOKEN_EXPIRATION_S, ) with pytest.raises(Forbidden): sample_update_route( dataset_id=str(dataset_id.id), token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )
def test_permission_required_to_a_access_specific_dataset( app_context, valid_organization, valid_dataset, other_valid_dataset): organization_id, organization_node_id = valid_organization dataset_id_1, _ = valid_dataset dataset_id_2, dataset_node_id_2 = other_valid_dataset claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole(id=dataset_id_2, node_id=dataset_node_id_2, role=RoleType.OWNER), ], ), TOKEN_EXPIRATION_S, ) with pytest.raises(Forbidden): sample_update_route( dataset_id=str(dataset_id_1.id), token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )
def test_authorization_resolves_dataset_id_from_api_with_wildcard_claim( app_context, api_client, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset api_client.get_dataset_response = api.Dataset(id=dataset_node_id, int_id=dataset_id.id, name="foo") claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole(id=DatasetId("*"), role=RoleType.EDITOR), ], ), TOKEN_EXPIRATION_S, ) sample_view_route( dataset_id=dataset_node_id, token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )(organization_id.id, dataset_id.id)
def test_authorization_rejects_nonexistent_dataset_integer_id( valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole(id=dataset_id, node_id=dataset_node_id, role=RoleType.OWNER), ], ), TOKEN_EXPIRATION_S, ) with pytest.raises(Forbidden): sample_update_route( dataset_id=9999, token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )(organization_id.id, dataset_id.id)
def test_permission_required_raises_forbidden_when_dataset_role_is_too_low( app_context, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole(id=dataset_id, node_id=dataset_node_id, role=RoleType.VIEWER), ], ), TOKEN_EXPIRATION_S, ) # sample_route requires EDITOR permissions, which are higher than VIEWER: with pytest.raises(Forbidden): sample_update_route( dataset_id=str(dataset_id.id), token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )
def test_authorization_allows_updates_with_locked_false_claim( app_context, api_client, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset api_client.get_dataset_response = api.Dataset(id=dataset_node_id, int_id=dataset_id.id, name="foo") claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole(id=dataset_id, role=RoleType.EDITOR, locked=False), ], ), TOKEN_EXPIRATION_S, ) sample_update_route( dataset_id=str(dataset_id.id), token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )
def test_authorization_requires_integer_organization_id( app_context, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole(id=dataset_id, node_id=dataset_node_id, role=RoleType.OWNER), ], ), TOKEN_EXPIRATION_S, ) with pytest.raises(OAuthProblem): sample_update_route( dataset_id=str(dataset_id.id), token_info=claim, organization_id=str(organization_node_id), body={"k": 1}, )(organization_id.id, dataset_id.id)
def authorized_service_token( config, valid_organization, valid_dataset, other_valid_dataset ): organization_id, organization_node_id = valid_organization dataset_id_1, dataset_node_id_1 = valid_dataset dataset_id_2, dataset_node_id_2 = other_valid_dataset data = ServiceClaim( roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER ), DatasetRole( id=dataset_id_1, node_id=dataset_node_id_1, role=RoleType.OWNER, locked=False, ), DatasetRole( id=dataset_id_2, node_id=dataset_node_id_2, role=RoleType.OWNER, locked=False, ), ] ) claim = Claim.from_claim_type(data, seconds=JWT_EXPIRATION_SECS) return to_utf8(claim.encode(config.jwt_config))
def test_permission_required_decorator(app_context, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole( id=dataset_id, node_id=dataset_node_id, role=RoleType.OWNER, locked=False, ), ], ), TOKEN_EXPIRATION_S, ) sample_update_route( dataset_id=str(dataset_id.id), token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )(organization_id.id, dataset_id.id)
def service_claim(organization_id, dataset_id, jwt_config: JwtConfig) -> str: data = ServiceClaim(roles=[ OrganizationRole(id=OrganizationId(organization_id), role=RoleType.OWNER), DatasetRole(id=DatasetId(dataset_id), role=RoleType.OWNER), ]) claim = Claim.from_claim_type(data, seconds=30) return to_utf8(claim.encode(jwt_config))
def unauthorized_user_token(jwt_config, invalid_organization, valid_dataset): organization_id, organization_node_id = invalid_organization dataset_id, dataset_node_id = valid_dataset data = UserClaim( id=12345, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER ), DatasetRole(id=dataset_id, node_id=dataset_node_id, role=RoleType.OWNER), ], ) claim = Claim.from_claim_type(data, seconds=JWT_EXPIRATION_SECS) return to_utf8(claim.encode(jwt_config))
def expired_user_token(jwt_config, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset data = UserClaim( id=12345, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER ), DatasetRole(id=dataset_id, node_id=dataset_node_id, role=RoleType.OWNER), ], ) claim = Claim.from_claim_type(data, -1) return to_utf8(claim.encode(jwt_config))
def organization_token(config, valid_organization, other_valid_dataset, valid_user): organization_id, organization_node_id = valid_organization user_id, user_node_id = valid_user data = UserClaim( id=user_id, node_id=user_node_id, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER ) ], ) claim = Claim.from_claim_type(data, seconds=JWT_EXPIRATION_SECS) return to_utf8(claim.encode(config.jwt_config))
def authorize_search(organization_id: int, trace_id: TraceId, token_info: Claim): if not token_info.is_user_claim: raise OAuthProblem("Requires a user claim") if not token_info.has_organization_access( RoleOrganizationId(organization_id)): raise Forbidden user_id = UserId(token_info.content.node_id) datasets = PennsieveApiClient.get().get_datasets( headers=dict(**auth_header(), **with_trace_id_header(trace_id))) return SearchDatabase( db=current_app.config["db"], organization_id=organization_id, user_id=user_id, datasets=datasets, )
def test_authorization_rejects_nonexistent_dataset_node_ids_with_wildcard_claim( app_context, api_client, valid_organization, valid_dataset): organization_id, organization_node_id = valid_organization dataset_id, dataset_node_id = valid_dataset api_client.raise_exception( ExternalRequestError( status_code=404, method="GET", url="/datasets/N:dataset:does-not-exist", content="Dataset does not exist", )) claim = Claim.from_claim_type( UserClaim( id=DEFAULT_USER_ID, node_id=DEFAULT_USER_NODE_ID, roles=[ OrganizationRole( id=organization_id, node_id=organization_node_id, role=RoleType.OWNER, ), DatasetRole(id="*", role=RoleType.EDITOR), ], ), TOKEN_EXPIRATION_S, ) with pytest.raises(OAuthProblem, match="Dataset does not exist"): sample_update_route( dataset_id="N:dataset:does-not-exist", token_info=claim, organization_id=str(organization_id.id), body={"k": 1}, )(organization_id.id, dataset_id.id)
def decode_token(token: str) -> Claim: """ Decode a JWT into a claim. Raises ------ - connexion.exceptions.OAuthProblem - jwt.exceptions.ExpiredSignatureError """ try: claim = Claim.from_token(token, Config.from_app().jwt_config) except DecodeError: raise connexion.exceptions.OAuthProblem(description="Invalid token") # This is a hack to work around Connexion internals. The security wrapper # expects the claim to be a dictionary, but we have already built the # `Claim` object. The wrapper is trying to extract a user, which we don't # need so stub out `get` to always return `None` # # Ref: https://github.com/zalando/connexion/blob/08e4536e5e6c284aaabcfb6fa159c738dbae7758/connexion/decorators/security.py#L297 claim.get = lambda x, y=None: None return claim