Beispiel #1
0
 def _save_user(req: Request, db_user: ScimApiUser) -> None:
     try:
         ctx_userdb(req).save(db_user)
     except DuplicateKeyError as e:
         if 'external-id' in e.details['errmsg']:
             raise BadRequest(detail='externalID must be unique')
         raise BadRequest(detail='Duplicated key error')
Beispiel #2
0
    def on_post(self, req: Request, resp: Response):
        """
        POST /Groups/.search
        Host: scim.eduid.se
        Accept: application/scim+json

        {
            "schemas": ["urn:ietf:params:scim:api:messages:2.0:SearchRequest"],
            "filter": "displayName eq \"some display name\"",
        }

        HTTP/1.1 200 OK
        Content-Type: application/scim+json
        Location: https://example.com/Users/.search

        {
            "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
            "totalResults": 1,
            "Resources": [
                {
                    "displayName": "test group",
                    "id": "46aee99f-f417-41fc-97f0-1ee8970078db"
                },
            ]
        }
        """
        self.context.logger.info('Searching for group(s)')
        try:
            query: SearchRequest = SearchRequestSchema().load(req.media)
        except ValidationError as e:
            raise BadRequest(detail=f"{e}")

        filter = parse_search_filter(query.filter)

        if filter.attr == 'displayname':
            groups, total_count = self._filter_display_name(
                req, filter, skip=query.start_index - 1, limit=query.count)
        elif filter.attr == 'meta.lastmodified':
            groups, total_count = self._filter_lastmodified(
                req, filter, skip=query.start_index - 1, limit=query.count)
        elif filter.attr.startswith('extensions.data.'):
            groups, total_count = self._filter_extensions_data(
                req, filter, skip=query.start_index - 1, limit=query.count)
        else:
            raise BadRequest(
                scim_type='invalidFilter',
                detail=f'Can\'t filter on attribute {filter.attr}')

        resources = []
        for this in groups:
            resources.append({
                'id': str(this.scim_id),
                'displayName': this.display_name
            })
        list_response = ListResponse(total_results=total_count,
                                     resources=resources)
        resp.media = ListResponseSchema().dump(list_response)
Beispiel #3
0
    def on_post(self, req: Request, resp: Response):
        """
           POST /Invites/.search
           Host: scim.eduid.se
           Accept: application/scim+json

           {
             "schemas": ["urn:ietf:params:scim:api:messages:2.0:SearchRequest"],
             "attributes": ["id"],
             "filter": "meta.lastModified ge \"2020-09-14T12:49:45\"",
             "encryptionKey": "h026jGKrSW%2BTTekkA8Y8mv8%2FGqkGgAfLzaj3ucD3STQ"
             "startIndex": 1,
             "count": 1
           }

           HTTP/1.1 200 OK
           Content-Type: application/scim+json
           Location: https://example.com/Invites/.search

           {
             "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
             "totalResults": 1,
             "itemsPerPage": 1,
             "startIndex": 1,
             "Resources": [
               {
                 "id": "fb96a6d0-1837-4c3b-9945-4249c476875c",
               }
             ]
           }
        """
        self.context.logger.info(f'Searching for users(s)')

        try:
            query: SearchRequest = SearchRequestSchema().load(req.media)
        except ValidationError as e:
            raise BadRequest(detail=f'{e}')

        self.context.logger.debug(f'Parsed user search query: {query}')

        filter = parse_search_filter(query.filter)

        if filter.attr == 'meta.lastmodified':
            # SCIM start_index 1 equals item 0
            users, total_count = self._filter_lastmodified(
                req, filter, skip=query.start_index - 1, limit=query.count)
        else:
            raise BadRequest(
                scim_type='invalidFilter',
                detail=f'Can\'t filter on attribute {filter.attr}')

        list_response = ListResponse(
            resources=self._invites_to_resources_dicts(req, users),
            total_results=total_count)

        resp.media = ListResponseSchema().dump(list_response)
Beispiel #4
0
def _get_scim_referenced(
        req: Request,
        resource: NutidEventResource) -> Optional[ScimApiResourceBase]:
    if resource.resource_type == SCIMResourceType.USER:
        return ctx_userdb(req).get_user_by_scim_id(str(resource.scim_id))
    elif resource.resource_type == SCIMResourceType.GROUP:
        return ctx_groupdb(req).get_group_by_scim_id(str(resource.scim_id))
    elif resource.resource_type == SCIMResourceType.INVITE:
        return ctx_invitedb(req).get_invite_by_scim_id(str(resource.scim_id))
    elif resource.resource_type == SCIMResourceType.EVENT:
        raise BadRequest(detail=f'Events can not refer to other events')
    raise BadRequest(
        detail=
        f'Events for resource {resource.resource_type.value} not implemented')
Beispiel #5
0
    def _filter_externalid(req: Request,
                           filter: SearchFilter) -> List[ScimApiUser]:
        if filter.op != 'eq':
            raise BadRequest(scim_type='invalidFilter',
                             detail='Unsupported operator')
        if not isinstance(filter.val, str):
            raise BadRequest(scim_type='invalidFilter',
                             detail='Invalid externalId')

        user = ctx_userdb(req).get_user_by_external_id(filter.val)

        if not user:
            return []

        return [user]
Beispiel #6
0
 def _filter_lastmodified(
         req: Request,
         filter: SearchFilter,
         skip: Optional[int] = None,
         limit: Optional[int] = None) -> Tuple[List[ScimApiInvite], int]:
     if filter.op not in ['gt', 'ge']:
         raise BadRequest(scim_type='invalidFilter',
                          detail='Unsupported operator')
     if not isinstance(filter.val, str):
         raise BadRequest(scim_type='invalidFilter',
                          detail='Invalid datetime')
     return ctx_invitedb(req).get_invites_by_last_modified(
         operator=filter.op,
         value=datetime.fromisoformat(filter.val),
         skip=skip,
         limit=limit)
Beispiel #7
0
    def on_delete(self, req: Request, resp: Response, scim_id: str):
        self.context.logger.info(f'Deleting group {scim_id}')
        db_group = ctx_groupdb(req).get_group_by_scim_id(scim_id=scim_id)
        self.context.logger.debug(f'Found group: {db_group}')
        if not db_group:
            raise NotFound(detail="Group not found")

        # Check version
        if not self._check_version(req, db_group):
            raise BadRequest(detail="Version mismatch")

        res = ctx_groupdb(req).remove_group(db_group)

        add_api_event(
            context=self.context,
            data_owner=req.context['data_owner'],
            db_obj=db_group,
            resource_type=SCIMResourceType.GROUP,
            level=EventLevel.INFO,
            status=EventStatus.DELETED,
            message='Group was deleted',
        )

        self.context.logger.debug(f'Remove group result: {res}')
        resp.status = HTTP_204
Beispiel #8
0
    def on_delete(self, req: Request, resp: Response, scim_id: str):
        self.context.logger.info(f'Deleting invite {scim_id}')
        db_invite = ctx_invitedb(req).get_invite_by_scim_id(scim_id=scim_id)
        self.context.logger.debug(f'Found invite: {db_invite}')

        if not db_invite:
            raise NotFound(detail="Invite not found")
        # Check version
        if not self._check_version(req, db_invite):
            raise BadRequest(detail="Version mismatch")

        # Remove signup invite
        ref = self._create_signup_ref(req, db_invite)
        signup_invite = self.context.signup_invitedb.get_invite_by_reference(
            ref)
        self.context.signup_invitedb.remove_document(signup_invite.invite_id)

        # Remove scim invite
        res = ctx_invitedb(req).remove(db_invite)

        add_api_event(
            context=self.context,
            data_owner=req.context['data_owner'],
            db_obj=db_invite,
            resource_type=SCIMResourceType.INVITE,
            level=EventLevel.INFO,
            status=EventStatus.DELETED,
            message='Group was deleted',
        )

        self.context.logger.debug(f'Remove invite result: {res}')
        resp.status = HTTP_204
Beispiel #9
0
    def on_post(self, req: Request, resp: Response):
        """
        POST /Groups  HTTP/1.1
        Host: example.com
        Accept: application/scim+json
        Content-Type: application/scim+json
        Authorization: Bearer h480djs93hd8
        Content-Length: ...

        {
            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
            "displayName": "Test SCIMv2",
            "members": []
        }


        HTTP/1.1 201 Created
        Date: Tue, 10 Sep 2019 04:54:18 GMT
        Content-Type: text/json;charset=UTF-8

        {
            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
            "id": "abf4dd94-a4c0-4f67-89c9-76b03340cb9b",
            "displayName": "Test SCIMv2",
            "members": [],
            "meta": {
                "resourceType": "Group"
            }
        }
        """
        self.context.logger.info('Creating group')
        try:
            create_request = GroupCreateRequestSchema().load(req.media)
        except ValidationError as e:
            raise BadRequest(detail=f"{e}")
        self.context.logger.debug(create_request)
        created_group = ctx_groupdb(req).create_group(
            create_request=create_request)
        # Load the group from the database to ensure results are consistent with subsequent GETs.
        # For example, timestamps have higher resolution in created_group than after a load.
        db_group = ctx_groupdb(req).get_group_by_scim_id(
            str(created_group.scim_id))
        assert db_group  # please mypy

        add_api_event(
            context=self.context,
            data_owner=req.context['data_owner'],
            db_obj=db_group,
            resource_type=SCIMResourceType.GROUP,
            level=EventLevel.INFO,
            status=EventStatus.CREATED,
            message='Group was created',
        )

        self._db_group_to_response(resp, db_group)
        resp.status = HTTP_201
Beispiel #10
0
 def on_get(self,
            req: Request,
            resp: Response,
            scim_id: Optional[str] = None):
     if scim_id is None:
         raise BadRequest(detail='Not implemented')
     self.context.logger.info(f'Fetching event {scim_id}')
     db_event = ctx_eventdb(req).get_event_by_scim_id(scim_id)
     if not db_event:
         raise NotFound(detail='Event not found')
     self._db_event_to_response(req, resp, db_event)
Beispiel #11
0
    def on_get(self,
               req: Request,
               resp: Response,
               scim_id: Optional[str] = None):
        if scim_id is None:
            raise BadRequest(detail='Not implemented')
        self.context.logger.info(f'Fetching user {scim_id}')
        db_user = ctx_userdb(req).get_user_by_scim_id(scim_id)
        if not db_user:
            raise NotFound(detail='User not found')

        self._db_user_to_response(req=req, resp=resp, db_user=db_user)
Beispiel #12
0
    def _filter_extensions_data(
        self,
        req: Request,
        filter: SearchFilter,
        skip: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> Tuple[List[ScimApiGroup], int]:
        if filter.op != 'eq':
            raise BadRequest(scim_type='invalidFilter',
                             detail='Unsupported operator')

        match = re.match(r'^extensions\.data\.([a-z_]+)$', filter.attr)
        if not match:
            raise BadRequest(scim_type='invalidFilter',
                             detail='Unsupported extension search key')

        self.context.logger.debug(
            f'Searching for groups with {filter.attr} {filter.op} {repr(filter.val)}'
        )
        groups, count = ctx_groupdb(req).get_groups_by_property(
            key=filter.attr, value=filter.val, skip=skip, limit=limit)
        return groups, count
Beispiel #13
0
def parse_search_filter(filter: str) -> SearchFilter:
    match = re.match('(.+?) (..) (.+)', filter)
    if not match:
        logger.debug(f'Unrecognised filter: {filter}')
        raise BadRequest(scim_type='invalidFilter', detail='Unrecognised filter')

    val: Union[str, int]
    attr, op, val = match.groups()

    if len(val) and val[0] == '"' and val[-1] == '"':
        val = val[1:-1]
        if not val.isprintable():
            logger.debug(f'Unrecognised string value in filter: {repr(val)}')
            raise BadRequest(scim_type='invalidFilter', detail='Unrecognised string value in filter')
    elif val.isdecimal():
        val = int(val)
    else:
        logger.debug(f'Unrecognised type of value (not string or integer) in filter: {val}')
        raise BadRequest(
            scim_type='invalidFilter', detail='Unrecognised type of value (not string or integer) in filter'
        )

    return SearchFilter(attr=attr.lower(), op=op.lower(), val=val)
Beispiel #14
0
    def _filter_display_name(
        self,
        req: Request,
        filter: SearchFilter,
        skip: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> Tuple[List[ScimApiGroup], int]:
        if filter.op != 'eq':
            raise BadRequest(scim_type='invalidFilter',
                             detail='Unsupported operator')
        if not isinstance(filter.val, str):
            raise BadRequest(scim_type='invalidFilter',
                             detail='Invalid displayName')

        self.context.logger.debug(
            f'Searching for group with display name {repr(filter.val)}')
        groups, count = ctx_groupdb(req).get_groups_by_property(
            key='display_name', value=filter.val, skip=skip, limit=limit)

        if not groups:
            return [], 0

        return groups, count
Beispiel #15
0
 def on_get(self,
            req: Request,
            resp: Response,
            scim_id: Optional[str] = None):
     if scim_id is None:
         raise BadRequest(detail='Not implemented')
     self.context.logger.info(f'Fetching invite {scim_id}')
     db_invite = ctx_invitedb(req).get_invite_by_scim_id(scim_id)
     if not db_invite:
         raise NotFound(detail='Invite not found')
     ref = self._create_signup_ref(req, db_invite)
     signup_invite = self.context.signup_invitedb.get_invite_by_reference(
         ref)
     self._db_invite_to_response(req, resp, db_invite, signup_invite)
Beispiel #16
0
    def on_post(self, req: Request, resp: Response):
        """
               POST /Invites  HTTP/1.1
               Host: example.com
               Accept: application/scim+json
               Content-Type: application/scim+json
               Authorization: Bearer h480djs93hd8
               Content-Length: ...

                {
                    'schemas': ['https://scim.eduid.se/schema/nutid/invite/v1',
                                'https://scim.eduid.se/schema/nutid/user/v1'],
                    'expiresAt': '2021-03-02T14:35:52',
                    'groups': [],
                    'phoneNumbers': [
                        {'type': 'fax', 'value': 'tel:+461234567', 'primary': True},
                        {'type': 'home', 'value': 'tel:+5-555-555-5555', 'primary': False},
                    ],
                    'meta': {
                        'location': 'http://*****:*****@example.com', 'primary': True},
                        {'type': 'home', 'value': '*****@*****.**', 'primary': False},
                    ],
                }
        """
        self.context.logger.info(f'Creating invite')
        try:
            create_request: InviteCreateRequest = InviteCreateRequestSchema(
            ).load(req.media)
            self.context.logger.debug(create_request)
        except ValidationError as e:
            raise BadRequest(detail=f"{e}")

        profiles = {}
        for profile_name, profile in create_request.nutid_user_v1.profiles.items(
        ):
            profiles[profile_name] = ScimApiProfile(
                attributes=profile.attributes, data=profile.data)

        db_invite = ScimApiInvite(
            external_id=create_request.external_id,
            name=ScimApiName(**asdict(create_request.nutid_invite_v1.name)),
            emails=[
                ScimApiEmail(**asdict(email))
                for email in create_request.nutid_invite_v1.emails
            ],
            phone_numbers=[
                ScimApiPhoneNumber(**asdict(number))
                for number in create_request.nutid_invite_v1.phone_numbers
            ],
            nin=create_request.nutid_invite_v1.national_identity_number,
            preferred_language=create_request.nutid_invite_v1.
            preferred_language,
            groups=create_request.nutid_invite_v1.groups,
            profiles=profiles,
        )
        signup_invite = self._create_signup_invite(req, resp, create_request,
                                                   db_invite)
        self.context.signup_invitedb.save(signup_invite)
        ctx_invitedb(req).save(db_invite)
        if signup_invite.send_email:
            self._send_invite_mail(signup_invite)

        add_api_event(
            context=self.context,
            data_owner=req.context['data_owner'],
            db_obj=db_invite,
            resource_type=SCIMResourceType.INVITE,
            level=EventLevel.INFO,
            status=EventStatus.CREATED,
            message='Invite was created',
        )

        self._db_invite_to_response(req, resp, db_invite, signup_invite)
        resp.status = HTTP_201
Beispiel #17
0
    def on_post(self, req: Request, resp: Response):
        """
               POST /Events  HTTP/1.1
               Host: example.com
               Accept: application/scim+json
               Content-Type: application/scim+json
               Authorization: Bearer h480djs93hd8
               Content-Length: ...

               {
                   'schemas': ['https://scim.eduid.se/schema/nutid/event/core-v1',
                               'https://scim.eduid.se/schema/nutid/event/v1'],
                   'https://scim.eduid.se/schema/nutid/event/v1': {
                       'ref': {'resourceType': 'User',
                               'id': '199745a8-a4f5-46b9-9ae9-531da967bfb1',
                               'externalId': '*****@*****.**'
                               },
                       'data': {'create_test': True},
                       'expiresAt': '2021-02-23T14:36:15+00:00',
                       'level': 'debug',
                       'source': 'eduid.se',
                       'timestamp': '2021-02-18T14:36:15+00:00'
                       }
               }
        """
        self.context.logger.info(f'Creating event')
        try:
            create_request: EventCreateRequest = EventCreateRequestSchema(
            ).load(req.media)
            self.context.logger.debug(create_request)
        except ValidationError as e:
            raise BadRequest(detail=str(e))

        # TODO: Instead of checking input here we should use dump_only for the fields in the schema
        if create_request.nutid_event_v1.source:
            raise BadRequest(detail='source is read-only')
        if create_request.nutid_event_v1.expires_at:
            raise BadRequest(detail='expiresAt is read-only')
        if create_request.nutid_event_v1.resource.external_id:
            raise BadRequest(detail='resource.externalId is read-only')
        if create_request.nutid_event_v1.resource.location:
            raise BadRequest(detail='resource.location is read-only')

        # TODO: This check should move to schema validation
        if create_request.nutid_event_v1.timestamp:
            earliest_allowed = utc_now() - timedelta(days=1)
            if create_request.nutid_event_v1.timestamp < earliest_allowed:
                raise BadRequest(detail='timestamp is too old')

        referenced = _get_scim_referenced(
            req, create_request.nutid_event_v1.resource)
        if not referenced:
            raise BadRequest(detail='referenced object not found')

        _timestamp = utc_now()
        if create_request.nutid_event_v1.timestamp:
            _timestamp = create_request.nutid_event_v1.timestamp
        _expires_at = utc_now() + timedelta(days=1)

        event = ScimApiEvent(
            resource=ScimApiEventResource(
                resource_type=create_request.nutid_event_v1.resource.
                resource_type,
                scim_id=referenced.scim_id,
                external_id=referenced.external_id,
            ),
            level=create_request.nutid_event_v1.level,
            source=req.context['data_owner'],
            data=create_request.nutid_event_v1.data,
            expires_at=_expires_at,
            timestamp=_timestamp,
        )
        ctx_eventdb(req).save(event)

        # Send notification
        message = self.context.notification_relay.format_message(
            version=1,
            data={
                'location':
                self.resource_url(SCIMResourceType.EVENT, event.scim_id)
            })
        self.context.notification_relay.notify(
            data_owner=req.context['data_owner'], message=message)

        self._db_event_to_response(req, resp, event)
Beispiel #18
0
    def on_put(self, req: Request, resp: Response, scim_id):
        try:
            self.context.logger.info(f'Updating user {scim_id}')

            update_request: UserUpdateRequest = UserUpdateRequestSchema().load(
                req.media)
            self.context.logger.debug(update_request)
            if scim_id != str(update_request.id):
                self.context.logger.error(f'Id mismatch')
                self.context.logger.debug(f'{scim_id} != {update_request.id}')
                raise BadRequest(detail='Id mismatch')

            db_user = ctx_userdb(req).get_user_by_scim_id(scim_id)
            if not db_user:
                raise NotFound(detail="User not found")

            # Check version
            if not self._check_version(req, db_user):
                raise BadRequest(detail="Version mismatch")

            self.context.logger.debug(
                f'Extra debug: user {scim_id} as dict:\n{db_user.to_dict()}')

            core_changed = False
            if SCIMSchema.CORE_20_USER in update_request.schemas:
                name_in = ScimApiName(**asdict(update_request.name))
                emails_in = set(
                    ScimApiEmail(**asdict(email))
                    for email in update_request.emails)
                phone_numbers_in = set(
                    ScimApiPhoneNumber(**asdict(number))
                    for number in update_request.phone_numbers)
                # external_id
                if update_request.external_id != db_user.external_id:
                    db_user = replace(db_user,
                                      external_id=update_request.external_id)
                    core_changed = True
                # preferred_language
                if update_request.preferred_language != db_user.preferred_language:
                    db_user = replace(
                        db_user,
                        preferred_language=update_request.preferred_language)
                    core_changed = True
                # name
                if name_in != db_user.name:
                    db_user = replace(db_user, name=name_in)
                    core_changed = True
                # emails
                if emails_in != set(db_user.emails):
                    db_user = replace(db_user, emails=list(emails_in))
                    core_changed = True
                # phone_numbers
                if phone_numbers_in != set(db_user.phone_numbers):
                    db_user = replace(db_user,
                                      phone_numbers=list(phone_numbers_in))
                    core_changed = True

            nutid_changed = False
            if SCIMSchema.NUTID_USER_V1 in update_request.schemas:
                # Look for changes in profiles
                for this in update_request.nutid_user_v1.profiles.keys():
                    if this not in db_user.profiles:
                        self.context.logger.info(
                            f'Adding profile {this}/{update_request.nutid_user_v1.profiles[this]} to user'
                        )
                        nutid_changed = True
                    elif update_request.nutid_user_v1.profiles[this].to_dict(
                    ) != db_user.profiles[this].to_dict():
                        self.context.logger.info(
                            f'Profile {this}/{update_request.nutid_user_v1.profiles[this]} updated'
                        )
                        nutid_changed = True
                    else:
                        self.context.logger.info(
                            f'Profile {this}/{update_request.nutid_user_v1.profiles[this]} not changed'
                        )
                for this in db_user.profiles.keys():
                    if this not in update_request.nutid_user_v1.profiles:
                        self.context.logger.info(
                            f'Profile {this}/{db_user.profiles[this]} removed')
                        nutid_changed = True

                if nutid_changed:
                    for profile_name, profile in update_request.nutid_user_v1.profiles.items(
                    ):
                        db_profile = ScimApiProfile(
                            attributes=profile.attributes, data=profile.data)
                        db_user.profiles[profile_name] = db_profile

            self.context.logger.debug(
                f'Core changed: {core_changed}, nutid_changed: {nutid_changed}'
            )
            if core_changed or nutid_changed:
                self._save_user(req, db_user)
                add_api_event(
                    context=self.context,
                    data_owner=req.context['data_owner'],
                    db_obj=db_user,
                    resource_type=SCIMResourceType.USER,
                    level=EventLevel.INFO,
                    status=EventStatus.UPDATED,
                    message='User was updated',
                )
            else:
                self.context.logger.info(f'No changes detected')

            self._db_user_to_response(req=req, resp=resp, db_user=db_user)
        except ValidationError as e:
            raise BadRequest(detail=f"{e}")
Beispiel #19
0
    def on_post(self, req: Request, resp: Response):
        """
               POST /Users  HTTP/1.1
               Host: example.com
               Accept: application/scim+json
               Content-Type: application/scim+json
               Authorization: Bearer h480djs93hd8
               Content-Length: ...

               {
                 "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
                 "userName":"******",
                 "externalId":"bjensen",
                 "name":{
                   "formatted":"Ms. Barbara J Jensen III",
                   "familyName":"Jensen",
                   "givenName":"Barbara"
                 }
               }


               HTTP/1.1 201 Created
               Content-Type: application/scim+json
               Location:
                https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646
               ETag: W/"e180ee84f0671b1"

               {
                 "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
                 "id":"2819c223-7f76-453a-919d-413861904646",
                 "externalId":"bjensen",
                 "meta":{
                   "resourceType":"User",
                   "created":"2011-08-01T21:32:44.882Z",
                   "lastModified":"2011-08-01T21:32:44.882Z",
                   "location":
               "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
                   "version":"W\/\"e180ee84f0671b1\""
                 },
                 "name":{
                   "formatted":"Ms. Barbara J Jensen III",
                   "familyName":"Jensen",
                   "givenName":"Barbara"
                 },
                 "userName":"******"
               }
        """
        try:
            self.context.logger.info(f'Creating user')

            create_request: UserCreateRequest = UserCreateRequestSchema().load(
                req.media)
            self.context.logger.debug(create_request)

            profiles = {}
            for profile_name, profile in create_request.nutid_user_v1.profiles.items(
            ):
                profiles[profile_name] = ScimApiProfile(
                    attributes=profile.attributes, data=profile.data)

            db_user = ScimApiUser(
                external_id=create_request.external_id,
                name=ScimApiName(**asdict(create_request.name)),
                emails=[
                    ScimApiEmail(**asdict(email))
                    for email in create_request.emails
                ],
                phone_numbers=[
                    ScimApiPhoneNumber(**asdict(number))
                    for number in create_request.phone_numbers
                ],
                preferred_language=create_request.preferred_language,
                profiles=profiles,
            )

            self._save_user(req, db_user)
            add_api_event(
                context=self.context,
                data_owner=req.context['data_owner'],
                db_obj=db_user,
                resource_type=SCIMResourceType.USER,
                level=EventLevel.INFO,
                status=EventStatus.CREATED,
                message='User was created',
            )

            self._db_user_to_response(req=req, resp=resp, db_user=db_user)
            resp.status = HTTP_201
        except ValidationError as e:
            raise BadRequest(detail=f"{e}")
Beispiel #20
0
    def on_put(self, req: Request, resp: Response, scim_id: str):
        """
        PUT /Groups/c3819cbe-c893-4070-824c-fe3d0db8f955  HTTP/1.1
        Host: example.com
        Accept: application/scim+json
        Content-Type: application/scim+json
        Authorization: Bearer h480djs93hd8
        If-Match: W/"5e79df24f77769b475177bc7"
        Content-Length: ...

        {
            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
            "displayName": "Test SCIMv2",
            "members": [
               {
                 "value": "2819c223-7f76-453a-919d-413861904646",
                 "$ref": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
                 "display": "Babs Jensen"
               },
            ]
        }

        HTTP/1.1 200 OK
        content-type: application/json; charset=UTF-8
        etag: W/"5e79df24f77769b475177bc7"
        location: http://scimapi.eduid.docker/scim/test/Groups/c3819cbe-c893-4070-824c-fe3d0db8f955

        {
            "displayName": "test group",
            "id": "c3819cbe-c893-4070-824c-fe3d0db8f955",
            "members": [
               {
                 "value": "2819c223-7f76-453a-919d-413861904646",
                 "$ref": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
                 "display": "Babs Jensen"
               },
            ],
            "meta": {
                "created": "2020-03-24T10:21:24.686000",
                "lastModified": 2020-03-25T14:42:24.686000,
                "location": "http://scimapi.eduid.docker/scim/test/Groups/c3819cbe-c893-4070-824c-fe3d0db8f955",
                "resourceType": "Group",
                "version": "3e79d424f77269f475177bc5"
            },
            "schemas": [
                "urn:ietf:params:scim:schemas:core:2.0:Group"
            ]
        }

        """
        self.context.logger.info('Updating group')
        try:
            update_request = GroupUpdateRequestSchema().load(req.media)
        except ValidationError as e:
            raise BadRequest(detail=f"{e}")
        self.context.logger.debug(update_request)
        if scim_id != str(update_request.id):
            self.context.logger.error(f'Id mismatch')
            self.context.logger.debug(f'{scim_id} != {update_request.id}')
            raise BadRequest(detail='Id mismatch')

        self.context.logger.info(f"Fetching group {scim_id}")
        db_group = ctx_groupdb(req).get_group_by_scim_id(str(
            update_request.id))
        self.context.logger.debug(f'Found group: {db_group}')
        if not db_group:
            raise NotFound(detail="Group not found")

        # Check version
        if not self._check_version(req, db_group):
            raise BadRequest(detail="Version mismatch")

        # Check that members exists in their respective db
        self.context.logger.info(f'Checking if group and user members exists')
        for member in update_request.members:
            if member.is_group:
                if not ctx_groupdb(req).group_exists(str(member.value)):
                    self.context.logger.error(
                        f'Group {member.value} not found')
                    raise BadRequest(detail=f'Group {member.value} not found')
            if member.is_user:
                if not ctx_userdb(req).user_exists(scim_id=str(member.value)):
                    self.context.logger.error(f'User {member.value} not found')
                    raise BadRequest(detail=f'User {member.value} not found')

        updated_group, changed = ctx_groupdb(req).update_group(
            update_request=update_request, db_group=db_group)
        # Load the group from the database to ensure results are consistent with subsequent GETs.
        # For example, timestamps have higher resolution in updated_group than after a load.
        db_group = ctx_groupdb(req).get_group_by_scim_id(
            str(updated_group.scim_id))
        assert db_group  # please mypy

        if changed:
            add_api_event(
                context=self.context,
                data_owner=req.context['data_owner'],
                db_obj=db_group,
                resource_type=SCIMResourceType.GROUP,
                level=EventLevel.INFO,
                status=EventStatus.UPDATED,
                message='Group was updated',
            )

        self._db_group_to_response(resp, db_group)