示例#1
0
 def _update_user(self,
                  req: Dict[str, Any],
                  scim_id: UUID,
                  version: Optional[ObjectId],
                  expect_success: bool = True) -> UserApiResult:
     if 'schemas' not in req:
         _schemas = [SCIMSchema.CORE_20_USER.value]
         if SCIMSchema.NUTID_USER_V1.value in req:
             _schemas += [SCIMSchema.NUTID_USER_V1.value]
         req['schemas'] = _schemas
     if 'id' not in req:
         req['id'] = str(scim_id)
     _headers = dict(self.headers)  # copy
     if version:
         _headers['IF-MATCH'] = make_etag(version)
     result = self.client.simulate_put(path=f'/Users/{scim_id}',
                                       body=self.as_json(req),
                                       headers=_headers)
     if expect_success:
         self._assertResponse(result)
     response: UserResponse = UserResponseSchema().load(result.json)
     return UserApiResult(request=req,
                          nutid_user=response.nutid_user_v1,
                          result=result,
                          response=response)
示例#2
0
 def test_update_user_set_external_id(self):
     db_user = self.add_user(identifier=str(uuid4()),
                             profiles={'test': self.test_profile})
     req = {
         'schemas':
         [SCIMSchema.CORE_20_USER.value, SCIMSchema.NUTID_USER_V1.value],
         'id':
         str(db_user.scim_id),
         'externalId':
         'test-id-1',
         SCIMSchema.NUTID_USER_V1.value: {
             'profiles': {
                 'test': {
                     'attributes': {
                         'displayName': 'New display name'
                     },
                     'data': {
                         'test_key': 'new value'
                     }
                 }
             },
         },
     }
     self.headers['IF-MATCH'] = make_etag(db_user.version)
     response = self.client.simulate_put(path=f'/Users/{db_user.scim_id}',
                                         body=self.as_json(req),
                                         headers=self.headers)
     self._assertResponse(response)
     db_user = self.userdb.get_user_by_scim_id(response.json['id'])
     self._assertUserUpdateSuccess(req, response, db_user)
示例#3
0
    def test_delete_group(self):
        group = self.add_group(uuid4(), 'Test Group 1')

        # Verify we can find the group in the database
        db_group1 = self.groupdb.get_group_by_scim_id(str(group.scim_id))
        self.assertIsNotNone(db_group1)

        req = {'schemas': [SCIMSchema.CORE_20_GROUP.value]}
        self.headers['IF-MATCH'] = make_etag(group.version)
        response = self.client.simulate_delete(path=f'/Groups/{group.scim_id}',
                                               body=self.as_json(req),
                                               headers=self.headers)
        self.assertEqual(204, response.status_code)

        # Verify the group is no longer in the database
        db_group2 = self.groupdb.get_group_by_scim_id(group.scim_id)
        self.assertIsNone(db_group2)

        # check that the action resulted in an event in the database
        events = self.eventdb.get_events_by_resource(SCIMResourceType.GROUP,
                                                     db_group1.scim_id)
        assert len(events) == 1
        event = events[0]
        assert event.resource.external_id is None
        assert event.data['status'] == EventStatus.DELETED.value
示例#4
0
    def test_removing_group_member(self):
        db_group = self.add_group(uuid4(), 'Test Group 1')
        subgroup = self.add_group(uuid4(), 'Test Group 2')
        db_group = self.add_member(db_group, subgroup, 'Test User')
        user = self.add_user(identifier=str(uuid4()), external_id='not-used')
        db_group = self.add_member(db_group, user, 'Test User')

        # Load group to verify it has two members
        _g1 = self.groupdb.get_group_by_scim_id(str(db_group.scim_id))
        self.assertEqual(
            2, len(_g1.graph.members),
            'Group loaded from database does not have two members')
        self.assertEqual(
            1, len(_g1.graph.member_users),
            'Group loaded from database does not have one member user')
        self.assertEqual(
            1, len(_g1.graph.member_groups),
            'Group loaded from database does not have one member group')

        members = [
            {
                'value': str(user.scim_id),
                '$ref': f'http://localhost:8000/Users/{user.scim_id}',
                'display': 'Test User 1',
            },
        ]
        req = {
            'schemas':
            [SCIMSchema.CORE_20_GROUP.value, SCIMSchema.NUTID_GROUP_V1.value],
            'id':
            str(db_group.scim_id),
            'displayName':
            db_group.display_name,
            'members':
            members,
            SCIMSchema.NUTID_GROUP_V1.value: {
                'data': {
                    'test': 'updated'
                }
            },
        }

        self.headers['IF-MATCH'] = make_etag(db_group.version)
        response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                            body=self.as_json(req),
                                            headers=self.headers)

        self._assertGroupUpdateSuccess(req, response, db_group)

        # Load group to verify it has one less member now
        _g2 = self.groupdb.get_group_by_scim_id(str(db_group.scim_id))
        self.assertEqual(
            1, len(_g2.graph.members),
            'Group loaded from database does not have two members')
        self.assertEqual(
            1, len(_g2.graph.member_users),
            'Group loaded from database does not have one member user')
        self.assertEqual(
            0, len(_g2.graph.member_groups),
            'Group loaded from database does not have one member group')
示例#5
0
    def _db_group_to_response(self, resp: Response,
                              db_group: ScimApiGroup) -> None:
        members = self._get_group_members(db_group)
        location = self.url_for("Groups", str(db_group.scim_id))
        meta = Meta(
            location=location,
            last_modified=db_group.last_modified or db_group.created,
            resource_type=SCIMResourceType.GROUP,
            created=db_group.created,
            version=db_group.version,
        )
        schemas = [SCIMSchema.CORE_20_GROUP]
        if db_group.extensions.data:
            schemas.append(SCIMSchema.NUTID_GROUP_V1)
        group = GroupResponse(
            display_name=db_group.graph.display_name,
            members=members,
            id=db_group.scim_id,
            external_id=db_group.external_id,
            meta=meta,
            schemas=list(
                schemas
            ),  # extra list() needed to work with _both_ mypy and marshmallow
            nutid_group_v1=NutidGroupExtensionV1(
                data=db_group.extensions.data),
        )

        resp.set_header("Location", location)
        resp.set_header("ETag", make_etag(db_group.version))
        dumped_group = GroupResponseSchema().dump(group)
        if SCIMSchema.NUTID_GROUP_V1 not in group.schemas and SCIMSchema.NUTID_GROUP_V1.value in dumped_group:
            # Serialization will always put the NUTID_GROUP_V1 in the dumped_group, even if there was no data
            del dumped_group[SCIMSchema.NUTID_GROUP_V1.value]
        resp.media = dumped_group
示例#6
0
    def test_update_group(self):
        db_group = self.add_group(uuid4(), 'Test Group 1')
        subgroup = self.add_group(uuid4(), 'Test Group 2')
        user = self.add_user(identifier=str(uuid4()), external_id='not-used')
        members = [
            {
                'value': str(user.scim_id),
                '$ref': f'http://localhost:8000/Users/{user.scim_id}',
                'display': 'Test User 1',
            },
            {
                'value': str(subgroup.scim_id),
                '$ref': f'http://localhost:8000/Groups/{subgroup.scim_id}',
                'display': 'Test Group 2',
            },
        ]
        req = {
            'schemas': [SCIMSchema.CORE_20_GROUP.value],
            'id': str(db_group.scim_id),
            'displayName': db_group.display_name,
            'members': members,
        }
        self.headers['IF-MATCH'] = make_etag(db_group.version)
        response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                            body=self.as_json(req),
                                            headers=self.headers)

        self._assertGroupUpdateSuccess(req, response, db_group)
示例#7
0
    def test_update_existing_group(self):
        db_group = self.add_group(uuid4(), 'Test Group 1')
        subgroup = self.add_group(uuid4(), 'Test Group 2')
        user = self.add_user(identifier=str(uuid4()), external_id='not-used')
        members = [
            {
                'value': str(user.scim_id),
                '$ref': f'http://localhost:8000/Users/{user.scim_id}',
                'display': 'Test User 1',
            },
            {
                'value': str(subgroup.scim_id),
                '$ref': f'http://localhost:8000/Groups/{subgroup.scim_id}',
                'display': 'Test Group 2',
            },
        ]
        db_group.display_name = 'Another display name'
        db_group.external_id = 'external id'
        req = {
            'schemas':
            [SCIMSchema.CORE_20_GROUP.value, SCIMSchema.NUTID_GROUP_V1.value],
            'id':
            str(db_group.scim_id),
            'externalId':
            db_group.external_id,
            'displayName':
            db_group.display_name,
            'members':
            members,
            SCIMSchema.NUTID_GROUP_V1.value: {
                'data': {
                    'test': 'updated'
                }
            },
        }
        self.headers['IF-MATCH'] = make_etag(db_group.version)
        response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                            body=self.as_json(req),
                                            headers=self.headers)

        self._assertGroupUpdateSuccess(req, response, db_group)

        members[0]['display'] += ' (updated)'
        members[1]['display'] += ' (also updated)'

        self.headers['IF-MATCH'] = response.headers['Etag']
        response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                            body=self.as_json(req),
                                            headers=self.headers)
        self._assertGroupUpdateSuccess(req, response, db_group)

        # check that the action resulted in an event in the database
        events = self.eventdb.get_events_by_resource(SCIMResourceType.GROUP,
                                                     db_group.scim_id)
        assert len(events) == 2
        event = events[0]
        assert event.resource.external_id == req['externalId']
        assert event.data['status'] == EventStatus.UPDATED.value
示例#8
0
 def _check_version(
         self, req: Request, db_obj: Union[ScimApiGroup, ScimApiUser,
                                           ScimApiInvite]) -> bool:
     if req.headers.get('IF-MATCH') == make_etag(db_obj.version):
         return True
     self.context.logger.error(f'Version mismatch')
     self.context.logger.debug(
         f'{req.headers.get("IF-MATCH")} != {make_etag(db_obj.version)}')
     return False
示例#9
0
    def test_version_mismatch(self):
        group = self.add_group(uuid4(), 'Test Group 1')

        req = {'schemas': [SCIMSchema.CORE_20_GROUP.value]}
        self.headers['IF-MATCH'] = make_etag(ObjectId())
        response = self.client.simulate_delete(path=f'/Groups/{group.scim_id}',
                                               body=self.as_json(req),
                                               headers=self.headers)
        self._assertScimError(response.json, detail='Version mismatch')
示例#10
0
    def test_update_user(self):
        db_user = self.add_user(identifier=str(uuid4()),
                                external_id='test-id-1',
                                profiles={'test': self.test_profile})
        req = {
            'schemas':
            [SCIMSchema.CORE_20_USER.value, SCIMSchema.NUTID_USER_V1.value],
            'id':
            str(db_user.scim_id),
            'externalId':
            'test-id-1',
            'name': {
                'familyName': 'Testsson',
                'givenName': 'Test',
                'middleName': 'Testaren'
            },
            'emails': [{
                'primary': True,
                'type': 'home',
                'value': '*****@*****.**'
            }],
            'phoneNumbers': [{
                'primary': True,
                'type': 'mobile',
                'value': 'tel:+1-202-456-1414'
            }],
            'preferredLanguage':
            'en',
            SCIMSchema.NUTID_USER_V1.value: {
                'profiles': {
                    'test': {
                        'attributes': {
                            'displayName': 'New display name'
                        },
                        'data': {
                            'test_key': 'new value'
                        }
                    }
                },
            },
        }
        self.headers['IF-MATCH'] = make_etag(db_user.version)
        response = self.client.simulate_put(path=f'/Users/{db_user.scim_id}',
                                            body=self.as_json(req),
                                            headers=self.headers)
        self._assertResponse(response)
        db_user = self.userdb.get_user_by_scim_id(response.json['id'])
        self._assertUserUpdateSuccess(req, response, db_user)

        # check that the action resulted in an event in the database
        events = self.eventdb.get_events_by_resource(SCIMResourceType.USER,
                                                     db_user.scim_id)
        assert len(events) == 1
        event = events[0]
        assert event.resource.external_id == req['externalId']
        assert event.data['status'] == EventStatus.UPDATED.value
示例#11
0
    def _db_invite_to_response(self, req: Request, resp: Response,
                               db_invite: ScimApiInvite,
                               signup_invite: SignupInvite):
        location = self.url_for("Invites", db_invite.scim_id)
        meta = Meta(
            location=location,
            last_modified=db_invite.last_modified,
            resource_type=SCIMResourceType.INVITE,
            created=db_invite.created,
            version=db_invite.version,
        )

        schemas = [
            SCIMSchema.NUTID_INVITE_CORE_V1, SCIMSchema.NUTID_INVITE_V1,
            SCIMSchema.NUTID_USER_V1
        ]
        _profiles = {
            k: Profile(attributes=v.attributes, data=v.data)
            for k, v in db_invite.profiles.items()
        }
        invite_extension = NutidInviteExtensionV1(
            completed=db_invite.completed,
            name=Name(**asdict(db_invite.name)),
            emails=[Email(**asdict(email)) for email in db_invite.emails],
            phone_numbers=[
                PhoneNumber(**asdict(number))
                for number in db_invite.phone_numbers
            ],
            national_identity_number=db_invite.nin,
            preferred_language=db_invite.preferred_language,
            groups=db_invite.groups,
            send_email=signup_invite.send_email,
            finish_url=signup_invite.finish_url,
            expires_at=signup_invite.expires_at,
            inviter_name=signup_invite.inviter_name,
        )
        # Only add invite url in response if no email should be sent to the invitee
        if signup_invite.send_email is False:
            invite_url = f'{self.context.config.invite_url}/{signup_invite.invite_code}'
            invite_extension = replace(invite_extension, invite_url=invite_url)

        scim_invite = InviteResponse(
            id=db_invite.scim_id,
            external_id=db_invite.external_id,
            meta=meta,
            schemas=list(
                schemas
            ),  # extra list() needed to work with _both_ mypy and marshmallow
            nutid_invite_v1=invite_extension,
            nutid_user_v1=NutidUserExtensionV1(profiles=_profiles),
        )

        resp.set_header("Location", location)
        resp.set_header("ETag", make_etag(db_invite.version))
        resp.media = InviteResponseSchema().dump(scim_invite)
示例#12
0
 def test_version_mismatch(self):
     db_group = self.add_group(uuid4(), 'Test Group 1')
     req = {
         'schemas': [SCIMSchema.CORE_20_GROUP.value],
         'id': str(db_group.scim_id),
         'displayName': 'Another display name',
     }
     self.headers['IF-MATCH'] = make_etag(ObjectId())
     response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                         body=self.as_json(req),
                                         headers=self.headers)
     self._assertScimError(response.json, detail='Version mismatch')
示例#13
0
    def test_delete_invite(self):
        db_invite = self.add_invite()
        self.headers['IF-MATCH'] = make_etag(db_invite.version)
        self.client.simulate_delete(path=f'/Invites/{db_invite.scim_id}',
                                    headers=self.headers)
        reference = SCIMReference(data_owner=self.data_owner,
                                  scim_id=db_invite.scim_id)
        self.assertIsNone(
            self.invitedb.get_invite_by_scim_id(str(db_invite.scim_id)))
        self.assertIsNone(
            self.signup_invitedb.get_invite_by_reference(reference))

        # check that the action resulted in an event in the database
        events = self.eventdb.get_events_by_resource(SCIMResourceType.INVITE,
                                                     db_invite.scim_id)
        assert len(events) == 1
        event = events[0]
        assert event.data['status'] == EventStatus.DELETED.value
示例#14
0
    def _db_user_to_response(self, req: Request, resp: Response,
                             db_user: ScimApiUser):
        location = self.url_for("Users", db_user.scim_id)
        meta = Meta(
            location=location,
            last_modified=db_user.last_modified,
            resource_type=SCIMResourceType.USER,
            created=db_user.created,
            version=db_user.version,
        )

        schemas = [SCIMSchema.CORE_20_USER]
        if db_user.profiles:
            schemas.append(SCIMSchema.NUTID_USER_V1)

        # Convert one type of Profile into another
        _profiles = {
            k: Profile(attributes=v.attributes, data=v.data)
            for k, v in db_user.profiles.items()
        }

        user = UserResponse(
            id=db_user.scim_id,
            external_id=db_user.external_id,
            name=Name(**asdict(db_user.name)),
            emails=[Email(**asdict(email)) for email in db_user.emails],
            phone_numbers=[
                PhoneNumber(**asdict(number))
                for number in db_user.phone_numbers
            ],
            preferred_language=db_user.preferred_language,
            groups=self._get_user_groups(req=req, db_user=db_user),
            meta=meta,
            schemas=list(
                schemas
            ),  # extra list() needed to work with _both_ mypy and marshmallow
            nutid_user_v1=NutidUserExtensionV1(profiles=_profiles),
        )

        resp.set_header("Location", location)
        resp.set_header("ETag", make_etag(db_user.version))
        resp.media = UserResponseSchema().dump(user)
示例#15
0
 def test_update_group_subgroup_does_not_exist(self):
     db_group = self.add_group(uuid4(), 'Test Group 1')
     _subgroup_scim_id = str(uuid4())
     members = [{
         'value': _subgroup_scim_id,
         '$ref': f'http://localhost:8000/Groups/{_subgroup_scim_id}',
         'display': 'Test Group 2',
     }]
     req = {
         'schemas': [SCIMSchema.CORE_20_GROUP.value],
         'id': str(db_group.scim_id),
         'displayName': 'Another display name',
         'members': members,
     }
     self.headers['IF-MATCH'] = make_etag(db_group.version)
     response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                         body=self.as_json(req),
                                         headers=self.headers)
     self._assertScimError(response.json,
                           detail=f'Group {_subgroup_scim_id} not found')
示例#16
0
 def test_update_user_duplicated_external_id(self):
     external_id = 'test-id-1'
     # Create two existing users with different external_id
     self.add_user(identifier=str(uuid4()),
                   external_id=external_id,
                   profiles={'test': self.test_profile})
     db_user = self.add_user(identifier=str(uuid4()),
                             external_id='test-id-2',
                             profiles={'test': self.test_profile})
     # Try to update the second user with the external_id of the first
     req = {
         'schemas':
         [SCIMSchema.CORE_20_USER.value, SCIMSchema.NUTID_USER_V1.value],
         'id':
         str(db_user.scim_id),
         'externalId':
         external_id,
         SCIMSchema.NUTID_USER_V1.value: {
             'profiles': {
                 'test': {
                     'attributes': {
                         'displayName': 'New display name'
                     },
                     'data': {
                         'test_key': 'new value'
                     }
                 }
             }
         },
     }
     self.headers['IF-MATCH'] = make_etag(db_user.version)
     response = self.client.simulate_put(path=f'/Users/{db_user.scim_id}',
                                         body=self.as_json(req),
                                         headers=self.headers)
     self._assertScimError(
         response.json,
         schemas=['urn:ietf:params:scim:api:messages:2.0:Error'],
         detail='externalID must be unique')
示例#17
0
    def _db_event_to_response(self, req: Request, resp: Response,
                              db_event: ScimApiEvent):
        location = self.resource_url(SCIMResourceType.EVENT, db_event.scim_id)
        meta = Meta(
            location=location,
            last_modified=db_event.last_modified,
            resource_type=SCIMResourceType.EVENT,
            created=db_event.created,
            version=db_event.version,
        )

        schemas = [SCIMSchema.NUTID_EVENT_CORE_V1, SCIMSchema.NUTID_EVENT_V1]
        response = EventResponse(
            id=db_event.scim_id,
            meta=meta,
            schemas=list(
                schemas
            ),  # extra list() needed to work with _both_ mypy and marshmallow
            nutid_event_v1=NutidEventExtensionV1(
                level=db_event.level,
                data=db_event.data,
                source=db_event.source,
                expires_at=db_event.expires_at,
                timestamp=db_event.timestamp,
                resource=NutidEventResource(
                    resource_type=db_event.resource.resource_type,
                    scim_id=db_event.resource.scim_id,
                    external_id=db_event.resource.external_id,
                    location=self.resource_url(db_event.resource.resource_type,
                                               db_event.resource.scim_id),
                ),
            ),
        )

        resp.set_header("Location", location)
        resp.set_header("ETag", make_etag(db_event.version))
        resp.media = EventResponseSchema().dump(response)
示例#18
0
    def test_add_member_to_existing_group(self):
        db_group = self.add_group(uuid4(), 'Test Group 1')
        user = self.add_user(identifier=str(uuid4()), external_id='not-used')
        members = [{
            'value': str(user.scim_id),
            '$ref': f'http://localhost:8000/Users/{user.scim_id}',
            'display': 'Test User 1',
        }]
        req = {
            'schemas': [SCIMSchema.CORE_20_GROUP.value],
            'id': str(db_group.scim_id),
            'displayName': db_group.display_name,
            'members': members,
        }
        self.headers['IF-MATCH'] = make_etag(db_group.version)
        response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                            body=self.as_json(req),
                                            headers=self.headers)

        self._assertGroupUpdateSuccess(req, response, db_group)

        # Now, add another user and make a new request

        added_user = self.add_user(identifier=str(uuid4()),
                                   external_id='not-used-2')
        members += [{
            'value': str(added_user.scim_id),
            '$ref': f'http://localhost:8000/Users/{added_user.scim_id}',
            'display': 'Added User',
        }]

        self.headers['IF-MATCH'] = response.headers['Etag']
        response = self.client.simulate_put(path=f'/Groups/{db_group.scim_id}',
                                            body=self.as_json(req),
                                            headers=self.headers)
        self._assertGroupUpdateSuccess(req, response, db_group)
示例#19
0
    def test_create_and_fetch_event(self):
        user = self.add_user(identifier=str(uuid4()),
                             external_id='*****@*****.**')
        event = {
            'resource': {
                'resourceType': SCIMResourceType.USER.value,
                'id': str(user.scim_id)
            },
            'level': EventLevel.DEBUG.value,
            'data': {
                'create_fetch_test': True
            },
        }
        created = self._create_event(event=event)

        # check that the create resulted in an event in the database
        events = self.eventdb.get_events_by_resource(SCIMResourceType.USER,
                                                     scim_id=user.scim_id)
        assert len(events) == 1
        db_event = events[0]

        # Now fetch the event using SCIM
        fetched = self._fetch_event(created.response.id)
        assert fetched.response.id == db_event.scim_id

        # Verify that create and fetch returned the same data.
        # Compare as dict first because the output is easier to read.
        assert asdict(created.event) == asdict(fetched.event)
        assert created.event == fetched.event

        # For once, verify the actual SCIM message format too
        expected = {
            'schemas': [
                'https://scim.eduid.se/schema/nutid/event/core-v1',
                'https://scim.eduid.se/schema/nutid/event/v1',
            ],
            'id':
            str(db_event.scim_id),
            'meta': {
                'created': db_event.created.isoformat(),
                'lastModified': db_event.last_modified.isoformat(),
                'location': f'http://localhost:8000/Events/{db_event.scim_id}',
                'resourceType': 'Event',
                'version': make_etag(db_event.version),
            },
            'https://scim.eduid.se/schema/nutid/event/v1': {
                'data': {
                    'create_fetch_test': True
                },
                'expiresAt':
                (db_event.timestamp + timedelta(days=1)).isoformat(),
                'level': 'debug',
                'source': 'eduid.se',
                'timestamp': db_event.timestamp.isoformat(),
                'resource': {
                    'resourceType': 'User',
                    'id': str(user.scim_id),
                    'externalId': user.external_id,
                    'location': f'http://localhost:8000/Users/{user.scim_id}',
                },
            },
        }
        assert fetched.result.json == expected
示例#20
0
 def _serialize(self, value: ObjectId, attr, obj, **kwargs):
     if value is None:
         return missing
     return make_etag(value)