class DHCPServer(UCSSchoolHelperAbstractClass): name = DHCPServerName(_('Server name')) dhcp_service = DHCPServiceAttribute(_('DHCP service'), required=True) def get_own_container(self): if self.dhcp_service: return self.dhcp_service.dn @classmethod def get_container(cls, school): return cls.get_search_base(school).dhcp def get_superordinate(self, lo): if self.dhcp_service: return self.dhcp_service.get_udm_object(lo) @classmethod def find_any_dn_with_name(cls, name, lo): logger.debug('Searching first dhcpServer with cn=%s', name) try: dn = lo.searchDn(filter=filter_format( '(&(objectClass=dhcpServer)(cn=%s))', [name]), base=ucr.get('ldap/base'))[0] except IndexError: dn = None logger.debug('... %r found', dn) return dn class Meta: udm_module = 'dhcp/server' name_is_unique = True
class DHCPSubnet(UCSSchoolHelperAbstractClass): name = DHCPSubnetName(_('Subnet address')) subnet_mask = DHCPSubnetMask(_('Netmask')) broadcast = BroadcastAddress(_('Broadcast')) dhcp_service = DHCPServiceAttribute(_('DHCP service'), required=True) def get_own_container(self): if self.dhcp_service: return self.dhcp_service.dn @classmethod def get_container(cls, school): return cls.get_search_base(school).dhcp def get_superordinate(self, lo): if self.dhcp_service: return self.dhcp_service.get_udm_object(lo) def get_ipv4_subnet(self): network_str = '%s/%s' % (self.name, self.subnet_mask) try: return ipaddr.IPv4Network(network_str) except ValueError as exc: logger.info('%r is no valid IPv4Network:\n%s', network_str, exc) @classmethod def find_all_dns_below_base(cls, dn, lo): logger.debug('Searching all univentionDhcpSubnet in %r', dn) return lo.searchDn(filter='(objectClass=univentionDhcpSubnet)', base=dn) class Meta: udm_module = 'dhcp/subnet'
def validate(self, value): super(DCName, self).validate(value) if value: regex = re.compile('^[a-zA-Z0-9](([a-zA-Z0-9-]*)([a-zA-Z0-9]$))?$') if not regex.match(value): raise ValueError(_('Invalid Domain Controller name')) if ucr.is_true('ucsschool/singlemaster', False): if len(value) > 12: raise ValueError( _('A valid NetBIOS hostname can not be longer than 12 characters.' )) if sum([len(value), 1, len(ucr.get('domainname', ''))]) > 63: raise ValueError( _('The length of fully qualified domain name is greater than 63 characters.' ))
def validate(self, value): if value is not None: if self.value_type and not isinstance(value, self.value_type): raise ValueError( _('"%(label)s" needs to be a %(type)s') % { 'type': self.value_type.__name__, 'label': self.label }) values = value if self.value_type else [value] self._validate_syntax(values) else: if self.required: raise ValueError( _('"%s" is required. Please provide this information.') % self.label)
class Student(User): type_name = _('Student') type_filter = '(&(objectClass=ucsschoolStudent)(!(objectClass=ucsschoolExam)))' roles = [role_pupil] default_options = ('ucsschoolStudent', ) def do_school_change(self, udm_obj, lo, old_school): try: exam_user = ExamStudent.from_student_dn(lo, old_school, self.old_dn) except noObject as exc: logger.info('No exam user for %r found: %s', self.old_dn, exc) else: logger.info('Removing exam user %r', exam_user.dn) exam_user.remove(lo) super(Student, self).do_school_change(udm_obj, lo, old_school) @classmethod def get_container(cls, school): return cls.get_search_base(school).students @classmethod def get_exam_container(cls, school): return cls.get_search_base(school).examUsers def get_specific_groups(self, lo): groups = super(Student, self).get_specific_groups(lo) groups.extend(self.get_students_groups()) return groups
class SchoolClass(Group, _MayHaveSchoolPrefix): name = SchoolClassName(_('Name')) ShareClass = ClassShare def create_without_hooks(self, lo, validate): success = super(SchoolClass, self).create_without_hooks(lo, validate) if self.exists(lo): success = success and self.create_share( self.get_machine_connection()) return success def create_share(self, lo): share = self.ShareClass.from_school_group(self) return share.exists(lo) or share.create(lo) def modify_without_hooks(self, lo, validate=True, move_if_necessary=None): share = self.ShareClass.from_school_group(self) if self.old_dn: old_name = self.get_name_from_dn(self.old_dn) if old_name != self.name: # recreate the share. # if the name changed # from_school_group will have initialized # share.old_dn incorrectly share = self.ShareClass(name=old_name, school=self.school, school_group=self) share.name = self.name success = super(SchoolClass, self).modify_without_hooks(lo, validate, move_if_necessary) if success: lo_machine = self.get_machine_connection() if share.exists(lo_machine): success = share.modify(lo_machine) else: success = self.create_share(lo_machine) return success def remove_without_hooks(self, lo): success = super(SchoolClass, self).remove_without_hooks(lo) share = self.ShareClass.from_school_group(self) success = success and share.remove(self.get_machine_connection()) return success @classmethod def get_container(cls, school): return cls.get_search_base(school).classes def to_dict(self): ret = super(SchoolClass, self).to_dict() ret['name'] = self.get_relative_name() return ret @classmethod def get_class_for_udm_obj(cls, udm_obj, school): if not cls.is_school_class(school, udm_obj.dn): return # is a workgroup return cls
class BasicGroup(Group): school = None container = Attribute(_('Container'), required=True) def __init__(self, name=None, school=None, **kwargs): if 'container' not in kwargs: kwargs['container'] = 'cn=groups,%s' % ucr.get('ldap/base') super(BasicGroup, self).__init__(name=name, school=school, **kwargs) def create_without_hooks(self, lo, validate): # prepare LDAP: create containers where this basic group lives if necessary container_dn = self.get_own_container()[:-len(ucr.get('ldap/base')) - 1] containers = str2dn(container_dn) super_container_dn = ucr.get('ldap/base') for container_info in reversed(containers): dn_part, cn = container_info[0][0:2] if dn_part.lower() == 'ou': container = OU(name=cn) else: container = Container(name=cn, school='', group_path='1') container.position = super_container_dn super_container_dn = container.create(lo, False) return super(BasicGroup, self).create_without_hooks(lo, validate) def get_own_container(self): return self.container def build_hook_line(self, hook_time, func_name): return None @classmethod def get_container(cls, school=None): return ucr.get('ldap/base')
def _alter_udm_obj(self, udm_obj): super(SchoolComputer, self)._alter_udm_obj(udm_obj) inventory_numbers = self.get_inventory_numbers() if inventory_numbers: udm_obj['inventoryNumber'] = inventory_numbers ipv4_network = self.get_ipv4_network() if ipv4_network: if self._ip_is_set_to_subnet(ipv4_network): logger.info( 'IP was set to subnet. Unsetting it on the computer so that UDM can do some magic: Assign next free IP!' ) udm_obj['ip'] = '' else: udm_obj['ip'] = str(ipv4_network.ip) # set network after ip. Otherwise UDM does not do any # nextIp magic... network = self.get_network() if network: # reset network, so that next line triggers free ip udm_obj.old_network = None try: udm_obj['network'] = network.dn except nextFreeIp: logger.error( 'Tried to set IP automatically, but failed! %r is full', network) raise nextFreeIp( _('There are no free addresses left in the subnet!'))
class Container(OU): user_path = ContainerPath(_('User path'), udm_name='userPath') computer_path = ContainerPath(_('Computer path'), udm_name='computerPath') network_path = ContainerPath(_('Network path'), udm_name='networkPath') group_path = ContainerPath(_('Group path'), udm_name='groupPath') dhcp_path = ContainerPath(_('DHCP path'), udm_name='dhcpPath') policy_path = ContainerPath(_('Policy path'), udm_name='policyPath') share_path = ContainerPath(_('Share path'), udm_name='sharePath') printer_path = ContainerPath(_('Printer path'), udm_name='printerPath') class Meta: udm_module = 'container/cn'
def validate(self, lo, validate_unlikely_changes=False): super(SchoolComputer, self).validate(lo, validate_unlikely_changes) if self.ip_address: name, ip_address = escape_filter_chars( self.name), escape_filter_chars(self.ip_address) if AnyComputer.get_first_udm_obj( lo, '&(!(cn=%s))(ip=%s)' % (name, ip_address)): self.add_error( 'ip_address', _('The ip address is already taken by another computer. Please change the ip address.' )) if self.mac_address: name, mac_address = escape_filter_chars( self.name), escape_filter_chars(self.mac_address) if AnyComputer.get_first_udm_obj( lo, '&(!(cn=%s))(mac=%s)' % (name, mac_address)): self.add_error( 'mac_address', _('The mac address is already taken by another computer. Please change the mac address.' ))
def validate(self, lo, validate_unlikely_changes=False): from ucsschool.lib.models.school import School self.errors.clear() self.warnings.clear() for name, attr in self._attributes.iteritems(): value = getattr(self, name) try: attr.validate(value) except ValueError as e: self.add_error(name, str(e)) if self._meta.name_is_unique and not self._meta.allow_school_change: if self.exists_outside_school(lo): self.add_error( 'name', _('The name is already used somewhere outside the school. It may not be taken twice and has to be changed.' )) if self.supports_school() and self.school: if not School.cache(self.school).exists(lo): self.add_error( 'school', _('The school "%s" does not exist. Please choose an existing one or create it.' ) % self.school) if validate_unlikely_changes: if self.exists(lo): udm_obj = self.get_udm_object(lo) try: original_self = self.from_udm_obj(udm_obj, self.school, lo) except (UnknownModel, WrongModel): pass else: for name, attr in self._attributes.iteritems(): if attr.unlikely_to_change: new_value = getattr(self, name) old_value = getattr(original_self, name) if new_value and old_value: if new_value != old_value: self.add_warning( name, _('The value changed from %(old)s. This seems unlikely.' ) % {'old': old_value})
class TeachersAndStaff(Teacher): type_name = _('Teacher and Staff') type_filter = '(&(objectClass=ucsschoolStaff)(objectClass=ucsschoolTeacher))' roles = [role_teacher, role_staff] default_options = ('ucsschoolStaff', ) @classmethod def get_container(cls, school): return cls.get_search_base(school).teachersAndStaff def get_specific_groups(self, lo): groups = super(TeachersAndStaff, self).get_specific_groups(lo) groups.extend(self.get_staff_groups()) return groups
def validate(self, lo, validate_unlikely_changes=False): super(User, self).validate(lo, validate_unlikely_changes) try: udm_obj = self.get_udm_object(lo) except UnknownModel: udm_obj = None except WrongModel as exc: udm_obj = None self.add_error( 'name', _('It is not supported to change the role of a user. %(old_role)s %(name)s cannot become a %(new_role)s.' ) % { 'old_role': exc.model.type_name, 'name': self.name, 'new_role': self.type_name }) if udm_obj: original_class = self.get_class_for_udm_obj(udm_obj, self.school) if original_class is not self.__class__: self.add_error( 'name', _('It is not supported to change the role of a user. %(old_role)s %(name)s cannot become a %(new_role)s.' ) % { 'old_role': original_class.type_name, 'name': self.name, 'new_role': self.type_name }) if self.email: name, email = escape_filter_chars(self.name), escape_filter_chars( self.email) if self.get_first_udm_obj( lo, '&(!(uid=%s))(mailPrimaryAddress=%s)' % (name, email)): self.add_error( 'email', _('The email address is already taken by another user. Please change the email address.' ))
class ExamStudent(Student): type_name = _('Exam student') type_filter = '(&(objectClass=ucsschoolStudent)(objectClass=ucsschoolExam))' default_options = ('ucsschoolExam', ) @classmethod def get_container(cls, school): return cls.get_search_base(school).examUsers @classmethod def from_student_dn(cls, lo, school, dn): examUserPrefix = ucr.get('ucsschool/ldap/default/userprefix/exam', 'exam-') dn = 'uid=%s%s,%s' % (escape_dn_chars(examUserPrefix), explode_dn(dn, True)[0], cls.get_container(school)) return cls.from_dn(dn, school, lo)
class ComputerRoom(Group, _MayHaveSchoolPrefix): hosts = Hosts(_('Hosts')) users = None def to_dict(self): ret = super(ComputerRoom, self).to_dict() ret['name'] = self.get_relative_name() return ret @classmethod def get_container(cls, school): return cls.get_search_base(school).rooms def get_computers(self, ldap_connection): from ucsschool.lib.models.computer import SchoolComputer for host in self.hosts: try: yield SchoolComputer.from_dn(host, self.school, ldap_connection) except noObject: continue
class Staff(User): school_classes = None type_name = _('Staff') roles = [role_staff] type_filter = '(&(!(objectClass=ucsschoolTeacher))(objectClass=ucsschoolStaff))' default_options = ('ucsschoolStaff', ) @classmethod def get_container(cls, school): return cls.get_search_base(school).staff def get_samba_home_path(self, lo): """ Do not set sambaHomePath for staff users. """ return None def get_samba_home_drive(self): """ Do not set sambaHomeDrive for staff users. """ return None def get_samba_netlogon_script_path(self): """ Do not set sambaLogonScript for staff users. """ return None def get_profile_path(self, lo): """ Do not set sambaProfilePath for staff users. """ return None def get_school_class_objs(self): return [] @classmethod def get_school_classes(cls, udm_obj, obj): return {} def get_specific_groups(self, lo): groups = super(Staff, self).get_specific_groups(lo) groups.extend(self.get_staff_groups()) return groups
class Share(UCSSchoolHelperAbstractClass): name = ShareName(_('Name')) school_group = SchoolClassAttribute(_('School class'), required=True, internal=True) @classmethod def from_school_group(cls, school_group): return cls(name=school_group.name, school=school_group.school, school_group=school_group) from_school_class = from_school_group # legacy @classmethod def get_container(cls, school): return cls.get_search_base(school).shares def do_create(self, udm_obj, lo): gid = self.school_group.get_udm_object(lo)['gidNumber'] udm_obj['host'] = self.get_server_fqdn(lo) udm_obj['path'] = self.get_share_path() udm_obj['writeable'] = '1' udm_obj['sambaWriteable'] = '1' udm_obj['sambaBrowseable'] = '1' udm_obj['sambaForceGroup'] = '+%s' % self.name udm_obj['sambaCreateMode'] = '0770' udm_obj['sambaDirectoryMode'] = '0770' udm_obj['owner'] = '0' udm_obj['group'] = gid udm_obj['directorymode'] = '0770' if ucr.is_false('ucsschool/default/share/nfs', True): try: udm_obj.options.remove('nfs') # deactivate NFS except ValueError: pass logger.info('Creating share on "%s"', udm_obj['host']) return super(Share, self).do_create(udm_obj, lo) def get_share_path(self): if ucr.is_true('ucsschool/import/roleshare', True): return '/home/%s/groups/%s' % (self.school_group.school, self.name) else: return '/home/groups/%s' % self.name def do_modify(self, udm_obj, lo): old_name = self.get_name_from_dn(self.old_dn) if old_name != self.name: head, tail = os.path.split(udm_obj['path']) tail = self.name udm_obj['path'] = os.path.join(head, tail) if udm_obj['sambaName'] == old_name: udm_obj['sambaName'] = self.name if udm_obj['sambaForceGroup'] == '+%s' % old_name: udm_obj['sambaForceGroup'] = '+%s' % self.name return super(Share, self).do_modify(udm_obj, lo) def get_server_fqdn(self, lo): domainname = ucr.get('domainname') school = self.get_school_obj(lo) school_dn = school.dn # fetch serverfqdn from OU result = lo.get(school_dn, ['ucsschoolClassShareFileServer']) if result: server_domain_name = lo.get( result['ucsschoolClassShareFileServer'][0], ['associatedDomain']) if server_domain_name: server_domain_name = server_domain_name['associatedDomain'][0] else: server_domain_name = domainname result = lo.get(result['ucsschoolClassShareFileServer'][0], ['cn']) if result: return '%s.%s' % (result['cn'][0], server_domain_name) # get alternative server (defined at ou object if a dc slave is responsible for more than one ou) ou_attr_ldap_access_write = lo.get(school_dn, ['univentionLDAPAccessWrite']) alternative_server_dn = None if len(ou_attr_ldap_access_write) > 0: alternative_server_dn = ou_attr_ldap_access_write[ 'univentionLDAPAccessWrite'][0] if len(ou_attr_ldap_access_write) > 1: logger.warning( 'more than one corresponding univentionLDAPAccessWrite found at ou=%s', self.school) # build fqdn of alternative server and set serverfqdn if alternative_server_dn: alternative_server_attr = lo.get(alternative_server_dn, ['uid']) if len(alternative_server_attr) > 0: alternative_server_uid = alternative_server_attr['uid'][0] alternative_server_uid = alternative_server_uid.replace( '$', '') if len(alternative_server_uid) > 0: return '%s.%s' % (alternative_server_uid, domainname) # fallback return '%s.%s' % (school.get_dc_name_fallback(), domainname) class Meta: udm_module = 'shares/share'
class SchoolComputer(UCSSchoolHelperAbstractClass): ip_address = IPAddress(_('IP address'), required=True) subnet_mask = SubnetMask(_('Subnet mask')) mac_address = MACAddress(_('MAC address'), required=True) inventory_number = InventoryNumber(_('Inventory number')) zone = Attribute(_('Zone')) type_name = _('Computer') DEFAULT_PREFIX_LEN = 24 # 255.255.255.0 def get_inventory_numbers(self): if isinstance(self.inventory_number, basestring): return [inv.strip() for inv in self.inventory_number.split(',')] if isinstance(self.inventory_number, (list, tuple)): return list(self.inventory_number) return [] def _alter_udm_obj(self, udm_obj): super(SchoolComputer, self)._alter_udm_obj(udm_obj) inventory_numbers = self.get_inventory_numbers() if inventory_numbers: udm_obj['inventoryNumber'] = inventory_numbers ipv4_network = self.get_ipv4_network() if ipv4_network: if self._ip_is_set_to_subnet(ipv4_network): logger.info( 'IP was set to subnet. Unsetting it on the computer so that UDM can do some magic: Assign next free IP!' ) udm_obj['ip'] = '' else: udm_obj['ip'] = str(ipv4_network.ip) # set network after ip. Otherwise UDM does not do any # nextIp magic... network = self.get_network() if network: # reset network, so that next line triggers free ip udm_obj.old_network = None try: udm_obj['network'] = network.dn except nextFreeIp: logger.error( 'Tried to set IP automatically, but failed! %r is full', network) raise nextFreeIp( _('There are no free addresses left in the subnet!')) @classmethod def get_container(cls, school): return cls.get_search_base(school).computers def create(self, lo, validate=True): if self.subnet_mask is None: self.subnet_mask = self.DEFAULT_PREFIX_LEN return super(SchoolComputer, self).create(lo, validate) def create_without_hooks(self, lo, validate): self.create_network(lo) return super(SchoolComputer, self).create_without_hooks(lo, validate) def modify_without_hooks(self, lo, validate=True, move_if_necessary=None): self.create_network(lo) return super(SchoolComputer, self).modify_without_hooks(lo, validate, move_if_necessary) def get_ipv4_network(self): if self.subnet_mask is not None: network_str = '%s/%s' % (self.ip_address, self.subnet_mask) else: network_str = str(self.ip_address) try: return IPv4Network(network_str) except (AddressValueError, NetmaskValueError, ValueError): logger.warning('Unparsable network: %r', network_str) def _ip_is_set_to_subnet(self, ipv4_network=None): ipv4_network = ipv4_network or self.get_ipv4_network() if ipv4_network: return ipv4_network.ip == ipv4_network.network def get_network(self): ipv4_network = self.get_ipv4_network() if ipv4_network: network_name = '%s-%s' % (self.school.lower(), ipv4_network.network) network = str(ipv4_network.network) netmask = str(ipv4_network.netmask) broadcast = str(ipv4_network.broadcast) return Network.cache(network_name, self.school, network=network, netmask=netmask, broadcast=broadcast) def create_network(self, lo): network = self.get_network() if network: network.create(lo) return network def validate(self, lo, validate_unlikely_changes=False): super(SchoolComputer, self).validate(lo, validate_unlikely_changes) if self.ip_address: name, ip_address = escape_filter_chars( self.name), escape_filter_chars(self.ip_address) if AnyComputer.get_first_udm_obj( lo, '&(!(cn=%s))(ip=%s)' % (name, ip_address)): self.add_error( 'ip_address', _('The ip address is already taken by another computer. Please change the ip address.' )) if self.mac_address: name, mac_address = escape_filter_chars( self.name), escape_filter_chars(self.mac_address) if AnyComputer.get_first_udm_obj( lo, '&(!(cn=%s))(mac=%s)' % (name, mac_address)): self.add_error( 'mac_address', _('The mac address is already taken by another computer. Please change the mac address.' )) @classmethod def get_class_for_udm_obj(cls, udm_obj, school): oc = udm_obj.lo.get(udm_obj.dn, ['objectClass']) object_classes = oc.get('objectClass', []) if 'univentionWindows' in object_classes: return WindowsComputer if 'univentionMacOSClient' in object_classes: return MacComputer if 'univentionCorporateClient' in object_classes: return UCCComputer if 'univentionClient' in object_classes: return IPComputer @classmethod def from_udm_obj(cls, udm_obj, school, lo): from ucsschool.lib.models.school import School obj = super(SchoolComputer, cls).from_udm_obj(udm_obj, school, lo) obj.ip_address = udm_obj['ip'] school_obj = School.cache(obj.school) edukativnetz_group = school_obj.get_administrative_group_name( 'educational', domain_controller=False, as_dn=True) if edukativnetz_group in udm_obj['groups']: obj.zone = 'edukativ' verwaltungsnetz_group = school_obj.get_administrative_group_name( 'administrative', domain_controller=False, as_dn=True) if verwaltungsnetz_group in udm_obj['groups']: obj.zone = 'verwaltung' network_dn = udm_obj['network'] if network_dn: netmask = Network.get_netmask(network_dn, school, lo) obj.subnet_mask = netmask obj.inventory_number = ', '.join(udm_obj['inventoryNumber']) return obj def build_hook_line(self, hook_time, func_name): module_part = self._meta.udm_module.split('/')[1] return self._build_hook_line(module_part, self.name, self.mac_address, self.school, self.get_ipv4_network(), ','.join(self.get_inventory_numbers()), self.zone) def to_dict(self): ret = super(SchoolComputer, self).to_dict() ret['type_name'] = self.type_name ret['type'] = self._meta.udm_module_short return ret class Meta: udm_module = 'computers/computer' name_is_unique = True
class UCSSchoolHelperAbstractClass(object): ''' Base class of all UCS@school models. Hides UDM. Attributes used for a class are defined like this: class MyModel(UCSSchoolHelperAbstractClass): my_attribute = Attribute('Label', required=True, udm_name='myAttr') From there on my_attribute=value may be passed to __init__, my_model.my_attribute can be accessed and the value will be saved as obj['myAttr'] in UDM when saving this instance. If an attribute of a base class is not wanted, it can be overridden: class MyModel(UCSSchoolHelperAbstractClass): school = None Meta information about the class are defined like this: class MyModel(UCSSchoolHelperAbstractClass): class Meta: udm_module = 'my/model' The meta information is then accessible in cls._meta Important functions: __init__(**kwargs): kwargs should be the defined attributes create(lo) lo is an LDAP connection, specifically univention.admin.access. creates a new object. Returns False is the object already exists. And True after the creation modify(lo) modifies an existing object. Returns False if the object does not exist and True after the modification (regardless whether something actually changed or not) remove(lo) deletes the object. Returns False if the object does not exist and True after the deletion. get_all(lo, school, filter_str, easy_filter=False) classmethod; retrieves all objects found for this school. filter can be a string that is used to narrow down a search. Each property of the class' udm_module that is include_in_default_search is queried for that string. Example: User.get_all(lo, 'school', filter_str='name', easy_filter=True) will search in cn=users,ou=school,$base for users/user UDM objects with |(username=*name*)(firstname=*name*)(...) and return User objects (not UDM objects) With easy_filter=False (default) it will use this very filter_str get_container(school) a classmethod that points to the container where new instances are created and existing ones are searched. dn property, current distinguishable name of the instance. Calculated on the fly, it changes if instance.name or instance.school changes. instance.old_dn will be set to the original dn when the instance was created get_udm_object(lo) searches UDM for an entry that corresponds to self. Normally uses the old_dn or dn. If cls._meta.name_is_unique then any object with self.name will match exists(lo) whether this object can be found in UDM. from_udm_obj(udm_obj, school, lo) classmethod; maps the info of udm_obj into a new instance (and sets school) from_dn(dn, school, lo) finds dn in LDAP and uses from_udm_obj get_first_udm_obj(lo, filter_str) returns the first found object of type cls._meta.udm_module that matches an arbitrary filter_str More features: * Validation: There are some auto checks built in: Attributes of the model that have a UDM syntax attached are validated against this syntax. Attributes that are required must be present. Attributes that are unlikely_to_change give a warning (not error) if the object already exists with other values. If the Meta information states that name_is_unique, the complete LDAP is searched for the instance's name before continuing. validate() can be further customized. * Hooks: Before create, modify, move and remove, hooks are called if build_hook_line() returns something. If the operation was successful, another set of hooks are called. All scripts in /usr/share/ucs-school-import/hooks/%(module)s_{create|modify|move|remove}_{pre|post}.d/ are called with the name of a temporary file containing the hook_line via run-parts. %(module)s is 'ucc' for cls._meta.udm_module == 'computers/ucc' by default and can be explicitely set with class Meta: hook_path = 'computer' ''' __metaclass__ = UCSSchoolHelperMetaClass _cache = {} _search_base_cache = {} _initialized_udm_modules = [] _empty_hook_paths = set() hook_sep_char = '\t' hook_path = '/usr/share/ucs-school-import/hooks/' name = CommonName(_('Name'), aka=['Name']) school = SchoolAttribute(_('School'), aka=['School']) @classmethod def cache(cls, *args, **kwargs): '''Initializes a new instance and caches it for subsequent calls. Useful when using School.cache(school_name) a lot in different functions, in loops, etc. ''' args = list(args) if args: kwargs['name'] = args.pop(0) if args: kwargs['school'] = args.pop(0) key = [cls.__name__] + [(k, kwargs[k]) for k in sorted(kwargs)] key = tuple(key) if key not in cls._cache: logger.debug('Initializing %r', key) obj = cls(**kwargs) cls._cache[key] = obj return cls._cache[key] @classmethod def invalidate_all_caches(cls): from ucsschool.lib.models.user import User from ucsschool.lib.models.network import Network from ucsschool.lib.models.utils import _pw_length_cache cls._cache.clear() # cls._search_base_cache.clear() # useless to clear _pw_length_cache.clear() Network._netmask_cache.clear() User._profile_path_cache.clear() User._samba_home_path_cache.clear() @classmethod def invalidate_cache(cls): for key in cls._cache.keys(): if key[0] == cls.__name__: logger.debug('Invalidating %r', key) cls._cache.pop(key) @classmethod def supports_school(cls): return 'school' in cls._attributes def __init__(self, name=None, school=None, **kwargs): '''Initializes a new instance with kwargs. Not every kwarg is accepted, though: The name must be defined as a attribute at class level (or by a base class). All attributes are initialized at least with None Sets self.old_dn to self.dn, i.e. the name in __init__ will determine the old_dn, changing it after __init__ will result in trying to move the object! ''' self._udm_obj_searched = False self._udm_obj = None kwargs['name'] = name kwargs['school'] = school for key, attr in self._attributes.items(): default = attr.value_default if callable(default): default = default() setattr(self, key, kwargs.get(key, default)) self.__position = None self.old_dn = None self.old_dn = self.dn self.errors = {} self.warnings = {} @classmethod @LDAP_Connection() def get_machine_connection(cls, ldap_user_read=None, ldap_machine_write=None): """Shortcut to get a cached ldap connection to the DC Master using this host's credentials""" return ldap_machine_write @property def position(self): if self.__position is None: return self.get_own_container() return self.__position @position.setter def position(self, position): if self.position != position: # allow dynamic school changes until creation self.__position = position @property def dn(self): '''Generates a DN where the lib would assume this instance to be. Changing name or school of self will most likely change the outcome of self.dn as well ''' if self.name and self.position: name = self._meta.ldap_map_function(self.name) return '%s=%s,%s' % (self._meta.ldap_name_part, escape_dn_chars(name), self.position) return self.old_dn def set_dn(self, dn): '''Does not really set dn, as this is generated on-the-fly. Instead, sets old_dn in case it was missed in the beginning or after create/modify/remove/move Also resets cached udm_obj as it may point to somewhere else ''' self._udm_obj_searched = False self.position = ldap.dn.dn2str(ldap.dn.str2dn(dn)[1:]) self.old_dn = dn def validate(self, lo, validate_unlikely_changes=False): from ucsschool.lib.models.school import School self.errors.clear() self.warnings.clear() for name, attr in self._attributes.iteritems(): value = getattr(self, name) try: attr.validate(value) except ValueError as e: self.add_error(name, str(e)) if self._meta.name_is_unique and not self._meta.allow_school_change: if self.exists_outside_school(lo): self.add_error( 'name', _('The name is already used somewhere outside the school. It may not be taken twice and has to be changed.' )) if self.supports_school() and self.school: if not School.cache(self.school).exists(lo): self.add_error( 'school', _('The school "%s" does not exist. Please choose an existing one or create it.' ) % self.school) if validate_unlikely_changes: if self.exists(lo): udm_obj = self.get_udm_object(lo) try: original_self = self.from_udm_obj(udm_obj, self.school, lo) except (UnknownModel, WrongModel): pass else: for name, attr in self._attributes.iteritems(): if attr.unlikely_to_change: new_value = getattr(self, name) old_value = getattr(original_self, name) if new_value and old_value: if new_value != old_value: self.add_warning( name, _('The value changed from %(old)s. This seems unlikely.' ) % {'old': old_value}) def add_warning(self, attribute, warning_message): warnings = self.warnings.setdefault(attribute, []) if warning_message not in warnings: warnings.append(warning_message) def add_error(self, attribute, error_message): errors = self.errors.setdefault(attribute, []) if error_message not in errors: errors.append(error_message) def exists(self, lo): return self.get_udm_object(lo) is not None def exists_outside_school(self, lo): if not self.supports_school(): return False from ucsschool.lib.models.school import School udm_obj = self.get_udm_object(lo) if udm_obj is None: return False return not udm_obj.dn.endswith(School.cache(self.school).dn) def call_hooks(self, hook_time, func_name): '''Calls run-parts in os.path.join(self.hook_path, '%s_%s_%s.d' % (self._meta.hook_path, func_name, hook_time)) if self.build_hook_line(hook_time, func_name) returns a non-empty string Usage in lib itself: hook_time in ['pre', 'post'] func_name in ['create', 'modify', 'remove'] In the lib, post-hooks are only called if the corresponding function returns True ''' # verify path hook_path = self._meta.hook_path path = os.path.join(self.hook_path, '%s_%s_%s.d' % (hook_path, func_name, hook_time)) if path in self._empty_hook_paths: return None if not os.path.isdir(path) or not os.listdir(path): logger.debug('%s not found or empty.', path) self._empty_hook_paths.add(path) return None logger.debug('%s shall be executed', path) dn = None if hook_time == 'post': dn = self.old_dn logger.debug('Building hook line: %r.build_hook_line(%r, %r)', self, hook_time, func_name) line = self.build_hook_line(hook_time, func_name) if not line: logger.debug('No line. Skipping!') return None line = line.strip() + '\n' # create temporary file with data with tempfile.NamedTemporaryFile() as tmpfile: tmpfile.write(line) tmpfile.flush() # invoke hook scripts # <script> <temporary file> [<ldap dn>] command = ['run-parts', path, '--arg', tmpfile.name] if dn: command.extend(('--arg', dn)) ret_code = subprocess.call(command) return ret_code == 0 def build_hook_line(self, hook_time, func_name): '''Must be overridden if the model wants to support hooks. Do so by something like: return self._build_hook_line(self.attr1, self.attr2, 'constant') ''' return None def _alter_udm_obj(self, udm_obj): for name, attr in self._attributes.iteritems(): if attr.udm_name: value = getattr(self, name) if value is not None: udm_obj[attr.udm_name] = value def create(self, lo, validate=True): ''' Creates a new UDM instance. Calls pre-hooks. If the object already exists, returns False. If the object does not yet exist, creates it, returns True and calls post-hooks. ''' self.call_hooks('pre', 'create') success = self.create_without_hooks(lo, validate) if success: self.call_hooks('post', 'create') return success def create_without_hooks(self, lo, validate): if self.exists(lo): return False logger.info('Creating %r', self) if validate: self.validate(lo) if self.errors: raise ValidationError(self.errors.copy()) pos = udm_uldap.position(ucr.get('ldap/base')) container = self.position if not container: logger.error('%r cannot determine a container. Unable to create!', self) return False try: pos.setDn(container) udm_obj = udm_modules.get(self._meta.udm_module).object( None, lo, pos, superordinate=self.get_superordinate(lo)) udm_obj.open() # here is the real logic self.do_create(udm_obj, lo) # get it fresh from the database (needed for udm_obj._exists ...) self.set_dn(self.dn) logger.info('%r successfully created', self) return True finally: self.invalidate_cache() def do_create(self, udm_obj, lo): '''Actual udm_obj manipulation. Override this if you want to further change values of udm_obj, e.g. def do_create(self, udm_obj, lo): udm_obj['used_in_ucs_school'] = '1' super(MyModel, self).do_create(udm_obj, lo) ''' self._alter_udm_obj(udm_obj) udm_obj.create() def modify(self, lo, validate=True, move_if_necessary=None): ''' Modifies an existing UDM instance. Calls pre-hooks. If the object does not exist, returns False. If the object exists, modifies it, returns True and calls post-hooks. ''' self.call_hooks('pre', 'modify') success = self.modify_without_hooks(lo, validate, move_if_necessary) if success: self.call_hooks('post', 'modify') return success def modify_without_hooks(self, lo, validate=True, move_if_necessary=None): logger.info('Modifying %r', self) if move_if_necessary is None: move_if_necessary = self._meta.allow_school_change if validate: self.validate(lo, validate_unlikely_changes=True) if self.errors: raise ValidationError(self.errors.copy()) udm_obj = self.get_udm_object(lo) if not udm_obj: logger.info('%s does not exist!', self.old_dn) return False try: old_attrs = deepcopy(udm_obj.info) self.do_modify(udm_obj, lo) # get it fresh from the database self.set_dn(self.dn) udm_obj = self.get_udm_object(lo) same = old_attrs == udm_obj.info if move_if_necessary: if udm_obj.dn != self.dn: if self.move_without_hooks(lo, udm_obj, force=True): same = False if same: logger.info('%r not modified. Nothing changed', self) else: logger.info('%r successfully modified', self) # return not same return True finally: self.invalidate_cache() def do_modify(self, udm_obj, lo): '''Actual udm_obj manipulation. Override this if you want to further change values of udm_obj, e.g. def do_modify(self, udm_obj, lo): udm_obj['used_in_ucs_school'] = '1' super(MyModel, self).do_modify(udm_obj, lo) ''' self._alter_udm_obj(udm_obj) udm_obj.modify(ignore_license=1) def move(self, lo, udm_obj=None, force=False): self.call_hooks('pre', 'move') success = self.move_without_hooks(lo, udm_obj, force) if success: self.call_hooks('post', 'move') return success def move_without_hooks(self, lo, udm_obj, force=False): if udm_obj is None: udm_obj = self.get_udm_object(lo) if udm_obj is None: logger.warning('No UDM object found to move from (%r)', self) return False if self.supports_school() and self.get_school_obj(lo) is None: logger.warn('%r wants to move itself to a not existing school', self) return False logger.info('Moving %r to %r', udm_obj.dn, self) if udm_obj.dn == self.dn: logger.warning('%r wants to move to its own DN!', self) return False if force or self._meta.allow_school_change: try: self.do_move(udm_obj, lo) finally: self.invalidate_cache() self.set_dn(self.dn) else: logger.warning( 'Would like to move %s to %r. But it is not allowed!', udm_obj.dn, self) return False return True def do_move(self, udm_obj, lo): old_school, new_school = self.get_school_from_dn( self.old_dn), self.get_school_from_dn(self.dn) udm_obj.move(self.dn, ignore_license=1) if self.supports_school() and old_school and old_school != new_school: self.do_school_change(udm_obj, lo, old_school) def change_school(self, school, lo): if self.school in self.schools: self.schools.remove(self.school) if school not in self.schools: self.schools.append(school) self.school = school self.position = self.get_own_container() return self.move(lo, force=True) def do_school_change(self, udm_obj, lo, old_school): logger.info('Going to move %r from school %r to %r', self.old_dn, old_school, self.school) def remove(self, lo): ''' Removes an existing UDM instance. Calls pre-hooks. If the object does not exist, returns False. If the object exists, removes it, returns True and calls post-hooks. ''' self.call_hooks('pre', 'remove') success = self.remove_without_hooks(lo) if success: self.call_hooks('post', 'remove') return success def remove_without_hooks(self, lo): logger.info('Deleting %r', self) udm_obj = self.get_udm_object(lo) if udm_obj: try: udm_obj.remove(remove_childs=True) udm_objects.performCleanup(udm_obj) self.set_dn(None) logger.info('%r successfully removed', self) return True finally: self.invalidate_cache() logger.info('%r does not exist!', self) return False @classmethod def get_name_from_dn(cls, dn): if dn: try: name = explode_dn(dn, 1)[0] except ldap.DECODING_ERROR: name = '' return cls._meta.ldap_unmap_function([name]) @classmethod def get_school_from_dn(cls, dn): return SchoolSearchBase.getOU(dn) @classmethod def find_field_label_from_name(cls, field): for name, attr in cls._attributes.items(): if name == field: return attr.label def get_error_msg(self): error_msg = '' for key, errors in self.errors.iteritems(): label = self.find_field_label_from_name(key) error_str = '' for error in errors: error_str += error if not (error.endswith('!') or error.endswith('.')): error_str += '.' error_str += ' ' error_msg += '%s: %s' % (label, error_str) return error_msg[:-1] def get_udm_object(self, lo): '''Returns the UDM object that corresponds to self. If self._meta.name_is_unique it searches for any UDM object with self.name. If not (which is the default) it searches for self.old_dn or self.dn Returns None if no object was found. Caches the result, even None If you want to re-search, you need to explicitely set self._udm_obj_searched = False ''' self.init_udm_module(lo) if self._udm_obj_searched is False or ( self._udm_obj and self._udm_obj.lo.binddn != lo.binddn): dn = self.old_dn or self.dn superordinate = self.get_superordinate(lo) if dn is None: logger.debug('Getting %s UDM object: No DN!', self.__class__.__name__) return if self._meta.name_is_unique: if self.name is None: return None udm_name = self._attributes['name'].udm_name name = self.get_name_from_dn(dn) filter_str = '%s=%s' % (udm_name, escape_filter_chars(name)) self._udm_obj = self.get_first_udm_obj(lo, filter_str, superordinate) else: logger.debug('Getting %s UDM object by dn: %s', self.__class__.__name__, dn) try: self._udm_obj = udm_modules.lookup( self._meta.udm_module, None, lo, scope='base', base=dn, superordinate=superordinate)[0] except (noObject, IndexError): self._udm_obj = None else: self._udm_obj.open() self._udm_obj_searched = True return self._udm_obj def get_school_obj(self, lo): from ucsschool.lib.models.school import School if not self.supports_school(): return None school = School.cache(self.school) try: return School.from_dn(school.dn, None, lo) except noObject: logger.warning('%r does not exist!', school) return None def get_superordinate(self, lo): return None def get_own_container(self): if self.supports_school() and not self.school: return None return self.get_container(self.school) @classmethod def get_container(cls, school): '''raises NotImplementedError by default. Needs to be overridden! ''' raise NotImplementedError('%s.get_container()' % (cls.__name__, )) @classmethod def get_search_base(cls, school_name): from ucsschool.lib.models.school import School if school_name not in cls._search_base_cache: school = School(name=school_name) cls._search_base_cache[school_name] = SchoolSearchBase( [school.name], dn=school.dn) return cls._search_base_cache[school_name] @classmethod def init_udm_module(cls, lo): if cls._meta.udm_module in cls._initialized_udm_modules: return pos = udm_uldap.position(lo.base) udm_modules.init(lo, pos, udm_modules.get(cls._meta.udm_module)) cls._initialized_udm_modules.append(cls._meta.udm_module) @classmethod def get_all(cls, lo, school, filter_str=None, easy_filter=False, superordinate=None): ''' Returns a list of all objects that can be found in cls.get_container() with the correct udm_module If filter_str is given, all udm properties with include_in_default_search are queried for that string (so that it should be the value) ''' cls.init_udm_module(lo) complete_filter = cls._meta.udm_filter if easy_filter: filter_from_filter_str = cls.build_easy_filter(filter_str) else: filter_from_filter_str = filter_str if filter_from_filter_str: if complete_filter: complete_filter = conjunction( '&', [complete_filter, filter_from_filter_str]) else: complete_filter = filter_from_filter_str complete_filter = str(complete_filter) logger.debug('Getting all %s of %s with filter %r', cls.__name__, school, complete_filter) ret = [] for udm_obj in cls.lookup(lo, school, complete_filter, superordinate=superordinate): udm_obj.open() try: ret.append(cls.from_udm_obj(udm_obj, school, lo)) except UnknownModel: continue return ret @classmethod def lookup(cls, lo, school, filter_s='', superordinate=None): try: return udm_modules.lookup(cls._meta.udm_module, None, lo, filter=filter_s, base=cls.get_container(school), scope='sub', superordinate=superordinate) except noObject: logger.warning( 'Error while getting all %s of %s: probably %r does not exist!', cls.__name__, school, cls.get_container(school)) return [] @classmethod def _attrs_for_easy_filter(cls): ret = [] module = udm_modules.get(cls._meta.udm_module) for key, prop in module.property_descriptions.iteritems(): if prop.include_in_default_search: ret.append(key) return ret @classmethod def build_easy_filter(cls, filter_str): if filter_str: sanitizer = LDAPSearchSanitizer() filter_str = sanitizer.sanitize('filter_str', {'filter_str': filter_str}) expressions = [] for key in cls._attrs_for_easy_filter(): expressions.append(expression(key, filter_str)) if expressions: return conjunction('|', expressions) @classmethod def from_udm_obj( cls, udm_obj, school, lo ): # Design fault. school is part of the DN or the ucsschoolSchool attribute. '''Creates a new instance with attributes of the udm_obj. Uses get_class_for_udm_obj() ''' cls.init_udm_module(lo) klass = cls.get_class_for_udm_obj(udm_obj, school) if klass is None: logger.warning( 'UDM object %s does not correspond to a class in UCS school lib!', udm_obj.dn) raise UnknownModel(udm_obj.dn, cls) if klass is not cls: logger.info('UDM object %s is not %s, but actually %s', udm_obj.dn, cls.__name__, klass.__name__) if not issubclass(klass, cls): # security! # ExamStudent must not be converted into Teacher/Student/etc., # SchoolClass must not be converted into ComputerRoom # while Group must be converted into ComputerRoom, etc. and User must be converted into Student, etc. raise WrongModel(udm_obj.dn, klass, cls) return klass.from_udm_obj(udm_obj, school, lo) udm_obj.open() attrs = { 'school': cls.get_school_from_dn(udm_obj.dn) or school } # TODO: is this adjustment okay? for name, attr in cls._attributes.iteritems(): if attr.udm_name: udm_value = udm_obj[attr.udm_name] if udm_value == '': udm_value = None attrs[name] = udm_value obj = cls(**deepcopy(attrs)) obj.set_dn(udm_obj.dn) obj._udm_obj_searched = True obj._udm_obj = udm_obj return obj @classmethod def get_class_for_udm_obj(cls, udm_obj, school): '''Returns cls by default. Can be overridden for base classes: class User(UCSSchoolHelperAbstractClass): @classmethod def get_class_for_udm_obj(cls, udm_obj, school) if something: return SpecialUser return cls class SpecialUser(User): pass Now, User.get_all() will return a list of User and SpecialUser objects If this function returns None for a udm_obj, that obj will not yield a new instance in get_all() and from_udm_obj() will return None for that udm_obj ''' return cls def __repr__(self): dn = self.dn dn = '%r, old_dn=%r' % (dn, self.old_dn) if dn != self.old_dn else repr(dn) if self.supports_school(): return '%s(name=%r, school=%r, dn=%s)' % ( self.__class__.__name__, self.name, self.school, dn) else: return '%s(name=%r, dn=%s)' % (self.__class__.__name__, self.name, dn) def __lt__(self, other): return self.name < other.name @classmethod def from_dn(cls, dn, school, lo, superordinate=None): '''Returns a new instance based on the UDM object found at dn raises noObject if the udm_module does not match the dn or dn is not found ''' cls.init_udm_module(lo) if school is None and cls.supports_school(): school = cls.get_school_from_dn(dn) if school is None: logger.warn('Unable to guess school from %r', dn) try: logger.debug('Looking up %s with dn %r', cls.__name__, dn) udm_obj = udm_modules.lookup(cls._meta.udm_module, None, lo, filter=cls._meta.udm_filter, base=dn, scope='base', superordinate=superordinate)[0] except IndexError: # happens when cls._meta.udm_module does not "match" the dn raise WrongObjectType(dn, cls) return cls.from_udm_obj(udm_obj, school, lo) @classmethod def get_only_udm_obj(cls, lo, filter_str, superordinate=None, base=None): '''Returns the one UDM object of class cls._meta.udm_module that matches a given filter. If more than one is found, a MultipleObjectsError is raised If none is found, None is returned ''' cls.init_udm_module(lo) if cls._meta.udm_filter: filter_str = '(&(%s)(%s))' % (cls._meta.udm_filter, filter_str) logger.debug('Getting %s UDM object by filter: %s', cls.__name__, filter_str) objs = udm_modules.lookup(cls._meta.udm_module, None, lo, scope='sub', base=base or ucr.get('ldap/base'), filter=str(filter_str), superordinate=superordinate) if len(objs) == 0: return None if len(objs) > 1: raise MultipleObjectsError(objs) obj = objs[0] obj.open() return obj @classmethod def get_first_udm_obj(cls, lo, filter_str, superordinate=None): '''Returns the first UDM object of class cls._meta.udm_module that matches a given filter ''' try: return cls.get_only_udm_obj(lo, filter_str, superordinate) except MultipleObjectsError as exc: obj = exc.objs[0] obj.open() return obj @classmethod def find_udm_superordinate(cls, dn, lo): module = udm_modules.get(cls._meta.udm_module) return udm_objects.get_superordinate(module, None, lo, dn) def to_dict(self): '''Returns a dictionary somewhat representing this instance. This dictionary is usually used when sending the instance to a browser as JSON. By default the attributes are present as well as the dn and the udm_module.''' ret = {'$dn$': self.dn, 'objectType': self._meta.udm_module} for name, attr in self._attributes.iteritems(): if not attr.internal: ret[name] = getattr(self, name) return ret def _map_func_name_to_code(self, func_name): if func_name == 'create': return 'A' elif func_name == 'modify': return 'M' elif func_name == 'remove': return 'D' elif func_name == 'move': return 'MV' def _build_hook_line(self, *args): attrs = [] for arg in args: val = arg if arg is None: val = '' if arg is False: val = 0 if arg is True: val = 1 attrs.append(str(val)) return self.hook_sep_char.join(attrs)
class School(UCSSchoolHelperAbstractClass): name = SchoolName(_('School name')) dc_name = DCName(_('DC Name')) dc_name_administrative = DCName(_('DC Name administrative server')) class_share_file_server = ShareFileServer(_('Server for class shares'), udm_name='ucsschoolClassShareFileServer') home_share_file_server = ShareFileServer(_('Server for Windows home directories'), udm_name='ucsschoolHomeShareFileServer') display_name = DisplayName(_('Display name')) school = None educational_servers = Attribute(_('Educational servers'), unlikely_to_change=True) administrative_servers = Attribute(_('Administrative servers'), unlikely_to_change=True) def __init__(self, name=None, school=None, **kwargs): super(School, self).__init__(name=name, **kwargs) self.display_name = self.display_name or self.name def build_hook_line(self, hook_time, func_name): if func_name == 'create': return self._build_hook_line(self.name, self.get_dc_name(or_fallback=False)) def get_district(self): if ucr.is_true('ucsschool/ldap/district/enable'): return self.name[:2] def get_own_container(self): district = self.get_district() if district: return 'ou=%s,%s' % (escape_dn_chars(district), self.get_container()) return self.get_container() @classmethod def get_container(cls, school=None): return ucr.get('ldap/base') @classmethod def cn_name(cls, name, default): ucr_var = 'ucsschool/ldap/default/container/%s' % name return ucr.get(ucr_var, default) def create_default_containers(self, lo): cn_pupils = self.cn_name('pupils', 'schueler') cn_teachers = self.cn_name('teachers', 'lehrer') cn_admins = self.cn_name('admins', 'admins') cn_classes = self.cn_name('class', 'klassen') cn_rooms = self.cn_name('rooms', 'raeume') user_containers = [cn_pupils, cn_teachers, cn_admins] group_containers = [cn_pupils, [cn_classes], cn_teachers, cn_rooms] if self.shall_create_administrative_objects(): cn_staff = self.cn_name('staff', 'mitarbeiter') cn_teachers_staff = self.cn_name('teachers-and-staff', 'lehrer und mitarbeiter') user_containers.extend([cn_staff, cn_teachers_staff]) group_containers.append(cn_staff) containers_with_path = { 'printer_path': ['printers'], 'user_path': ['users', user_containers], 'computer_path': ['computers', ['server', ['dc']]], 'network_path': ['networks'], 'group_path': ['groups', group_containers], 'dhcp_path': ['dhcp'], 'policy_path': ['policies'], 'share_path': ['shares', [cn_classes]], } def _add_container(name, last_dn, base_dn, path, lo): if isinstance(name, (list, tuple)): base_dn = last_dn for cn in name: last_dn = _add_container(cn, last_dn, base_dn, path, lo) else: container = Container(name=name, school=self.name) setattr(container, path, '1') container.position = base_dn last_dn = container.dn if not container.exists(lo): last_dn = container.create(lo, False) return last_dn last_dn = self.dn path = None for path, containers in containers_with_path.iteritems(): for cn in containers: last_dn = _add_container(cn, last_dn, self.dn, path, lo) def group_name(self, prefix_var, default_prefix): ucr_var = 'ucsschool/ldap/default/groupprefix/%s' % prefix_var name_part = ucr.get(ucr_var, default_prefix) school_part = self.name.lower() return '%s%s' % (name_part, school_part) def get_umc_policy_dn(self, name): # at least the default ones should exist due to the join script return ucr.get('ucsschool/ldap/default/policy/umc/%s' % name, 'cn=ucsschool-umc-%s-default,cn=UMC,cn=policies,%s' % (name, ucr.get('ldap/base'))) def create_default_groups(self, lo): # DC groups administrative_group_container = 'cn=ucsschool,cn=groups,%s' % ucr.get('ldap/base') # DC-Edukativnetz # OU%s-DC-Edukativnetz # Member-Edukativnetz # OU%s-Member-Edukativnetz administrative_group_names = self.get_administrative_group_name('educational', domain_controller='both', ou_specific='both') if self.shall_create_administrative_objects(): administrative_group_names.extend(self.get_administrative_group_name('administrative', domain_controller='both', ou_specific='both')) # same with Verwaltungsnetz for administrative_group_name in administrative_group_names: group = BasicGroup.cache(name=administrative_group_name, container=administrative_group_container) group.create(lo) # cn=ouadmins admin_group_container = 'cn=ouadmins,cn=groups,%s' % ucr.get('ldap/base') group = BasicGroup.cache(self.group_name('admins', 'admins-'), container=admin_group_container) group.create(lo) group.add_umc_policy(self.get_umc_policy_dn('admins'), lo) try: udm_obj = group.get_udm_object(lo) except noObject: logger.error('Could not load OU admin group %r for adding "school" value', group.dn) else: admin_option = 'ucsschoolAdministratorGroup' if admin_option not in udm_obj.options: udm_obj.options.append(admin_option) udm_obj['school'] = [self.name] udm_obj.modify() # cn=schueler group = Group.cache(self.group_name('pupils', 'schueler-'), self.name) group.create(lo) group.add_umc_policy(self.get_umc_policy_dn('pupils'), lo) # cn=lehrer group = Group.cache(self.group_name('teachers', 'lehrer-'), self.name) group.create(lo) group.add_umc_policy(self.get_umc_policy_dn('teachers'), lo) # cn=mitarbeiter if self.shall_create_administrative_objects(): group = Group.cache(self.group_name('staff', 'mitarbeiter-'), self.name) group.create(lo) group.add_umc_policy(self.get_umc_policy_dn('staff'), lo) if ucr.is_true('ucsschool/import/attach/policy/default-umc-users', True): # cn=Domain Users %s group = Group.cache("Domain Users %s" % (self.name,), self.name) group.create(lo) group.add_umc_policy("cn=default-umc-users,cn=UMC,cn=policies,%s" % (ucr.get('ldap/base'),), lo) def get_dc_name_fallback(self, administrative=False): if administrative: return 'dc%sv-01' % self.name.lower() # this is the naming convention, a trailing v for Verwaltungsnetz DCs else: return 'dc%s-01' % self.name.lower() def get_dc_name(self, administrative=False, or_fallback=True): if ucr.is_true('ucsschool/singlemaster', False): return ucr.get('hostname') elif self.dc_name: if administrative: return '%sv' % self.dc_name else: return self.dc_name else: if or_fallback: return self.get_dc_name_fallback(administrative=administrative) else: return None def get_share_fileserver_dn(self, set_by_self, lo): if set_by_self: set_by_self = self.get_name_from_dn(set_by_self) or set_by_self hostname = set_by_self or self.get_dc_name() if hostname == self.get_dc_name_fallback(): # does not matter if exists or not - dc object will be created later host = SchoolDC(name=hostname, school=self.name) return host.dn host = AnyComputer.get_first_udm_obj(lo, 'cn=%s' % escape_filter_chars(hostname)) if host: return host.dn else: logger.warning('Could not find %s. Using this host as ShareFileServer ("%s").', hostname, ucr.get('hostname')) return ucr.get('ldap/hostdn') def get_class_share_file_server(self, lo): return self.get_share_fileserver_dn(self.class_share_file_server, lo) def get_home_share_file_server(self, lo): return self.get_share_fileserver_dn(self.home_share_file_server, lo) def get_administrative_group_name(self, group_type, domain_controller=True, ou_specific=False, as_dn=False): if domain_controller == 'both': return flatten([self.get_administrative_group_name(group_type, True, ou_specific, as_dn), self.get_administrative_group_name(group_type, False, ou_specific, as_dn)]) if ou_specific == 'both': return flatten([self.get_administrative_group_name(group_type, domain_controller, False, as_dn), self.get_administrative_group_name(group_type, domain_controller, True, as_dn)]) if group_type == 'administrative': name = 'Verwaltungsnetz' else: name = 'Edukativnetz' if domain_controller: name = 'DC-%s' % name else: name = 'Member-%s' % name if ou_specific: name = 'OU%s-%s' % (self.name.lower(), name) if as_dn: return 'cn=%s,cn=ucsschool,cn=groups,%s' % (name, ucr.get('ldap/base')) else: return name def get_administrative_server_names(self, lo): dn = self.get_administrative_group_name('administrative', ou_specific=True, as_dn=True) return lo.get(dn, ['uniqueMember']).get('uniqueMember', []) def get_educational_server_names(self, lo): dn = self.get_administrative_group_name('educational', ou_specific=True, as_dn=True) return lo.get(dn, ['uniqueMember']).get('uniqueMember', []) def add_host_to_dc_group(self, lo): logger.info('School.add_host_to_dc_group(): ou_name=%r dc_name=%r', self.name, self.dc_name) if self.dc_name: dc = SchoolDCSlave(name=self.dc_name, school=self.name) dc.create(lo) dc_udm_obj = dc.get_udm_object(lo) groups = self.get_administrative_group_name('educational', ou_specific='both', as_dn=True) for grp in groups: if grp not in dc_udm_obj['groups']: dc_udm_obj['groups'].append(grp) dc_udm_obj.modify() def shall_create_administrative_objects(self): return ucr.is_true('ucsschool/ldap/noneducational/create/objects', True) def create_dc_slave(self, lo, name, administrative=False): if administrative and not self.shall_create_administrative_objects(): logger.warning('Not creating %s: An administrative DC shall not be created as by UCR variable %r', name, 'ucsschool/ldap/noneducational/create/objects') return False if not self.exists(lo): logger.error('%r does not exist. Cannot create %s', self, name) return False if administrative: groups = self.get_administrative_group_name('administrative', ou_specific='both', as_dn=True) else: groups = self.get_administrative_group_name('educational', ou_specific='both', as_dn=True) logger.debug('DC shall become member of %r', groups) dc = SchoolDCSlave(name=name, school=self.name, groups=groups) if dc.exists(lo): logger.info('%r exists. Setting groups, do not move to %r!', dc, self) # call dc.move() if really necessary to move return dc.modify(lo, move_if_necessary=False) else: existing_host = AnyComputer.get_first_udm_obj(lo, 'cn=%s' % escape_filter_chars(name)) if existing_host: logger.error('Given host name "%s" is already in use and no domaincontroller slave system. Please choose another name.', name) return False return dc.create(lo) def add_domain_controllers(self, lo): logger.info('School.add_domain_controllers(): ou_name=%r', self.name) school_dcs = ucr.get('ucsschool/ldap/default/dcs', 'edukativ').split() for dc in school_dcs: administrative = dc == 'verwaltung' dc_name = self.get_dc_name(administrative=administrative) server = AnyComputer.get_first_udm_obj(lo, 'cn=%s' % escape_filter_chars(dc_name)) logger.info('School.add_domain_controllers(): administrative=%r dc_name=%s self.dc_name=%r server=%r', administrative, dc_name, self.dc_name, server) if not server and not self.dc_name: if administrative: administrative_type = 'administrative' else: administrative_type = 'educational' group_dn = self.get_administrative_group_name(administrative_type, ou_specific=True, as_dn=True) try: hostlist = lo.get(group_dn, ['uniqueMember']).get('uniqueMember', []) except ldap.NO_SUCH_OBJECT: hostlist = [] except Exception, e: logger.error('cannot read %s: %s', group_dn, e) return if hostlist: continue # if at least one DC has control over this OU then jump to next 'school_dcs' item ==> do not create default slave objects self.create_dc_slave(lo, dc_name, administrative=administrative) dhcp_service = self.get_dhcp_service(dc_name) dhcp_service.create(lo) dhcp_service.add_server(dc_name, lo) return True
class DHCPService(UCSSchoolHelperAbstractClass): name = DHCPServiceName(_('Service')) hostname = Attribute(_('Hostname')) domainname = Attribute(_('Domain')) def do_create(self, udm_obj, lo): udm_obj.options.append('options') udm_obj['option'] = [ 'wpad "http://%s.%s/proxy.pac"' % (self.hostname, self.domainname) ] return super(DHCPService, self).do_create(udm_obj, lo) @classmethod def get_container(cls, school): return cls.get_search_base(school).dhcp def add_server(self, dc_name, lo, force_dhcp_server_move=False): """ Create the given DHCP server within the DHCP service. If the DHCP server object already exists somewhere else within the LDAP tree, it may be moved to the DHCP service. PLEASE NOTE: In multiserver environments an existing DHCP server object is always moved to the current DHCP service. In single server environments the DHCP server object is *ONLY* moved, if the UCR variable dhcpd/ldap/base matches to the current DHCP service. """ from ucsschool.lib.models.school import School # create dhcp-server if not exsistant school = School.cache(self.school) dhcp_server = DHCPServer(name=dc_name, school=school.name, dhcp_service=self) existing_dhcp_server_dn = DHCPServer.find_any_dn_with_name(dc_name, lo) if existing_dhcp_server_dn: logger.info('DHCP server %s exists!', existing_dhcp_server_dn) old_dhcp_server_container = lo.parentDn(existing_dhcp_server_dn) dhcpd_ldap_base = ucr.get('dhcpd/ldap/base', '') # only move if # - forced via kwargs OR # - in multiserver environments OR # - desired dhcp server DN matches with UCR config if force_dhcp_server_move or not ucr.is_true( 'ucsschool/singlemaster', False) or dhcp_server.dn.endswith(',%s' % dhcpd_ldap_base): # move if existing DN does not match with desired DN if existing_dhcp_server_dn != dhcp_server.dn: # move existing dhcp server object to OU/DHCP service logger.info( 'DHCP server %s not in school %r! Removing and creating new one at %s!', existing_dhcp_server_dn, school, dhcp_server.dn) old_superordinate = DHCPServer.find_udm_superordinate( existing_dhcp_server_dn, lo) old_dhcp_server = DHCPServer.from_dn( existing_dhcp_server_dn, None, lo, superordinate=old_superordinate) old_dhcp_server.remove(lo) dhcp_server.create(lo) # copy subnets # find local interfaces interfaces = [] for interface_name in set([ key.split('/')[1] for key in ucr.keys() if key.startswith('interfaces/eth') ]): try: address = ipaddr.IPv4Network('%s/%s' % ( ucr['interfaces/%s/address' % interface_name], ucr['interfaces/%s/netmask' % interface_name], )) interfaces.append(address) except ValueError as exc: logger.info('Skipping invalid interface %s:\n%s', interface_name, exc) subnet_dns = DHCPSubnet.find_all_dns_below_base( old_dhcp_server_container, lo) for subnet_dn in subnet_dns: dhcp_service = DHCPSubnet.find_udm_superordinate(subnet_dn, lo) dhcp_subnet = DHCPSubnet.from_dn(subnet_dn, self.school, lo, superordinate=dhcp_service) subnet = dhcp_subnet.get_ipv4_subnet() if subnet in interfaces: # subnet matches any local subnet logger.info('Creating new DHCPSubnet from %s', subnet_dn) new_dhcp_subnet = DHCPSubnet(**dhcp_subnet.to_dict()) new_dhcp_subnet.dhcp_service = self new_dhcp_subnet.position = new_dhcp_subnet.get_own_container( ) new_dhcp_subnet.set_dn(new_dhcp_subnet.dn) new_dhcp_subnet.create(lo) else: logger.info('Skipping non-local subnet %s', subnet) else: logger.info('No DHCP server named %s found! Creating new one!', dc_name) dhcp_server.create(lo) def get_servers(self, lo): ret = [] for dhcp_server in DHCPServer.get_all(lo, self.school, superordinate=self): dhcp_server.dhcp_service = self ret.append(dhcp_server) return ret class Meta: udm_module = 'dhcp/service'
class Group(UCSSchoolHelperAbstractClass): name = GroupName(_('Name')) description = Description(_('Description')) users = Users(_('Users')) @classmethod def get_container(cls, school): return cls.get_search_base(school).groups @classmethod def is_school_group(cls, school, group_dn): return cls.get_search_base(school).isGroup(group_dn) @classmethod def is_school_workgroup(cls, school, group_dn): return cls.get_search_base(school).isWorkgroup(group_dn) @classmethod def is_school_class(cls, school, group_dn): return cls.get_search_base(school).isClass(group_dn) @classmethod def is_computer_room(cls, school, group_dn): return cls.get_search_base(school).isRoom(group_dn) def self_is_workgroup(self): return self.is_school_workgroup(self.school, self.dn) def self_is_class(self): return self.is_school_class(self.school, self.dn) def self_is_computerroom(self): return self.is_computer_room(self.school, self.dn) @classmethod def get_class_for_udm_obj(cls, udm_obj, school): if cls.is_school_class(school, udm_obj.dn): return SchoolClass elif cls.is_computer_room(school, udm_obj.dn): return ComputerRoom elif cls.is_school_workgroup(school, udm_obj.dn): return WorkGroup elif cls.is_school_group(school, udm_obj.dn): return SchoolGroup return cls def add_umc_policy(self, policy_dn, lo): if not policy_dn or policy_dn.lower() == 'none': logger.warning('No policy added to %r', self) return try: policy = UMCPolicy.from_dn(policy_dn, self.school, lo) except noObject: logger.warning( 'Object to be referenced does not exist (or is no UMC-Policy): %s', policy_dn) else: policy.attach(self, lo) def build_hook_line(self, hook_time, func_name): code = self._map_func_name_to_code(func_name) if code != 'M': return self._build_hook_line( code, self.school, self.name, self.description, ) else: # This is probably a bug. See ucs-school-import and Bug #34736 old_name = self.get_name_from_dn(self.old_dn) new_name = self.name if old_name != new_name: return self._build_hook_line( code, old_name, new_name, ) class Meta: udm_module = 'groups/group' name_is_unique = True
class UCCComputer(SchoolComputer): type_name = _('Univention Corporate Client') class Meta(SchoolComputer.Meta): udm_module = 'computers/ucc' hook_path = 'computer'
class MacComputer(SchoolComputer): type_name = _('Mac OS X') class Meta(SchoolComputer.Meta): udm_module = 'computers/macos' hook_path = 'computer'
class WindowsComputer(SchoolComputer): type_name = _('Windows system') class Meta(SchoolComputer.Meta): udm_module = 'computers/windows' hook_path = 'computer'
class DHCPDNSPolicy(Policy): empty_attributes = EmptyAttributes(_('Empty attributes')) class Meta: udm_module = 'policies/dhcp_dns'
class IPComputer(SchoolComputer): type_name = _('Device with IP address') class Meta(SchoolComputer.Meta): udm_module = 'computers/ipmanagedclient' hook_path = 'computer'
class User(UCSSchoolHelperAbstractClass): name = Username(_('Username'), aka=['Username', 'Benutzername']) schools = Schools(_('Schools')) firstname = Firstname(_('First name'), aka=['First name', 'Vorname'], required=True, unlikely_to_change=True) lastname = Lastname(_('Last name'), aka=['Last name', 'Nachname'], required=True, unlikely_to_change=True) birthday = Birthday(_('Birthday'), aka=['Birthday', 'Geburtstag'], unlikely_to_change=True) email = Email(_('Email'), aka=['Email', 'E-Mail'], unlikely_to_change=True) password = Password(_('Password'), aka=['Password', 'Passwort']) disabled = Disabled(_('Disabled'), aka=['Disabled', 'Gesperrt']) school_classes = SchoolClassesAttribute(_('Class'), aka=['Class', 'Klasse']) type_name = None type_filter = '(|(objectClass=ucsschoolTeacher)(objectClass=ucsschoolStaff)(objectClass=ucsschoolStudent))' _profile_path_cache = {} _samba_home_path_cache = {} # _samba_home_path_cache is invalidated in School.invalidate_cache() roles = [] default_options = () def __init__(self, *args, **kwargs): super(User, self).__init__(*args, **kwargs) if self.school_classes is None: self.school_classes = {} # set a dict for Staff if self.school and not self.schools: self.schools.append(self.school) @classmethod def shall_create_mail_domain(cls): return ucr.is_true('ucsschool/import/generate/mail/domain') def get_roleshare_home_subdir(self): from ucsschool.lib.roleshares import roleshare_home_subdir return roleshare_home_subdir(self.school, self.roles, ucr) def get_samba_home_drive(self): return ucr.get('ucsschool/import/set/homedrive') def get_samba_netlogon_script_path(self): return ucr.get('ucsschool/import/set/netlogon/script/path') def get_samba_home_path(self, lo): school = School.cache(self.school) # if defined then use UCR value ucr_variable = ucr.get('ucsschool/import/set/sambahome') if ucr_variable is not None: samba_home_path = r'\\%s' % ucr_variable.strip('\\') # in single server environments the master is always the fileserver elif ucr.is_true('ucsschool/singlemaster', False): samba_home_path = r'\\%s' % ucr.get('hostname') # if there's a cached result then use it elif school.dn not in self._samba_home_path_cache: samba_home_path = None # get windows home server from OU object school = self.get_school_obj(lo) home_share_file_server = school.home_share_file_server if home_share_file_server: samba_home_path = r'\\%s' % self.get_name_from_dn( home_share_file_server) self._samba_home_path_cache[school.dn] = samba_home_path else: samba_home_path = self._samba_home_path_cache[school.dn] if samba_home_path is not None: return r'%s\%s' % (samba_home_path, self.name) def get_profile_path(self, lo): ucr_variable = ucr.get('ucsschool/import/set/serverprofile/path') if ucr_variable is not None: return ucr_variable school = School.cache(self.school) if school.dn not in self._profile_path_cache: profile_path = r'%s\%%USERNAME%%\windows-profiles\default' for computer in AnyComputer.get_all( lo, self.school, 'univentionService=Windows Profile Server'): profile_path = profile_path % (r'\\%s' % computer.name) break else: profile_path = profile_path % '%LOGONSERVER%' self._profile_path_cache[school.dn] = profile_path return self._profile_path_cache[school.dn] def is_student(self, lo): return self.__check_object_class(lo, 'ucsschoolStudent', self._legacy_is_student) def is_exam_student(self, lo): return self.__check_object_class(lo, 'ucsschoolExam', self._legacy_is_exam_student) def is_teacher(self, lo): return self.__check_object_class(lo, 'ucsschoolTeacher', self._legacy_is_teacher) def is_staff(self, lo): return self.__check_object_class(lo, 'ucsschoolStaff', self._legacy_is_staff) def is_administrator(self, lo): return self.__check_object_class(lo, 'ucsschoolAdministrator', self._legacy_is_admininstrator) @classmethod def _legacy_is_student(cls, school, dn): logger.warning('Using deprecated method is_student()') return dn.endswith(cls.get_search_base(school).students) @classmethod def _legacy_is_exam_student(cls, school, dn): logger.warning('Using deprecated method is_exam_student()') return dn.endswith(cls.get_search_base(school).examUsers) @classmethod def _legacy_is_teacher(cls, school, dn): logger.warning('Using deprecated method is_teacher()') search_base = cls.get_search_base(school) return dn.endswith(search_base.teachers) or dn.endswith( search_base.teachersAndStaff) or dn.endswith(search_base.admins) @classmethod def _legacy_is_staff(cls, school, dn): logger.warning('Using deprecated method is_staff()') search_base = cls.get_search_base(school) return dn.endswith(search_base.staff) or dn.endswith( search_base.teachersAndStaff) @classmethod def _legacy_is_admininstrator(cls, school, dn): logger.warning('Using deprecated method is_admininstrator()') return dn.endswith(cls.get_search_base(school).admins) def __check_object_class(self, lo, object_class, fallback): obj = self.get_udm_object(lo) if not obj: raise noObject('Could not read %r' % (self.dn, )) if 'ucsschoolSchool' in obj.oldattr: return object_class in obj.oldattr.get('objectClass', []) return fallback(self.school, self.dn) @classmethod def get_class_for_udm_obj(cls, udm_obj, school): ocs = set(udm_obj.oldattr.get('objectClass', [])) if ocs >= set(['ucsschoolTeacher', 'ucsschoolStaff']): return TeachersAndStaff if ocs >= set(['ucsschoolExam', 'ucsschoolStudent']): return ExamStudent if 'ucsschoolTeacher' in ocs: return Teacher if 'ucsschoolStaff' in ocs: return Staff if 'ucsschoolStudent' in ocs: return Student if 'ucsschoolAdministrator' in ocs: return Teacher # we have no class for a school administrator # legacy DN based checks if cls._legacy_is_student(school, udm_obj.dn): return Student if cls._legacy_is_teacher(school, udm_obj.dn): if cls._legacy_is_staff(school, udm_obj.dn): return TeachersAndStaff return Teacher if cls._legacy_is_staff(school, udm_obj.dn): return Staff if cls._legacy_is_exam_student(school, udm_obj.dn): return ExamStudent return User @classmethod def from_udm_obj(cls, udm_obj, school, lo): obj = super(User, cls).from_udm_obj(udm_obj, school, lo) obj.password = None obj.school_classes = cls.get_school_classes(udm_obj, obj) return obj def do_create(self, udm_obj, lo): if not self.schools: self.schools = [self.school] self.set_default_options(udm_obj) self.create_mail_domain(lo) password_created = False if not self.password: logger.debug('No password given. Generating random one') old_password = self.password # None or '' self.password = create_passwd(dn=self.dn) password_created = True udm_obj['primaryGroup'] = self.primary_group_dn(lo) udm_obj['groups'] = self.groups_used(lo) subdir = self.get_roleshare_home_subdir() udm_obj['unixhome'] = '/home/' + os.path.join(subdir, self.name) udm_obj['overridePWHistory'] = '1' udm_obj['overridePWLength'] = '1' if self.disabled is None: udm_obj['disabled'] = 'none' if 'mailbox' in udm_obj: udm_obj['mailbox'] = '/var/spool/%s/' % self.name samba_home = self.get_samba_home_path(lo) if samba_home: udm_obj['sambahome'] = samba_home profile_path = self.get_profile_path(lo) if profile_path: udm_obj['profilepath'] = profile_path home_drive = self.get_samba_home_drive() if home_drive is not None: udm_obj['homedrive'] = home_drive script_path = self.get_samba_netlogon_script_path() if script_path is not None: udm_obj['scriptpath'] = script_path success = super(User, self).do_create(udm_obj, lo) if password_created: # to not show up in host_hooks self.password = old_password return success def do_modify(self, udm_obj, lo): self.create_mail_domain(lo) self.password = self.password or None removed_schools = set(udm_obj['school']) - set(self.schools) if removed_schools: # change self.schools back, so schools can be removed by remove_from_school() self.schools = udm_obj['school'] for removed_school in removed_schools: logger.info('Removing %r from school %r...', self, removed_school) if not self.remove_from_school(removed_school, lo): logger.error('Error removing %r from school %r.', self, removed_school) return False mandatory_groups = self.groups_used(lo) for group_dn in udm_obj['groups'][:]: logger.debug('Checking group %s for removal', group_dn) if group_dn not in mandatory_groups: logger.debug('Group not mandatory! Part of a school?') try: school_class = SchoolClass.from_dn(group_dn, None, lo) except noObject: logger.debug('No. Leaving it alone...') continue logger.debug('Yes, part of %s!', school_class.school) if school_class.school not in self.school_classes: continue # if the key isn't set we don't change anything to the groups. to remove the groups it has to be an empty list classes = self.school_classes[school_class.school] remove = school_class.name not in classes and school_class.get_relative_name( ) not in classes if remove: logger.debug('Removing it!') udm_obj['groups'].remove(group_dn) else: logger.debug( 'Leaving it alone: Part of own school and either non-school class or new school classes were not defined at all' ) for group_dn in mandatory_groups: logger.debug('Checking group %s for adding', group_dn) if group_dn not in udm_obj['groups']: logger.debug('Group is not yet part of the user. Adding...') udm_obj['groups'].append(group_dn) return super(User, self).do_modify(udm_obj, lo) def do_school_change(self, udm_obj, lo, old_school): super(User, self).do_school_change(udm_obj, lo, old_school) school = self.school logger.info('User is part of the following groups: %r', udm_obj['groups']) self.remove_from_groups_of_school(old_school, lo) self._udm_obj_searched = False self.school_classes.pop(old_school, None) udm_obj = self.get_udm_object(lo) udm_obj['primaryGroup'] = self.primary_group_dn(lo) groups = set(udm_obj['groups']) at_least_groups = set(self.groups_used(lo)) if (groups | at_least_groups) != groups: udm_obj['groups'] = list(groups | at_least_groups) subdir = self.get_roleshare_home_subdir() udm_obj['unixhome'] = '/home/' + os.path.join(subdir, self.name) samba_home = self.get_samba_home_path(lo) if samba_home: udm_obj['sambahome'] = samba_home profile_path = self.get_profile_path(lo) if profile_path: udm_obj['profilepath'] = profile_path home_drive = self.get_samba_home_drive() if home_drive is not None: udm_obj['homedrive'] = home_drive script_path = self.get_samba_netlogon_script_path() if script_path is not None: udm_obj['scriptpath'] = script_path if udm_obj['departmentNumber'] == old_school: udm_obj['departmentNumber'] = school if school not in udm_obj['school']: udm_obj['school'].append(school) if old_school in udm_obj['school']: udm_obj['school'].remove(old_school) udm_obj.modify(ignore_license=True) def _alter_udm_obj(self, udm_obj): if self.email is not None: udm_obj['e-mail'] = self.email udm_obj['departmentNumber'] = self.school ret = super(User, self)._alter_udm_obj(udm_obj) return ret def get_mail_domain(self): if self.email: domain_name = self.email.split('@')[-1] return MailDomain.cache(domain_name) def create_mail_domain(self, lo): mail_domain = self.get_mail_domain() if mail_domain is not None and not mail_domain.exists(lo): if self.shall_create_mail_domain(): mail_domain.create(lo) else: logger.warning('Not allowed to create %r.', mail_domain) def set_default_options(self, udm_obj): for option in self.get_default_options(): if option not in udm_obj.options: udm_obj.options.append(option) @classmethod def get_default_options(cls): options = set() for kls in cls.__bases__: # u-s-import uses multiple inheritance, we have to cover all parents try: options.update(kls.get_default_options()) except AttributeError: pass options.update(cls.default_options) return options def get_specific_groups(self, lo): groups = self.get_domain_users_groups() for school_class in self.get_school_class_objs(): groups.append( self.get_class_dn(school_class.name, school_class.school, lo)) return groups def validate(self, lo, validate_unlikely_changes=False): super(User, self).validate(lo, validate_unlikely_changes) try: udm_obj = self.get_udm_object(lo) except UnknownModel: udm_obj = None except WrongModel as exc: udm_obj = None self.add_error( 'name', _('It is not supported to change the role of a user. %(old_role)s %(name)s cannot become a %(new_role)s.' ) % { 'old_role': exc.model.type_name, 'name': self.name, 'new_role': self.type_name }) if udm_obj: original_class = self.get_class_for_udm_obj(udm_obj, self.school) if original_class is not self.__class__: self.add_error( 'name', _('It is not supported to change the role of a user. %(old_role)s %(name)s cannot become a %(new_role)s.' ) % { 'old_role': original_class.type_name, 'name': self.name, 'new_role': self.type_name }) if self.email: name, email = escape_filter_chars(self.name), escape_filter_chars( self.email) if self.get_first_udm_obj( lo, '&(!(uid=%s))(mailPrimaryAddress=%s)' % (name, email)): self.add_error( 'email', _('The email address is already taken by another user. Please change the email address.' )) # mail_domain = self.get_mail_domain(lo) # if not mail_domain.exists(lo) and not self.shall_create_mail_domain(): # self.add_error('email', _('The mail domain is unknown. Please change the email address or create the mail domain "%s" using the Univention Directory Manager.') % mail_domain.name) def remove_from_school(self, school, lo): if not self.exists(lo): logger.warning('User does not exists, not going to remove.') return False try: (self.schools or [school]).remove(school) except ValueError: logger.warning('User is not part of school %r. Not removing.', school) return False if not self.schools: logger.warning('User %r not part of any school, removing it.', self) return self.remove(lo) if self.school == school: if not self.change_school(self.schools[0], lo): return False else: self.remove_from_groups_of_school(school, lo) self.school_classes.pop(school, None) return True def remove_from_groups_of_school(self, school, lo): for cls in (SchoolClass, WorkGroup, SchoolGroup): for group in cls.get_all( lo, school, filter_format('uniqueMember=%s', (self.dn, ))): try: group.users.remove(self.dn) except ValueError: pass else: logger.info('Removing %r from group %r of school %r.', self.dn, group.dn, school) group.modify(lo) def get_group_dn(self, group_name, school): return Group.cache(group_name, school).dn def get_class_dn(self, class_name, school, lo): # Bug #32337: check if the class exists without OU prefix # if it does not exist the class name with OU prefix is used school_class = SchoolClass.cache(class_name, school) if school_class.get_relative_name() == school_class.name: if not school_class.exists(lo): class_name = '%s-%s' % (school, class_name) school_class = SchoolClass.cache(class_name, school) return school_class.dn def primary_group_dn(self, lo): dn = self.get_group_dn('Domain Users %s' % self.school, self.school) return self.get_or_create_group_udm_object(dn, lo).dn def get_domain_users_groups(self): return [ self.get_group_dn('Domain Users %s' % school, school) for school in self.schools ] def get_students_groups(self): prefix = ucr.get('ucsschool/ldap/default/groupprefix/pupils', 'schueler-') return [ self.get_group_dn('%s%s' % (prefix, school), school) for school in self.schools ] def get_teachers_groups(self): prefix = ucr.get('ucsschool/ldap/default/groupprefix/teachers', 'lehrer-') return [ self.get_group_dn('%s%s' % (prefix, school), school) for school in self.schools ] def get_staff_groups(self): prefix = ucr.get('ucsschool/ldap/default/groupprefix/staff', 'mitarbeiter-') return [ self.get_group_dn('%s%s' % (prefix, school), school) for school in self.schools ] def groups_used(self, lo): group_dns = self.get_specific_groups(lo) for group_dn in group_dns: self.get_or_create_group_udm_object(group_dn, lo) return group_dns @classmethod def get_or_create_group_udm_object(cls, group_dn, lo, fresh=False): name = cls.get_name_from_dn(group_dn) school = cls.get_school_from_dn(group_dn) if Group.is_school_class(school, group_dn): group = SchoolClass.cache(name, school) else: group = Group.cache(name, school) if fresh: group._udm_obj_searched = False group.create(lo) return group def is_active(self): return self.disabled != 'all' def build_hook_line(self, hook_time, func_name): code = self._map_func_name_to_code(func_name) is_teacher = isinstance(self, Teacher) or isinstance( self, TeachersAndStaff) is_staff = isinstance(self, Staff) or isinstance( self, TeachersAndStaff) school_class = ','.join(self.school_classes.get( self.school, [])) # legacy format: only classes of the primary school return self._build_hook_line( code, self.name, self.lastname, self.firstname, self.school, school_class, '', # TODO: rights? self.email, is_teacher, self.is_active(), is_staff, self.password, ) def to_dict(self): ret = super(User, self).to_dict() display_name = [] if self.firstname: display_name.append(self.firstname) if self.lastname: display_name.append(self.lastname) ret['display_name'] = ' '.join(display_name) school_classes = {} for school_class in self.get_school_class_objs(): school_classes.setdefault(school_class.school, []).append(school_class.name) ret['school_classes'] = school_classes ret['type_name'] = self.type_name ret['type'] = self.__class__.__name__ ret['type'] = ret['type'][0].lower() + ret['type'][1:] return ret def get_school_class_objs(self): ret = [] for school, classes in self.school_classes.iteritems(): for school_class in classes: ret.append(SchoolClass.cache(school_class, school)) return ret @classmethod def get_school_classes(cls, udm_obj, obj): school_classes = {} for group in udm_obj['groups']: for school in obj.schools: if Group.is_school_class(school, group): school_class_name = cls.get_name_from_dn(group) school_classes.setdefault(school, []).append(school_class_name) return school_classes @classmethod def get_container(cls, school): return cls.get_search_base(school).users @classmethod def lookup(cls, lo, school, filter_s='', superordinate=None): filter_object_type = conjunction('&', [ parse(cls.type_filter), parse(filter_format('ucsschoolSchool=%s', [school])) ]) if filter_s: filter_object_type = conjunction( '&', [filter_object_type, parse(filter_s)]) objects = udm_modules.lookup(cls._meta.udm_module, None, lo, filter=unicode(filter_object_type), scope='sub', superordinate=superordinate) objects.extend(obj for obj in super(User, cls).lookup( lo, school, filter_s, superordinate=superordinate) if not any(obj.dn == x.dn for x in objects)) return objects class Meta: udm_module = 'users/user' name_is_unique = True allow_school_change = False
class SchoolDCSlave(SchoolDC): groups = Groups(_('Groups')) def do_create(self, udm_obj, lo): udm_obj['unixhome'] = '/dev/null' udm_obj['shell'] = '/bin/bash' udm_obj['primaryGroup'] = BasicGroup.cache('DC Slave Hosts').dn return super(SchoolDCSlave, self).do_create(udm_obj, lo) def _alter_udm_obj(self, udm_obj): if self.groups: for group in self.groups: if group not in udm_obj['groups']: udm_obj['groups'].append(group) return super(SchoolDCSlave, self)._alter_udm_obj(udm_obj) def move_without_hooks(self, lo, udm_obj=None, force=False): try: if udm_obj is None: try: udm_obj = self.get_only_udm_obj( lo, 'cn=%s' % escape_filter_chars(self.name)) except MultipleObjectsError: logger.error( 'Found more than one DC Slave with hostname "%s"', self.name) return False if udm_obj is None: logger.error('Cannot find DC Slave with hostname "%s"', self.name) return False old_dn = udm_obj.dn school = self.get_school_obj(lo) group_dn = school.get_administrative_group_name('educational', ou_specific=True, as_dn=True) if group_dn not in udm_obj['groups']: logger.error('%r has no LDAP access to %r', self, school) return False if old_dn == self.dn: logger.info( 'DC Slave "%s" is already located in "%s" - stopping here', self.name, self.school) self.set_dn(old_dn) if self.exists_outside_school(lo): if not force: logger.error('DC Slave "%s" is located in another OU - %s', self.name, udm_obj.dn) logger.error('Use force=True to override') return False if school is None: logger.error( 'Cannot move DC Slave object - School does not exist: %r', school) return False self.modify_without_hooks(lo) if school.class_share_file_server == old_dn: school.class_share_file_server = self.dn if school.home_share_file_server == old_dn: school.home_share_file_server = self.dn school.modify_without_hooks(lo) removed = False # find dhcp server object by checking all dhcp service objects for dhcp_service in AnyDHCPService.get_all(lo, None): for dhcp_server in dhcp_service.get_servers(lo): if dhcp_server.name == self.name and not dhcp_server.dn.endswith( ',%s' % school.dn): dhcp_server.remove(lo) removed = True if removed: own_dhcp_service = school.get_dhcp_service() dhcp_server = DHCPServer(name=self.name, school=self.school, dhcp_service=own_dhcp_service) dhcp_server.create(lo) logger.info('Move complete') logger.warning('The DC Slave has to be rejoined into the domain!') finally: self.invalidate_cache() return True class Meta: udm_module = 'computers/domaincontroller_slave' name_is_unique = True allow_school_change = True
def validate(self, value): super(SchoolName, self).validate(value) if ucr.is_true('ucsschool/singlemaster', False): regex = re.compile('^[a-zA-Z0-9](([a-zA-Z0-9-]*)([a-zA-Z0-9]$))?$') if not regex.match(value): raise ValueError(_('Invalid school name'))