Example #1
0
    def _search_and_merge(self, user_id, cis_profile_object):
        """
        Search for an existing user in the vault for the given profile
        If one exist, merge the given profile with the existing user
        If not, return the given profile

        WARNING: This function also verifies the publishers are valid, as this verification requires knowledge of the
        incoming user profile, profile in the vault, and resulting merged profile.

        @cis_profile_object cis_profile.User object of an incoming user
        @user_id str the user id of cis_profile_object

        Returns a cis_profile.User object
        """

        try:
            self._connect()
            vault = user.Profile(self.identity_vault_client.get("table"),
                                 self.identity_vault_client.get("client"))
            res = vault.find_by_id(user_id)
            logger.info("Search user in vault results: {}".format(
                len(res["Items"])))

        except Exception as e:
            logger.error(
                "Problem finding user profile in identity vault due to: {}".
                format(e))
            res = {"Items": []}

        if len(res["Items"]) > 0:
            # This profile exists in the vault and will be merged and it's publishers verified
            self.condition = "update"
            logger.info(
                "A record already exists in the identity vault for user: {}.".
                format(user_id),
                extra={"user_id": user_id},
            )

            old_user_profile = User(
                user_structure_json=json.loads(res["Items"][0]["profile"]))
            new_user_profile = copy.deepcopy(old_user_profile)
            difference = new_user_profile.merge(cis_profile_object)

            if ((difference == ["user_id"]) or
                (new_user_profile.active.value == old_user_profile.active.value
                 and difference == ["active"]) or (len(difference) == 0)
                    or ((new_user_profile.active.value
                         == old_user_profile.active.value and
                         (new_user_profile.uuid.value is None
                          and new_user_profile.primary_username.value is None))
                        and sorted(difference) == sorted(
                            ["active", "uuid", "primary_username"]))):
                logger.info(
                    "Will not merge user as there were no difference found with the vault instance of the user"
                    .format(extra={"user_id": user_id}))
                return None
            else:
                logger.info(
                    "Differences found during merge: {}".format(difference),
                    extra={"user_id": user_id})

            # XXX This is safe but this is not great. Probably should have a route to deactivate since its a CIS
            # attribute.
            if difference == ["active"]:
                logger.info(
                    "Partial update only contains the `active` attribute, bypassing publisher verification as CIS "
                    "will enforce this check on it's own'",
                    extra={"user_id": user_id},
                )
                return new_user_profile

            if self.config("verify_publishers", namespace="cis") == "true":
                logger.info("Verifying publishers", extra={"user_id": user_id})
                try:
                    new_user_profile.verify_all_publishers(old_user_profile)
                except Exception as e:
                    logger.error(
                        "The merged profile failed to pass publisher verification",
                        extra={
                            "user_id": user_id,
                            "profile": new_user_profile.as_dict(),
                            "reason": e,
                            "trace": format_exc(),
                        },
                    )
                    raise VerificationError(
                        {
                            "code": "invalid_publisher",
                            "description": "{}".format(e)
                        }, 403)
            else:
                logger.warning(
                    "Bypassing profile publisher verification due to `verify_publishers` setting being false",
                    extra={"user_id": user_id},
                )
            return new_user_profile
        else:
            # This is a new profile, set uuid and primary_username and verify.
            self.condition = "create"
            logger.info(
                "A record does not exist in the identity vault for user: {}.".
                format(user_id),
                extra={"user_id": user_id},
            )
            # We raise an exception if uuid or primary_username is already set. This must not happen.
            if cis_profile_object.uuid.value is not None or cis_profile_object.primary_username.value is not None:
                logger.error(
                    "Trying to create profile, but uuid ({}) or primary_username ({}) was already "
                    "set".format(cis_profile_object.uuid.value,
                                 cis_profile_object.primary_username.value),
                    extra={
                        "user_id": user_id,
                        "profile": cis_profile_object.as_dict()
                    },
                )
                raise VerificationError(
                    {
                        "code":
                        "uuid_or_primary_username_set",
                        "description":
                        "The fields primary_username or uuid have been set in a new profile.",
                    },
                    403,
                )
            cis_profile_object.initialize_uuid_and_primary_username()
            cis_profile_object.sign_attribute("uuid", "cis")
            cis_profile_object.sign_attribute("primary_username", "cis")

            if self.config("verify_publishers", namespace="cis") == "true":
                logger.info("Verifying publishers", extra={"user_id": user_id})
                try:
                    cis_profile_object.verify_all_publishers(
                        cis_profile.User())
                except Exception as e:
                    logger.error(
                        "The profile failed to pass publisher verification",
                        extra={
                            "user_id": user_id,
                            "profile": cis_profile_object.as_dict(),
                            "reason": e,
                            "trace": format_exc(),
                        },
                    )
                    raise VerificationError(
                        {
                            "code": "invalid_publisher",
                            "description": "{}".format(e)
                        }, 403)
            else:
                logger.warning(
                    "Bypassing profile publisher verification due to `verify_publishers` setting being false",
                    extra={"user_id": user_id},
                )
            return cis_profile_object
Example #2
0
    def put_profiles(self, profiles):
        """
        Merge profile data as necessary with existing profile data for a given user
        Verify profile data is correctly signed and published by allowed publishers
        Write back the result to the identity vault.
        @profiles list of str or cis_profile.User object

        Returns a dictionary containing vault results
        """

        # User profiles that have been verified, validated, merged, etc.
        profiles_to_store = []

        for user_profile in profiles:
            # Ensure we always have a cis_profile.User at this point (compat)
            if isinstance(user_profile, str):
                user_profile = cis_profile.User(
                    user_structure_json=user_profile)
            elif isinstance(user_profile, dict):
                user_profile = cis_profile.User(
                    user_structure_json=json.dumps(user_profile))

            # For single put_profile events the user_id is passed as argument
            if self.user_id:
                user_id = self.user_id
                # Verify that we're passing the same as the signed user_id for safety reasons
                if user_profile._attribute_value_set(
                        user_profile.user_id) and (user_id !=
                                                   user_profile.user_id.value):
                    raise IntegrationError(
                        {
                            "code":
                            "integration_exception",
                            "description":
                            "user_id query parameter does not match profile, that looks wrong",
                        },
                        400,
                    )

            else:
                user_id = user_profile.user_id.value
            logger.info(
                "Attempting integration of profile data into the vault",
                extra={"user_id": user_id})

            # Ensure we merge user_profile data when we have an existing user in the vault
            # This also does publisher verification
            current_user = self._search_and_merge(user_id, user_profile)
            # No difference found, no merging occured, skip!
            if current_user is None:
                logger.info(
                    "User {} already exists and proposed update has no difference, skipping"
                    .format(user_id),
                    extra={"user_id": user_id},
                )
                continue

            # Check profile signatures
            if self.config("verify_signatures", namespace="cis") == "true":
                try:
                    current_user.verify_all_signatures()
                except Exception as e:
                    logger.error(
                        "The profile failed to pass signature verification for user_id: {}"
                        .format(user_id),
                        extra={
                            "user_id": user_id,
                            "profile": current_user.as_dict(),
                            "reason": e,
                            "trace": format_exc(),
                        },
                    )
                    raise VerificationError(
                        {
                            "code": "invalid_signature",
                            "description": "{}".format(e)
                        }, 403)
            else:
                logger.warning(
                    "Bypassing profile signature verification (`verify_signatures` setting is false)"
                )

            # Update any CIS-owned attributes
            current_user = self._update_attr_owned_by_cis(
                user_id, current_user)

            profiles_to_store.append(current_user)

        if len(profiles_to_store) == 0:
            logger.info("No profiles to store in vault")
            return {"creates": None, "updates": None, "status": 202}

        # Store resulting user in the vault
        logger.info("Will store {} verified profiles".format(
            len(profiles_to_store)))
        return self._store_in_vault(profiles_to_store)