def test_known_users(self, mock_request_get, mock_authzero, mock_secrets): mock_secrets.return_value = "hi" mock_authzero.return_value = "hi" class FakeResponse: def __init__(self, fake={}): self.fake = fake self.text = str(fake) def json(self): return self.fake def ok(self): return True mu = [{ "user_id": "auser", "uuid": "093249324", "primary_email": "*****@*****.**" }] mock_request_get.return_value = FakeResponse(fake=mu) profiles = [cis_profile.User()] publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") u = publisher.get_known_cis_users() assert u == mu
def test_create_bad(self, mock_authzero, mock_secrets, mock_request_get, mock_request_post): """ test that we'll correctly fixup profiles on creation """ mock_authzero.return_value = "dinopark" mock_secrets.return_value = "is_pretty_cool" class FakeResponse: def __init__(self, fake={}): self.fake = fake self.text = str(fake) def json(self): return self.fake def ok(self): return True mock_request_post.return_value = FakeResponse() mock_request_get.return_value = FakeResponse(fake=self.mu) u = cis_profile.User() # Bad bad u.user_id.value = "anotherauser" u.fun_title.metadata.display = "private" u.fun_title.value = None profiles = [u] publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") publisher.filter_known_cis_users(profiles) assert publisher.profiles[0].fun_title.metadata.display != "private"
def side_effect(*args, **kwargs): fake_user = cis_profile.User(user_id=args[0]) if args[0] == "ad|Mozilla-LDAP|community": fake_user.staff_information.staff.value = False else: fake_user.staff_information.staff.value = True return fake_user
def test_post_specific_user(self, mock_validate, mock_authzero, mock_secrets, mock_request_get, mock_request_post): mock_authzero.return_value = "dinopark" mock_secrets.return_value = "is_pretty_cool" mock_validate.return_value = True class FakeResponse: def __init__(self, fake={}): self.fake = fake self.text = str(fake) def json(self): return self.fake def ok(self): return True mock_request_post.return_value = FakeResponse() mock_request_get.return_value = FakeResponse() profiles = [cis_profile.User(user_id="test")] publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") publisher.post_all(user_ids=["test"]) assert publisher.profiles[0].user_id.value == "test"
def test_user_deactivate(self, mock_cis_user): hris_data = {} with open("tests/fixture/workday.json") as fd: hris_data = json.load(fd) def side_effect(*args, **kwargs): fake_user = cis_profile.User(user_id=args[0]) if args[0] == "ad|Mozilla-LDAP|community": fake_user.staff_information.staff.value = False else: fake_user.staff_information.staff.value = True return fake_user mock_cis_user.side_effect = side_effect hris = cis_publisher.hris.HRISPublisher() publisher = cis_publisher.Publish([], login_method="ad", publisher_name="hris") # we pass 2 fake users, ndonna is in the fixture, nolongerexist is not in the fixture but "CIS" "has it" publisher.known_cis_users_by_user_id = { "ad|Mozilla-LDAP|NDonna": "*****@*****.**", "ad|Mozilla-LDAP|notexist": "*****@*****.**", "ad|Mozilla-LDAP|community": "*****@*****.**", } publisher.known_cis_users_by_email = { "*****@*****.**": "ad|Mozilla-LDAP|NDonna", "*****@*****.**": "ad|Mozilla-LDAP|notexist", "*****@*****.**": "ad|Mozilla-LDAP|community", } for uid in publisher.known_cis_users_by_user_id: p = cis_profile.User( user_id=uid, primary_email=publisher.known_cis_users_by_user_id[uid]) if uid == "ad|Mozilla-LDAP|community": p.staff_information.staff.value = False else: p.staff_information.staff.value = True publisher.all_known_profiles[uid] = p profiles = hris.convert_hris_to_cis_profiles( hris_data, publisher.known_cis_users_by_user_id, publisher.known_cis_users_by_email, user_ids=[ "ad|Mozilla-LDAP|NDonna", "ad|Mozilla-LDAP|notexist", "ad|Mozilla-LDAP|community" ], ) profiles = hris.deactivate_users(publisher, profiles, hris_data) # nolongerexist is returned by fake cis reply, but is not in hris workday fixture, so it should be active.value # = false # Community doesnt exist in HRIS but should not be touched so we should have 2 profiles back (ie community is # excluded) assert len(profiles) == 2 assert profiles[1].active.value is False assert profiles[1].primary_email.value == "*****@*****.**" assert profiles[0].active.value is True assert profiles[0].primary_email.value == "*****@*****.**"
def test_post(self, mock_authzero, mock_secrets, mock_request_post): mock_authzero.return_value = "dinopark" mock_secrets.return_value = "is_pretty_cool" class FakeResponse: def ok(self): return True mock_request_post.return_value = FakeResponse() profiles = [cis_profile.User()] cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap")
def test_filter_cis_users(self, mock_authzero, mock_secrets, mock_request_get, mock_request_post): mock_authzero.return_value = "dinopark" mock_secrets.return_value = "is_pretty_cool" class FakeResponse: def __init__(self, fake={}): self.fake = fake self.text = str(fake) def json(self): return self.fake def ok(self): return True mock_request_post.return_value = FakeResponse() mu = [{ "user_id": "auser", "uuid": "0932493241", "primary_email": "*****@*****.**" }] mock_request_get.return_value = FakeResponse(fake=mu) profiles = [cis_profile.User()] profiles[0].user_id.value = "auser" profiles[0].first_name.value = "firstname" profiles[0].first_name.signature.publisher.name = "wrong" profiles[0].access_information.hris.values = {"test": "test"} profiles[0].access_information.hris.signature.publisher.name = "wrong" profiles[0].access_information.ldap.values = {"test": "test"} profiles[0].access_information.ldap.signature.publisher.name = "ldap" publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") publisher.filter_known_cis_users() # Should be filtered out because publisher = "wrong" assert publisher.profiles[0].first_name.value != "firstname" # Should not because publisher = "ldap" and thats "us" assert publisher.profiles[0].as_dict( )["access_information"]["ldap"]["values"] == { "test": "test" } # Should be filtered out because publisher = "wrong" assert publisher.profiles[0].as_dict( )["access_information"]["hris"]["values"] is None
def publish(self, user_ids=None): """ Glue to create or fetch cis_profile.User profiles for this publisher Then pass everything over to the Publisher class None, ALL profiles are sent. @user_ids: list of str - user ids to publish. If None, all users are published. """ logger.info("Starting LDAP Publisher") profiles_xz = self.fetch_from_s3() # If there are memory issues here, use lzma.LZMADecompressor() instead raw = lzma.decompress(profiles_xz) profiles_json = json.loads(raw) # Free some memory del profiles_xz del raw profiles = [] logger.info("Processing {} profiles".format(len(profiles_json))) for p in profiles_json: str_p = json.dumps(profiles_json[p]) if (user_ids is None) or (profiles_json[p]["user_id"]["value"] in user_ids): profiles.append(cis_profile.User(user_structure_json=str_p)) logger.info("Will publish {} profiles".format(len(profiles))) publisher = cis_publisher.Publish(profiles, publisher_name="ldap", login_method="ad") failures = [] try: publisher.filter_known_cis_users() failures = publisher.post_all(user_ids=user_ids) except Exception as e: logger.error( "Failed to post_all() LDAP profiles. Trace: {}".format( format_exc())) raise e if len(failures) > 0: logger.error("Failed to post {} profiles: {}".format( len(failures), failures))
def test_known_users(self, mock_request_get, mock_authzero, mock_secrets): mock_secrets.return_value = "hi" mock_authzero.return_value = "hi" class FakeResponse: def __init__(self, fake={}): self.fake = fake self.text = str(fake) def json(self): return self.fake def ok(self): return True mock_request_get.return_value = FakeResponse(fake=self.mu) profiles = [cis_profile.User()] publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") u = publisher.get_known_cis_users() assert u == self.mu["users"]
def dedup_changes(self, publisher, profiles): """ Remove profiles that have no HRIS changes so that we don't send them for no reason XXX this function is not currently used (check!) @publisher cis_publisher.Publisher object @profiles list of cis_profile.User """ dedup = [] for p in profiles: user_id = publisher.known_cis_users_by_email[p.primary_email.value] if user_id is None: dedup.append(p) continue cis_p = cis_profile.User(publisher.all_known_profiles[user_id]) if p.timezone.value is not None: if ((p.staff_information == cis_p.staff_information) and (p.access_information.hris == cis_p.access_information.hris) and (p.active == cis_p.active)): continue dedup.append(p) return dedup
def test_known_users_by_attribute(self, mock_request_get, mock_authzero, mock_secrets): mock_secrets.return_value = "hi" mock_authzero.return_value = "hi" class FakeResponse: def __init__(self, fake={}): self.fake = fake self.text = str(fake) def json(self): return self.fake def ok(self): return True mock_request_get.return_value = FakeResponse(fake=self.mu2) profiles = [cis_profile.User()] publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") attributes = {"staff_information.staff": True, "active": True} u = publisher.get_known_cis_user_by_attribute_paginated(attributes) print(u) assert u["auser"] == self.mu2["users"][0]
def test_wrong_publisher(self, fake_jwks): """ This verifies a wrong-publisher can't update it creates a valid user, then wrongly modify an attribute its not allowed to """ os.environ["CIS_CONFIG_INI"] = "tests/mozilla-cis-verify.ini" os.environ["AWS_XRAY_SDK_ENABLED"] = "false" os.environ["CIS_ENVIRONMENT"] = "local" os.environ["CIS_DYNALITE_PORT"] = self.dynalite_port os.environ["CIS_REGION_NAME"] = "us-east-1" os.environ["AWS_ACCESS_KEY_ID"] = "foo" os.environ["AWS_SECRET_ACCESS_KEY"] = "bar" os.environ["DEFAULT_AWS_REGION"] = "us-east-1" os.environ["CIS_VERIFY_SIGNATURES"] = "true" os.environ["CIS_VERIFY_PUBLISHERS"] = "true" from cis_change_service import api fake_new_user = FakeUser( config=FakeProfileConfig().minimal().no_display()) # Create a brand new user patched_user_profile = ensure_appropriate_publishers_and_sign( fake_new_user.as_dict(), self.publisher_rules, "create") # Ensure a first_name is set as we'll use that for testing patched_user_profile.first_name.value = "test" patched_user_profile.first_name.signature.publisher.name = "ldap" patched_user_profile.first_name.metadata.display = "public" patched_user_profile.sign_attribute("first_name", "ldap") f = FakeBearer() fake_jwks.return_value = json_form_of_pk token = f.generate_bearer_without_scope() api.app.testing = True self.app = api.app.test_client() result = self.app.post( "/v2/user", headers={"Authorization": "Bearer " + token}, data=json.dumps(patched_user_profile.as_json()), content_type="application/json", follow_redirects=True, ) response = json.loads(result.get_data()) assert result.status_code == 200 assert response["condition"] == "create" # sign first_name again but with wrong publisher (but same value as before) new_user = cis_profile.User(user_id=patched_user_profile.user_id.value) new_user.first_name = patched_user_profile.first_name new_user.sign_attribute("first_name", "access_provider") result = self.app.post( "/v2/user", headers={"Authorization": "Bearer " + token}, data=json.dumps(new_user.as_json()), content_type="application/json", follow_redirects=True, ) response = json.loads(result.get_data()) assert response["status_code"] == 202 # sign first_name again but with wrong publisher and different display (but same value as before) new_user = cis_profile.User(user_id=patched_user_profile.user_id.value) new_user.first_name = patched_user_profile.first_name new_user.first_name.metadata.display = "staff" new_user.sign_attribute("first_name", "access_provider") result = self.app.post( "/v2/user", headers={"Authorization": "Bearer " + token}, data=json.dumps(new_user.as_json()), content_type="application/json", follow_redirects=True, ) response = json.loads(result.get_data()) assert response["code"] == "invalid_publisher" # sign first_name again but with wrong publisher and wrong value (it should fail) new_user.first_name.value = "new-test" new_user.sign_attribute("first_name", "access_provider") result = self.app.post( "/v2/user", headers={"Authorization": "Bearer " + token}, data=json.dumps(new_user.as_json()), content_type="application/json", follow_redirects=True, ) response = json.loads(result.get_data()) assert result.status_code != 200
def convert_hris_to_cis_profiles(self, hris_data): """ @hris_data list dict of HRIS data returns: cis_profile.Profile """ def tz_convert(hris_tz): tzmap = { "GMT United Kingdom Time (London)": "UTC+0000 Europe/London", "GMT Western European Time (Casablanca)": "UTC+0100 Africa/Casablanca", "GMT+01:00 Central European Time (Amsterdam)": "UTC+0200 Europe/Amsterdam", "GMT+01:00 Central European Time (Berlin)": "UTC+01:00 Europe/Berlin", "GMT+01:00 Central European Time (Oslo)": "UTC+01:00 Europe/Oslo", "GMT+01:00 Central European Time (Paris)": "UTC+01:00 Europe/Paris", "GMT+01:00 Central European Time (Prague)": "UTC+01:00 Europe/Prague", "GMT+01:00 Central European Time (Stockholm)": "UTC+01:00 Europe/Stockholm", "GMT+02:00 Eastern European Time (Athens)": "UTC+02:00 Europe/Athens", "GMT+02:00 Eastern European Time (Bucharest)": "UTC+02:00 Europe/Bucharest", "GMT+02:00 Eastern European Time (Helsinki)": "UTC+02:00 Europe/Helsinki", "GMT+02:00 South Africa Standard Time (Johannesburg)": "UTC+02:00 Africa/Johannesburg", "GMT+03:00 East Africa Time (Nairobi)": "UTC+03:00 Africa/Nairobi", "GMT+03:00 Moscow Standard Time (Moscow)": "UTC+03:00 Europe/Moscow", "GMT+05:00 Pakistan Standard Time (Karachi)": "UTC+05:00 Pakistan/Karachi", "GMT+05:30 India Standard Time (Kolkata)": "UTC+05:30 Asia/Kolkata", "GMT+07:00 Western Indonesia Time (Jakarta)": "UTC+07:00 Asia/Jakarta", "GMT+08:00 Australian Western Standard Time (Perth)": "UTC+08:00 Australia/Perth", "GMT+08:00 China Standard Time (Shanghai)": "UTC+08:00 Asia/Shanghai", "GMT+08:00 Taipei Standard Time (Taipei)": "UTC+08:00 Asia/Taipei", "GMT+09:00 Japan Standard Time (Tokyo)": "UTC+09:00 Asia/Tokyo", "GMT+10:00 Australian Eastern Standard Time (Brisbane)": "UTC+10:00 Australia/Brisbane", "GMT+12:00 New Zealand Time (Auckland)": "UTC+12:00 Pacific/Auckland", "GMT-03:00 Argentina Standard Time (Buenos Aires)": "UTC-0300 America/Buenos_Aires", "GMT-03:00 Brasilia Standard Time (Recife)": "UTC-0300 America/Recife", "GMT-04:00 Atlantic Time (Halifax)": "UTC-0400 America/Halifax", "GMT-05:00 Eastern Time": "UTC-0500 US/Eastern", "GMT-06:00 Central Standard Time (Regina)": "UTC-0600 America/Regina", "GMT-06:00 Central Time (Chicago)": "UTC-0600 America/Chicago", "GMT-06:00 Central Time": "UTC-0600 US/Central", "GMT-07:00 Mountain Time": "UTC-0700 US/Mountain", "GMT-08:00 Pacific Time (Los Angeles)": "UTC-0800 America/Los_Angeles", "GMT-08:00 Pacific Time (Tijuana)": "UTC-0800 America/Tijuana", "GMT-08:00 Pacific Time": "UTC-0800 US/Pacific", } try: tzmap[hris_tz] except KeyError: self.logger.warning( "Unknown timezone in workday, defaulting to UTC. Timezone from HRIS was" " {}.".format(hris_tz)) return "UTC+0000 Europe/London" return tzmap[hris_tz] def cost_center_convert(cc): """ Cost centers can have decimal points So it's a float """ return str(float(cc.split(" ")[0])) def strbool_convert(v): return v.lower() in ("yes", "true", "t", "1") user_array = [] for hruser in hris_data.get("Report_Entry"): p = cis_profile.User() # Note: Never use non-preferred names here p.primary_email.value = hruser.get("PrimaryWorkEmail") p.last_name.value = hruser.get("Preferred_Name_-_Last_Name") p.first_name.value = hruser.get("PreferredFirstName") p.timezone.value = tz_convert(hruser.get("Time_Zone")) p.staff_information.manager.value = strbool_convert( hruser.get("IsManager")) p.staff_information.director.value = strbool_convert( hruser.get("isDirectorOrAbove")) if len(hruser.get("EmployeeID")) > 0: p.staff_information.staff.value = True else: p.staff_information.staff.value = False p.staff_information.title.value = hruser.get("businessTitle") p.staff_information.team.value = hruser.get("Team") p.staff_information.cost_center.value = cost_center_convert( hruser.get("Cost_Center")) p.staff_information.worker_type.value = hruser.get("WorkerType") p.staff_information.wpr_desk_number.value = hruser.get( "WPRDeskNumber") p.staff_information.office_location.value = hruser.get( "LocationDescription") p.access_information.hris["values"]["employee_id"] = hruser.get( "EmployeeID") p.access_information.hris["values"]["worker_type"] = hruser.get( "WorkerType") p.access_information.hris["values"][ "manager_employee_id"] = hruser.get( "WorkersManagersEmployeeID") p.access_information.hris["values"][ "egencia_pos_country"] = hruser.get("EgenciaPOSCountry") # Typical required user values p.active.value = True p.initialize_timestamps() try: p.sign_all(publisher_name="hris") except Exception as e: self.logger.critical( "Profile data signing failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e)) try: p.validate() except Exception as e: self.logger.critical( "Profile schema validation failed for user {} - skipped validation, verification " "WILL FAIL({})".format(p.primary_email.value, e)) try: p.verify_all_publishers(cis_profile.User()) except Exception as e: self.logger.critical( "Profile signing failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e)) user_array.append(p) return user_array
def test_profile_validate(self): profiles = [cis_profile.User()] publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") publisher.validate()
def test_obj(self): profiles = [cis_profile.User()] publisher = cis_publisher.Publish(profiles, login_method="ad", publisher_name="ldap") assert isinstance(publisher, object)
def deactivate_users(self, publisher, profiles, report): """ Deactivate users present in CIS but not in HRIS @publisher cis_publisher.Publisher object @profiles list of cis_profile.User that were converted @report HRIS report """ user_ids_in_hris = [] for hruser in report.get("Report_Entry"): hruser_work_email = hruser.get("PrimaryWorkEmail").lower() try: current_user_id = publisher.known_cis_users_by_email[ hruser_work_email] except KeyError: logger.critical( "Repeated: There is no user_id in CIS Person API for HRIS User:{}" .format(hruser_work_email)) continue user_ids_in_hris.append(current_user_id) delta = set(publisher.known_cis_users_by_user_id.keys()) - set( user_ids_in_hris) # XXX this is a slow work-around to figure out who is staff # This data should eventually be directly queriable from Person API with a filter (this does not currently # exist) user_ids_to_deactivate = [] for potential_user_id in delta: if potential_user_id in publisher.all_known_profiles: profile = publisher.all_known_profiles[potential_user_id] # Convert as needed to a dict try: profile = profile.as_dict() except (TypeError, AttributeError): pass # Check if it has the attributes try: ldap_groups = profile["access_information"]["ldap"][ "values"] is_staff = profile["staff_information"]["staff"]["value"] # Not a staff user f these fields don't exist except KeyError: logger.debug( "staff_information.staff or access_information.ldap fields are null," " won't deactivate {}".format(potential_user_id)) continue if (is_staff is True) or (is_staff is False and (ldap_groups is not None and "admin_accounts" not in ldap_groups)): user_ids_to_deactivate.append(potential_user_id) if len(user_ids_to_deactivate) > 0: logger.info( "Will deactivate {} users because they're in CIS but not in HRIS" .format(user_ids_to_deactivate)) for user in user_ids_to_deactivate: logger.info("User selected for deactivation: {}".format(user)) # user from cis try: p = cis_profile.User(publisher.all_known_profiles[user]) except TypeError: p = publisher.all_known_profiles[user] # our partial update newp = cis_profile.User() newp.user_id = p.user_id newp.primary_email = p.primary_email newp.active.value = False newp.active.signature.publisher.name = "hris" try: newp.sign_attribute("active", publisher_name="hris") except Exception as e: logger.critical( "Profile data signing failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(newp.primary_email.value, e)) logger.debug("Profile data {}".format(newp.as_dict())) profiles.append(newp) return profiles
def convert_hris_to_cis_profiles(self, hris_data, user_ids=None): """ @hris_data list dict of HRIS data @user_ids list of user ids to convert returns: cis_profile.Profile """ def tz_convert(hris_tz): tzmap = { "GMT United Kingdom Time (London)": "UTC+0000 Europe/London", "GMT Western European Time (Casablanca)": "UTC+0100 Africa/Casablanca", "GMT+01:00 Central European Time (Amsterdam)": "UTC+0200 Europe/Amsterdam", "GMT+01:00 Central European Time (Berlin)": "UTC+01:00 Europe/Berlin", "GMT+01:00 Central European Time (Oslo)": "UTC+01:00 Europe/Oslo", "GMT+01:00 Central European Time (Paris)": "UTC+01:00 Europe/Paris", "GMT+01:00 Central European Time (Prague)": "UTC+01:00 Europe/Prague", "GMT+01:00 Central European Time (Stockholm)": "UTC+01:00 Europe/Stockholm", "GMT+02:00 Eastern European Time (Athens)": "UTC+02:00 Europe/Athens", "GMT+02:00 Eastern European Time (Bucharest)": "UTC+02:00 Europe/Bucharest", "GMT+02:00 Eastern European Time (Helsinki)": "UTC+02:00 Europe/Helsinki", "GMT+02:00 South Africa Standard Time (Johannesburg)": "UTC+02:00 Africa/Johannesburg", "GMT+03:00 East Africa Time (Nairobi)": "UTC+03:00 Africa/Nairobi", "GMT+03:00 Moscow Standard Time (Moscow)": "UTC+03:00 Europe/Moscow", "GMT+05:00 Pakistan Standard Time (Karachi)": "UTC+05:00 Pakistan/Karachi", "GMT+05:30 India Standard Time (Kolkata)": "UTC+05:30 Asia/Kolkata", "GMT+07:00 Western Indonesia Time (Jakarta)": "UTC+07:00 Asia/Jakarta", "GMT+08:00 Australian Western Standard Time (Perth)": "UTC+08:00 Australia/Perth", "GMT+08:00 China Standard Time (Shanghai)": "UTC+08:00 Asia/Shanghai", "GMT+08:00 Taipei Standard Time (Taipei)": "UTC+08:00 Asia/Taipei", "GMT+09:00 Japan Standard Time (Tokyo)": "UTC+09:00 Asia/Tokyo", "GMT+10:00 Australian Eastern Standard Time (Brisbane)": "UTC+10:00 Australia/Brisbane", "GMT+12:00 New Zealand Time (Auckland)": "UTC+12:00 Pacific/Auckland", "GMT-03:00 Argentina Standard Time (Buenos Aires)": "UTC-0300 America/Buenos_Aires", "GMT-03:00 Brasilia Standard Time (Recife)": "UTC-0300 America/Recife", "GMT-04:00 Atlantic Time (Halifax)": "UTC-0400 America/Halifax", "GMT-05:00 Eastern Time": "UTC-0500 US/Eastern", "GMT-06:00 Central Standard Time (Regina)": "UTC-0600 America/Regina", "GMT-06:00 Central Time (Chicago)": "UTC-0600 America/Chicago", "GMT-06:00 Central Time": "UTC-0600 US/Central", "GMT-07:00 Mountain Time": "UTC-0700 US/Mountain", "GMT-08:00 Pacific Time (Los Angeles)": "UTC-0800 America/Los_Angeles", "GMT-08:00 Pacific Time (Tijuana)": "UTC-0800 America/Tijuana", "GMT-08:00 Pacific Time": "UTC-0800 US/Pacific", } try: tzmap[hris_tz] except KeyError: logger.warning( "Unknown timezone in workday, defaulting to UTC. Timezone from HRIS was" " {}.".format(hris_tz) ) return "UTC+0000 Europe/London" return tzmap[hris_tz] def cost_center_convert(cc): """ Cost centers can have decimal points So it's a float """ return str(float(cc.split(" ")[0])) def strbool_convert(v): return v.lower() in ("yes", "true", "t", "1") user_array = [] for hruser in hris_data.get("Report_Entry"): # Attempt a rough guess at the user-id. this may not match all user ids correctly # We assume this is ok as user_ids are only passed for testing purposes or fixing purposes if user_ids is not None: if "ad|Mozilla-LDAP|" + hruser.get("PrimaryWorkEmail").split("@")[0] not in user_ids: if "ad|Mozilla-LDAP-Dev|" + hruser.get("PrimaryWorkEmail").split("@")[0] not in user_ids: # Skip user continue p = cis_profile.User() # Note: Never use non-preferred names here # Uncomment when LDAP is no longer the creator for these # When that happens, code to disable the user will also be necessary! # p.active.value = True # p.last_name.value = hruser.get("Preferred_Name_-_Last_Name") # p.last_name.signature.publisher.name = "hris" # p.first_name.value = hruser.get("PreferredFirstName") # p.first_name.signature.publisher.name = "hris" p.primary_email.value = hruser.get("PrimaryWorkEmail") p.primary_email.signature.publisher.name = "hris" p.timezone.value = tz_convert(hruser.get("Time_Zone")) p.timezone.signature.publisher.name = "hris" p.timezone.metadata.display = "staff" p.staff_information.manager.value = strbool_convert(hruser.get("IsManager")) p.staff_information.manager.signature.publisher.name = "hris" p.staff_information.manager.metadata.display = "staff" p.staff_information.director.value = strbool_convert(hruser.get("isDirectorOrAbove")) p.staff_information.director.signature.publisher.name = "hris" p.staff_information.director.metadata.display = "staff" if len(hruser.get("EmployeeID")) > 0: p.staff_information.staff.value = True else: p.staff_information.staff.value = False p.staff_information.staff.signature.publisher.name = "hris" p.staff_information.staff.metadata.display = "staff" p.staff_information.title.value = hruser.get("businessTitle") p.staff_information.title.signature.publisher.name = "hris" p.staff_information.title.metadata.display = "staff" p.staff_information.team.value = hruser.get("Team") p.staff_information.team.signature.publisher.name = "hris" p.staff_information.team.metadata.display = "staff" p.staff_information.cost_center.value = cost_center_convert(hruser.get("Cost_Center")) p.staff_information.cost_center.signature.publisher.name = "hris" p.staff_information.cost_center.metadata.display = "staff" p.staff_information.worker_type.value = hruser.get("WorkerType") p.staff_information.worker_type.signature.publisher.name = "hris" p.staff_information.worker_type.metadata.display = "staff" p.staff_information.wpr_desk_number.value = hruser.get("WPRDeskNumber") p.staff_information.wpr_desk_number.signature.publisher.name = "hris" p.staff_information.wpr_desk_number.metadata.display = "staff" p.staff_information.office_location.value = hruser.get("LocationDescription") p.staff_information.office_location.signature.publisher.name = "hris" p.staff_information.office_location.metadata.display = "staff" p.access_information.hris["values"] = {} p.access_information.hris.signature.publisher.name = "hris" p.access_information.hris["values"]["employee_id"] = hruser.get("EmployeeID") p.access_information.hris["values"]["worker_type"] = hruser.get("WorkerType") p.access_information.hris["values"]["primary_work_email"] = hruser.get("PrimaryWorkEmail") p.access_information.hris["values"]["managers_primary_work_email"] = hruser.get( "Worker_s_Manager_s_Email_Address" ) p.access_information.hris["values"]["egencia_pos_country"] = hruser.get("EgenciaPOSCountry") try: p.sign_all(publisher_name="hris") except Exception as e: logger.critical( "Profile data signing failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e) ) logger.debug("Profile data {}".format(p.as_dict())) try: p.validate() except Exception as e: logger.critical( "Profile schema validation failed for user {} - skipped validation, verification " "WILL FAIL({})".format(p.primary_email.value, e) ) logger.debug("Profile data {}".format(p.as_dict())) try: p.verify_all_publishers(cis_profile.User()) except Exception as e: logger.critical( "Profile publisher verification failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e) ) logger.debug("Profile data {}".format(p.as_dict())) logger.info("Processed (signed and verified) HRIS report's user {}".format(p.primary_email.value)) user_array.append(p) return user_array
def convert_hris_to_cis_profiles(self, hris_data, cis_users_by_user_id, cis_users_by_email, user_ids): """ @hris_data list dict of HRIS data @cis_users_by_user_id dict of Person API known users by user_id=>email @cis_users_by_email dict of Person API known users by email=>user_id to convert to user_ids @user_ids list of user ids to convert returns: list of cis_profile.Profile """ def tz_convert(hris_tz): tzmap = { "GMT United Kingdom Time (London)": "UTC+0000 Europe/London", "GMT Western European Time (Casablanca)": "UTC+0100 Africa/Casablanca", "GMT+01:00 Central European Time (Amsterdam)": "UTC+0200 Europe/Amsterdam", "GMT+01:00 Central European Time (Berlin)": "UTC+01:00 Europe/Berlin", "GMT+01:00 Central European Time (Oslo)": "UTC+01:00 Europe/Oslo", "GMT+01:00 Central European Time (Paris)": "UTC+01:00 Europe/Paris", "GMT+01:00 Central European Time (Prague)": "UTC+01:00 Europe/Prague", "GMT+01:00 Central European Time (Stockholm)": "UTC+01:00 Europe/Stockholm", "GMT+02:00 Eastern European Time (Athens)": "UTC+02:00 Europe/Athens", "GMT+02:00 Eastern European Time (Bucharest)": "UTC+02:00 Europe/Bucharest", "GMT+02:00 Eastern European Time (Helsinki)": "UTC+02:00 Europe/Helsinki", "GMT+02:00 South Africa Standard Time (Johannesburg)": "UTC+02:00 Africa/Johannesburg", "GMT+03:00 East Africa Time (Nairobi)": "UTC+03:00 Africa/Nairobi", "GMT+03:00 Moscow Standard Time (Moscow)": "UTC+03:00 Europe/Moscow", "GMT+05:00 Pakistan Standard Time (Karachi)": "UTC+05:00 Pakistan/Karachi", "GMT+05:30 India Standard Time (Kolkata)": "UTC+05:30 Asia/Kolkata", "GMT+07:00 Western Indonesia Time (Jakarta)": "UTC+07:00 Asia/Jakarta", "GMT+08:00 Australian Western Standard Time (Perth)": "UTC+08:00 Australia/Perth", "GMT+08:00 China Standard Time (Shanghai)": "UTC+08:00 Asia/Shanghai", "GMT+08:00 Taipei Standard Time (Taipei)": "UTC+08:00 Asia/Taipei", "GMT+09:00 Japan Standard Time (Tokyo)": "UTC+09:00 Asia/Tokyo", "GMT+10:00 Australian Eastern Standard Time (Brisbane)": "UTC+10:00 Australia/Brisbane", "GMT+12:00 New Zealand Time (Auckland)": "UTC+12:00 Pacific/Auckland", "GMT-03:00 Argentina Standard Time (Buenos Aires)": "UTC-0300 America/Buenos_Aires", "GMT-03:00 Brasilia Standard Time (Recife)": "UTC-0300 America/Recife", "GMT-04:00 Atlantic Time (Halifax)": "UTC-0400 America/Halifax", "GMT-05:00 Eastern Time": "UTC-0500 US/Eastern", "GMT-06:00 Central Standard Time (Regina)": "UTC-0600 America/Regina", "GMT-06:00 Central Time (Chicago)": "UTC-0600 America/Chicago", "GMT-06:00 Central Time": "UTC-0600 US/Central", "GMT-07:00 Mountain Time": "UTC-0700 US/Mountain", "GMT-08:00 Pacific Time (Los Angeles)": "UTC-0800 America/Los_Angeles", "GMT-08:00 Pacific Time (Tijuana)": "UTC-0800 America/Tijuana", "GMT-08:00 Pacific Time": "UTC-0800 US/Pacific", } try: tzmap[hris_tz] except KeyError: logger.warning( "Unknown timezone in workday, defaulting to UTC. Timezone from HRIS was" " {}.".format(hris_tz)) return "UTC+0000 Europe/London" return tzmap[hris_tz] def cost_center_convert(cc): """ Cost centers can have decimal points So it's a float """ return str(float(cc.split(" ")[0])) def strbool_convert(v): return v.lower() in ("yes", "true", "t", "1") # Convert user_array = [] for hruser in hris_data.get("Report_Entry"): hruser_work_email = hruser.get("PrimaryWorkEmail").lower() logger.debug( "filtering fields for user email {}".format(hruser_work_email)) # NOTE: # The HRIS setup will DELETE users when they need to be deactivated and removed. It will set INACTIVE when # users may not yet be provisioned. This is important as if this changes the code here would have to also # change. hruser_active_state = int(hruser.get("CurrentlyActive")) if hruser_active_state == 0: logger.debug( "User {} is currently set to inactive in HRIS Report, skipping integration" .format(hruser_work_email)) continue current_user_id = None try: current_user_id = cis_users_by_email[hruser_work_email] except KeyError: logger.critical( "There is no user_id in CIS Person API for HRIS User {}." " This user may not be created by HRIS yet?".format( hruser_work_email)) continue user_ids_lower_case = [x.lower() for x in user_ids] if current_user_id.lower() not in user_ids_lower_case: # Skip this user, it's not in the list requested to convert logger.debug( "skipping user {}, not in requested conversion list". format(current_user_id)) continue p = cis_profile.User() # Note: Never use non-preferred names here # p.last_name.value = hruser.get("Preferred_Name_-_Last_Name") # p.last_name.signature.publisher.name = "hris" # p.first_name.value = hruser.get("PreferredFirstName") # p.first_name.signature.publisher.name = "hris" p.active.value = True p.active.signature.publisher.name = "hris" p.primary_email.value = hruser_work_email p.primary_email.signature.publisher.name = "hris" p.timezone.value = tz_convert(hruser.get("Time_Zone")) p.timezone.signature.publisher.name = "hris" p.timezone.metadata.display = "staff" p.staff_information.manager.value = strbool_convert( hruser.get("IsManager")) p.staff_information.manager.signature.publisher.name = "hris" p.staff_information.manager.metadata.display = "staff" p.staff_information.director.value = strbool_convert( hruser.get("isDirectorOrAbove")) p.staff_information.director.signature.publisher.name = "hris" p.staff_information.director.metadata.display = "staff" if len(hruser.get("EmployeeID")) > 0: p.staff_information.staff.value = True else: p.staff_information.staff.value = False p.staff_information.staff.signature.publisher.name = "hris" p.staff_information.staff.metadata.display = "staff" p.staff_information.title.value = hruser.get("businessTitle") p.staff_information.title.signature.publisher.name = "hris" p.staff_information.title.metadata.display = "staff" p.staff_information.team.value = hruser.get("Team") p.staff_information.team.signature.publisher.name = "hris" p.staff_information.team.metadata.display = "staff" p.staff_information.cost_center.value = cost_center_convert( hruser.get("Cost_Center")) p.staff_information.cost_center.signature.publisher.name = "hris" p.staff_information.cost_center.metadata.display = "staff" p.staff_information.worker_type.value = hruser.get("WorkerType") p.staff_information.worker_type.signature.publisher.name = "hris" p.staff_information.worker_type.metadata.display = "staff" p.staff_information.wpr_desk_number.value = hruser.get( "WPRDeskNumber") p.staff_information.wpr_desk_number.signature.publisher.name = "hris" p.staff_information.wpr_desk_number.metadata.display = "staff" p.staff_information.office_location.value = hruser.get( "LocationDescription") p.staff_information.office_location.signature.publisher.name = "hris" p.staff_information.office_location.metadata.display = "staff" p.access_information.hris["values"] = {} p.access_information.hris.signature.publisher.name = "hris" p.access_information.hris["values"]["employee_id"] = hruser.get( "EmployeeID") p.access_information.hris["values"]["worker_type"] = hruser.get( "WorkerType") p.access_information.hris["values"][ "primary_work_email"] = hruser_work_email p.access_information.hris["values"][ "managers_primary_work_email"] = hruser.get( "Worker_s_Manager_s_Email_Address") p.access_information.hris["values"][ "egencia_pos_country"] = hruser.get("EgenciaPOSCountry") try: p.sign_all(publisher_name="hris") except Exception as e: logger.critical( "Profile data signing failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e)) logger.debug("Profile data {}".format(p.as_dict())) try: p.validate() except Exception as e: logger.critical( "Profile schema validation failed for user {} - skipped validation, verification " "WILL FAIL({})".format(p.primary_email.value, e)) logger.debug("Profile data {}".format(p.as_dict())) try: p.verify_all_publishers(cis_profile.User()) except Exception as e: logger.critical( "Profile publisher verification failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e)) logger.debug("Profile data {}".format(p.as_dict())) logger.info( "Processed (signed and verified) HRIS report's user {}".format( p.primary_email.value)) user_array.append(p) return user_array
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)
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 convert_az_users(self, az_users): """ Convert a list of auth0 user fields to cis_profile Users @az_users list of dicts with user attributes Returns [cis_profile.Users] """ profiles = [] logger.info( "Converting auth0 users into CIS Profiles ({} user(s))".format( len(az_users))) for u in az_users: p = cis_profile.User() # Must have fields p.user_id.value = u["user_id"] p.user_id.signature.publisher.name = "access_provider" p.update_timestamp("user_id") if "blocked" in u.keys(): if u["blocked"]: p.active.value = False else: p.active.value = True p.active.signature.publisher.name = "access_provider" p.update_timestamp("active") p.primary_email.value = u["email"] p.primary_email.metadata.display = "private" p.primary_email.signature.publisher.name = "access_provider" p.update_timestamp("primary_email") try: p.login_method.value = u["identities"][0]["connection"] p.update_timestamp("login_method") except IndexError: logger.critical( "Could not find login method for user {}, skipping integration" .format(p.user_id.value)) continue # Should have fields (cannot be "None" or "" but can be " ") tmp = u.get( "given_name", u.get("name", u.get("family_name", u.get("nickname", " ")))) p.first_name.value = tmp p.first_name.metadata.display = "private" p.first_name.signature.publisher.name = "access_provider" p.update_timestamp("first_name") tmp = u.get("family_name", " ") p.last_name.value = tmp p.last_name.metadata.display = "private" p.last_name.signature.publisher.name = "access_provider" p.update_timestamp("last_name") # May have fields (its ok if these are not set) tmp = u.get("node_id", None) if tmp is not None: p.identities.github_id_v4.value = tmp p.identities.github_id_v4.display = "private" p.identities.github_id_v4.signature.publisher.name = "access_provider" p.update_timestamp("identities.github_id_v4") if "identities" in u.keys(): for ident in u["identities"]: if ident.get( "provider") in self.az_blacklisted_connections: logger.warning( "ad/LDAP account returned from search - this should not happen. User will be skipped." " User_id: {}".format(p.user_id.value)) continue elif ident.get("provider") == "google-oauth2": p.identities.google_oauth2_id.value = ident.get( "user_id") p.identities.google_oauth2_id.metadata.display = "private" p.identities.google_oauth2_id.signature.publisher.name = "access_provider" p.update_timestamp("identities.google_oauth2_id") p.identities.google_primary_email.value = p.primary_email.value p.identities.google_primary_email.metadata.display = "private" p.identities.google_primary_email.signature.publisher.name = "access_provider" p.update_timestamp("identities.google_primary_email") elif ident.get("provider") == "oauth2" and ident.get( "connection") == "firefoxaccounts": p.identities.firefox_accounts_id.value = ident.get( "user_id") p.identities.firefox_accounts_id.metadata.display = "private" p.identities.firefox_accounts_id.signature.publisher.name = "access_provider" p.update_timestamp("identities.firefox_accounts_id") p.identities.firefox_accounts_primary_email.value = p.primary_email.value p.identities.firefox_accounts_primary_email.metadata.display = "private" p.identities.firefox_accounts_primary_email.signature.publisher.name = "access_provider" p.update_timestamp( "identities.firefox_accounts_primary_email") elif ident.get("provider") == "github": p.identities.github_id_v3.value = ident.get("user_id") p.identities.github_id_v3.metadata.display = "private" p.identities.github_id_v3.signature.publisher.name = "access_provider" p.update_timestamp("identities.github_id_v3") if "profileData" in ident.keys(): p.identities.github_primary_email.value = ident[ "profileData"].get("email") p.identities.github_primary_email.metadata.verified = ident[ "profileData"].get("email_verified", False) p.identities.github_primary_email.metadata.display = "private" p.identities.github_primary_email.signature.publisher.name = "access_provider" p.update_timestamp( "identities.github_primary_email") p.identities.github_id_v4.value = ident[ "profileData"].get("node_id") p.identities.github_id_v4.metadata.display = "private" p.identities.github_id_v4.signature.publisher.name = "access_provider" p.update_timestamp("identities.github_id_v4") # Sign and verify everything try: p.sign_all(publisher_name="access_provider") except Exception as e: logger.critical( "Profile data signing failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e)) logger.debug("Profile data {}".format(p.as_dict())) try: p.validate() except Exception as e: logger.critical( "Profile schema validation failed for user {} - skipped validation, verification " "WILL FAIL({})".format(p.primary_email.value, e)) logger.debug("Profile data {}".format(p.as_dict())) try: p.verify_all_publishers(cis_profile.User()) except Exception as e: logger.critical( "Profile publisher verification failed for user {} - skipped signing, verification " "WILL FAIL ({})".format(p.primary_email.value, e)) logger.debug("Profile data {}".format(p.as_dict())) logger.debug( "Profile signed and ready to publish for user_id {}".format( p.user_id.value)) profiles.append(p) logger.info( "All profiles in this request were converted to CIS Profiles") return profiles