def create_new_adconnection(self, adconnection_alias, make_default=False, description=""): aliases = self.get_adconnection_aliases() if adconnection_alias in aliases: logger.error('Azure AD connection alias %s is already listed in UCR %s.', adconnection_alias, adconnection_alias_ucrv) return None target_path = os.path.join(ADCONNECTION_CONF_BASEPATH, adconnection_alias) if os.path.exists(target_path): logger.error('Path %s already exists, but no UCR configuration for the Azure AD connection was found.', target_path) return None os.mkdir(target_path, 0o700) os.chown(target_path, pwd.getpwnam('listener').pw_uid, 0) for filename in ('cert.fp', 'cert.pem', 'key.pem'): src = os.path.join(ADCONNECTION_CONF_BASEPATH, filename) shutil.copy2(src, target_path) os.chown(os.path.join(target_path, filename), pwd.getpwnam('listener').pw_uid, 0) AzureAuth.uninitialize(adconnection_alias) ucrv = ['{}{}=uninitialized'.format(adconnection_alias_ucrv, adconnection_alias)] if make_default: ucrv.append('{}={}'.format(default_adconnection_alias_ucrv, adconnection_alias)) handler_set(ucrv) UDMHelper.create_udm_adconnection(adconnection_alias, description) self.configure_wizard_for_adconnection(adconnection_alias) self.listener_restart()
def __init__(self, listener, name, attrs, ldap_cred, dn, adconnection_alias=None): """ :param listener: listener object or None :param name: str, prepend to log messages :param attrs: {"listener": [attributes, listener, listens, on], ... } :param ldap_cred: {ldapserver: FQDN, binddn: cn=admin,$ldap_base, basedn: $ldap_base, bindpw: s3cr3t} or None :param dn of LDAP object to work on """ self.listener = listener self.attrs = attrs self.udm = UDMHelper(ldap_cred, adconnection_alias) # self.ldap_cred = ldap_cred self.dn = dn self.adconnection_alias = adconnection_alias logger.debug('adconnection_alias=%r', adconnection_alias) if self.listener: self.ucr = self.listener.configRegistry else: # allow use of this class outside listener from univention.config_registry import ConfigRegistry self.ucr = ConfigRegistry() self.ucr.load() self.not_migrated_to_v3 = self.ucr.is_false( 'office365/migrate/adconnectionalias') self.ah = AzureHandler(self.ucr, name, self.adconnection_alias)
def clean(): """ Remove univentionOffice365ObjectID and univentionOffice365Data from all user objects. """ adconnection_filter = get_adconnection_filter(listener.configRegistry, initialized_adconnection) logger.info( "Removing Office 365 ObjectID and Data from all users (adconnection_filter=%r)...", adconnection_filter) UDMHelper.clean_udm_objects("users/user", listener.configRegistry["ldap/base"], ldap_cred, adconnection_filter)
def remove_adconnection(self, adconnection_alias): aliases = self.get_adconnection_aliases() # Checks if adconnection_alias not in aliases: logger.error('Azure AD connection alias %s is not listed in UCR %s.', adconnection_alias, adconnection_alias_ucrv) return None target_path = os.path.join(ADCONNECTION_CONF_BASEPATH, adconnection_alias) if not os.path.exists(target_path): logger.info('Configuration files for the Azure AD connection in %s do not exist. Removing Azure AD connection anyway...', target_path) UDMHelper.remove_udm_adconnection(adconnection_alias) shutil.rmtree(target_path) ucrv_unset = '%s%s' % (adconnection_alias_ucrv, adconnection_alias) handler_unset([ucrv_unset]) self.listener_restart()
def load(dn): """ Load a subscription profile. :param dn: str: DN of profile :return: a SubscriptionProfile object """ profile = UDMHelper.get_udm_officeprofile(dn) logger.debug('loading profile: %r, with settings %r', dn, dict(profile)) return SubscriptionProfile( name=profile.get('name'), subscription=profile.get('subscription'), whitelisted_plans=profile.get('whitelisted_plans'), blacklisted_plans=profile.get('blacklisted_plans'))
def get_profiles_for_groups(cls, dns): """ Retrieve subscription profiles for groups. :param dns: list of group DNs [str, str, ..] :return: list of SubscriptionProfile objects """ # collect extended attribute values from groups profiles = list() for dn in dns: group = UDMHelper.get_udm_group(dn) try: profile = group['UniventionOffice365Profile'] if profile: profiles.append(profile) except KeyError: logger.debug('No Profile for group %r.', dn) pass # load SubscriptionProfiles return [cls.load(p) for p in profiles]
def handler(dn, new, old, command): logger.debug("%s.handler() command: %r dn: %r", name, command, dn) if not initialized_adconnection: raise RuntimeError( "{}.handler() Office 365 App not initialized for any AD connection yet, please run wizard." .format(name)) if command == 'r': save_old(old) return elif command == 'a': old = load_old(old) adconnection_aliases_old = set( old.get('univentionOffice365ADConnectionAlias', [])) adconnection_aliases_new = set( new.get('univentionOffice365ADConnectionAlias', [])) logger.info('adconnection_alias_old=%r adconnection_alias_new=%r', adconnection_aliases_old, adconnection_aliases_new) udm_helper = UDMHelper(ldap_cred) old_enabled = bool(int( old.get("univentionOffice365Enabled", ["0"])[0])) # "" when disabled, "1" when enabled if old_enabled: udm_user = udm_helper.get_udm_user(dn, old) enabled = not is_deactived_locked_or_expired(udm_user) logger.debug( "old was %s.", "enabled" if enabled else "deactivated, locked or expired") old_enabled &= enabled old_adconnection_enabled = adconnection_aliases_old.issubset( initialized_adconnection) logger.debug( "old Azure AD connection is %s.", "enabled" if old_adconnection_enabled else "not initialized") old_enabled &= old_adconnection_enabled new_enabled = bool(int(new.get("univentionOffice365Enabled", ["0"])[0])) if new_enabled: udm_user = udm_helper.get_udm_user(dn, new) enabled = not is_deactived_locked_or_expired(udm_user) logger.debug( "new is %s.", "enabled" if enabled else "deactivated, locked or expired") new_enabled &= enabled new_adconnection_enabled = adconnection_aliases_new.issubset( initialized_adconnection) logger.debug( "new Azure AD connection is %s.", "enabled" if new_adconnection_enabled else "not initialized") new_enabled &= new_adconnection_enabled logger.debug("new_enabled=%r old_enabled=%r", new_enabled, old_enabled) # # Add or remove user from AD connections -> delete and create # if new_enabled and old_enabled: logger.info( "new_enabled and adconnection_alias_old=%r and adconnection_alias_new=%r -> MODIFY (DELETE old, CREATE new) (%s)", adconnection_aliases_old, adconnection_aliases_new, dn) connections_to_be_deleted = adconnection_aliases_old - adconnection_aliases_new logger.info("DELETE (%s | %s)", connections_to_be_deleted, dn) for conn in connections_to_be_deleted: try: ol = Office365Listener(listener, name, _attrs, ldap_cred, dn, conn) delete_user(ol, dn, new, old) except NoIDsStored: logger.warn( 'Connection %r is not initialized, when trying to delete user %r. Ignoring.', conn, dn) connections_to_be_created = adconnection_aliases_new - adconnection_aliases_old logger.info("CREATE (%s | %s)", connections_to_be_created, dn) for conn in connections_to_be_created: try: ol = Office365Listener(listener, name, _attrs, ldap_cred, dn, conn) new_or_reactivate_user(ol, dn, new, old) except NoIDsStored: logger.warn( 'Connection %r is not initialized, when trying to create user %r. Ignoring.', conn, dn) # # NEW or REACTIVATED account # if new_enabled and not old_enabled: if listener.configRegistry.get( default_adconnection_alias_ucrv) in initialized_adconnection: # Migration script to App version 3 has not run - put user in default ad connection if not_migrated_to_v3: adconnection_aliases_new.add( listener.configRegistry.get( default_adconnection_alias_ucrv)) # If no connection is set for a newly enabled object, and a default connection is configured via UCR, # add that default to the new object if not adconnection_aliases_new and listener.configRegistry.get( default_adconnection_alias_ucrv): logger.info( "No ad connection defined, using default (%s | %s)", listener.configRegistry.get( default_adconnection_alias_ucrv), dn) adconnection_aliases_new.add( listener.configRegistry.get( default_adconnection_alias_ucrv)) else: if not_migrated_to_v3 or (not adconnection_aliases_new and listener.configRegistry.get( default_adconnection_alias_ucrv)): logger.info( "Cannot put user in default connection (%s), because it is not initialized", listener.configRegistry.get( default_adconnection_alias_ucrv)) return if not adconnection_aliases_new: logger.info("No ad connection defined for new object, do nothing") return logger.info( "new_enabled and not old_enabled -> NEW or REACTIVATED (%s | %s)", adconnection_aliases_new, dn) for conn in adconnection_aliases_new: ol = Office365Listener(listener, name, _attrs, ldap_cred, dn, conn) new_or_reactivate_user(ol, dn, new, old) return # # DELETE account # if old and not new: logger.info("old and not new -> DELETE (%s | %s)", adconnection_aliases_old, dn) for conn in adconnection_aliases_old: ol = Office365Listener(listener, name, _attrs, ldap_cred, dn, conn) delete_user(ol, dn, new, old) return # # DEACTIVATE account # if new and not new_enabled: logger.info("new and not new_enabled -> DEACTIVATE (%s | %s)", adconnection_aliases_old, dn) for conn in adconnection_aliases_old: ol = Office365Listener(listener, name, _attrs, ldap_cred, dn, conn) deactivate_user(ol, dn, new, old) return # # MODIFY account # if old_enabled and new_enabled: logger.info("old_enabled and new_enabled -> MODIFY (%s | %s)", adconnection_aliases_new, dn) for conn in adconnection_aliases_new: ol = Office365Listener(listener, name, _attrs, ldap_cred, dn, conn) modify_user(ol, dn, new, old) return
class Office365Listener(object): def __init__(self, listener, name, attrs, ldap_cred, dn, adconnection_alias=None): """ :param listener: listener object or None :param name: str, prepend to log messages :param attrs: {"listener": [attributes, listener, listens, on], ... } :param ldap_cred: {ldapserver: FQDN, binddn: cn=admin,$ldap_base, basedn: $ldap_base, bindpw: s3cr3t} or None :param dn of LDAP object to work on """ self.listener = listener self.attrs = attrs self.udm = UDMHelper(ldap_cred, adconnection_alias) # self.ldap_cred = ldap_cred self.dn = dn self.adconnection_alias = adconnection_alias logger.debug('adconnection_alias=%r', adconnection_alias) if self.listener: self.ucr = self.listener.configRegistry else: # allow use of this class outside listener from univention.config_registry import ConfigRegistry self.ucr = ConfigRegistry() self.ucr.load() self.not_migrated_to_v3 = self.ucr.is_false( 'office365/migrate/adconnectionalias') self.ah = AzureHandler(self.ucr, name, self.adconnection_alias) @property def verified_domains(self): # Use handler.get_verified_domain_from_disk() for user creation. return map(itemgetter("name"), self.ah.list_verified_domains()) def create_user(self, new): udm_attrs = self._get_sync_values(self.attrs["listener"], new) logger.debug("udm_attrs=%r adconnection_alias=%r", udm_attrs, self.adconnection_alias) attributes = dict() for k, v in udm_attrs.items(): azure_ldap_attribute_name = self.attrs["mapping"][k] if azure_ldap_attribute_name in attributes: # property exists already, value must be a list if not isinstance(attributes[azure_ldap_attribute_name], list): attributes[azure_ldap_attribute_name] = [ attributes[azure_ldap_attribute_name] ] # if value is a list extend, else append if isinstance(v, list): if any([ vv in attributes[azure_ldap_attribute_name] for vv in v ]): # avoid 400: "Request contains a property with duplicate values." continue list_method = list.extend else: if v in attributes[azure_ldap_attribute_name]: # avoid 400: "Request contains a property with duplicate values." continue list_method = list.append list_method(attributes[azure_ldap_attribute_name], v) else: attributes[azure_ldap_attribute_name] = v # mandatory attributes, not to be overwritten by user local_part_of_email_address = new["mailPrimaryAddress"][0].rpartition( "@")[0] mandatory_attributes = dict( immutableId=base64.b64encode(new["entryUUID"][0]), accountEnabled=True, passwordProfile=dict(password=self.ah.create_random_pw(), forceChangePasswordNextLogin=False), userPrincipalName="{0}@{1}".format( local_part_of_email_address, self.ah.get_verified_domain_from_disk()), mailNickname=local_part_of_email_address, displayName=attributes.get("displayName", "no name"), usageLocation=self._get_usage_location(new), ) attributes.update(mandatory_attributes) self.ah.create_user(attributes) user = self.ah.list_users(ofilter="userPrincipalName eq '{}'".format( attributes["userPrincipalName"])) if user["value"]: new_user = user["value"][0] else: raise RuntimeError( "Office365Listener.create_user() created user {!r} cannot be retrieved ({!r})." .format(attributes["userPrincipalName"], self.adconnection_alias)) try: self.assign_subscription(new, new_user) except AddLicenseError as exc: logger.warn( 'Could not add license for subscription %r to user %r: %s', exc.user_id, exc.sku_id, exc.message) self.ah.invalidate_all_tokens_for_user(new_user["objectId"]) return new_user def _object_id_from_attrs(self, old_or_new): """ Lookup objectId for adconnection_alias from either univentionOffice365ObjectID (pre v3) or univentionOffice365Data (v3) :param old_or_new: list: attributes of user or group object :raises: KeyError :return: string: object_id """ if self.not_migrated_to_v3: default_adconnection = self.ucr[ default_adconnection_alias_ucrv] or "defaultADconnection" azure_data = { default_adconnection: { "objectId": old_or_new["univentionOffice365ObjectID"][0], } } else: try: azure_data_encoded = old_or_new['univentionOffice365Data'][0] try: azure_data = self.decode_o365data(azure_data_encoded) or {} except Exception: azure_data = {} except (KeyError, IndexError): azure_data = {} # May throw KeyError: azure_connection_data = azure_data[self.adconnection_alias] object_id = azure_connection_data["objectId"] return object_id def _object_id_from_user_attrs_with_fallback_to_entryUUID(self, obj): try: object_id = self._object_id_from_attrs(obj) except KeyError: # Fallback to lookup by entryUUID object_id = self.find_aad_user_by_entryUUID(obj["entryUUID"][0]) return object_id def _object_id_from_udm_object(self, udm_obj): """ Lookup objectId for adconnection_alias from either univentionOffice365ObjectID (pre v3) or univentionOffice365Data (v3) :param udm_obj: UDM user or group object :raises: KeyError :return: string: object_id """ if self.not_migrated_to_v3: default_adconnection = self.ucr[ default_adconnection_alias_ucrv] or "defaultADconnection" azure_data = { default_adconnection: { "objectId": udm_obj["UniventionOffice365ObjectID"], } } else: try: azure_data_encoded = udm_obj['UniventionOffice365Data'] try: azure_data = self.decode_o365data(azure_data_encoded) or {} except Exception: azure_data = {} except KeyError: azure_data = {} # May throw KeyError: azure_connection_data = azure_data[self.adconnection_alias] object_id = azure_connection_data["objectId"] if not object_id: raise KeyError return object_id def delete_user(self, old): object_id = self._object_id_from_user_attrs_with_fallback_to_entryUUID( old) if not object_id: logger.error( "Couldn't find object_id for user %r (%s), cannot delete.", old["uid"][0], self.adconnection_alias) return try: return self.ah.delete_user(object_id) except ResourceNotFoundError as exc: logger.error("User %r didn't exist in Azure (%s): %r.", old["uid"][0], self.adconnection_alias, exc) return def deactivate_user(self, old_or_new): object_id = self._object_id_from_user_attrs_with_fallback_to_entryUUID( old_or_new) if not object_id: return return self.ah.deactivate_user(object_id) def modify_user(self, old, new): modifications = self._diff_old_new(self.attrs["listener"], old, new) # If there are properties in azure that get their value from multiple # attributes in LDAP, then add all those attributes to the modifications # list, or their existing value will be lost, when overwriting them. multiples_may_be_none = list() for k, v in self.attrs["multiple"].items(): if any([ldap_attr in modifications for ldap_attr in v]): modifications.extend(v) multiples_may_be_none.extend(v) modifications = list(set(modifications)) logger.debug("modifications=%r", modifications) udm_attrs = self._get_sync_values(modifications, new, modify=True) logger.debug("udm_attrs=%r", udm_attrs) if udm_attrs: attributes = dict() for k, v in udm_attrs.items(): if v is None and k in multiples_may_be_none: # property was never set, don't add unnecessary None continue azure_property_name = self.attrs["mapping"][k] if azure_property_name in attributes: # must be a list type property, append/extend if not isinstance(attributes[azure_property_name], list): attributes[azure_property_name] = [ attributes[azure_property_name] ] if isinstance(v, list): attributes[azure_property_name].extend(v) else: attributes[azure_property_name].append(v) # no duplicate values attributes[azure_property_name] = list( set(attributes[azure_property_name])) else: attributes[azure_property_name] = v # recreate userPrincipalName is mailPrimaryAddress changed if k == 'mailPrimaryAddress': local_part_of_email_address = v.rpartition("@")[0] attributes['userPrincipalName'] = "{0}@{1}".format( local_part_of_email_address, self.ah.get_verified_domain_from_disk()) if "usageLocation" in attributes: attributes["usageLocation"] = self._get_usage_location(new) object_id = self._object_id_from_user_attrs_with_fallback_to_entryUUID( new) if not object_id: logger.error( "Couldn't find object_id for user %r (%s), cannot modify.", new["uid"][0], self.adconnection_alias) return return self.ah.modify_user(object_id=object_id, modifications=attributes) else: logger.debug("No modifications - nothing to do.") return def get_user(self, user): """ fetch Azure user object :param user: listener old or new :return: dict """ object_id = self._object_id_from_user_attrs_with_fallback_to_entryUUID( user) if not object_id: return list() return self.ah.list_users(objectid=object_id) def create_group(self, name, description, group_dn, add_members=True): self.ah.create_group(name, description) new_group = self.find_aad_group_by_name(name) if not new_group: raise RuntimeError( "Office365Listener.create_group() created group {!r} cannot be retrieved ({!r})." .format(name, self.adconnection_alias)) if add_members: self.add_ldap_members_to_azure_group(group_dn, new_group["objectId"]) return new_group def create_group_from_new(self, new): desc = new.get("description", [""])[0] or None name = new["cn"][0] return self.create_group(name, desc, self.dn) def create_group_from_ldap(self, groupdn, add_members=True): udm_group = self.udm.get_udm_group(groupdn) desc = udm_group.get("description", None) name = udm_group["name"] return self.create_group(name, desc, groupdn, add_members) def create_group_from_udm(self, udm_group, add_members=True): desc = udm_group.get("description", None) name = udm_group["name"] return self.create_group(name, desc, udm_group.dn, add_members) def delete_group(self, old): try: object_id = self._object_id_from_attrs(old) except KeyError: logger.error( "Couldn't find object_id for group %r (%s), cannot delete.", old["cn"][0], self.adconnection_alias) return try: azure_group = self.ah.delete_group(object_id) logger.info("Deleted group %r from Azure AD '%r'", old["cn"][0], self.adconnection_alias) return azure_group except ResourceNotFoundError as exc: logger.error("Group %r didn't exist in Azure: %r.", old["cn"][0], exc) return def set_adconnection_object_id(self, udm_group, object_id): if self.not_migrated_to_v3: udm_group["UniventionOffice365ObjectID"] = object_id else: azure_data_encoded = udm_group.get("UniventionOffice365Data") try: azure_data = self.decode_o365data(azure_data_encoded) # The account already has an Azure AD connection except TypeError: azure_data = {} if object_id: azure_connection_data = {'objectId': object_id} new_azure_data = { self.adconnection_alias: azure_connection_data } if azure_data: azure_data.update(new_azure_data) new_azure_data = azure_data new_azure_data_encoded = self.encode_o365data(new_azure_data) udm_group["UniventionOffice365Data"] = new_azure_data_encoded udm_group["UniventionOffice365ADConnectionAlias"].append( self.adconnection_alias) else: if self.adconnection_alias in azure_data: del azure_data[self.adconnection_alias] new_azure_data_encoded = self.encode_o365data( azure_data) if azure_data else None udm_group[ "UniventionOffice365Data"] = new_azure_data_encoded udm_group["UniventionOffice365ADConnectionAlias"] = [ x for x in udm_group["UniventionOffice365ADConnectionAlias"] if x != self.adconnection_alias ] udm_group.modify() def delete_empty_group(self, group_id, udm_group=None): """ Recursively look if a group or any of it parent groups is empty and remove it. :param group_id: str: object id of group (and its parents) to check :return: bool: if the group was deleted """ logger.debug("group_id=%r (%s)", group_id, self.adconnection_alias) # get IDs of groups this group is a member of before deleting it nested_parent_group_ids = self.ah.member_of_groups(group_id, "groups")["value"] # check members members = self.ah.get_groups_direct_members(group_id)["value"] if members: member_ids = self.ah.directory_object_urls_to_object_ids(members) azure_objs = list() for member_id in member_ids: try: azure_objs.append(self.ah.list_users(objectid=member_id)) except ResourceNotFoundError: # that's OK - it is probably not a user but a group try: azure_objs.append( self.ah.list_groups(objectid=member_id)) except ResourceNotFoundError: # ignore logger.error( "Office365Listener.delete_empty_group() found unexpected object in group: %r, ignoring.", member_id) if all(azure_obj["mailNickname"].startswith("ZZZ_deleted_") for azure_obj in azure_objs): logger.info( "All members of group %r (%s) are deactivated, deleting it.", group_id, self.adconnection_alias) self.ah.delete_group(group_id) if not udm_group: try: azure_group = self.ah.list_groups(objectid=group_id) except ResourceNotFoundError: # ignore azure_group = None logger.error( "Office365Listener.delete_empty_group() failed to find own group: %r, ignoring.", group_id) if azure_group: udm_group = self.udm.lookup_udm_group( azure_group["displayName"]) if udm_group: # TODO: lookup group in UDM if not given self.set_adconnection_object_id(udm_group, None) else: logger.debug("Group has active members, not deleting it.") return False else: logger.info("Removing empty group %r (%s)...", group_id, self.adconnection_alias) self.ah.delete_group(group_id) if not udm_group: try: azure_group = self.ah.list_groups(objectid=group_id) except ResourceNotFoundError: # ignore azure_group = None logger.error( "Office365Listener.delete_empty_group() failed to find own group: %r, ignoring.", group_id) if azure_group: udm_group = self.udm.lookup_udm_group( azure_group["displayName"]) if udm_group: # TODO: lookup group in UDM if not given self.set_adconnection_object_id(udm_group, None) # check parent groups for nested_parent_group_id in nested_parent_group_ids: self.delete_empty_group(nested_parent_group_id) return True def modify_group(self, old, new): modification_attributes = self._diff_old_new(self.attrs["listener"], old, new) logger.debug("dn=%r modification_attributes=%r (%s)", self.dn, modification_attributes, self.adconnection_alias) try: object_id = self._object_id_from_attrs(old) except KeyError: object_id = None if not modification_attributes: logger.debug("No modifications found, ignoring.") return dict(objectId=object_id) udm_group = self.udm.get_udm_group(self.dn) if not object_id: # just create a new group logger.info( "No objectID for group %r found, creating a new azure group...", self.dn) azure_group = self.create_group_from_new(new) object_id = azure_group["objectId"] modification_attributes = dict() self.set_adconnection_object_id(udm_group, object_id) try: azure_group = self.ah.list_groups(objectid=object_id) if azure_group["mailNickname"].startswith("ZZZ_deleted_"): logger.info("Reactivating azure group %r...", azure_group["displayName"]) name = new["cn"][0] attributes = dict(description=new.get("description", [""])[0] or None, displayName=name, mailEnabled=False, mailNickname=name.replace(" ", "_-_"), securityEnabled=True) azure_group = self.ah.modify_group(object_id, attributes) except ResourceNotFoundError: logger.warn( "Office365Listener.modify_group() azure group doesn't exist (anymore), creating it instead." ) azure_group = self.create_group_from_new(new) modification_attributes = dict() object_id = azure_group["objectId"] if "uniqueMember" in modification_attributes: # In uniqueMember users and groups are both listed. There is no # secure way to distinguish between them, so lets have UDM do that # for us. modification_attributes.remove("uniqueMember") set_old = set(old.get("uniqueMember", [])) set_new = set(new.get("uniqueMember", [])) removed_members = set_old - set_new added_members = set_new - set_old logger.debug("dn=%r added_members=%r removed_members=%r", self.dn, added_members, removed_members) # add new members to Azure users_and_groups_to_add = list() for added_member in added_members: if added_member in udm_group["users"]: # it's a user udm_user = self.udm.get_udm_user(added_member) if int(udm_user.get("UniventionOffice365Enabled", "0")): try: member_object_id = self._object_id_from_udm_object( udm_user) users_and_groups_to_add.append(member_object_id) except KeyError: pass elif added_member in udm_group["nestedGroup"]: # it's a group # check if this group or any of its nested groups has azure_users for group_with_azure_users in self.udm.udm_groups_with_azure_users( added_member): logger.debug( "Found nested group %r with azure users...", group_with_azure_users) udm_group_with_azure_users = self.udm.get_udm_group( group_with_azure_users) try: member_object_id = self._object_id_from_udm_object( udm_group_with_azure_users) except KeyError: new_group = self.create_group_from_udm( udm_group_with_azure_users) member_object_id = new_group["objectId"] self.set_adconnection_object_id( udm_group_with_azure_users, member_object_id) if group_with_azure_users in udm_group[ "nestedGroup"]: # only add direct members to group users_and_groups_to_add.append(member_object_id) else: raise RuntimeError( "Office365Listener.modify_group() {!r} from new[uniqueMember] not in " "'nestedGroup' or 'users' ({!r}).".format( added_member, self.adconnection_alias)) if users_and_groups_to_add: self.ah.add_objects_to_azure_group(object_id, users_and_groups_to_add) # remove members for removed_member in removed_members: member_id = None # try with UDM user udm_obj = self.udm.get_udm_user(removed_member) try: member_id = self._object_id_from_udm_object(udm_obj) except (KeyError, TypeError): # try with UDM group udm_obj = self.udm.get_udm_group(removed_member) try: member_id = self._object_id_from_udm_object(udm_obj) except (KeyError, TypeError): pass if not member_id: # group may have been deleted or group may not be an Azure group # let's try to remove it from Azure anyway # get group using name and search m = re.match(r"^cn=(.*?),.*", removed_member) if m: object_name = m.groups()[0] # do not try with a user account: it will either have # been deleted, in which case it will be removed from # all groups by AzureHandler.deactivate_user() or if it # existed, we'd have found it already at the top of the # for loop with self.get_udm_user(removed_member). # try with a group azure_group = self.find_aad_group_by_name(object_name) if azure_group: member_id = azure_group["objectId"] else: # not an Azure user or group or already deleted in Azure logger.warn( "Office365Listener.modify_group(), removing members: couldn't figure out object name from dn %r", removed_member) continue else: logger.warn( "Office365Listener.modify_group(), removing members: couldn't figure out object name from dn %r", removed_member) continue self.ah.delete_group_member(group_id=object_id, member_id=member_id) # remove group if it became empty deleted = self.delete_empty_group(object_id, udm_group) if deleted: return None # modify other attributes modifications = dict([(mod_attr, new[mod_attr]) for mod_attr in modification_attributes]) if modification_attributes: return self.ah.modify_group(object_id=object_id, modifications=modifications) return dict(objectId=object_id) # for listener to store in UDM object def add_ldap_members_to_azure_group(self, group_dn, object_id): """ Recursively look for users and groups to add to the Azure group. :param group_dn: DN of UDM group :param object_id: Azure object ID of group to add users/groups to :return: None """ logger.debug("group_dn=%r object_id=%r adconnection_alias=%r", group_dn, object_id, self.adconnection_alias) udm_target_group = self.udm.get_udm_group(group_dn) # get all users for the adconnection (ignoring group membership) and compare # with group members to get azure IDs, because it's faster than # iterating (and opening!) lots of UDM objects all_users_lo = self.udm.get_lo_o365_users( attributes=['univentionOffice365Data'], adconnection_alias=self.adconnection_alias) all_user_dns = set(all_users_lo.keys()) member_dns = all_user_dns.intersection(set(udm_target_group["users"])) def get_object_id(attr): try: return self._object_id_from_attrs(attr) except KeyError: # Object is not synchronized to this Azure AD pass users_and_groups_to_add = [ oid for oid in [ get_object_id(attr) for dn, attr in all_users_lo.items() if dn in member_dns ] if oid is not None ] # search tree downwards, create groups as we go, add users to them later for groupdn in udm_target_group["nestedGroup"]: # check if this group or any of its nested groups has azure_users for group_with_azure_users_dn in self.udm.udm_groups_with_azure_users( groupdn): udm_group = self.udm.get_udm_group(group_with_azure_users_dn) try: member_object_id = self._object_id_from_udm_object( udm_group) except KeyError: new_group = self.create_group_from_udm(udm_group, add_members=False) member_object_id = new_group["objectId"] self.set_adconnection_object_id(udm_group, member_object_id) if group_with_azure_users_dn in udm_target_group[ "nestedGroup"]: users_and_groups_to_add.append(member_object_id) # add users to groups if users_and_groups_to_add: self.ah.add_objects_to_azure_group(object_id, users_and_groups_to_add) # search tree upwards, create groups as we go, don't add users def _groups_up_the_tree(group): for member_dn in group["memberOf"]: udm_member = self.udm.get_udm_group(member_dn) try: member_object_id = self._object_id_from_udm_object( udm_member) except KeyError: new_group = self.create_group_from_udm(udm_member, add_members=False) member_object_id = new_group["objectId"] self.set_adconnection_object_id(udm_member, member_object_id) _groups_up_the_tree(udm_member) _groups_up_the_tree(udm_target_group) def assign_subscription(self, new, azure_user): msg_no_allocatable_subscriptions = 'User {}/{} created in Azure AD ({}), but no allocatable subscriptions' \ ' found.'.format(new['uid'][0], azure_user['objectId'], self.adconnection_alias) msg_multiple_subscriptions = 'More than one usable Office 365 subscription found.' # check subscription availability in azure subscriptions_online = self.ah.get_enabled_subscriptions() if len(subscriptions_online) < 1: raise NoAllocatableSubscriptions(azure_user, msg_no_allocatable_subscriptions, self.adconnection_alias) # get SubscriptionProfiles for users groups users_group_dns = self.udm.get_udm_user(new['entryDN'][0])['groups'] users_subscription_profiles = SubscriptionProfile.get_profiles_for_groups( users_group_dns) logger.info('SubscriptionProfiles found for %r (%s): %r', new['uid'][0], self.adconnection_alias, users_subscription_profiles) if not users_subscription_profiles: logger.warn( 'No SubscriptionProfiles: using all available subscriptions (%s).', self.adconnection_alias) if len(subscriptions_online) > 1: logger.warn(msg_multiple_subscriptions) self.ah.add_license(azure_user['objectId'], subscriptions_online[0]['skuId']) return # find subscription with free seats seats = dict((s["skuPartNumber"], (s["prepaidUnits"]["enabled"], s["consumedUnits"], s['skuId'])) for s in subscriptions_online) logger.debug('seats in subscriptions_online: %r', seats) subscription_profile_to_use = None for subscription_profile in users_subscription_profiles: skuPartNumber = subscription_profile.subscription if skuPartNumber not in seats: logger.warn( 'Subscription from profile %r (%s) could not be found in the enabled subscriptions in Azure.', subscription_profile, self.adconnection_alias) continue if seats[skuPartNumber][0] > seats[skuPartNumber][1]: subscription_profile.skuId = seats[skuPartNumber][2] subscription_profile_to_use = subscription_profile break if not subscription_profile_to_use: raise NoAllocatableSubscriptions(azure_user, msg_no_allocatable_subscriptions, self.adconnection_alias) logger.info('Using subscription profile %r (skuId: %r).', subscription_profile_to_use, subscription_profile_to_use.skuId) # calculate plan restrictions # get all plans of this subscription plan_names_to_ids = dict() for subscription in subscriptions_online: if subscription[ 'skuPartNumber'] == subscription_profile_to_use.subscription: for plan in subscription['servicePlans']: plan_names_to_ids[ plan['servicePlanName']] = plan['servicePlanId'] if subscription_profile_to_use.whitelisted_plans: deactivate_plans = set(plan_names_to_ids.keys()) - set( subscription_profile_to_use.whitelisted_plans) else: deactivate_plans = set() deactivate_plans.update(subscription_profile_to_use.blacklisted_plans) logger.info('Deactivating plans %s (%s).' % (deactivate_plans, self.adconnection_alias)) deactivate_plan_ids = [ plan_names_to_ids[plan] for plan in deactivate_plans ] self.ah.add_license(azure_user['objectId'], subscription_profile_to_use.skuId, deactivate_plan_ids) def find_aad_user_by_entryUUID(self, entryUUID): user = self.ah.list_users( ofilter="immutableId eq '{}'".format(base64.b64encode(entryUUID))) if user["value"]: return user["value"][0]["objectId"] else: logger.error("Could not find user with entryUUID=%r (%s).", entryUUID, self.adconnection_alias) return None def find_aad_group_by_name(self, name): group = self.ah.list_groups(ofilter="displayName eq '{}'".format(name)) if group["value"]: return group["value"][0] else: logger.warn( "Could not find group with name=%r (%s), ignore this if it is a user.", name, self.adconnection_alias) return None @staticmethod def _anonymize(txt): return uuid.uuid4().get_hex() def _get_sync_values(self, attrs, user, modify=False): # anonymize > static > sync res = dict() for attr in attrs: if attr in attributes_system: # filter out univentionOffice365Enabled and account deactivation/locking attributes continue elif attr not in user and not modify: # only set empty values to unset properties when modifying continue elif attr in self.attrs["anonymize"]: tmp = map(self._anonymize, user[attr]) elif attr in self.attrs["static"]: tmp = [self.attrs["static"][attr]] elif attr in self.attrs["sync"]: tmp = user.get( attr) # Azure does not like empty strings - it wants None! else: raise RuntimeError( "Attribute to sync {!r} is not configured through UCR.". format(attr)) if attr in res: if isinstance(res[attr], list): res[attr].append(tmp) else: raise RuntimeError( "Office365Listener._get_sync_values() res[{}] already exists with type {} and value '{}'." .format(attr, type(res[attr]), res[attr])) else: if tmp and len(tmp) == 1: res[attr] = tmp[0] else: res[attr] = tmp return res @staticmethod def _diff_old_new(attribs, old, new): """ :param attribs: list of attributes to take into consideration when looking for modifications :param old: listener 'old' dict :param new: listener 'new' dict :return: list of attributes that changed """ return [ attr for attr in attribs if attr in new and attr not in old or attr in old and attr not in new or (attr in old and attr in new and old[attr] != new[attr]) ] def _get_usage_location(self, user): if user.get("st"): res = user["st"][0] elif self.ucr.get("office365/attributes/usageLocation"): res = self.ucr["office365/attributes/usageLocation"] else: res = self.ucr["ssl/country"] if not res or len(res) != 2: raise RuntimeError( "Invalid usageLocation '{}' - user cannot be created.".format( res)) return res @classmethod def decode_o365data(cls, data): """ Decode ldap UniventionOffice365Data """ return json.loads(zlib.decompress(base64.b64decode(data))) @classmethod def encode_o365data(cls, data): """ Encode ldap UniventionOffice365Data """ return base64.b64encode(zlib.compress(json.dumps(data)))
def list_profiles(): return UDMHelper.list_udm_officeprofiles()