Source code for univention.appcenter.extended_attributes

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention App Center
#  univention-app wrapper for udm's settings/extended_attributes
#
# Copyright 2016-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
#

import os
import re

from ldap.dn import escape_dn_chars
from six import with_metaclass

from univention.appcenter.app import CaseSensitiveConfigParser
from univention.appcenter.log import get_base_logger
from univention.appcenter.meta import UniventionMetaClass, UniventionMetaInfo
from univention.appcenter.ucr import ucr_get, ucr_run_filter
from univention.appcenter.udm import create_object_if_not_exists, create_recursive_container, remove_object_if_exists, modify_object
from univention.appcenter.utils import underscore, get_md5, read_ini_file


attribute_logger = get_base_logger().getChild('attributes')


[docs]class Attribute(UniventionMetaInfo): pop = True save_as_list = '_attrs' auto_set_name = True def __init__(self, default_value=None): self.default_value = default_value super(Attribute, self).__init__()
[docs] def ldap_name(self): return self.name.upper().replace('_', '-')
[docs] def escape_value(self, value): return value
[docs] def to_schema(self, obj): name = self.ldap_name() value = self.get_value(obj) if value is not None: return "%s %s" % (name, value)
[docs] def get_value(self, obj): return self.escape_value(getattr(obj, self.name))
[docs]class HiddenAttribute(Attribute):
[docs] def to_schema(self, obj): return None
[docs]class StringAttribute(Attribute):
[docs] def escape_value(self, value): return "'%s'" % value
[docs]class DescAttribute(StringAttribute):
[docs] def ldap_name(self): return 'DESC'
[docs] def escape_value(self, value): return super(DescAttribute, self).escape_value(value or 'Attribute created by the App Center integration for Extended Attributes')
[docs]class BooleanAttribute(Attribute):
[docs] def to_schema(self, obj): value = self.get_value(obj) if value: return self.ldap_name()
[docs]class AttributeListAttribute(Attribute):
[docs] def escape_value(self, value): values = sorted(val for val in set(re.split(r'\s*,\s*', value or '')) if val) if values: return '( ' + ' $ '.join(values) + ' )'
[docs]class SyntaxAttribute(Attribute):
[docs] def to_schema(self, obj): ret = 'SYNTAX %s EQUALITY %s' % (obj._syntax, obj._equality) if obj._substr: ret = '%s SUBSTR %s' % (ret, obj._substr) return ret
[docs]class SchemaObject(with_metaclass(UniventionMetaClass, object)): ldap_type = None ldap_type_oid_suffix = None oid = HiddenAttribute() name = StringAttribute() ldap_desc = DescAttribute() def __init__(self, app, **kwargs): for attr in self._attrs: setattr(self, attr.name, kwargs.get(attr.name, attr.default_value))
[docs] def to_schema(self): info = [] for attr in self._attrs: attr_schema = attr.to_schema(self) if attr_schema: info.append(attr_schema) info = '\n '.join(info) return '%(ldap_type)s ( %(oid)s\n %(info)s\n )' % {'ldap_type': self.ldap_type, 'oid': self.oid, 'info': info}
[docs] def set_standard_oid(self, app, suffix): oid = '1.3.6.1.4.1.10176.5000' # Univention OID + 5000 (= reserved for App Center) app_hash = str(int(get_md5(app.id), 16)) while app_hash: new_prefix, app_hash = app_hash[:5], app_hash[5:] oid = '%s.%s' % (oid, int(new_prefix)) self.oid = '%s.%s.%s' % (oid, self.ldap_type_oid_suffix, suffix)
def __repr__(self): return '<%s(%s)>' % (type(self).__name__, ', '.join('%s=%r' % (attr.name, getattr(self, attr.name)) for attr in self._attrs))
[docs]class ExtendedAttribute(SchemaObject): ldap_type = 'attributetype' ldap_type_oid_suffix = 1 description = HiddenAttribute() description_de = HiddenAttribute() long_description = HiddenAttribute() long_description_de = HiddenAttribute() syntax = SyntaxAttribute('String') single_value = BooleanAttribute(True) default = HiddenAttribute() module = HiddenAttribute() belongs_to = HiddenAttribute() ldap_mapping = HiddenAttribute() position = HiddenAttribute() full_width = HiddenAttribute(False) disable_web = HiddenAttribute() copyable = HiddenAttribute() required = HiddenAttribute() options = HiddenAttribute() tab_name = HiddenAttribute() tab_name_de = HiddenAttribute() advanced = HiddenAttribute() hook = HiddenAttribute() group_position = HiddenAttribute() overwrite_tab = HiddenAttribute() overwrite_position = HiddenAttribute() not_editable = HiddenAttribute() dont_search = HiddenAttribute() tab_position = HiddenAttribute() group_name = HiddenAttribute() group_name_de = HiddenAttribute() delete_object_class = HiddenAttribute(True) may_change = HiddenAttribute(True) cli_name = HiddenAttribute() udm_syntax = HiddenAttribute() @property def dn(self): return 'cn=%s,%s,%s' % (escape_dn_chars(self.name), self.position, ucr_get('ldap/base')) def __init__(self, app, **kwargs): kwargs.setdefault('belongs_to', '%sUser' % (app.id,)) kwargs.setdefault('position', 'cn=%s,cn=custom attributes,cn=univention' % escape_dn_chars(app.id)) kwargs.setdefault('tab_name', app.name) kwargs.setdefault('ldap_mapping', kwargs['name']) kwargs['module'] = re.split(r'\s*,\s*', kwargs.get('module', 'users/user')) if 'options' in kwargs: kwargs['options'] = re.split(r'\s*,\s*', kwargs.get('options', [])) kwargs.setdefault('options', []) super(ExtendedAttribute, self).__init__(app, **kwargs) if self.syntax == 'Boolean': self._syntax = '1.3.6.1.4.1.1466.115.121.1.7' self._equality = 'booleanMatch' self._substr = None if not self.udm_syntax: self.udm_syntax = 'TrueFalseUp' elif self.syntax == 'BooleanString': self._syntax = '1.3.6.1.4.1.1466.115.121.1.15' self._equality = 'caseIgnoreMatch' self._substr = 'caseIgnoreSubstringsMatch' if not self.udm_syntax: self.udm_syntax = 'TrueFalseUp' elif self.syntax == 'String': self._syntax = '1.3.6.1.4.1.1466.115.121.1.15' self._equality = 'caseIgnoreMatch' self._substr = 'caseIgnoreSubstringsMatch' #self._syntax = '1.3.6.1.4.1.1466.115.121.1.26' #self._equality = 'caseIgnoreIA5Match' #self._substr = None if not self.udm_syntax: self.udm_syntax = 'string' else: attribute_logger.warn('Ignoring unknown syntax %r' % (self.syntax,))
[docs]class ExtendedOption(SchemaObject): name = HiddenAttribute() position = HiddenAttribute() description = HiddenAttribute() long_description = HiddenAttribute() description_de = HiddenAttribute() long_description_de = HiddenAttribute() default = HiddenAttribute() editable = HiddenAttribute(True) module = HiddenAttribute() object_class = HiddenAttribute() def __init__(self, app, **kwargs): kwargs.setdefault('position', 'cn=%s,cn=custom attributes,cn=univention' % escape_dn_chars(app.id)) kwargs.setdefault('description', app.name) kwargs['module'] = re.split(r'\s*,\s*', kwargs.get('module', 'users/user')) super(ExtendedOption, self).__init__(app, **kwargs) @property def icon(self): return '%s.svg' % (self.name,) @property def dn(self): return 'cn=%s,%s,%s' % (escape_dn_chars(self.name), self.position, ucr_get('ldap/base'))
[docs]class ObjectClass(SchemaObject): ldap_type = 'objectclass' ldap_type_oid_suffix = 2 auxiliary = BooleanAttribute(True) sup = Attribute('top') may = AttributeListAttribute('') must = AttributeListAttribute('') option_name = HiddenAttribute() def __init__(self, app, **kwargs): kwargs.setdefault('option_name', kwargs['name']) super(ObjectClass, self).__init__(app, **kwargs)
[docs]def get_extended_attributes(app): attributes = [] object_classes = [] extended_options = [] parser = read_ini_file(app.get_cache_file('attributes'), CaseSensitiveConfigParser) object_class_suffix = 1 attribute_suffix = 1 for section in parser.sections(): kwargs = dict([underscore(key), ucr_run_filter(value)] for key, value in parser.items(section)) kwargs['name'] = section kwargs.setdefault('type', 'ExtendedAttribute') if kwargs['type'] == 'ObjectClass': object_class = ObjectClass(app, **kwargs) if object_class.oid is None: object_class.set_standard_oid(app, object_class_suffix) object_class_suffix += 1 attribute_logger.debug('Adding %s to list of classes' % section) object_classes.append(object_class) # for backwards compatibility with UCS 4.3 we can't use the new type == ExtendedOption, so this flag is the equivalent if kwargs.get('add_extended_option') == '1': okwargs = kwargs.copy() okwargs['name'] = kwargs.get('option_name', '%sUser' % (app.id,)) okwargs.setdefault('object_class', object_class.name) option = ExtendedOption(app, **okwargs) attribute_logger.debug('Adding %s to list of options' % (okwargs['name'],)) extended_options.append(option) elif kwargs['type'] == 'ExtendedOption': # Can't be used if System < UCS 4.4, use add_extended_option instead! option = ExtendedOption(app, **kwargs) attribute_logger.debug('Adding %s to list of options' % section) extended_options.append(option) elif kwargs['type'] == 'ExtendedAttribute': attribute = ExtendedAttribute(app, **kwargs) attribute_logger.debug('Adding %s to list of attributes' % section) if attribute.oid is None: attribute.set_standard_oid(app, attribute_suffix) attribute_suffix += 1 attributes.append(attribute) else: # ignore, so that it is extensible for the future :-) attribute_logger.warn('Unknown attribute type for section %s: %r' % (section, kwargs['type'])) if app.generic_user_activation: attribute_name = app.generic_user_activation_attribute if attribute_name is not False: if attribute_name is True or not attribute_name: attribute_name = '%sActivated' % (app.id,) try: attribute = [attr for attr in attributes if attr.name == attribute_name][0] except IndexError: attribute_logger.debug('Adding %s to list of attributes' % attribute_name) attribute = ExtendedAttribute( app, module='users/user', name=attribute_name, description='Activate user for %s' % app.name, description_de='Nutzer für %s aktivieren' % app.name, syntax='Boolean', full_width=False, ) attribute.set_standard_oid(app, attribute_suffix) attributes.insert(0, attribute) option_name = app.generic_user_activation_option if option_name is True: option_name = '%sUser' % (app.id,) if option_name and option_name not in [opt.name for opt in extended_options]: attribute_logger.debug('Adding %s to list of options' % option_name) option = ExtendedOption( app, name=option_name, module='users/user', object_class=option_name, long_description='Activate user for %s' % app.name, long_description_de='Nutzer für %s aktivieren' % app.name ) extended_options.insert(0, option) for attribute in attributes: if attribute.belongs_to not in [obj.name for obj in object_classes]: class_name = attribute.belongs_to object_class = ObjectClass(app, name=class_name) object_class.set_standard_oid(app, object_class_suffix) object_class_suffix += 1 object_classes.insert(0, object_class) object_class = [obj for obj in object_classes if obj.name == attribute.belongs_to][0] if attribute.name not in re.split(r'\s*,\s*', object_class.must): object_class.may = '%s, %s' % (object_class.may, attribute.name) for option in extended_options: if option.name in (object_class.option_name, attribute.belongs_to): attribute.options.append(option.name) for option in extended_options: if option.object_class not in [obj.name for obj in object_classes]: class_name = option.object_class object_class = ObjectClass(app, name=class_name) object_class.set_standard_oid(app, object_class_suffix) object_class_suffix += 1 object_classes.insert(0, object_class) return attributes, object_classes, extended_options
[docs]def get_schema(app): ret = [] attributes, object_classes, __ = get_extended_attributes(app) for attribute in attributes: ret.append(attribute.to_schema()) for object_class in object_classes: ret.append(object_class.to_schema()) return '\n\n'.join(ret)
[docs]def create_extended_attribute(attribute, app, layout_position, lo, pos): attrs = {} attribute_position = '%s,%s' % (attribute.position, ucr_get('ldap/base')) create_recursive_container(attribute_position, lo, pos) pos.setDn(attribute_position) attrs['name'] = attribute.name attrs['shortDescription'] = attribute.description if attribute.long_description: attrs['longDescription'] = attribute.long_description if attribute.description_de: attrs['translationShortDescription'] = [('de_DE', attribute.description_de)] if attribute.long_description_de: attrs['translationLongDescription'] = [('de_DE', attribute.long_description_de)] attrs['syntax'] = attribute.udm_syntax attrs['multivalue'] = str(int(not attribute.single_value)) if attribute.default: attrs['default'] = attribute.default attrs['tabPosition'] = attribute.tab_position or str(layout_position) attrs['tabName'] = attribute.tab_name if attribute.tab_name_de: attrs['translationTabName'] = [('de_DE', attribute.tab_name_de)] attrs['groupName'] = attribute.group_name or app.name if attribute.group_name_de: attrs['translationGroupName'] = [('de_DE', attribute.group_name_de)] attrs['ldapMapping'] = attribute.ldap_mapping attrs['objectClass'] = attribute.belongs_to attrs['module'] = attribute.module attrs['deleteObjectClass'] = attribute.delete_object_class attrs['mayChange'] = attribute.may_change attrs['fullWidth'] = attribute.full_width attrs['hook'] = attribute.hook attrs['disableUDMWeb'] = attribute.disable_web attrs['groupPosition'] = attribute.group_position attrs['tabAdvanced'] = attribute.advanced attrs['overwriteTab'] = attribute.overwrite_tab attrs['overwritePosition'] = attribute.overwrite_position attrs['valueRequired'] = attribute.required attrs['notEditable'] = attribute.not_editable attrs['doNotSearch'] = attribute.dont_search attrs['copyable'] = attribute.copyable attrs['options'] = attribute.options attrs['CLIName'] = attribute.cli_name attrs = dict((key, value) for key, value in attrs.items() if value is not None) attribute_logger.debug('Creating DN: %s' % attribute.dn) if not create_object_if_not_exists('settings/extended_attribute', lo, pos, **attrs): attribute_logger.debug('... already exists. Overwriting!') modify_object('settings/extended_attribute', lo, pos, attribute.dn, **attrs)
[docs]def remove_extended_attribute(attribute, lo, pos): attribute_logger.debug('Removing DN: %s' % attribute.dn) remove_object_if_exists('settings/extended_attribute', lo, pos, attribute.dn)
[docs]def create_extended_option(option, app, lo, pos): attrs = {} option_position = '%s,%s' % (option.position, ucr_get('ldap/base')) create_recursive_container(option_position, lo, pos) pos.setDn(option_position) attrs['name'] = option.name attrs['shortDescription'] = option.description if option.long_description: attrs['longDescription'] = option.long_description if option.description_de: attrs['translationShortDescription'] = [('de_DE', option.description_de)] if option.long_description_de: attrs['translationLongDescription'] = [('de_DE', option.long_description_de)] attrs['default'] = option.default attrs['editable'] = option.editable attrs['module'] = option.module attrs['objectClass'] = option.object_class attrs['isApp'] = '1' attribute_logger.debug('Creating DN: %s' % option.dn) if not create_object_if_not_exists('settings/extended_options', lo, pos, **attrs): attribute_logger.debug('... already exists. Overwriting!') modify_object('settings/extended_options', lo, pos, option.dn, **attrs)
[docs]def create_option_icon(app): __, __, options = get_extended_attributes(app) options = set([option.icon for option in options] + ['%s.svg' % (attribute.split(':', 1)[-1],) for attribute in app.umc_options_attributes]) for option in options: icon = '/usr/share/univention-management-console-frontend/js/dijit/themes/umc/icons/scalable/%s' % (option,) if os.path.exists(icon) or os.path.islink(icon): os.unlink(icon) os.symlink(app.logo_name, icon)
[docs]def remove_extended_option(option, lo, pos): attribute_logger.debug('Removing DN: %s' % option.dn) remove_object_if_exists('settings/extended_options', lo, pos, option.dn)