Example #1
0
    def from_dict(self, d: Dict[str, Any]) -> None:
        """Consume a dictionary conforming to the SCIM User Schema. The
        dictionary was originally submitted as JSON by the client in
        PUT (update a user) and POST (create a new user) requests.  A
        PUT request tells us to update User attributes to match those
        passed in the dict.  A POST request tells us to create a new
        User with attributes as specified in the dict.

        The superclass implements some very basic default behavior,
        that doesn't support changing attributes via our actions.py
        functions (which update audit logs, send events, etc.) or
        doing application-specific validation.

        Thus, we've completely overridden the upstream implementation
        to store the values of the supported attributes that the
        request would like to change. Actually modifying the database
        is implemented in self.save().

        Given that SCIMUser is an adapter class, this method is meant
        to be completely overridden, and we can expect it remain the
        case that no important django-scim2 logic relies on the
        superclass's implementation of this function.
        """
        email = d.get("userName")
        assert isinstance(email, str)
        self.change_delivery_email(email)

        name_attr_dict = d.get("name", {})
        if self.config["name_formatted_included"]:
            full_name = name_attr_dict.get("formatted", "")
        else:
            # Some providers (e.g. Okta) don't provide name.formatted.
            first_name = name_attr_dict.get("givenName", "")
            last_name = name_attr_dict.get("familyName", "")
            full_name = f"{first_name} {last_name}".strip()

        if full_name:
            assert isinstance(full_name, str)
            self.change_full_name(full_name)

        if self.is_new_user() and not full_name:
            raise scim_exceptions.BadRequestError(
                "Must specify name.formatted, name.givenName or name.familyName when creating a new user"
            )

        active = d.get("active")
        if self.is_new_user() and not active:
            raise scim_exceptions.BadRequestError(
                "New user must have active=True")

        if active is not None:
            assert isinstance(active, bool)
            self.change_is_active(active)
Example #2
0
 def delete(self) -> None:
     """
     This is consistent with Okta SCIM - users don't get DELETEd, they're deactivated
     by changing their "active" attr to False.
     """
     raise scim_exceptions.BadRequestError(
         'DELETE operation not supported. Use PUT or PATCH to modify the "active" attribute instead.'
     )
Example #3
0
    def from_dict(self, d):
        """
        Consume a ``dict`` conforming to the SCIM User Schema, updating the
        internal user object with data from the ``dict``.

        Please note, the user object is not saved within this method. To
        persist the changes made by this method, please call ``.save()`` on the
        adapter. Eg::

            scim_user.from_dict(d)
            scim_user.save()
        """
        # Store dict for possible later use when saving user
        self._from_dict_copy = copy.deepcopy(d)

        self.obj.company_id = self.request.user.company_id

        self.parse_active(d.get('active'))

        self.obj.first_name = d.get('name', {}).get('givenName') or ''

        self.obj.last_name = d.get('name', {}).get('familyName') or ''

        self.parse_email(d.get('emails'))

        if self.is_new_user and not self.obj.email:
            raise scim_exceptions.BadRequestError('Empty email value')

        self.obj.scim_username = d.get('userName')
        self.obj.scim_external_id = d.get('externalId') or ''

        cleartext_password = d.get('password')
        if cleartext_password:
            self.obj.set_password(cleartext_password)
            self.obj._scim_cleartext_password = cleartext_password
            self.password_changed = True
Example #4
0
    def save(self) -> None:
        """
        This method is called at the end of operations modifying a user,
        and is responsible for actually applying the requested changes,
        writing them to the database.
        """
        realm = RequestNotes.get_notes(self._request).realm
        assert realm is not None

        email_new_value = getattr(self, "_email_new_value", None)
        is_active_new_value = getattr(self, "_is_active_new_value", None)
        full_name_new_value = getattr(self, "_full_name_new_value", None)
        password = getattr(self, "_password_set_to", None)

        # Clean up the internal "pending change" state, now that we've
        # fetched the values:
        self._email_new_value = None
        self._is_active_new_value = None
        self._full_name_new_value = None
        self._password_set_to = None

        if email_new_value:
            try:
                # Note that the validate_email check that usually
                # appears adjacent to email_allowed_for_realm is
                # present in save().
                email_allowed_for_realm(email_new_value, realm)
            except DomainNotAllowedForRealmError:
                raise scim_exceptions.BadRequestError(
                    "This email domain isn't allowed in this organization.")
            except DisposableEmailError:  # nocoverage
                raise scim_exceptions.BadRequestError(
                    "Disposable email domains are not allowed for this realm.")
            except EmailContainsPlusError:  # nocoverage
                raise scim_exceptions.BadRequestError(
                    "Email address can't contain + characters.")

            try:
                validate_email_not_already_in_realm(realm, email_new_value)
            except ValidationError as e:
                raise ConflictError("Email address already in use: " + str(e))

        if self.is_new_user():
            assert full_name_new_value is not None
            self.obj = do_create_user(
                email_new_value,
                password,
                realm,
                full_name_new_value,
                acting_user=None,
            )
            return

        # TODO: The below operations should ideally be executed in a single
        # atomic block to avoid failing with partial changes getting saved.
        # This can be fixed once we figure out how do_deactivate_user can be run
        # inside an atomic block.

        # We process full_name first here, since it's the only one that can fail.
        if full_name_new_value:
            check_change_full_name(self.obj,
                                   full_name_new_value,
                                   acting_user=None)

        if email_new_value:
            do_change_user_delivery_email(self.obj, email_new_value)

        if is_active_new_value is not None and is_active_new_value:
            do_reactivate_user(self.obj, acting_user=None)
        elif is_active_new_value is not None and not is_active_new_value:
            do_deactivate_user(self.obj, acting_user=None)
Example #5
0
 def validate_email(email):
     try:
         validator = core.validators.EmailValidator()
         validator(email)
     except core.exceptions.ValidationError:
         raise scim_exceptions.BadRequestError('Invalid email value')