diff --git a/greg/management/commands/import_sponsors_from_cerebrum.py b/greg/management/commands/import_sponsors_from_cerebrum.py new file mode 100644 index 0000000000000000000000000000000000000000..338592b682a717158088092b6f4f2acd8d89fd7a --- /dev/null +++ b/greg/management/commands/import_sponsors_from_cerebrum.py @@ -0,0 +1,212 @@ +""" +Fetch all OUs from OrgReg and add the complete tree to Greg. + +Ignores OrganizationalUnits without identifiers with source and name matching global +variables ORGREG_SOURCE and ORGREG_NAME + +Assumes that the header used for authentication is of the type +'X-Gravitee-Api-Key': 'token'. + +If the path to the endpoint of the OUs is oregreg/v3/ou/ you want to give +orgreg/v3/ as the url argument (note the trailing slash). +""" +from typing import Optional, Tuple +import cerebrum_client + +import structlog + +from cerebrum_client import CerebrumClient +from django.conf import settings +from django.core.management.base import BaseCommand + +from greg.models import OrganizationalUnit, Sponsor, SponsorOrganizationalUnit + +logger = structlog.getLogger(__name__) + + +class Command(BaseCommand): + help = __doc__ + + CEREBRUM_SOURCE = "cerebrum" + CEREBRUM_VALID_SOURCE_SYSTEM = ["DFO_SAP"] + CEREBRUM_FEIDE_INST = "uio.no" + CEREBRUM_NAME_SOURCE_PRIORITY = ["Cached", "Override", "DFO_SAP", "FS", "Manual"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.client = CerebrumClient(**settings.CEREBRUM_CLIENT) + + def _has_active_dfo_aff(self, person_id: str): + """Check that a person has a valid employee affiliation from DFØ.""" + + dfo_employee_affs = [ + x + for x in self.client.get_person_affiliations(person_id) + if x.source_system == "DFO_SAP" and x.affiliation == "ANSATT" + ] + + return len(dfo_employee_affs) > 0 + + def _upsert_sponsor_unit_link(self, sponsor: Sponsor, unit: OrganizationalUnit): + """Ensure a link between sponsor and unit.""" + try: + sunit = SponsorOrganizationalUnit.objects.get( + sponsor=sponsor, + organizational_unit=unit, + source=self.CEREBRUM_SOURCE, + automatic=True, + ) + logger.info("sponsor_ou_link_found", sponsor=sponsor.id, sunit=sunit.id) + except SponsorOrganizationalUnit.DoesNotExist: + sunit = SponsorOrganizationalUnit.objects.create( + sponsor=sponsor, + organizational_unit=unit, + hierarchical_access=settings.CEREBRUM_HIERARCHICAL_ACCESS, + automatic=True, + source=self.CEREBRUM_SOURCE, + ) + logger.info("sponsor_ou_link_create", sponsor=sponsor.id, sunit=sunit.id) + return SponsorOrganizationalUnit.objects.get(id=sunit.id) + + def _remove_sponsor_unit_link(self, sunit: SponsorOrganizationalUnit): + logger.info("sponsor_ou_deleted", sunit=sunit.id) + sunit.delete() + + def _get_person_name( + self, person: cerebrum_client.models.Person + ) -> Tuple[Optional[str], Optional[str]]: + """Get a persons chosen name.""" + first_names = {x.source_system: x for x in person.names if x.variant == "FIRST"} + last_names = {x.source_system: x for x in person.names if x.variant == "LAST"} + + for source_system in self.CEREBRUM_NAME_SOURCE_PRIORITY: + if source_system in first_names and source_system in last_names: + return first_names[source_system].name, last_names[source_system].name + + return None, None + + def _get_feide_id(self, person_id: str) -> Optional[str]: + """Infer the feide id from the primary user.""" + primary_uname = self.client.get_person_primary_account_name(person_id) + + if primary_uname: + return f"{primary_uname}@{self.CEREBRUM_FEIDE_INST}" + return None + + def _upsert_sponsor_from_cerebrum( + self, person_id: str, unit: OrganizationalUnit + ) -> Optional[Sponsor]: + """Insert or update a sponsor from Cerebum data.""" + # logger.bind(cerebrum_person_id=person_id) + + person = self.client.get_person(person_id) + if not person: + logger.warning("cerebrum_person_missing", cerebrum_person_id=person_id) + return None + + if not self._has_active_dfo_aff(person_id): + logger.warning("cerebrum_not_an_employee", cerebrum_person_id=person_id) + return None + + feide_id = self._get_feide_id(person_id) + if not feide_id: + logger.warning("cerebrum_no_primary_account", cerebrum_person_id=person_id) + return None + + # log = log.bind(feide_id=feide_id) + + first_name, last_name = self._get_person_name(person) + if not first_name or not last_name: + logger.warning("cerebrum_no_valid_name", cerebrum_person_id=person_id) + return None + + try: + sponsor = Sponsor.objects.get(feide_id=feide_id) + sponsor.first_name = first_name + sponsor.last_name = last_name + sponsor.save() + logger.info( + "sponsor_updated", sponsor=sponsor.id, cerebrum_person_id=person_id + ) + + except Sponsor.DoesNotExist: + sponsor = Sponsor.objects.create( + first_name=first_name, + last_name=last_name, + feide_id=feide_id, + ) + logger.info( + "sponsor_created", sponsor=sponsor.id, cerebrum_person_id=person_id + ) + return Sponsor.objects.get(id=sponsor.id) + + def handle(self, *args, **options): + """Import of Sponsors from Cerebrum.""" + active_units = OrganizationalUnit.objects.filter( + active=True, + deleted=False, + ) + + logger.info("import_start", nr_of_units=len(active_units)) + for unit in active_units: + logger.bind(unit=unit.id) + sko = unit.identifiers.filter(name="legacy_stedkode").first() + if not sko: + logger.warning("orgreg_unit_missing_legacy_stedkode") + continue + logger.bind(legacy_stedkode=sko.value) + + current_sponsors = unit.link_unit.filter( + automatic=True, source=self.CEREBRUM_SOURCE + ).all() + + group_name = f"adm-leder-{sko.value}" + group = self.client.get_group(group_name) + if not group: + # No group in cererbum, remove sponsors. + logger.info( + "cerebrum_group_not_found", + unit_id=unit.id, + cerebrum_group=group_name, + ) + for sponsor in current_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + continue + + if group.expire_date: + # Group is expired, remove sponsors + logger.info( + "cerebrum_group_expired", + unit_id=unit.id, + cerebrum_group=group_name, + ) + for sponsor in current_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + continue + + group_members = list(self.client.list_group_members(group.id)) + if not group_members: + # No members in group, remove sponsors + logger.info( + "cerebrum_group_empty", unit_id=unit.id, cerebrum_group=group_name + ) + for sponsor in current_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + continue + + cerebrum_sponsors = set() + for member in group_members: + if member.type == "person": + sponsor = self._upsert_sponsor_from_cerebrum(member.id, unit) + if sponsor: + sponsor_link = self._upsert_sponsor_unit_link( + sponsor=sponsor, unit=unit + ) + cerebrum_sponsors.add(sponsor_link) + + non_cerebrum_sponsors = set(current_sponsors) - cerebrum_sponsors + + for non_cerebrum_sponsor in non_cerebrum_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + + logger.info("import_end") diff --git a/gregsite/settings/dev.py b/gregsite/settings/dev.py index cae8f7159b5c2e54c6fb0c5f6b8cf66d800f18d0..e989299f3e37394bcb514220b71ec4a9ba6d2d1a 100644 --- a/gregsite/settings/dev.py +++ b/gregsite/settings/dev.py @@ -21,6 +21,7 @@ CEREBRUM_CLIENT = { "url": "https://example.com/fake/", "headers": {"X-Gravitee-Api-Key": "bar"}, } +CEREBRUM_HIERARCHICAL_ACCESS = True Q_CLUSTER = { "name": "greg",