1# -*- coding: ascii -*-
2"""
3web2ldap plugin classes for
4
5\xC6-DIR -- Authorized Entities Directory
6"""
7
8# Python's standard lib
9import re
10import time
11import socket
12from typing import Dict, List, Optional
13
14# from ldap0 package
15import ldap0
16import ldap0.filter
17from ldap0.filter import escape_str as escape_filter_str
18from ldap0.functions import strf_secs as ldap0_strf_secs
19from ldap0.pw import random_string
20from ldap0.controls.readentry import PreReadControl
21from ldap0.controls.deref import DereferenceControl
22from ldap0.filter import compose_filter, map_filter_parts
23from ldap0.dn import DNObj
24from ldap0.res import SearchResultEntry
25from ldap0.base import decode_list
26
27import web2ldapcnf
28
29from ...log import logger
30from ...web.forms import HiddenInput, Field
31from ..searchform import (
32    SEARCH_OPT_IS_EQUAL,
33    SEARCH_OPT_DN_SUBTREE,
34)
35from .nis import UidNumber, GidNumber, MemberUID, Shell
36from .inetorgperson import DisplayNameInetOrgPerson, CNInetOrgPerson
37from .groups import GroupEntryDN
38from .oath import OathHOTPToken
39from .opensshlpk import SshPublicKey
40from .posixautogen import HomeDirectory
41from .ppolicy import PwdPolicySubentry
42from .sudoers import SudoUserGroup
43from ..schema.syntaxes import (
44    ComposedAttribute,
45    DirectoryString,
46    DistinguishedName,
47    DNSDomain,
48    DerefDynamicDNSelectList,
49    DynamicValueSelectList,
50    IA5String,
51    Integer,
52    NotAfter,
53    NotBefore,
54    RFC822Address,
55    SelectList,
56    syntax_registry,
57)
58from .. import ErrorExit
59
60
61# OID arc for AE-DIR, see stroeder.com-oid-macros.schema
62AE_OID_PREFIX = '1.3.6.1.4.1.5427.1.389.100'
63
64# OIDs of AE-DIR's structural object classes
65AE_USER_OID = AE_OID_PREFIX+'.6.2'
66AE_GROUP_OID = AE_OID_PREFIX+'.6.1'
67AE_MAILGROUP_OID = AE_OID_PREFIX+'.6.27'
68AE_SRVGROUP_OID = AE_OID_PREFIX+'.6.13'
69AE_SUDORULE_OID = AE_OID_PREFIX+'.6.7'
70AE_HOST_OID = AE_OID_PREFIX+'.6.6.1'
71AE_SERVICE_OID = AE_OID_PREFIX+'.6.4'
72AE_ZONE_OID = AE_OID_PREFIX+'.6.20'
73AE_PERSON_OID = AE_OID_PREFIX+'.6.8'
74AE_TAG_OID = AE_OID_PREFIX+'.6.24'
75AE_POLICY_OID = AE_OID_PREFIX+'.6.26'
76AE_AUTHCTOKEN_OID = AE_OID_PREFIX+'.6.25'
77AE_DEPT_OID = AE_OID_PREFIX+'.6.29'
78AE_CONTACT_OID = AE_OID_PREFIX+'.6.5'
79AE_LOCATION_OID = AE_OID_PREFIX+'.6.35'
80AE_NWDEVICE_OID = AE_OID_PREFIX+'.6.6.2'
81
82
83syntax_registry.reg_at(
84    DNSDomain.oid, [
85        AE_OID_PREFIX+'.4.10', # aeFqdn
86    ]
87)
88
89
90def ae_validity_filter(secs=None):
91    if secs is None:
92        secs = time.time()
93    return (
94        '(&'
95          '(|(!(aeNotBefore=*))(aeNotBefore<={0}))'
96          '(|(!(aeNotAfter=*))(aeNotAfter>={0}))'
97        ')'
98    ).format(ldap0_strf_secs(secs))
99
100
101class AEObjectMixIn:
102    """
103    utility mix-in class for all aeObject entries
104    """
105
106    @property
107    def ae_status(self):
108        try:
109            ae_status = int(self._entry['aeStatus'][0])
110        except (KeyError, ValueError, IndexError):
111            ae_status = None
112        return ae_status
113
114    def _zone_entry(self, attrlist=None):
115        zone_dn = 'cn={0},{1}'.format(
116            self._get_zone_name(),
117            self._app.naming_context,
118        )
119        try:
120            zone = self._app.ls.l.read_s(
121                zone_dn,
122                attrlist=attrlist,
123                filterstr='(objectClass=aeZone)',
124            )
125        except ldap0.LDAPError:
126            res = {}
127        else:
128            if zone is None:
129                res = {}
130            else:
131                res = zone.entry_s
132        return res
133
134    def _get_zone_dn(self) -> str:
135        return str(self.dn.slice(-len(DNObj.from_str(self._app.naming_context))-1, None))
136
137    def _get_zone_name(self) -> str:
138        return self.dn[-len(DNObj.from_str(self._app.naming_context))-1][0][1]
139
140
141class AEHomeDirectory(HomeDirectory):
142    """
143    Plugin for attribute 'homeDirectory' in aeUser and aeService entries
144    """
145    oid: str = 'AEHomeDirectory-oid'
146    # all valid directory prefixes for attribute 'homeDirectory'
147    # but without trailing slash
148    homeDirectoryPrefixes = (
149        '/home',
150    )
151    homeDirectoryHidden = b'-/-'
152
153    def _validate(self, attr_value: bytes) -> bool:
154        av_u = self._app.ls.uc_decode(attr_value)[0]
155        if attr_value == self.homeDirectoryHidden:
156            return True
157        for prefix in self.homeDirectoryPrefixes:
158            if av_u.startswith(prefix):
159                uid = self._app.ls.uc_decode(self._entry.get('uid', [b''])[0])[0]
160                return av_u.endswith(uid)
161        return False
162
163    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
164        if attr_values == [self.homeDirectoryHidden]:
165            return attr_values
166        if 'uid' in self._entry:
167            uid = self._app.ls.uc_decode(self._entry['uid'][0])[0]
168        else:
169            uid = ''
170        if attr_values:
171            av_u = self._app.ls.uc_decode(attr_values[0])[0]
172            for prefix in self.homeDirectoryPrefixes:
173                if av_u.startswith(prefix):
174                    break
175            else:
176                prefix = self.homeDirectoryPrefixes[0]
177        else:
178            prefix = self.homeDirectoryPrefixes[0]
179        return [self._app.ls.uc_encode('/'.join((prefix, uid)))[0]]
180
181    def input_field(self) -> Field:
182        input_field = HiddenInput(
183            self._at,
184            ': '.join([self._at, self.desc]),
185            self.max_len,
186            self.max_values,
187            None,
188            default=self.form_value()
189        )
190        input_field.charset = self._app.form.accept_charset
191        return input_field
192
193syntax_registry.reg_at(
194    AEHomeDirectory.oid, [
195        '1.3.6.1.1.1.1.3', # homeDirectory
196    ],
197    structural_oc_oids=[AE_USER_OID, AE_SERVICE_OID], # aeUser and aeService
198)
199
200
201class AEUIDNumber(UidNumber):
202    """
203    Plugin for attribute 'uidNumber' in aeUser and aeService entries
204    """
205    oid: str = 'AEUIDNumber-oid'
206    desc: str = 'numeric Unix-UID'
207
208    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
209        return self._entry.get('gidNumber', [b''])
210
211    def input_field(self) -> Field:
212        input_field = HiddenInput(
213            self._at,
214            ': '.join([self._at, self.desc]),
215            self.max_len, self.max_values, None,
216            default=self.form_value()
217        )
218        input_field.charset = self._app.form.accept_charset
219        return input_field
220
221syntax_registry.reg_at(
222    AEUIDNumber.oid, [
223        '1.3.6.1.1.1.1.0', # uidNumber
224    ],
225    structural_oc_oids=[
226        AE_USER_OID,    # aeUser
227        AE_SERVICE_OID, # aeService
228    ],
229)
230
231
232class AEGIDNumber(GidNumber):
233    """
234    Plugin for attribute 'gidNumber' in aeUser, aeGroup and aeService entries
235    """
236    oid: str = 'AEGIDNumber-oid'
237    desc: str = 'numeric Unix-GID'
238    minNewValue = 30000
239    maxNewValue = 49999
240    id_pool_dn = None
241
242    def _get_id_pool_dn(self) -> str:
243        """
244        determine which ID pool entry to use
245        """
246        return self.id_pool_dn or str(self._app.naming_context)
247
248    def _get_next_gid(self) -> int:
249        """
250        consumes next ID by sending MOD_INCREMENT modify operation with
251        pre-read entry control
252        """
253        prc = PreReadControl(criticality=True, attrList=[self._at])
254        ldap_result = self._app.ls.l.modify_s(
255            self._get_id_pool_dn(),
256            [(ldap0.MOD_INCREMENT, self._app.ls.uc_encode(self._at)[0], [b'1'])],
257            req_ctrls=[prc],
258        )
259        return int(ldap_result.ctrls[0].res.entry_s[self._at][0])
260
261    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
262        if attr_values and attr_values[0]:
263            return attr_values
264        # first try to re-read gidNumber from existing entry
265        try:
266            ldap_result = self._app.ls.l.read_s(
267                self._dn,
268                attrlist=[self._at],
269                filterstr='({0}=*)'.format(self._at),
270            )
271        except (
272                ldap0.NO_SUCH_OBJECT,
273                ldap0.INSUFFICIENT_ACCESS,
274            ):
275            # search failed => ignore
276            pass
277        else:
278            if ldap_result:
279                return ldap_result.entry_as[self._at]
280        # return next ID from pool entry
281        return [str(self._get_next_gid()).encode('ascii')]
282
283    def form_value(self) -> str:
284        return Integer.form_value(self)
285
286    def input_field(self) -> Field:
287        return Integer.input_field(self)
288
289syntax_registry.reg_at(
290    AEGIDNumber.oid, [
291        '1.3.6.1.1.1.1.1', # gidNumber
292    ],
293    structural_oc_oids=[
294        AE_USER_OID,    # aeUser
295        AE_GROUP_OID,   # aeGroup
296        AE_SERVICE_OID, # aeService
297    ],
298)
299
300
301class AEUid(IA5String):
302    """
303    Base class for attribute 'uid' mainly for sanitizing input values
304    """
305    oid: str = 'AEUid-oid'
306    sani_funcs = (
307        bytes.strip,
308        bytes.lower,
309    )
310
311
312class AEUserUid(AEUid):
313    """
314    Class for auto-generating values for aeUser -> uid
315    """
316    oid: str = 'AEUserUid-oid'
317    desc: str = 'AE-DIR: User name'
318    max_values = 1
319    min_len: int = 4
320    max_len: int = 4
321    maxCollisionChecks: int = 15
322    UID_LETTERS = 'abcdefghijklmnopqrstuvwxyz'
323    pattern = re.compile('^[{}]+$'.format(UID_LETTERS))
324    genLen = 4
325    sani_funcs = (
326        bytes.strip,
327        bytes.lower,
328    )
329
330    def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=None):
331        IA5String.__init__(self, app, dn, schema, attrType, attr_value, entry=entry)
332
333    def _gen_uid(self):
334        uid_candidates = []
335        while len(uid_candidates) < self.maxCollisionChecks:
336            # generate new random UID candidate
337            uid_candidate = random_string(alphabet=self.UID_LETTERS, length=self.genLen)
338            # check whether UID candidate already exists
339            uid_result = self._app.ls.l.search_s(
340                str(self._app.naming_context),
341                ldap0.SCOPE_SUBTREE,
342                '(uid=%s)' % (escape_filter_str(uid_candidate)),
343                attrlist=['1.1'],
344            )
345            if not uid_result:
346                logger.info(
347                    'Generated aeUser-uid after %d collisions: %r',
348                    len(uid_candidates),
349                    uid_candidate,
350                )
351                return uid_candidate
352            uid_candidates.append(uid_candidate)
353        logger.error(
354            'Generating aeUser-uid stopped after %d collisions. Tried candidates: %r',
355            len(uid_candidates),
356            uid_candidates,
357        )
358        raise ErrorExit(
359            'Gave up generating new unique <em>uid</em> after {0:d} attempts.'.format(
360                len(uid_candidates),
361            )
362        )
363        # end of _gen_uid()
364
365    def form_value(self) -> str:
366        fval = IA5String.form_value(self)
367        if not self._av:
368            fval = self._gen_uid()
369        return fval
370
371    def input_field(self) -> Field:
372        return HiddenInput(
373            self._at,
374            ': '.join([self._at, self.desc]),
375            self.max_len, self.max_values, None,
376            default=self.form_value()
377        )
378
379    def sanitize(self, attr_value: bytes) -> bytes:
380        return attr_value.strip().lower()
381
382syntax_registry.reg_at(
383    AEUserUid.oid, [
384        '0.9.2342.19200300.100.1.1', # uid
385    ],
386    structural_oc_oids=[
387        AE_USER_OID, # aeUser
388    ],
389)
390
391
392class AEServiceUid(AEUid):
393    """
394    Plugin for attribute 'uid' in aeService entries
395    """
396    oid: str = 'AEServiceUid-oid'
397
398syntax_registry.reg_at(
399    AEServiceUid.oid, [
400        '0.9.2342.19200300.100.1.1', # uid
401    ],
402    structural_oc_oids=[
403        AE_SERVICE_OID, # aeService
404    ],
405)
406
407
408class AETicketId(IA5String):
409    """
410    Plugin for attribute 'aeTicketId' in all aeObject entries
411    """
412    oid: str = 'AETicketId-oid'
413    desc: str = 'AE-DIR: Ticket no. related to last change of entry'
414    sani_funcs = (
415        bytes.upper,
416        bytes.strip,
417    )
418
419syntax_registry.reg_at(
420    AETicketId.oid, [
421        AE_OID_PREFIX+'.4.3', # aeTicketId
422    ]
423)
424
425
426class AERootDynamicDNSelectList(DerefDynamicDNSelectList):
427    """
428    custom variant with smarter handling of search base
429    """
430    oid: str = 'AERootDynamicDNSelectList-oid'
431    input_fallback = False # no fallback to normal input field
432    suffix_attr = 'aeRoot'
433
434    def _search_root(self) -> str:
435        if self.lu_obj.dn == self.suffix_attr:
436            try:
437                ae_suffix = self._app.ls.l.read_rootdse_s(
438                    attrlist=['self.suffix_attr']
439                ).entry_s[self.suffix_attr][0]
440            except (ldap0.LDAPError, KeyError):
441                pass
442            else:
443                return ae_suffix
444        return DerefDynamicDNSelectList._search_root(self)
445
446
447class AEZoneDN(AERootDynamicDNSelectList):
448    """
449    Plugin for attributes holding DNs of aeZone entries
450    """
451    oid: str = 'AEZoneDN-oid'
452    desc: str = 'AE-DIR: Zone'
453    ldap_url = 'ldap:///_?cn?sub?(&(objectClass=aeZone)(aeStatus=0))'
454    ref_attrs = (
455        (None, 'Same zone', None, 'aeGroup', 'Search all groups constrained to same zone'),
456    )
457
458syntax_registry.reg_at(
459    AEZoneDN.oid, [
460        AE_OID_PREFIX+'.4.36', # aeMemberZone
461    ]
462)
463
464
465class AEHost(AERootDynamicDNSelectList):
466    """
467    Plugin for attribute 'host' in aeHost entries
468    """
469    oid: str = 'AEHost-oid'
470    desc: str = 'AE-DIR: Host'
471    ldap_url = 'ldap:///_?host?sub?(&(objectClass=aeHost)(aeStatus=0))'
472    ref_attrs = (
473        (None, 'Same host', None, 'aeService', 'Search all services running on same host'),
474    )
475
476syntax_registry.reg_at(
477    AEHost.oid, [
478        AE_OID_PREFIX+'.4.28', # aeHost
479    ]
480)
481
482
483class AENwDevice(AERootDynamicDNSelectList):
484    """
485    Plugin for attributes holding DNs of aeNwDevice entries
486    """
487    oid: str = 'AENwDevice-oid'
488    desc: str = 'AE-DIR: network interface'
489    ldap_url = 'ldap:///..?cn?sub?(&(objectClass=aeNwDevice)(aeStatus=0))'
490    ref_attrs = (
491        (None, 'Siblings', None, 'aeNwDevice', 'Search sibling network devices'),
492    )
493
494    def _search_root(self) -> str:
495        if self._dn.startswith('host='):
496            return self._dn
497        return DerefDynamicDNSelectList._search_root(self)
498
499    def _filterstr(self):
500        orig_filter = DerefDynamicDNSelectList._filterstr(self)
501        try:
502            dev_name = self._app.ls.uc_decode(self._entry['cn'][0])[0]
503        except (KeyError, IndexError):
504            result_filter = orig_filter
505        else:
506            result_filter = '(&{0}(!(cn={1})))'.format(orig_filter, dev_name)
507        return result_filter
508
509syntax_registry.reg_at(
510    AENwDevice.oid, [
511        AE_OID_PREFIX+'.4.34', # aeNwDevice
512    ]
513)
514
515
516class AEGroupMember(DerefDynamicDNSelectList, AEObjectMixIn):
517    """
518    Plugin for attribute 'member' in aeGroup entries
519    """
520    oid: str = 'AEGroupMember-oid'
521    desc: str = 'AE-DIR: Member of a group'
522    input_fallback = False # no fallback to normal input field
523    ldap_url = (
524        'ldap:///_?displayName?sub?'
525        '(&(|(objectClass=aeUser)(objectClass=aeService))(aeStatus=0))'
526    )
527    deref_person_attrs = ('aeDept', 'aeLocation')
528
529    def _zone_filter(self):
530        member_zones = [
531            self._app.ls.uc_decode(mezo)[0]
532            for mezo in self._entry.get('aeMemberZone', [])
533            if mezo
534        ]
535        if member_zones:
536            member_zone_filter = compose_filter(
537                '|',
538                map_filter_parts('entryDN:dnSubordinateMatch:', member_zones),
539            )
540        else:
541            member_zone_filter = ''
542        return member_zone_filter
543
544    def _deref_person_attrset(self):
545        result = {}
546        for attr_type in self.deref_person_attrs:
547            if attr_type in self._entry and list(filter(None, self._entry[attr_type])):
548                result[attr_type] = set(self._entry[attr_type])
549        return result
550
551    def _filterstr(self):
552        return '(&{0}{1})'.format(
553            DerefDynamicDNSelectList._filterstr(self),
554            self._zone_filter(),
555        )
556
557    def _extract_attr_value_dict(self, ldap_result, deref_person_attrset):
558        attr_value_dict: Dict[str, str] = SelectList.get_attr_value_dict(self)
559        for ldap_res in ldap_result:
560            if not isinstance(ldap_res, SearchResultEntry):
561                # ignore search continuations
562                continue
563            # process dn and entry
564            if ldap_res.ctrls:
565                deref_control = ldap_res.ctrls[0]
566                deref_entry = deref_control.derefRes['aePerson'][0].entry_as
567            elif deref_person_attrset:
568                # if we have constrained attributes, no deref response control
569                # means constraint not valid
570                continue
571            # check constrained values here
572            valid = True
573            for attr_type, attr_values in deref_person_attrset.items():
574                if (
575                        attr_type not in deref_entry
576                        or deref_entry[attr_type][0] not in attr_values
577                    ):
578                    valid = False
579                    break
580            if valid:
581                option_value = ldap_res.dn_s
582                try:
583                    option_text = ldap_res.entry_s['displayName'][0]
584                except KeyError:
585                    option_text = option_value
586                try:
587                    option_title = ldap_res.entry_s['description'][0]
588                except KeyError:
589                    option_title = option_value
590                attr_value_dict[option_value] = (option_text, option_title)
591        return attr_value_dict
592
593    def get_attr_value_dict(self) -> Dict[str, str]:
594        deref_person_attrset = self._deref_person_attrset()
595        if not deref_person_attrset:
596            return DerefDynamicDNSelectList.get_attr_value_dict(self)
597        member_filter = self._filterstr()
598        try:
599            # Use the existing LDAP connection as current user
600            ldap_result = self._app.ls.l.search_s(
601                self._search_root(),
602                self.lu_obj.scope or ldap0.SCOPE_SUBTREE,
603                filterstr=member_filter,
604                attrlist=self.lu_obj.attrs+['description'],
605                req_ctrls=[
606                    DereferenceControl(True, {'aePerson': deref_person_attrset.keys()})
607                ],
608                cache_ttl=min(30, 5*web2ldapcnf.ldap_cache_ttl),
609            )
610        except self.ignored_errors as ldap_err:
611            logger.warning(
612                '%s.get_attr_value_dict() searching %r failed: %s',
613                self.__class__.__name__,
614                member_filter,
615                ldap_err,
616            )
617            return SelectList.get_attr_value_dict(self)
618        return self._extract_attr_value_dict(ldap_result, deref_person_attrset)
619        # get_attr_value_dict()
620
621    def _validate(self, attr_value: bytes) -> bool:
622        if 'memberURL' in self._entry and self._entry['memberURL'] != [b'']:
623            # reduce to simple DN syntax check for dynamic groups
624            return DistinguishedName._validate(self, attr_value)
625        return SelectList._validate(self, attr_value)
626
627    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
628        if self.ae_status == 2:
629            return []
630        return DerefDynamicDNSelectList.transmute(self, attr_values)
631
632syntax_registry.reg_at(
633    AEGroupMember.oid, [
634        '2.5.4.31', # member
635    ],
636    structural_oc_oids=[
637        AE_GROUP_OID, # aeGroup
638    ],
639)
640
641
642class AEMailGroupMember(AEGroupMember):
643    """
644    Plugin for attribute 'member' in aeMailGroup entries
645    """
646    oid: str = 'AEMailGroupMember-oid'
647    desc: str = 'AE-DIR: Member of a mail group'
648    input_fallback = False # no fallback to normal input field
649    ldap_url = (
650        'ldap:///_?displayName?sub?'
651        '(&(|(objectClass=inetLocalMailRecipient)(objectClass=aeContact))(mail=*)(aeStatus=0))'
652    )
653
654syntax_registry.reg_at(
655    AEMailGroupMember.oid, [
656        '2.5.4.31', # member
657    ],
658    structural_oc_oids=[
659        AE_MAILGROUP_OID, # aeMailGroup
660    ],
661)
662
663
664class AEMemberUid(MemberUID, AEObjectMixIn):
665    """
666    Plugin for attribute 'memberUid' in aeGroup entries
667    """
668    oid: str = 'AEMemberUid-oid'
669    desc: str = 'AE-DIR: username (uid) of member of a group'
670    ldap_url = None
671    show_val_button = False
672
673    def _member_uids_from_member(self):
674        return [
675            dn[4:].split(b',')[0]
676            for dn in self._entry.get('member', [])
677        ]
678
679    def _validate(self, attr_value: bytes) -> bool:
680        """
681        Because AEMemberUid.transmute() always resets all attribute values it's
682        ok to not validate values at all
683        """
684        return True
685
686    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
687        if 'member' not in self._entry:
688            return []
689        if self.ae_status == 2:
690            return []
691        return list(filter(None, self._member_uids_from_member()))
692
693    def form_value(self) -> str:
694        return ''
695
696    def input_field(self) -> Field:
697        input_field = HiddenInput(
698            self._at,
699            ': '.join([self._at, self.desc]),
700            self.max_len, self.max_values, None,
701        )
702        input_field.charset = self._app.form.accept_charset
703        input_field.set_default(self.form_value())
704        return input_field
705
706    def display(self, vidx, links) -> str:
707        return IA5String.display(self, vidx, links)
708
709syntax_registry.reg_at(
710    AEMemberUid.oid, [
711        '1.3.6.1.1.1.1.12', # memberUid
712    ],
713    structural_oc_oids=[
714        AE_GROUP_OID, # aeGroup
715    ],
716)
717
718
719class AEGroupDN(AERootDynamicDNSelectList):
720    """
721    Plugin for attribute 'memberOf' in group member entries
722    """
723    oid: str = 'AEGroupDN-oid'
724    desc: str = 'AE-DIR: DN of user group entry'
725    ldap_url = 'ldap:///_??sub?(&(|(objectClass=aeGroup)(objectClass=aeMailGroup))(aeStatus=0))'
726    ref_attrs = (
727        ('memberOf', 'Members', None, 'Search all member entries of this user group'),
728    )
729
730    def display(self, vidx, links) -> str:
731        group_dn = DNObj.from_str(self.av_u)
732        group_cn = group_dn[0][0][1]
733        res = [
734            'cn=<strong>{0}</strong>,{1}'.format(
735                self._app.form.s2d(group_cn),
736                self._app.form.s2d(str(group_dn.parent())),
737            )
738        ]
739        if links:
740            res.extend(self._additional_links())
741        return web2ldapcnf.command_link_separator.join(res)
742
743syntax_registry.reg_at(
744    AEGroupDN.oid, [
745        '1.2.840.113556.1.2.102', # memberOf
746    ],
747    structural_oc_oids=[
748        AE_USER_OID,    # aeUser
749        AE_SERVICE_OID, # aeService
750        AE_CONTACT_OID, # aeContact
751    ],
752)
753
754
755class AEZoneAdminGroupDN(AEGroupDN):
756    """
757    Plugin for attributes holding DNs of zone admin groups
758    """
759    oid: str = 'AEZoneAdminGroupDN-oid'
760    desc: str = 'AE-DIR: DN of zone admin group entry'
761    ldap_url = (
762        'ldap:///_??sub?'
763        '(&'
764          '(objectClass=aeGroup)'
765          '(aeStatus=0)'
766          '(cn=*-zone-admins)'
767          '(!'
768            '(|'
769              '(cn:dn:=pub)'
770              '(cn:dn:=ae)'
771            ')'
772          ')'
773        ')'
774    )
775
776syntax_registry.reg_at(
777    AEZoneAdminGroupDN.oid, [
778        AE_OID_PREFIX+'.4.31', # aeZoneAdmins
779        AE_OID_PREFIX+'.4.33', # aePasswordAdmins
780    ]
781)
782
783
784class AEZoneAuditorGroupDN(AEGroupDN):
785    """
786    Plugin for attributes holding DNs of zone auditor groups
787    """
788    oid: str = 'AEZoneAuditorGroupDN-oid'
789    desc: str = 'AE-DIR: DN of zone auditor group entry'
790    ldap_url = (
791        'ldap:///_??sub?'
792        '(&'
793          '(objectClass=aeGroup)'
794          '(aeStatus=0)'
795          '(|'
796            '(cn=*-zone-admins)'
797            '(cn=*-zone-auditors)'
798          ')'
799          '(!'
800            '(|'
801              '(cn:dn:=pub)'
802              '(cn:dn:=ae)'
803            ')'
804          ')'
805        ')'
806    )
807
808syntax_registry.reg_at(
809    AEZoneAuditorGroupDN.oid, [
810        AE_OID_PREFIX+'.4.32',  # aeZoneAuditors
811    ]
812)
813
814
815class AESrvGroupRightsGroupDN(AEGroupDN):
816    """
817    Plugin class for attributes holding DNs of user groups
818    in aeSrvGroup entries
819    """
820    oid: str = 'AESrvGroupRightsGroupDN-oid'
821    desc: str = 'AE-DIR: DN of user group entry'
822    ldap_url = (
823      'ldap:///_??sub?'
824      '(&'
825        '(objectClass=aeGroup)'
826        '(aeStatus=0)'
827        '(!'
828          '(|'
829            '(cn:dn:=pub)'
830            '(cn=*-zone-admins)'
831            '(cn=*-zone-auditors)'
832          ')'
833        ')'
834      ')'
835    )
836
837syntax_registry.reg_at(
838    AESrvGroupRightsGroupDN.oid, [
839        AE_OID_PREFIX+'.4.4',  # aeLoginGroups
840        AE_OID_PREFIX+'.4.6',  # aeSetupGroups
841        AE_OID_PREFIX+'.4.7',  # aeLogStoreGroups
842        AE_OID_PREFIX+'.4.37', # aeABAccessGroups
843    ]
844)
845
846
847class AEDisplayNameGroups(AESrvGroupRightsGroupDN):
848    """
849    Plugin class for attribute 'aeDisplayNameGroups' in aeSrvGroup entries
850    """
851    oid: str = 'AEDisplayNameGroups-oid'
852    desc: str = 'AE-DIR: DN of visible user group entry'
853    ldap_url = (
854      'ldap:///_??sub?'
855      '(&'
856        '(|'
857          '(objectClass=aeGroup)'
858          '(objectClass=aeMailGroup)'
859        ')'
860        '(aeStatus=0)'
861        '(!'
862          '(|'
863            '(cn:dn:=pub)'
864            '(cn=*-zone-admins)'
865            '(cn=*-zone-auditors)'
866          ')'
867        ')'
868      ')'
869    )
870
871syntax_registry.reg_at(
872    AEDisplayNameGroups.oid, [
873        AE_OID_PREFIX+'.4.30', # aeDisplayNameGroups
874    ]
875)
876
877
878class AEVisibleGroups(AEDisplayNameGroups):
879    """
880    Plugin class for attribute 'aeVisibleGroups' in aeSrvGroup entries
881    """
882    oid: str = 'AEVisibleGroups-oid'
883    desc: str = 'AE-DIR: DN of visible user group entry'
884    always_add_groups = (
885        'aeLoginGroups',
886        'aeDisplayNameGroups',
887    )
888
889    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
890        attr_values = set(attr_values)
891        for attr_type in self.always_add_groups:
892            attr_values.update(self._entry.get(attr_type, []))
893        return list(attr_values)
894
895syntax_registry.reg_at(
896    AEVisibleGroups.oid, [
897        AE_OID_PREFIX+'.4.20', # aeVisibleGroups
898    ]
899)
900
901
902class AESameZoneObject(DerefDynamicDNSelectList, AEObjectMixIn):
903    """
904    Plugin class for attributes storing DN references limited to reference
905    entries within the same zone
906    """
907    oid: str = 'AESameZoneObject-oid'
908    desc: str = 'AE-DIR: DN of referenced aeSrvGroup entry this is proxy for'
909    input_fallback = False # no fallback to normal input field
910    ldap_url = 'ldap:///_?cn?sub?(&(objectClass=aeObject)(aeStatus=0))'
911
912    def _search_root(self):
913        return self._get_zone_dn()
914
915
916class AESrvGroupDN(AEGroupDN):
917    """
918    Plugin for attributes holding DNs of aeSrvGroup entries
919    """
920    oid: str = 'AESrvGroupDN-oid'
921    desc: str = 'AE-DIR: DN of a referenced aeSrvGroup entry'
922    ldap_url = 'ldap:///_?cn?sub?(&(objectClass=aeSrvGroup)(aeStatus=0))'
923    ref_attrs = DerefDynamicDNSelectList.ref_attrs
924
925
926class AESrvGroup(AESrvGroupDN, AESameZoneObject):
927    """
928    Plugin class for attribute 'aeSrvGroup' in aeUser and aeService entries
929    """
930    oid: str = 'AESrvGroup-oid'
931    desc: str = 'AE-DIR: DN of supplemental aeSrvGroup entry'
932    ldap_url = 'ldap:///_?cn?sub?(&(objectClass=aeSrvGroup)(aeStatus=0)(!(aeProxyFor=*)))'
933
934    def _filterstr(self):
935        filter_str = self.lu_obj.filterstr or '(objectClass=aeSrvGroup)'
936        return '(&%s(!(entryDN=%s)))' % (
937            filter_str,
938            escape_filter_str(str(self.dn.parent())),
939        )
940
941syntax_registry.reg_at(
942    AESrvGroup.oid, [
943        AE_OID_PREFIX+'.4.27', # aeSrvGroup
944    ]
945)
946
947
948class AERequires(AESrvGroupDN):
949    """
950    Plugin class for attribute 'aeRequires' in aeSrvGroup entries
951    """
952    oid: str = 'AERequires-oid'
953    desc: str = 'AE-DIR: DN of required aeSrvGroup'
954    ldap_url = 'ldap:///_?cn?sub?(&(objectClass=aeSrvGroup)(aeStatus=0))'
955    ref_attrs = (
956        (
957            'aeRequires', 'Same require', None, 'aeSrvGroup',
958            'Search all service groups depending on this service group.'
959        ),
960    )
961
962syntax_registry.reg_at(
963    AERequires.oid, [
964        AE_OID_PREFIX+'.4.48', # aeRequires
965    ]
966)
967
968
969class AEProxyFor(AESrvGroupDN, AESameZoneObject):
970    """
971    Plugin class for attribute 'aeProxyFor' in aeSrvGroup entries
972    """
973    oid: str = 'AEProxyFor-oid'
974    desc: str = 'AE-DIR: DN of referenced aeSrvGroup entry this is proxy for'
975    ldap_url = 'ldap:///_?cn?sub?(&(objectClass=aeSrvGroup)(aeStatus=0)(!(aeProxyFor=*)))'
976
977    def _filterstr(self):
978        filter_str = self.lu_obj.filterstr or '(objectClass=*)'
979        return '(&%s(!(entryDN=%s)))' % (
980            filter_str,
981            escape_filter_str(self._dn),
982        )
983
984syntax_registry.reg_at(
985    AEProxyFor.oid, [
986        AE_OID_PREFIX+'.4.25', # aeProxyFor
987    ]
988)
989
990
991class AETag(DynamicValueSelectList):
992    """
993    Plugin class for attribute 'aeTag' in all aeObject entries
994    """
995    oid: str = 'AETag-oid'
996    desc: str = 'AE-DIR: cn of referenced aeTag entry'
997    ldap_url = 'ldap:///_?cn,cn?sub?(&(objectClass=aeTag)(aeStatus=0))'
998
999syntax_registry.reg_at(
1000    AETag.oid, [
1001        AE_OID_PREFIX+'.4.24', # aeTag
1002    ]
1003)
1004
1005
1006class AEEntryDNAEPerson(DistinguishedName):
1007    """
1008    Plugin class for attribute 'entryDN' in aePerson entries
1009    """
1010    oid: str = 'AEEntryDNAEPerson-oid'
1011    desc: str = 'AE-DIR: entryDN of aePerson entry'
1012    ref_attrs = (
1013        ('manager', 'Manages', None, 'Search all entries managed by this person'),
1014        (
1015            'aePerson', 'Users', None, 'aeUser',
1016            'Search all personal AE-DIR user accounts (aeUser entries) of this person.'
1017        ),
1018        (
1019            'aeOwner', 'Devices', None, 'aeDevice',
1020            'Search all devices (aeDevice entries) assigned to this person.'
1021        ),
1022    )
1023
1024syntax_registry.reg_at(
1025    AEEntryDNAEPerson.oid, [
1026        '1.3.6.1.1.20', # entryDN
1027    ],
1028    structural_oc_oids=[
1029        AE_PERSON_OID, # aePerson
1030    ],
1031)
1032
1033
1034class AEEntryDNAEUser(DistinguishedName):
1035    """
1036    Plugin class for attribute 'entryDN' in aeUser entries
1037    """
1038    oid: str = 'AEEntryDNAEUser-oid'
1039    desc: str = 'AE-DIR: entryDN of aeUser entry'
1040
1041    def _additional_links(self):
1042        res = DistinguishedName._additional_links(self)
1043        res.append(self._app.anchor(
1044            'searchform', 'Created/Modified',
1045            (
1046                ('dn', self._dn),
1047                ('search_root', str(self._app.naming_context)),
1048                ('searchform_mode', 'adv'),
1049                ('search_mode', '(|%s)'),
1050                ('search_attr', 'creatorsName'),
1051                ('search_option', SEARCH_OPT_IS_EQUAL),
1052                ('search_string', self.av_u),
1053                ('search_attr', 'modifiersName'),
1054                ('search_option', SEARCH_OPT_IS_EQUAL),
1055                ('search_string', self.av_u),
1056            ),
1057            title='Search entries created or modified by %s' % (self.av_u),
1058        ))
1059        if self._app.audit_context:
1060            res.append(self._app.anchor(
1061                'search', 'Activity',
1062                (
1063                    ('dn', self._app.audit_context),
1064                    ('searchform_mode', 'adv'),
1065                    ('search_attr', 'objectClass'),
1066                    ('search_option', SEARCH_OPT_IS_EQUAL),
1067                    ('search_string', 'auditObject'),
1068                    ('search_attr', 'reqAuthzID'),
1069                    ('search_option', SEARCH_OPT_IS_EQUAL),
1070                    ('search_string', self.av_u),
1071                ),
1072                title='Search modifications made by %s in accesslog DB' % (self.av_u),
1073            ))
1074        return res
1075
1076syntax_registry.reg_at(
1077    AEEntryDNAEUser.oid, [
1078        '1.3.6.1.1.20', # entryDN
1079    ],
1080    structural_oc_oids=[
1081        AE_USER_OID, # aeUser
1082        AE_SERVICE_OID, # aeService
1083    ],
1084)
1085
1086
1087class AEEntryDNAEHost(DistinguishedName):
1088    """
1089    Plugin class for attribute 'entryDN' in aeHost entries
1090    """
1091    oid: str = 'AEEntryDNAEHost-oid'
1092    desc: str = 'AE-DIR: entryDN of aeUser entry'
1093    ref_attrs = (
1094        ('aeHost', 'Services', None, 'aeService', 'Search all services running on this host'),
1095    )
1096
1097    def _additional_links(self):
1098        res = DistinguishedName._additional_links(self)
1099        srv_group_assertion_values = [escape_filter_str(str(self.dn.parent()))]
1100        srv_group_assertion_values.extend([
1101            escape_filter_str(av.decode(self._app.ls.charset))
1102            for av in self._entry.get('aeSrvGroup', [])
1103        ])
1104        res.extend([
1105            self._app.anchor(
1106                'search', 'Siblings',
1107                (
1108                    ('dn', self._dn),
1109                    ('search_root', str(self._app.naming_context)),
1110                    ('searchform_mode', 'exp'),
1111                    (
1112                        'filterstr',
1113                        '(&(|(objectClass=aeHost)(objectClass=aeService))(|{0}{1}))'.format(
1114                            ''.join([
1115                                '(entryDN:dnSubordinateMatch:=%s)' % av
1116                                for av in srv_group_assertion_values
1117                            ]),
1118                            ''.join([
1119                                '(aeSrvGroup=%s)' % av
1120                                for av in srv_group_assertion_values
1121                            ]),
1122                        )
1123                    ),
1124                ),
1125                title=(
1126                    'Search all host entries which are member in '
1127                    'at least one common server group(s) with this host'
1128                ),
1129            ),
1130        ])
1131        return res
1132
1133syntax_registry.reg_at(
1134    AEEntryDNAEHost.oid, [
1135        '1.3.6.1.1.20', # entryDN
1136    ],
1137    structural_oc_oids=[
1138        AE_HOST_OID, # aeHost
1139    ],
1140)
1141
1142
1143class AEEntryDNAEZone(DistinguishedName):
1144    """
1145    Plugin class for attribute 'entryDN' in aeZone entries
1146    """
1147    oid: str = 'AEEntryDNAEZone-oid'
1148    desc: str = 'AE-DIR: entryDN of aeZone entry'
1149
1150    def _additional_links(self):
1151        res = DistinguishedName._additional_links(self)
1152        if self._app.audit_context:
1153            res.append(self._app.anchor(
1154                'search', 'Audit all',
1155                (
1156                    ('dn', self._app.audit_context),
1157                    ('searchform_mode', 'adv'),
1158                    ('search_attr', 'objectClass'),
1159                    ('search_option', SEARCH_OPT_IS_EQUAL),
1160                    ('search_string', 'auditObject'),
1161                    ('search_attr', 'reqDN'),
1162                    ('search_option', SEARCH_OPT_DN_SUBTREE),
1163                    ('search_string', self.av_u),
1164                ),
1165                title='Search all audit log entries for sub-tree %s' % (self.av_u),
1166            ))
1167            res.append(self._app.anchor(
1168                'search', 'Audit writes',
1169                (
1170                    ('dn', self._app.audit_context),
1171                    ('searchform_mode', 'adv'),
1172                    ('search_attr', 'objectClass'),
1173                    ('search_option', SEARCH_OPT_IS_EQUAL),
1174                    ('search_string', 'auditObject'),
1175                    ('search_attr', 'reqDN'),
1176                    ('search_option', SEARCH_OPT_DN_SUBTREE),
1177                    ('search_string', self.av_u),
1178                ),
1179                title='Search audit log entries for write operation within sub-tree %s' % (
1180                    self.av_u
1181                ),
1182            ))
1183        return res
1184
1185syntax_registry.reg_at(
1186    AEEntryDNAEZone.oid, [
1187        '1.3.6.1.1.20', # entryDN
1188    ],
1189    structural_oc_oids=[
1190        AE_ZONE_OID, # aeZone
1191    ],
1192)
1193
1194
1195class AEEntryDNAEMailGroup(GroupEntryDN):
1196    """
1197    Plugin class for attribute 'entryDN' in aeMailGroup entries
1198    """
1199    oid: str = 'AEEntryDNAEMailGroup-oid'
1200    desc: str = 'AE-DIR: entryDN of aeGroup entry'
1201    ref_attrs = (
1202        ('memberOf', 'Members', None, 'Search all member entries of this mail group'),
1203        (
1204            'aeVisibleGroups', 'Visible', None, 'aeSrvGroup',
1205            'Search all server/service groups (aeSrvGroup)\n'
1206            'on which this mail group is visible'
1207        ),
1208    )
1209
1210syntax_registry.reg_at(
1211    AEEntryDNAEMailGroup.oid, [
1212        '1.3.6.1.1.20', # entryDN
1213    ],
1214    structural_oc_oids=[
1215        AE_MAILGROUP_OID, # aeMailGroup
1216    ],
1217)
1218
1219
1220class AEEntryDNAEGroup(GroupEntryDN):
1221    """
1222    Plugin class for attribute 'entryDN' in aeGroup entries
1223    """
1224    oid: str = 'AEEntryDNAEGroup-oid'
1225    desc: str = 'AE-DIR: entryDN of aeGroup entry'
1226    ref_attrs = (
1227        ('memberOf', 'Members', None, 'Search all member entries of this user group'),
1228        (
1229            'aeLoginGroups', 'Login', None, 'aeSrvGroup',
1230            'Search all server/service groups (aeSrvGroup)\n'
1231            'on which this user group has login right'
1232        ),
1233        (
1234            'aeLogStoreGroups', 'View Logs', None, 'aeSrvGroup',
1235            'Search all server/service groups (aeSrvGroup)\n'
1236            'on which this user group has log view right'
1237        ),
1238        (
1239            'aeSetupGroups', 'Setup', None, 'aeSrvGroup',
1240            'Search all server/service groups (aeSrvGroup)\n'
1241            'on which this user group has setup/installation rights'
1242        ),
1243        (
1244            'aeVisibleGroups', 'Visible', None, 'aeSrvGroup',
1245            'Search all server/service groups (aeSrvGroup)\n'
1246            'on which this user group is at least visible'
1247        ),
1248    )
1249
1250    def _additional_links(self):
1251        aegroup_cn = self._entry['cn'][0].decode(self._app.ls.charset)
1252        ref_attrs = list(AEEntryDNAEGroup.ref_attrs)
1253        if aegroup_cn.endswith('zone-admins'):
1254            ref_attrs.extend([
1255                (
1256                    'aeZoneAdmins', 'Zone Admins', None,
1257                    'Search all zones (aeZone)\n'
1258                    'for which members of this user group act as zone admins'
1259                ),
1260                (
1261                    'aePasswordAdmins', 'Password Admins', None,
1262                    'Search all zones (aeZone)\n'
1263                    'for which members of this user group act as password admins'
1264                ),
1265            ])
1266        if aegroup_cn.endswith('zone-auditors') or aegroup_cn.endswith('zone-admins'):
1267            ref_attrs.append(
1268                (
1269                    'aeZoneAuditors', 'Zone Auditors', None,
1270                    'Search all zones (aeZone)\n'
1271                    'for which members of this user group act as zone auditors'
1272                ),
1273            )
1274        self.ref_attrs = tuple(ref_attrs)
1275        res = DistinguishedName._additional_links(self)
1276        res.append(self._app.anchor(
1277            'search', 'SUDO rules',
1278            (
1279                ('dn', self._dn),
1280                ('search_root', str(self._app.naming_context)),
1281                ('searchform_mode', 'adv'),
1282                ('search_attr', 'sudoUser'),
1283                ('search_option', SEARCH_OPT_IS_EQUAL),
1284                ('search_string', '%'+self._entry['cn'][0].decode(self._app.ls.charset)),
1285            ),
1286            title='Search for SUDO rules\napplicable with this user group',
1287        ))
1288        return res
1289
1290syntax_registry.reg_at(
1291    AEEntryDNAEGroup.oid, [
1292        '1.3.6.1.1.20', # entryDN
1293    ],
1294    structural_oc_oids=[
1295        AE_GROUP_OID, # aeGroup
1296    ],
1297)
1298
1299
1300class AEEntryDNAESrvGroup(DistinguishedName):
1301    """
1302    Plugin class for attribute 'entryDN' in aeSrvGroup entries
1303    """
1304    oid: str = 'AEEntryDNAESrvGroup-oid'
1305    desc: str = 'AE-DIR: entryDN'
1306    ref_attrs = (
1307        (
1308            'aeProxyFor', 'Proxy', None, 'aeSrvGroup',
1309            'Search access gateway/proxy group for this server group'
1310        ),
1311        (
1312            'aeRequires', 'Required by', None, 'aeSrvGroup',
1313            'Search all service groups depending on this service group.'
1314        ),
1315    )
1316
1317    def _additional_links(self):
1318        res = DistinguishedName._additional_links(self)
1319        res.append(
1320            self._app.anchor(
1321                'search', 'All members',
1322                (
1323                    ('dn', self._dn),
1324                    ('search_root', str(self._app.naming_context)),
1325                    ('searchform_mode', 'exp'),
1326                    (
1327                        'filterstr',
1328                        (
1329                            '(&'
1330                            '(|(objectClass=aeHost)(objectClass=aeService))'
1331                            '(|(entryDN:dnSubordinateMatch:={0})(aeSrvGroup={0}))'
1332                            ')'
1333                        ).format(self.av_u)
1334                    ),
1335                ),
1336                title=(
1337                    'Search all service and host entries '
1338                    'which are member in this service/host group {0}'
1339                ).format(self.av_u),
1340            )
1341        )
1342        return res
1343
1344syntax_registry.reg_at(
1345    AEEntryDNAESrvGroup.oid, [
1346        '1.3.6.1.1.20', # entryDN
1347    ],
1348    structural_oc_oids=[
1349        AE_SRVGROUP_OID, # aeSrvGroup
1350    ],
1351)
1352
1353
1354class AEEntryDNSudoRule(DistinguishedName):
1355    """
1356    Plugin class for attribute 'entryDN' in aeSudoRule entries
1357    """
1358    oid: str = 'AEEntryDNSudoRule-oid'
1359    desc: str = 'AE-DIR: entryDN'
1360    ref_attrs = (
1361        (
1362            'aeVisibleSudoers', 'Used on', None, 'aeSrvGroup',
1363            'Search all server groups (aeSrvGroup) referencing this SUDO rule'
1364        ),
1365    )
1366
1367syntax_registry.reg_at(
1368    AEEntryDNSudoRule.oid, [
1369        '1.3.6.1.1.20', # entryDN
1370    ],
1371    structural_oc_oids=[
1372        AE_SUDORULE_OID, # aeSudoRule
1373    ],
1374)
1375
1376
1377class AEEntryDNAELocation(DistinguishedName):
1378    """
1379    Plugin class for attribute 'entryDN' in aeLocation entries
1380    """
1381    oid: str = 'AEEntryDNAELocation-oid'
1382    desc: str = 'AE-DIR: entryDN of aeLocation entry'
1383    ref_attrs = (
1384        (
1385            'aeLocation', 'Persons', None, 'aePerson',
1386            'Search all persons assigned to this location.'
1387        ),
1388        (
1389            'aeLocation', 'Zones', None, 'aeZone',
1390            'Search all location-based zones associated with this location.'
1391        ),
1392        (
1393            'aeLocation', 'Groups', None, 'groupOfEntries',
1394            'Search all location-based zones associated with this location.'
1395        ),
1396    )
1397
1398syntax_registry.reg_at(
1399    AEEntryDNAELocation.oid, [
1400        '1.3.6.1.1.20', # entryDN
1401    ],
1402    structural_oc_oids=[
1403        AE_LOCATION_OID, # aeLocation
1404    ],
1405)
1406
1407
1408class AELocation(AERootDynamicDNSelectList):
1409    """
1410    Plugin class for attribute 'aeLocation' in various entries
1411    """
1412    oid: str = 'AELocation-oid'
1413    desc: str = 'AE-DIR: DN of location entry'
1414    ldap_url = 'ldap:///_?displayName?sub?(&(objectClass=aeLocation)(aeStatus=0))'
1415    ref_attrs = AEEntryDNAELocation.ref_attrs
1416    desc_sep: str = '<br>'
1417
1418syntax_registry.reg_at(
1419    AELocation.oid, [
1420        AE_OID_PREFIX+'.4.35', # aeLocation
1421    ]
1422)
1423
1424
1425class AEEntryDNAEDept(DistinguishedName):
1426    """
1427    Plugin class for attribute 'entryDN' in aeDept entries
1428    """
1429    oid: str = 'AEEntryDNAEDept-oid'
1430    desc: str = 'AE-DIR: entryDN of aePerson entry'
1431    ref_attrs = (
1432        (
1433            'aeDept', 'Persons', None, 'aePerson',
1434            'Search all persons assigned to this department.'
1435        ),
1436        (
1437            'aeDept', 'Zones', None, 'aeZone',
1438            'Search all team-related zones associated with this department.'
1439        ),
1440        (
1441            'aeDept', 'Groups', None, 'groupOfEntries',
1442            'Search all team-related groups associated with this department.'
1443        ),
1444    )
1445
1446syntax_registry.reg_at(
1447    AEEntryDNAEDept.oid, [
1448        '1.3.6.1.1.20', # entryDN
1449    ],
1450    structural_oc_oids=[
1451        AE_DEPT_OID, # aeDept
1452    ],
1453)
1454
1455
1456class AEDept(AERootDynamicDNSelectList):
1457    """
1458    Plugin class for attribute 'aeDept' in various entries
1459    """
1460    oid: str = 'AEDept-oid'
1461    desc: str = 'AE-DIR: DN of department entry'
1462    ldap_url = 'ldap:///_?displayName?sub?(&(objectClass=aeDept)(aeStatus=0))'
1463    ref_attrs = AEEntryDNAEDept.ref_attrs
1464    desc_sep: str = '<br>'
1465
1466syntax_registry.reg_at(
1467    AEDept.oid, [
1468        AE_OID_PREFIX+'.4.29', # aeDept
1469    ]
1470)
1471
1472
1473class AEOwner(AERootDynamicDNSelectList):
1474    """
1475    Plugin class for attribute 'aeOwner' in aeDevice and aeSession entries
1476    """
1477    oid: str = 'AEOwner-oid'
1478    desc: str = 'AE-DIR: DN of owner entry'
1479    ldap_url = 'ldap:///_?displayName?sub?(&(objectClass=aePerson)(aeStatus=0))'
1480    ref_attrs = (
1481        (
1482            'aeOwner', 'Devices', None, 'aeDevice',
1483            'Search all devices (aeDevice entries) assigned to same owner.'
1484        ),
1485    )
1486    desc_sep: str = '<br>'
1487
1488syntax_registry.reg_at(
1489    AEOwner.oid, [
1490        AE_OID_PREFIX+'.4.2', # aeOwner
1491    ]
1492)
1493
1494
1495class AEPerson(DerefDynamicDNSelectList, AEObjectMixIn):
1496    """
1497    Plugin class for attribute 'aePerson' in aeUser entries
1498    """
1499    oid: str = 'AEPerson-oid'
1500    desc: str = 'AE-DIR: DN of person entry'
1501    ldap_url = 'ldap:///_?displayName?sub?(objectClass=aePerson)'
1502    ref_attrs = (
1503        (
1504            'aePerson', 'Users', None, 'aeUser',
1505            'Search all personal AE-DIR user accounts (aeUser entries) of this person.'
1506        ),
1507    )
1508    desc_sep: str = '<br>'
1509    ae_status_map = {
1510        -1: (-1, 0),
1511        0: (0,),
1512        1: (0, 1, 2),
1513        2: (0, 1, 2),
1514    }
1515    deref_attrs = ('aeDept', 'aeLocation')
1516
1517    def _status_filter(self):
1518        ae_status = self.ae_status or 0
1519        return compose_filter(
1520            '|',
1521            map_filter_parts(
1522                'aeStatus',
1523                map(str, self.ae_status_map.get(ae_status, [])),
1524            ),
1525        )
1526
1527    def _filterstr(self):
1528        filter_components = [
1529            DerefDynamicDNSelectList._filterstr(self),
1530            self._status_filter(),
1531            #ae_validity_filter(),
1532        ]
1533        zone_entry = self._zone_entry(attrlist=self.deref_attrs)
1534        for deref_attr_type in self.deref_attrs:
1535            deref_attr_values = [
1536                z
1537                for z in zone_entry.get(deref_attr_type, [])
1538                if z
1539            ]
1540            if deref_attr_values:
1541                filter_components.append(
1542                    compose_filter(
1543                        '|',
1544                        map_filter_parts(deref_attr_type, deref_attr_values),
1545                    )
1546                )
1547        ocs = self._entry.object_class_oid_set()
1548        if 'inetLocalMailRecipient' not in ocs:
1549            filter_components.append('(mail=*)')
1550        filter_str = '(&{})'.format(''.join(filter_components))
1551        return filter_str
1552
1553    def _validate(self, attr_value: bytes) -> bool:
1554        if self.ae_status == 2:
1555            return True
1556        return DerefDynamicDNSelectList._validate(self, attr_value)
1557
1558
1559syntax_registry.reg_at(
1560    AEPerson.oid, [
1561        AE_OID_PREFIX+'.4.16', # aePerson
1562    ]
1563)
1564
1565
1566class AEManager(AERootDynamicDNSelectList):
1567    """
1568    Plugin class for attribute 'aeManager' in aePerson and aeDept entries
1569    """
1570    oid: str = 'AEManager-oid'
1571    desc: str = 'AE-DIR: Manager responsible for a person/department'
1572    ldap_url = 'ldap:///_?displayName?sub?(&(objectClass=aePerson)(aeStatus=0))'
1573    desc_sep: str = '<br>'
1574
1575syntax_registry.reg_at(
1576    AEManager.oid, [
1577        '0.9.2342.19200300.100.1.10', # manager
1578    ],
1579    structural_oc_oids=[
1580        AE_PERSON_OID, # aePerson
1581        AE_DEPT_OID, # aeDept
1582    ]
1583)
1584
1585
1586class AEDerefAttribute(DirectoryString):
1587    """
1588    Plugin class for attributes referencing other entries
1589    """
1590    oid: str = 'AEDerefAttribute-oid'
1591    max_values: int = 1
1592    deref_object_class: Optional[str] = None
1593    deref_attribute_type: Optional[str] = None
1594    deref_filter_tmpl: str = (
1595        '(&(objectClass={deref_object_class})(aeStatus<=0)({attribute_type}=*))'
1596    )
1597
1598    def _read_person_attr(self):
1599        try:
1600            sre = self._app.ls.l.read_s(
1601                self._entry[self.deref_attribute_type][0].decode(self._app.ls.charset),
1602                attrlist=[self._at],
1603                filterstr=self.deref_filter_tmpl.format(
1604                    deref_object_class=self.deref_object_class,
1605                    attribute_type=self._at,
1606                ),
1607            )
1608        except ldap0.LDAPError:
1609            return None
1610        if sre is None:
1611            return None
1612        return sre.entry_s[self._at][0]
1613
1614    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
1615        if self.deref_attribute_type in self._entry:
1616            ae_person_attribute = self._read_person_attr()
1617            if ae_person_attribute is not None:
1618                result = [ae_person_attribute.encode(self._app.ls.charset)]
1619            else:
1620                result = []
1621        else:
1622            result = attr_values
1623        return result
1624
1625    def form_value(self) -> str:
1626        return ''
1627
1628    def input_field(self) -> Field:
1629        input_field = HiddenInput(
1630            self._at,
1631            ': '.join([self._at, self.desc]),
1632            self.max_len, self.max_values, None,
1633        )
1634        input_field.charset = self._app.form.accept_charset
1635        input_field.set_default(self.form_value())
1636        return input_field
1637
1638
1639class AEPersonAttribute(AEDerefAttribute):
1640    """
1641    Plugin class for aeUser attributes copied from referenced aePerson entries
1642    """
1643    oid: str = 'AEPersonAttribute-oid'
1644    max_values = 1
1645    deref_object_class = 'aePerson'
1646    deref_attribute_type = 'aePerson'
1647
1648
1649class AEUserNames(AEPersonAttribute, DirectoryString):
1650    """
1651    Plugin class for aeUser attributes 'sn' and 'givenName' copied
1652    from referenced aePerson entries
1653    """
1654    oid: str = 'AEUserNames-oid'
1655
1656syntax_registry.reg_at(
1657    AEUserNames.oid, [
1658        '2.5.4.4', # sn
1659        '2.5.4.42', # givenName
1660    ],
1661    structural_oc_oids=[
1662        AE_USER_OID, # aeUser
1663    ],
1664)
1665
1666
1667class AEMailLocalAddress(RFC822Address):
1668    """
1669    Plugin class for attribute 'mailLocalAddress' in aeUser and aeService entries
1670    """
1671    oid: str = 'AEMailLocalAddress-oid'
1672    sani_funcs = (
1673        bytes.strip,
1674        bytes.lower,
1675    )
1676
1677syntax_registry.reg_at(
1678    AEMailLocalAddress.oid, [
1679        '2.16.840.1.113730.3.1.13', # mailLocalAddress
1680    ],
1681    structural_oc_oids=[
1682        AE_USER_OID,    # aeUser
1683        AE_SERVICE_OID, # aeService
1684    ],
1685)
1686
1687
1688class AEUserMailaddress(AEPersonAttribute, RFC822Address, SelectList):
1689    """
1690    Plugin class for attribute 'mail' in aeUser entries
1691
1692    For primary mail user accounts this contains one of
1693    the values in attribute 'mailLocalAddress'.
1694    """
1695    oid: str = 'AEUserMailaddress-oid'
1696    max_values = 1
1697    input_fallback = False
1698    sani_funcs = (
1699        bytes.strip,
1700        bytes.lower,
1701    )
1702
1703    def get_attr_value_dict(self) -> Dict[str, str]:
1704        attr_value_dict: Dict[str, str] = {
1705            '': '-/-',
1706        }
1707        for addr in self._entry.get('mailLocalAddress', []):
1708            addr_u = addr.decode(self._app.ls.charset)
1709            attr_value_dict[addr_u] = addr_u
1710        return attr_value_dict
1711
1712    def _is_mail_account(self):
1713        return b'inetLocalMailRecipient' in self._entry['objectClass']
1714
1715    def _validate(self, attr_value: bytes) -> bool:
1716        if self._is_mail_account():
1717            return SelectList._validate(self, attr_value)
1718        return AEPersonAttribute._validate(self, attr_value)
1719
1720    def display(self, vidx, links) -> str:
1721        return RFC822Address.display(self, vidx, links)
1722
1723    def form_value(self) -> str:
1724        if self._is_mail_account():
1725            return SelectList.form_value(self)
1726        return AEPersonAttribute.form_value(self)
1727
1728    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
1729        if self._is_mail_account():
1730            # make sure only non-empty strings are in attribute value list
1731            if not list(filter(None, map(bytes.strip, attr_values))):
1732                try:
1733                    attr_values = [self._entry['mailLocalAddress'][0]]
1734                except KeyError:
1735                    attr_values = []
1736        else:
1737            attr_values = AEPersonAttribute.transmute(self, attr_values)
1738        return attr_values
1739
1740    def input_field(self) -> Field:
1741        if self._is_mail_account():
1742            return SelectList.input_field(self)
1743        return AEPersonAttribute.input_field(self)
1744
1745syntax_registry.reg_at(
1746    AEUserMailaddress.oid, [
1747        '0.9.2342.19200300.100.1.3', # mail
1748    ],
1749    structural_oc_oids=[
1750        AE_USER_OID, # aeUser
1751    ],
1752)
1753
1754
1755class AEPersonMailaddress(DynamicValueSelectList, RFC822Address):
1756    """
1757    Plugin class for attribute 'mail' in aePerson entries
1758
1759    If there exists a primary mail user account for this person this
1760    contains one of the values in attribute 'mailLocalAddress' in that
1761    aeUser entry.
1762    """
1763    oid: str = 'AEPersonMailaddress-oid'
1764    max_values = 1
1765    ldap_url = 'ldap:///_?mail,mail?sub?'
1766    input_fallback = True
1767    html_tmpl = RFC822Address.html_tmpl
1768
1769    def _validate(self, attr_value: bytes) -> bool:
1770        if not RFC822Address._validate(self, attr_value):
1771            return False
1772        attr_value_dict: Dict[str, str] = self.get_attr_value_dict()
1773        if (
1774                not attr_value_dict
1775                or (
1776                    len(attr_value_dict) == 1
1777                    and tuple(attr_value_dict.keys()) == ('',)
1778                    )
1779            ):
1780            return True
1781        return DynamicValueSelectList._validate(self, attr_value)
1782
1783    def _filterstr(self):
1784        return (
1785            '(&'
1786              '(objectClass=aeUser)'
1787              '(objectClass=inetLocalMailRecipient)'
1788              '(aeStatus=0)'
1789              '(aePerson=%s)'
1790              '(mailLocalAddress=*)'
1791            ')'
1792        ) % escape_filter_str(self._dn)
1793
1794syntax_registry.reg_at(
1795    AEPersonMailaddress.oid, [
1796        '0.9.2342.19200300.100.1.3', # mail
1797    ],
1798    structural_oc_oids=[
1799        AE_PERSON_OID, # aePerson
1800    ],
1801)
1802
1803
1804class AEDeptAttribute(AEDerefAttribute, DirectoryString):
1805    """
1806    Plugin class for aePerson attributes copied from referenced aeDept entries
1807    """
1808    oid: str = 'AEDeptAttribute-oid'
1809    max_values = 1
1810    deref_object_class = 'aeDept'
1811    deref_attribute_type = 'aeDept'
1812
1813syntax_registry.reg_at(
1814    AEDeptAttribute.oid, [
1815        '2.16.840.1.113730.3.1.2', # departmentNumber
1816        '2.5.4.11',                # ou, organizationalUnitName
1817    ],
1818    structural_oc_oids=[
1819        AE_PERSON_OID, # aePerson
1820    ],
1821)
1822
1823
1824class AEHostname(DNSDomain):
1825    """
1826    Plugin class for attribute 'host' in aeHost entries
1827    """
1828    oid: str = 'AEHostname-oid'
1829    desc: str = 'Canonical hostname / FQDN'
1830    host_lookup = 0
1831
1832    def _validate(self, attr_value: bytes) -> bool:
1833        if not DNSDomain._validate(self, attr_value):
1834            return False
1835        if self.host_lookup:
1836            try:
1837                ip_addr = socket.gethostbyname(self._app.ls.uc_decode(attr_value)[0])
1838            except (socket.gaierror, socket.herror):
1839                return False
1840            if self.host_lookup >= 2:
1841                try:
1842                    reverse_hostname = socket.gethostbyaddr(ip_addr)[0]
1843                except (socket.gaierror, socket.herror):
1844                    return False
1845                else:
1846                    return reverse_hostname == attr_value
1847        return True
1848
1849    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
1850        result = []
1851        for attr_value in attr_values:
1852            attr_value.lower().strip()
1853            if self.host_lookup:
1854                try:
1855                    ip_addr = socket.gethostbyname(self._app.ls.uc_decode(attr_value)[0])
1856                    reverse_hostname = socket.gethostbyaddr(ip_addr)[0]
1857                except (socket.gaierror, socket.herror):
1858                    pass
1859                else:
1860                    attr_value = reverse_hostname.encode(self._app.ls.charset)
1861            result.append(attr_value)
1862        return attr_values
1863
1864syntax_registry.reg_at(
1865    AEHostname.oid, [
1866        '0.9.2342.19200300.100.1.9', # host
1867    ],
1868    structural_oc_oids=[
1869        AE_HOST_OID, # aeHost
1870    ],
1871)
1872
1873
1874class AEDisplayNameUser(ComposedAttribute, DirectoryString):
1875    """
1876    Plugin class for attribute 'displayName' in aeUser entries
1877    """
1878    oid: str = 'AEDisplayNameUser-oid'
1879    desc: str = 'Attribute displayName in object class aeUser'
1880    compose_templates = (
1881        '{givenName} {sn} ({uid}/{uidNumber})',
1882        '{givenName} {sn} ({uid})',
1883    )
1884
1885syntax_registry.reg_at(
1886    AEDisplayNameUser.oid, [
1887        '2.16.840.1.113730.3.1.241', # displayName
1888    ],
1889    structural_oc_oids=[AE_USER_OID], # aeUser
1890)
1891
1892
1893class AEDisplayNameContact(ComposedAttribute, DirectoryString):
1894    """
1895    Plugin class for attribute 'displayName' in aeContact entries
1896    """
1897    oid: str = 'AEDisplayNameContact-oid'
1898    desc: str = 'Attribute displayName in object class aeContact'
1899    compose_templates = (
1900        '{cn} <{mail}>',
1901        '{cn}',
1902    )
1903
1904syntax_registry.reg_at(
1905    AEDisplayNameContact.oid, [
1906        '2.16.840.1.113730.3.1.241', # displayName
1907    ],
1908    structural_oc_oids=[AE_CONTACT_OID], # aeContact
1909)
1910
1911
1912class AEDisplayNameDept(ComposedAttribute, DirectoryString):
1913    """
1914    Plugin class for attribute 'displayName' in aeDept entries
1915    """
1916    oid: str = 'AEDisplayNameDept-oid'
1917    desc: str = 'Attribute displayName in object class aeDept'
1918    compose_templates = (
1919        '{ou} ({departmentNumber})',
1920        '{ou}',
1921        '#{departmentNumber}',
1922    )
1923
1924syntax_registry.reg_at(
1925    AEDisplayNameDept.oid, [
1926        '2.16.840.1.113730.3.1.241', # displayName
1927    ],
1928    structural_oc_oids=[AE_DEPT_OID], # aeDept
1929)
1930
1931
1932class AEDisplayNameLocation(ComposedAttribute, DirectoryString):
1933    """
1934    Plugin class for attribute 'displayName' in aeLocation entries
1935    """
1936    oid: str = 'AEDisplayNameLocation-oid'
1937    desc: str = 'Attribute displayName in object class aeLocation'
1938    compose_templates = (
1939        '{cn}: {l}, {street}',
1940        '{cn}: {l}',
1941        '{cn}: {street}',
1942        '{cn}: {st}',
1943        '{cn}',
1944    )
1945
1946syntax_registry.reg_at(
1947    AEDisplayNameLocation.oid, [
1948        '2.16.840.1.113730.3.1.241', # displayName
1949    ],
1950    structural_oc_oids=[AE_LOCATION_OID], # aeLocation
1951)
1952
1953
1954class AEDisplayNamePerson(DisplayNameInetOrgPerson):
1955    """
1956    Plugin class for attribute 'displayName' in aePerson entries
1957    """
1958    oid: str = 'AEDisplayNamePerson-oid'
1959    desc: str = 'Attribute displayName in object class aePerson'
1960    # do not stuff confidential employeeNumber herein!
1961    compose_templates = (
1962        '{givenName} {sn} / {ou}',
1963        '{givenName} {sn} / #{departmentNumber}',
1964        '{givenName} {sn} ({uniqueIdentifier})',
1965        '{givenName} {sn}',
1966    )
1967
1968syntax_registry.reg_at(
1969    AEDisplayNamePerson.oid, [
1970        '2.16.840.1.113730.3.1.241', # displayName
1971    ],
1972    structural_oc_oids=[AE_PERSON_OID], # aePerson
1973)
1974
1975
1976class AEUniqueIdentifier(DirectoryString):
1977    """
1978    Plugin class for attribute 'uniqueIdentifier' in aePerson entries
1979    """
1980    oid: str = 'AEUniqueIdentifier-oid'
1981    max_values = 1
1982    gen_template = 'web2ldap-{timestamp}'
1983
1984    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
1985        if not attr_values or not attr_values[0].strip():
1986            return [self.gen_template.format(timestamp=time.time()).encode(self._app.ls.charset)]
1987        return attr_values
1988
1989    def input_field(self) -> Field:
1990        input_field = HiddenInput(
1991            self._at,
1992            ': '.join([self._at, self.desc]),
1993            self.max_len, self.max_values, None,
1994            default=self.form_value(),
1995        )
1996        input_field.charset = self._app.form.accept_charset
1997        return input_field
1998
1999syntax_registry.reg_at(
2000    AEUniqueIdentifier.oid, [
2001        '0.9.2342.19200300.100.1.44', # uniqueIdentifier
2002    ],
2003    structural_oc_oids=[
2004        AE_PERSON_OID, # aePerson
2005    ]
2006)
2007
2008
2009class AEDepartmentNumber(DirectoryString):
2010    """
2011    Plugin class for attribute 'departmentNumber' in aeDept entries
2012    """
2013    oid: str = 'AEDepartmentNumber-oid'
2014    max_values = 1
2015
2016syntax_registry.reg_at(
2017    AEDepartmentNumber.oid, [
2018        '2.16.840.1.113730.3.1.2', # departmentNumber
2019    ],
2020    structural_oc_oids=[
2021        AE_DEPT_OID,   # aeDept
2022    ]
2023)
2024
2025
2026class AECommonName(DirectoryString):
2027    """
2028    Base class for all plugin classes handling 'cn' in xC6-DIR plugin classes,
2029    not directly used
2030    """
2031    oid: str = 'AECommonName-oid'
2032    desc: str = 'AE-DIR: common name of aeObject'
2033    max_values = 1
2034    sani_funcs = (
2035        bytes.strip,
2036    )
2037
2038
2039class AECommonNameAEZone(AECommonName):
2040    """
2041    Plugin for attribute 'cn' in aeZone entries
2042    """
2043    oid: str = 'AECommonNameAEZone-oid'
2044    desc: str = 'AE-DIR: common name of aeZone'
2045    sani_funcs = (
2046        bytes.strip,
2047        bytes.lower,
2048    )
2049
2050syntax_registry.reg_at(
2051    AECommonNameAEZone.oid, [
2052        '2.5.4.3', # cn alias commonName
2053    ],
2054    structural_oc_oids=[
2055        AE_ZONE_OID, # aeZone
2056    ],
2057)
2058
2059
2060class AECommonNameAELocation(AECommonName):
2061    """
2062    Plugin for attribute 'cn' in aeLocation entries
2063    """
2064    oid: str = 'AECommonNameAELocation-oid'
2065    desc: str = 'AE-DIR: common name of aeLocation'
2066
2067syntax_registry.reg_at(
2068    AECommonNameAELocation.oid, [
2069        '2.5.4.3', # cn alias commonName
2070    ],
2071    structural_oc_oids=[
2072        AE_LOCATION_OID, # aeLocation
2073    ],
2074)
2075
2076
2077class AECommonNameAEHost(AECommonName):
2078    """
2079    Plugin for attribute 'cn' in aeHost entries
2080    """
2081    oid: str = 'AECommonNameAEHost-oid'
2082    desc: str = 'Canonical hostname'
2083    derive_from_host = True
2084    host_begin_item = 0
2085    host_end_item = None
2086
2087    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
2088        if self.derive_from_host:
2089            return list({
2090                b'.'.join(av.strip().lower().split(b'.')[self.host_begin_item:self.host_end_item])
2091                for av in self._entry['host']
2092            })
2093        return attr_values
2094
2095syntax_registry.reg_at(
2096    AECommonNameAEHost.oid, [
2097        '2.5.4.3', # cn alias commonName
2098    ],
2099    structural_oc_oids=[
2100        AE_HOST_OID, # aeHost
2101    ],
2102)
2103
2104
2105class AEZonePrefixCommonName(AECommonName, AEObjectMixIn):
2106    """
2107    Base class for handling 'cn' in entries which must have zone name as prefix
2108    """
2109    oid: str = 'AEZonePrefixCommonName-oid'
2110    desc: str = 'AE-DIR: Attribute values have to be prefixed with zone name'
2111    pattern = re.compile(r'^[a-z0-9]+-[a-z0-9-]+$')
2112    special_names = {
2113        'zone-admins',
2114        'zone-auditors',
2115    }
2116
2117    def sanitize(self, attr_value: bytes) -> bytes:
2118        return attr_value.strip()
2119
2120    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
2121        attr_values = [attr_values[0].lower()]
2122        return attr_values
2123
2124    def _validate(self, attr_value: bytes) -> bool:
2125        result = DirectoryString._validate(self, attr_value)
2126        if result and attr_value:
2127            zone_cn = self._get_zone_name()
2128            result = (
2129                zone_cn and
2130                (
2131                    zone_cn == 'pub'
2132                    or attr_value.decode(self._app.ls.charset).startswith(zone_cn+'-')
2133                )
2134            )
2135        return result
2136
2137    def form_value(self) -> str:
2138        result = DirectoryString.form_value(self)
2139        zone_cn = self._get_zone_name()
2140        if zone_cn:
2141            if not self._av:
2142                result = zone_cn+'-'
2143            elif self._av_u in self.special_names:
2144                result = '-'.join((zone_cn, self.av_u))
2145        return result
2146
2147
2148class AECommonNameAEGroup(AEZonePrefixCommonName):
2149    """
2150    Plugin for attribute 'cn' in aeGroup entries
2151    """
2152    oid: str = 'AECommonNameAEGroup-oid'
2153
2154syntax_registry.reg_at(
2155    AECommonNameAEGroup.oid, [
2156        '2.5.4.3', # cn alias commonName
2157    ],
2158    structural_oc_oids=[
2159        AE_GROUP_OID,     # aeGroup
2160        AE_MAILGROUP_OID, # aeMailGroup
2161    ]
2162)
2163
2164
2165class AECommonNameAESrvGroup(AEZonePrefixCommonName):
2166    """
2167    Plugin for attribute 'cn' in aeSrvGroup entries
2168    """
2169    oid: str = 'AECommonNameAESrvGroup-oid'
2170
2171syntax_registry.reg_at(
2172    AECommonNameAESrvGroup.oid, [
2173        '2.5.4.3', # cn alias commonName
2174    ],
2175    structural_oc_oids=[
2176        AE_SRVGROUP_OID, # aeSrvGroup
2177    ]
2178)
2179
2180
2181class AECommonNameAETag(AEZonePrefixCommonName):
2182    """
2183    Plugin for attribute 'cn' in aeTag entries
2184    """
2185    oid: str = 'AECommonNameAETag-oid'
2186
2187    def display(self, vidx, links) -> str:
2188        display_value = AEZonePrefixCommonName.display(self, vidx, links)
2189        if links:
2190            search_anchor = self._app.anchor(
2191                'searchform', '&raquo;',
2192                (
2193                    ('dn', self._dn),
2194                    ('search_root', str(self._app.naming_context)),
2195                    ('searchform_mode', 'adv'),
2196                    ('search_attr', 'aeTag'),
2197                    ('search_option', SEARCH_OPT_IS_EQUAL),
2198                    ('search_string', self.av_u),
2199                ),
2200                title='Search all entries tagged with this tag',
2201            )
2202        else:
2203            search_anchor = ''
2204        return ''.join((display_value, search_anchor))
2205
2206syntax_registry.reg_at(
2207    AECommonNameAETag.oid, [
2208        '2.5.4.3', # cn alias commonName
2209    ],
2210    structural_oc_oids=[
2211        AE_TAG_OID, # aeTag
2212    ]
2213)
2214
2215
2216class AECommonNameAESudoRule(AEZonePrefixCommonName):
2217    """
2218    Plugin for attribute 'cn' in aeSudoRule entries
2219    """
2220    oid: str = 'AECommonNameAESudoRule-oid'
2221
2222syntax_registry.reg_at(
2223    AECommonNameAESudoRule.oid, [
2224        '2.5.4.3', # cn alias commonName
2225    ],
2226    structural_oc_oids=[
2227        AE_SUDORULE_OID, # aeSudoRule
2228    ]
2229)
2230
2231syntax_registry.reg_at(
2232    CNInetOrgPerson.oid, [
2233        '2.5.4.3', # commonName
2234    ],
2235    structural_oc_oids=[
2236        AE_PERSON_OID, # aePerson
2237        AE_USER_OID,   # aeUser
2238    ]
2239)
2240
2241
2242class AESudoRuleDN(AERootDynamicDNSelectList):
2243    """
2244    Plugin for attribute 'aeVisibleSudoers' in aeSrvGroup entries
2245    """
2246    oid: str = 'AESudoRuleDN-oid'
2247    desc: str = 'AE-DIR: DN(s) of visible SUDO rules'
2248    ldap_url = 'ldap:///_?cn?sub?(&(objectClass=aeSudoRule)(aeStatus=0))'
2249
2250syntax_registry.reg_at(
2251    AESudoRuleDN.oid, [
2252        AE_OID_PREFIX+'.4.21', # aeVisibleSudoers
2253    ]
2254)
2255
2256
2257class AENotBefore(NotBefore):
2258    """
2259    Plugin for attribute 'aeNotBefore' in all aeObject entries
2260    """
2261    oid: str = 'AENotBefore-oid'
2262    desc: str = 'AE-DIR: begin of validity period'
2263
2264syntax_registry.reg_at(
2265    AENotBefore.oid, [
2266        AE_OID_PREFIX+'.4.22', # aeNotBefore
2267    ]
2268)
2269
2270
2271class AENotAfter(NotAfter):
2272    """
2273    Plugin for attribute 'aeNotAfter' in all aeObject entries
2274    """
2275    oid: str = 'AENotAfter-oid'
2276    desc: str = 'AE-DIR: begin of validity period'
2277
2278    def _validate(self, attr_value: bytes) -> bool:
2279        result = NotAfter._validate(self, attr_value)
2280        if result:
2281            ae_not_after = time.strptime(attr_value.decode('ascii'), '%Y%m%d%H%M%SZ')
2282            if (
2283                    'aeNotBefore' not in self._entry
2284                    or not self._entry['aeNotBefore']
2285                    or not self._entry['aeNotBefore'][0]
2286                ):
2287                return True
2288            try:
2289                ae_not_before = time.strptime(
2290                    self._entry['aeNotBefore'][0].decode('ascii'),
2291                    '%Y%m%d%H%M%SZ',
2292                )
2293            except KeyError:
2294                result = True
2295            except (UnicodeDecodeError, ValueError):
2296                result = False
2297            else:
2298                result = (ae_not_before <= ae_not_after)
2299        return result
2300
2301syntax_registry.reg_at(
2302    AENotAfter.oid, [
2303        AE_OID_PREFIX+'.4.23', # aeNotAfter
2304    ]
2305)
2306
2307
2308class AEStatus(SelectList, Integer):
2309    """
2310    Plugin for attribute 'aeStatus' in all aeObject entries
2311    """
2312    oid: str = 'AEStatus-oid'
2313    desc: str = 'AE-DIR: Status of object'
2314    attr_value_dict: Dict[str, str] = {
2315        '-1': 'requested',
2316        '0': 'active',
2317        '1': 'deactivated',
2318        '2': 'archived',
2319    }
2320
2321    def _validate(self, attr_value: bytes) -> bool:
2322        result = SelectList._validate(self, attr_value)
2323        if not result or not attr_value:
2324            return result
2325        ae_status = int(attr_value)
2326        current_time = time.gmtime(time.time())
2327        try:
2328            ae_not_before = time.strptime(
2329                self._entry['aeNotBefore'][0].decode('ascii'),
2330                '%Y%m%d%H%M%SZ',
2331            )
2332        except (KeyError, IndexError, ValueError, UnicodeDecodeError):
2333            ae_not_before = time.strptime('19700101000000Z', '%Y%m%d%H%M%SZ')
2334        try:
2335            ae_not_after = time.strptime(
2336                self._entry['aeNotAfter'][0].decode('ascii'),
2337                '%Y%m%d%H%M%SZ',
2338            )
2339        except (KeyError, IndexError, ValueError, UnicodeDecodeError):
2340            ae_not_after = current_time
2341        # see https://www.ae-dir.com/docs.html#schema-validity-period
2342        if current_time > ae_not_after:
2343            result = ae_status >= 1
2344        elif current_time < ae_not_before:
2345            result = ae_status == -1
2346        else:
2347            result = ae_not_before <= current_time <= ae_not_after
2348        return result
2349
2350    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
2351        if not attr_values or not attr_values[0]:
2352            return attr_values
2353        ae_status = int(attr_values[0].decode('ascii'))
2354        current_time = time.gmtime(time.time())
2355        try:
2356            ae_not_before = time.strptime(
2357                self._entry['aeNotBefore'][0].decode('ascii'),
2358                '%Y%m%d%H%M%SZ',
2359            )
2360        except (KeyError, IndexError, ValueError):
2361            pass
2362        else:
2363            if ae_status == 0 and current_time < ae_not_before:
2364                ae_status = -1
2365        try:
2366            ae_not_after = time.strptime(
2367                self._entry['aeNotAfter'][0].decode('ascii'),
2368                '%Y%m%d%H%M%SZ',
2369            )
2370        except (KeyError, IndexError, ValueError):
2371            ae_not_after = None
2372        else:
2373            if current_time > ae_not_after:
2374                try:
2375                    ae_expiry_status = int(
2376                        self._entry.get('aeExpiryStatus', ['1'])[0].decode('ascii')
2377                    )
2378                except (KeyError, IndexError, ValueError):
2379                    pass
2380                else:
2381                    ae_status = max(ae_status, ae_expiry_status)
2382        return [str(ae_status).encode('ascii')]
2383
2384    def display(self, vidx, links) -> str:
2385        if not links:
2386            return Integer.display(self, vidx, links)
2387        return SelectList.display(self, vidx, links)
2388
2389syntax_registry.reg_at(
2390    AEStatus.oid, [
2391        AE_OID_PREFIX+'.4.5', # aeStatus
2392    ]
2393)
2394
2395
2396class AEExpiryStatus(SelectList):
2397    """
2398    Plugin for attribute 'aeExpiryStatus' in all aeObject entries
2399    """
2400    oid: str = 'AEExpiryStatus-oid'
2401    desc: str = 'AE-DIR: Expiry status of object'
2402    attr_value_dict: Dict[str, str] = {
2403        '-/-': '',
2404        '1': 'deactivated',
2405        '2': 'archived',
2406    }
2407
2408syntax_registry.reg_at(
2409    AEStatus.oid, [
2410        AE_OID_PREFIX+'.4.46', # aeExpiryStatus
2411    ]
2412)
2413
2414
2415class AESudoUser(SudoUserGroup):
2416    """
2417    Plugin for attribute 'sudoUser' in aeSudoRule entries
2418    """
2419    oid: str = 'AESudoUser-oid'
2420    desc: str = 'AE-DIR: sudoUser'
2421    ldap_url = (
2422        'ldap:///_?cn,cn?sub?'
2423        '(&'
2424          '(objectClass=aeGroup)'
2425          '(aeStatus=0)'
2426          '(!(|'
2427            '(cn=ae-admins)'
2428            '(cn=ae-auditors)'
2429            '(cn=ae-providers)'
2430            '(cn=ae-replicas)'
2431            '(cn=ae-login-proxies)'
2432            '(cn=*-zone-admins)'
2433            '(cn=*-zone-auditors)'
2434          '))'
2435        ')'
2436    )
2437
2438syntax_registry.reg_at(
2439    AESudoUser.oid, [
2440        '1.3.6.1.4.1.15953.9.1.1', # sudoUser
2441    ],
2442    structural_oc_oids=[
2443        AE_SUDORULE_OID, # aeSudoRule
2444    ]
2445)
2446
2447
2448class AEServiceSshPublicKey(SshPublicKey):
2449    """
2450    Plugin for attribute 'sshPublicKey' in aeService entries
2451
2452    Mainly this can be used to assign specific regex pattern
2453    e.g. for limiting values to certain OpenSSH key types
2454    in aeService entries.
2455    """
2456    oid: str = 'AEServiceSshPublicKey-oid'
2457    desc: str = 'AE-DIR: aeService:sshPublicKey'
2458
2459syntax_registry.reg_at(
2460    AEServiceSshPublicKey.oid, [
2461        '1.3.6.1.4.1.24552.500.1.1.1.13', # sshPublicKey
2462    ],
2463    structural_oc_oids=[
2464        AE_SERVICE_OID, # aeService
2465    ]
2466)
2467
2468
2469class AEUserSshPublicKey(SshPublicKey):
2470    """
2471    Plugin for attribute 'sshPublicKey' in aeUser entries
2472
2473    Mainly this can be used to assign specific regex pattern
2474    e.g. for limiting values to certain OpenSSH key types
2475    in aeUser entries.
2476    """
2477    oid: str = 'AEUserSshPublicKey-oid'
2478    desc: str = 'AE-DIR: aeUser:sshPublicKey'
2479
2480syntax_registry.reg_at(
2481    AEUserSshPublicKey.oid, [
2482        '1.3.6.1.4.1.24552.500.1.1.1.13', # sshPublicKey
2483    ],
2484    structural_oc_oids=[
2485        AE_USER_OID, # aeUser
2486    ]
2487)
2488
2489
2490class AEEntryDNAEAuthcToken(DistinguishedName):
2491    """
2492    Plugin for attribute 'entryDN' in aeAuthcToken entries
2493    """
2494    oid: str = 'AEEntryDNAEAuthcToken-oid'
2495    desc: str = 'AE-DIR: entryDN of aeAuthcToken entry'
2496    ref_attrs = (
2497        (
2498            'oathToken', 'Users', None, 'aeUser',
2499            'Search all personal user accounts using this OATH token.'
2500        ),
2501    )
2502
2503syntax_registry.reg_at(
2504    AEEntryDNAEAuthcToken.oid, [
2505        '1.3.6.1.1.20', # entryDN
2506    ],
2507    structural_oc_oids=[
2508        AE_AUTHCTOKEN_OID, # aeAuthcToken
2509    ],
2510)
2511
2512
2513class AEEntryDNAEPolicy(DistinguishedName):
2514    """
2515    Plugin for attribute 'entryDN' in aePolicy entries
2516    """
2517    oid: str = 'AEEntryDNAEPolicy-oid'
2518    desc: str = 'AE-DIR: entryDN of aePolicy entry'
2519    ref_attrs = (
2520        (
2521            'pwdPolicySubentry', 'Users', None, 'aeUser',
2522            'Search all personal user accounts restricted by this password policy.'
2523        ),
2524        (
2525            'pwdPolicySubentry', 'Services', None, 'aeService',
2526            'Search all service accounts restricted by this password policy.'
2527        ),
2528        (
2529            'pwdPolicySubentry', 'Tokens', None, 'aeAuthcToken',
2530            'Search all authentication tokens restricted by this password policy.'
2531        ),
2532        (
2533            'oathHOTPParams', 'HOTP Tokens', None, 'oathHOTPToken',
2534            'Search all HOTP tokens affected by this HOTP parameters.'
2535        ),
2536        (
2537            'oathTOTPParams', 'TOTP Tokens', None, 'oathTOTPToken',
2538            'Search all TOTP tokens affected by this TOTP parameters.'
2539        ),
2540    )
2541
2542syntax_registry.reg_at(
2543    AEEntryDNAEPolicy.oid, [
2544        '1.3.6.1.1.20', # entryDN
2545    ],
2546    structural_oc_oids=[
2547        AE_POLICY_OID, # aePolicy
2548    ],
2549)
2550
2551
2552class AERFC822MailMember(DynamicValueSelectList, AEObjectMixIn):
2553    """
2554    Plugin for attribute 'rfc822MailMember' in aeMailGroup entries
2555    """
2556    oid: str = 'AERFC822MailMember-oid'
2557    desc: str = 'AE-DIR: rfc822MailMember'
2558    ldap_url = (
2559        'ldap:///_?mail,displayName?sub?'
2560        '(&(|(objectClass=inetLocalMailRecipient)(objectClass=aeContact))(mail=*)(aeStatus=0))'
2561    )
2562    html_tmpl = RFC822Address.html_tmpl
2563    show_val_button = False
2564
2565    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
2566        if 'member' not in self._entry:
2567            return []
2568        if self.ae_status == 2:
2569            return []
2570        entrydn_filter = compose_filter(
2571            '|',
2572            map_filter_parts(
2573                'entryDN',
2574                decode_list(self._entry['member'], encoding=self._app.ls.charset),
2575            ),
2576        )
2577        ldap_result = self._app.ls.l.search_s(
2578            self._search_root(),
2579            ldap0.SCOPE_SUBTREE,
2580            entrydn_filter,
2581            attrlist=['mail'],
2582        )
2583        mail_addresses = []
2584        for res in ldap_result or []:
2585            mail_addresses.extend(res.entry_as['mail'])
2586        return sorted(mail_addresses)
2587
2588    def input_field(self) -> Field:
2589        input_field = HiddenInput(
2590            self._at,
2591            ': '.join([self._at, self.desc]),
2592            self.max_len, self.max_values, None,
2593        )
2594        input_field.charset = self._app.form.accept_charset
2595        input_field.set_default(self.form_value())
2596        return input_field
2597
2598syntax_registry.reg_at(
2599    AERFC822MailMember.oid, [
2600        '1.3.6.1.4.1.42.2.27.2.1.15', # rfc822MailMember
2601    ],
2602    structural_oc_oids=[
2603        AE_MAILGROUP_OID, # aeMailGroup
2604    ]
2605)
2606
2607
2608class AEPwdPolicy(PwdPolicySubentry):
2609    """
2610    Plugin for attribute 'pwdPolicySubentry' in aeUser, aeService and aeHost entries
2611    """
2612    oid: str = 'AEPwdPolicy-oid'
2613    desc: str = 'AE-DIR: pwdPolicySubentry'
2614    ldap_url = 'ldap:///_??sub?(&(objectClass=aePolicy)(objectClass=pwdPolicy)(aeStatus=0))'
2615
2616syntax_registry.reg_at(
2617    AEPwdPolicy.oid, [
2618        '1.3.6.1.4.1.42.2.27.8.1.23', # pwdPolicySubentry
2619    ],
2620    structural_oc_oids=[
2621        AE_USER_OID,    # aeUser
2622        AE_SERVICE_OID, # aeService
2623        AE_HOST_OID,    # aeHost
2624    ]
2625)
2626
2627
2628class AESudoHost(IA5String):
2629    """
2630    Plugin for attribute 'sudoHost' in aeSudoRule entries
2631    """
2632    oid: str = 'AESudoHost-oid'
2633    desc: str = 'AE-DIR: sudoHost'
2634    max_values = 1
2635
2636    def transmute(self, attr_values: List[bytes]) -> List[bytes]:
2637        return [b'ALL']
2638
2639    def input_field(self) -> Field:
2640        input_field = HiddenInput(
2641            self._at,
2642            ': '.join([self._at, self.desc]),
2643            self.max_len, self.max_values, None,
2644            default=self.form_value()
2645        )
2646        input_field.charset = self._app.form.accept_charset
2647        return input_field
2648
2649syntax_registry.reg_at(
2650    AESudoHost.oid, [
2651        '1.3.6.1.4.1.15953.9.1.2', # sudoHost
2652    ],
2653    structural_oc_oids=[
2654        AE_SUDORULE_OID, # aeSudoRule
2655    ]
2656)
2657
2658
2659class AELoginShell(Shell):
2660    """
2661    Plugin for attribute 'loginShell' in aeUser and aeService entries
2662    """
2663    oid: str = 'AELoginShell-oid'
2664    desc: str = 'AE-DIR: Login shell for POSIX users'
2665    attr_value_dict: Dict[str, str] = {
2666        '/bin/bash': '/bin/bash',
2667        '/bin/true': '/bin/true',
2668        '/bin/false': '/bin/false',
2669    }
2670
2671syntax_registry.reg_at(
2672    AELoginShell.oid, [
2673        '1.3.6.1.1.1.1.4', # loginShell
2674    ],
2675    structural_oc_oids=[
2676        AE_USER_OID,    # aeUser
2677        AE_SERVICE_OID, # aeService
2678    ]
2679)
2680
2681
2682class AEOathHOTPToken(OathHOTPToken):
2683    """
2684    Plugin for attribute 'oathHOTPToken' in aeUser entries
2685    """
2686    oid: str = 'AEOathHOTPToken-oid'
2687    desc: str = 'DN of the associated oathHOTPToken entry in aeUser entry'
2688    ref_attrs = (
2689        (None, 'Users', None, None),
2690    )
2691    input_fallback = False
2692
2693    def _filterstr(self):
2694        if 'aePerson' in self._entry:
2695            return '(&{0}(aeOwner={1}))'.format(
2696                OathHOTPToken._filterstr(self),
2697                escape_filter_str(
2698                    self._entry['aePerson'][0].decode(self._app.form.accept_charset)
2699                ),
2700            )
2701        return OathHOTPToken._filterstr(self)
2702
2703syntax_registry.reg_at(
2704    AEOathHOTPToken.oid, [
2705        '1.3.6.1.4.1.5427.1.389.4226.4.9.1', # oathHOTPToken
2706    ],
2707    structural_oc_oids=[AE_USER_OID], # aeUser
2708)
2709
2710
2711# see sshd(AUTHORIZED_KEYS FILE FORMAT
2712# and the -O option in ssh-keygen(1)
2713class AESSHPermissions(SelectList):
2714    """
2715    Plugin for attribute 'aeSSHPermissions' in aeUser and aeService entries
2716    """
2717    oid: str = 'AESSHPermissions-oid'
2718    desc: str = 'AE-DIR: Status of object'
2719    attr_value_dict: Dict[str, str] = {
2720        'pty': 'PTY allocation',
2721        'X11-forwarding': 'X11 forwarding',
2722        'agent-forwarding': 'Key agent forwarding',
2723        'port-forwarding': 'Port forwarding',
2724        'user-rc': 'Execute ~/.ssh/rc',
2725    }
2726
2727syntax_registry.reg_at(
2728    AESSHPermissions.oid, [
2729        AE_OID_PREFIX+'.4.47', # aeSSHPermissions
2730    ]
2731)
2732
2733
2734class AERemoteHostAEHost(DynamicValueSelectList):
2735    """
2736    Plugin for attribute 'aeRemoteHost' in aeHost entries
2737    """
2738    oid: str = 'AERemoteHostAEHost-oid'
2739    desc: str = 'AE-DIR: aeRemoteHost in aeHost entry'
2740    ldap_url = 'ldap:///.?ipHostNumber,aeFqdn?one?(&(objectClass=aeNwDevice)(aeStatus=0))'
2741    input_fallback = True # fallback to normal input field
2742
2743syntax_registry.reg_at(
2744    AERemoteHostAEHost.oid, [
2745        AE_OID_PREFIX+'.4.8',  # aeRemoteHost
2746    ],
2747    structural_oc_oids=[AE_HOST_OID], # aeHost
2748)
2749
2750
2751class AEDescriptionAENwDevice(ComposedAttribute):
2752    """
2753    Plugin for attribute 'description' in aeNwDevice entries
2754    """
2755    oid: str = 'AEDescriptionAENwDevice-oid'
2756    desc: str = 'Attribute description in object class aeNwDevice'
2757    compose_templates = (
2758        '{cn}: {aeFqdn} / {ipHostNumber}',
2759        '{cn}: {ipHostNumber}',
2760    )
2761
2762syntax_registry.reg_at(
2763    AEDescriptionAENwDevice.oid, [
2764        '2.5.4.13', # description
2765    ],
2766    structural_oc_oids=[AE_NWDEVICE_OID], # aeNwDevice
2767)
2768
2769
2770class AEChildClasses(SelectList):
2771    """
2772    Plugin for attribute 'aeChildClasses' in aeZone entries
2773    """
2774    oid = 'AEChildClasses-oid'
2775    desc = 'AE-DIR: Structural object classes allowed to be added in child entries'
2776    attr_value_dict: Dict[str, str] = {
2777        '-/-': '',
2778        'aeAuthcToken': 'Authentication Token (aeAuthcToken)',
2779        'aeContact': 'Contact (aeContact)',
2780        'aeDept': 'Department (aeDept)',
2781        'aeLocation': 'Location (aeLocation)',
2782        'aeMailGroup': 'Mail Group (aeMailGroup)',
2783        'aePerson': 'Person (aePerson)',
2784        'aePolicy': 'Policy (aePolicy)',
2785        'aeService': 'Service/tool Account (aeService)',
2786        'aeSrvGroup': 'Service Group (aeSrvGroup)',
2787        'aeSudoRule': 'Sudoers Rule (sudoRole)',
2788        'aeUser': 'User account (aeUser)',
2789        'aeGroup': 'User group (aeGroup)',
2790        'aeTag': 'Tag (aeTag)',
2791    }
2792
2793syntax_registry.reg_at(
2794    AEChildClasses.oid, [
2795        AE_OID_PREFIX+'.4.49', # aeChildClasses
2796    ]
2797)
2798
2799
2800# Register all syntax classes in this module
2801syntax_registry.reg_syntaxes(__name__)
2802