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
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)