3web2ldap.app.schema.syntaxes: classes for known attribute types
5web2ldap - a web-based LDAP Client,
6see https://www.web2ldap.de for details
8(C) 1998-2022 by Michael Stroeder <michael@stroeder.com>
10This software is distributed under the terms of the
11Apache License Version 2.0 (Apache-2.0)
12https://www.apache.org/licenses/LICENSE-2.0
41 import defusedxml.ElementTree
43 DEFUSEDXML_AVAIL =
False
45 DEFUSEDXML_AVAIL =
True
50 PHONENUMBERS_AVAIL =
False
52 PHONENUMBERS_AVAIL =
True
54from collections
import defaultdict
59 from PIL
import Image
as PILImage
64 warnings.simplefilter(
'error', PILImage.DecompressionBombWarning)
70from ldap0.schema.models
import AttributeType, ObjectClass, OBJECTCLASS_KIND_STR
71from ldap0.controls.deref
import DereferenceControl
72from ldap0.dn
import DNObj, is_dn
73from ldap0.res
import SearchResultEntry
74from ldap0.schema.subentry
import SubSchema
78from ...
import ETC_DIR
79from ...web
import forms
as web_forms
80from ...msbase
import ascii_dump, chunks
81from ...utctime
import repr2ts, ts2repr, strftimeiso8601
82from ...ldaputil.oidreg
import OID_REG
83from ...log
import logger
85from .
import schema_anchor
86from ..tmpl
import get_variant_filename
87from ...utctime
import strptime
as utc_strptime
88from ..searchform
import (
89 SEARCH_OPT_ATTR_EXISTS,
91 SEARCH_SCOPE_STR_ONELEVEL,
97 syntax registry used to register plugin classes
110 register a syntax classes for an OID
112 assert isinstance(cls.oid, str), ValueError(
113 'Expected %s.oid to be str, got %r' % (cls.__name__, cls.oid,)
115 logger.debug(
'Register syntax class %r with OID %r', cls.__name__, cls.oid)
122 'Failed to register syntax class %s.%s with OID %s,'
123 ' already registered by %s.%s'
136 register all syntax classes found in given module
138 logger.debug('Register syntax classes from module %r', modulename)
139 for _, cls
in inspect.getmembers(sys.modules[modulename], inspect.isclass):
140 if issubclass(cls, LDAPSyntax)
and hasattr(cls,
'oid'):
143 def reg_at(self, syntax_oid: str, attr_types, structural_oc_oids=
None):
145 register an attribute type (by OID) to explicitly use a certain LDAPSyntax class
148 'Register syntax OID %s for %r / %r',
153 assert isinstance(syntax_oid, str), ValueError(
154 'Expected syntax_oid to be str, got %r' % (syntax_oid,)
156 structural_oc_oids = list(filter(
None, map(str.strip, structural_oc_oids
or [])))
or [
None]
157 for atype
in attr_types:
158 atype = atype.strip()
159 for oc_oid
in structural_oc_oids:
166 'Registering attribute type %r with syntax %r'
167 ' overrides existing registration with syntax %r'
173 self.
at2syntax[atype][oc_oid] = syntax_oid
175 def get_syntax(self, schema, attrtype_nameoroid, structural_oc):
177 returns LDAPSyntax class for given attribute type
179 assert isinstance(attrtype_nameoroid, str), ValueError(
180 'Expected attrtype_nameoroid to be str, got %r' % (attrtype_nameoroid,)
182 assert structural_oc
is None or isinstance(structural_oc, str), ValueError(
183 'Expected structural_oc to be str or None, got %r' % (structural_oc,)
185 attrtype_oid = schema.get_oid(AttributeType, attrtype_nameoroid)
187 structural_oc_oid = schema.get_oid(ObjectClass, structural_oc)
189 structural_oc_oid =
None
190 syntax_oid = LDAPSyntax.oid
192 syntax_oid = self.
at2syntax[attrtype_oid][structural_oc_oid]
195 syntax_oid = self.
at2syntax[attrtype_oid][
None]
197 attrtype_se = schema.get_inheritedobj(
202 if attrtype_se
and attrtype_se.syntax:
203 syntax_oid = attrtype_se.syntax
207 syntax_class = LDAPSyntax
210 def get_at(self, app, dn, schema, attr_type, attr_value, entry=None):
212 returns LDAPSyntax instance fully initialized for given attribute
215 structural_oc = entry.get_structural_oc()
218 syntax_class = self.
get_syntax(schema, attr_type, structural_oc)
219 attr_instance = syntax_class(app, dn, schema, attr_type, attr_value, entry)
224 check whether attribute registry dict contains references by OID
225 for which no LDAPSyntax
class are registered
228 'Checking %d LDAPSyntax classes and %d attribute type mappings',
233 for object_class
in self.
at2syntax[atype]:
235 logger.warning(
'No LDAPSyntax registered for (%r, %r)', atype, object_class)
245 Exception raised in case a syntax check failed
249class LDAPSyntaxRegexNoMatch(LDAPSyntaxValueError):
251 Exception raised in case a regex pattern check failed
257 Base class for all LDAP syntax and attribute value plugin classes
269 desc: str =
'Any LDAP syntax'
271 max_len: int = web2ldapcnf.input_maxfieldlen
272 max_values: int = web2ldapcnf.input_maxattrs
273 mime_type: str =
'application/octet-stream'
274 file_ext: str =
'bin'
275 editable: bool =
True
276 pattern: Optional[Pattern[str]] =
None
277 input_pattern: Optional[str] =
None
278 search_sep: str =
'<br>'
279 read_sep: str =
'<br>'
280 field_sep: str =
'<br>'
281 sani_funcs: Sequence[Callable] = (())
282 show_val_button: bool =
True
289 attrType: Optional[str],
290 attr_value: Optional[bytes],
294 entry = ldap0.schema.models.Entry(schema, dn, {})
295 assert isinstance(dn, str), \
296 TypeError(
"Argument 'dn' must be str, was %r" % (dn,))
297 assert isinstance(attrType, str)
or attrType
is None, \
298 TypeError(
"Argument 'attrType' must be str or None, was %r" % (attrType,))
299 assert isinstance(attr_value, bytes)
or attr_value
is None, \
300 TypeError(
"Argument 'attr_value' must be bytes or None, was %r" % (attr_value,))
301 assert entry
is None or isinstance(entry, ldap0.schema.models.Entry), \
302 TypeError(
'entry must be ldaputil.schema.Entry, was %r' % (entry,))
313 return DNObj.from_str(self.
_dn)
317 if (self.
_av is not None and self.
_av_u is None):
323 Transforms the HTML form input field values into LDAP string
324 representations and returns raw binary string.
326 This
is the inverse of LDAPSyntax.form_value().
328 When using this method one MUST NOT assume that the whole entry
is
331 for sani_func
in self.sani_funcs:
332 attr_value = sani_func(attr_value)
335 def transmute(self, attr_values: List[bytes]) -> List[bytes]:
337 This method can be implemented to transmute attribute values and has
338 to handle LDAP string representations (raw binary strings).
340 This method has access to the whole entry after processing all input.
342 Implementors should be prepared that this method could be called
343 more than once. If there
's nothing to change then simply return the
346 Exceptions KeyError or IndexError are caught by the calling code to
347 re-iterate invoking this method.
353 check the syntax of attr_value
355 Implementors can overload this method to apply arbitrary syntax checks.
362 if self.pattern
and (self.pattern.match(attr_value.decode(self.
_app.ls.charset))
is None):
364 "Class %s: %r does not match pattern %r." % (
365 self.__class__.__name__,
367 self.pattern.pattern,
372 "Class %s: %r does not comply to syntax (attr type %r)." % (
373 self.__class__.__name__,
382 return HTML markup of [+]
or [-] submit buttons
for adding/removing
386 row number
in input table
390 optionally override displayed link link_text
392 link_text = link_text or mode
394 not self.show_val_button
or
395 self.max_values <= 1
or
396 len(self.
_entry.get(self.
_at, [])) >= self.max_values
399 se_obj = self.
_schema.get_obj(AttributeType, self.
_at)
400 if se_obj
and se_obj.single_value:
404 ' formaction="%s#in_a_%s"'
410 self.
_app.form.action_url(command, self.
_app.sid),
417 Transform LDAP string representations to HTML form input field
418 values. Returns Unicode string to be encoded with the browser
's
421 This is the inverse of LDAPSyntax.sanitize().
424 result = self.
av_u or ''
425 except UnicodeDecodeError:
426 result =
'!!!snipped because of UnicodeDecodeError!!!'
433 input_field = web_forms.Input(
435 ': '.join([self.
_at, self.desc]),
440 size=min(self.max_len, self.input_size),
442 input_field.charset = self.
_app.form.accept_charset
449 except UnicodeDecodeError:
450 res = self.
_app.form.s2d(repr(self.
_av))
456 Plugin class for LDAP syntax
'Binary' (see RFC 2252)
458 oid: str = '1.3.6.1.4.1.1466.115.121.1.5'
460 editable: bool =
False
463 field = web_forms.File(
465 ': '.join([self.
_at, self.desc]),
466 self.max_len, self.max_values,
None, default=self.
_av, size=50
468 field.mime_type = self.mime_type
472 return '%d bytes | %s' % (
478 (
'read_attr', self.
_at),
479 (
'read_attrindex',
str(vidx)),
487 Plugin class for LDAP syntax
'Audio' (see RFC 2252)
489 oid: str = '1.3.6.1.4.1.1466.115.121.1.4'
491 mime_type: str =
'audio/basic'
495 with BytesIO(attr_value)
as fileobj:
496 res = sndhdr.test_au(attr_value, fileobj)
497 return res
is not None
500 mimetype = self.mime_type
502 '<embed type="%s" autostart="false" '
503 'src="%s?dn=%s&read_attr=%s&read_attrindex=%d">'
504 '%d bytes of audio data (%s)'
507 self.
_app.form.action_url(
'read', self.
_app.sid),
508 urllib.parse.quote(self.
_dn.encode(self.
_app.form.accept_charset)),
509 urllib.parse.quote(self.
_at),
518 Plugin class for LDAP syntax
'Directory String'
519 (see https://datatracker.ietf.org/doc/html/rfc4517
521 oid: str = '1.3.6.1.4.1.1466.115.121.1.15'
522 desc: str =
'Directory String'
527 self.
_app.ls.uc_decode(attr_value)
528 except UnicodeDecodeError:
540 Plugin class for LDAP syntax
'DN'
541 (see https://datatracker.ietf.org/doc/html/rfc4517
543 oid: str = '1.3.6.1.4.1.1466.115.121.1.12'
544 desc: str =
'Distinguished Name'
546 hasSubordinates =
False
547 ref_attrs: Optional[Sequence[Tuple[Optional[str], str, Optional[str], str]]] =
None
550 return is_dn(self.
_app.ls.uc_decode(attr_value)[0])
554 if self.
_at.lower() !=
'entrydn':
562 res.append(self.
_app.anchor(
566 (
'scope', SEARCH_SCOPE_STR_ONELEVEL),
567 (
'filterstr',
'(objectClass=*)'),
571 ldap_url_obj = self.
_app.ls.ldap_url(
'', add_login=
False)
577 (
'ldapurl',
str(ldap_url_obj)),
579 (
'login_who', self.
av_u),
581 title=
'Connect and bind new session as\r\n%s' % (self.
av_u)
585 for ref_attr_tuple
in self.ref_attrs
or tuple():
587 ref_attr, ref_text, ref_dn, ref_oc, ref_title = ref_attr_tuple
590 ref_attr, ref_text, ref_dn, ref_title = ref_attr_tuple
591 ref_attr = ref_attr
or self.
_at
592 if ref_attr
not in self.
_schema.name2oid[AttributeType]:
594 ref_dn = ref_dn
or self.
_dn
595 ref_title = ref_title
or 'Search %s entries referencing entry %s in attribute %s' % (
596 ref_oc, self.
av_u, ref_attr,
598 res.append(self.
_app.anchor(
599 'search', self.
_app.form.s2d(ref_text),
602 (
'search_root',
str(self.
_app.naming_context)),
603 (
'searchform_mode',
'adv'),
604 (
'search_attr',
'objectClass'),
608 True: SEARCH_OPT_ATTR_EXISTS,
609 False: SEARCH_OPT_IS_EQUAL,
612 (
'search_string', ref_oc
or ''),
613 (
'search_attr', ref_attr),
614 (
'search_option', SEARCH_OPT_IS_EQUAL),
615 (
'search_string', self.
av_u),
622 res = [self.
_app.form.s2d(self.
av_u or '- World -')]
625 return web2ldapcnf.command_link_separator.join(res)
630 Plugin class for DNs probably usable as bind-DN
632 oid: str = 'BindDN-oid'
633 desc: str =
'A Distinguished Name used to bind to a directory'
639 Plugin class for DNs used for authorization
641 oid: str = 'AuthzDN-oid'
642 desc: str =
'Authz Distinguished Name'
645 result = DistinguishedName.display(self, vidx, links)
647 simple_display_str = DistinguishedName.display(
652 whoami_display_str = self.
_app.display_authz_dn(who=self.
av_u)
653 if whoami_display_str != simple_display_str:
654 result =
'<br>'.join((whoami_display_str, result))
660 Plugin class for LDAP syntax
'Name and Optional UID'
661 (see https://datatracker.ietf.org/doc/html/rfc4517
663 oid: str = '1.3.6.1.4.1.1466.115.121.1.34'
664 desc: str =
'Name And Optional UID'
669 sep_ind = val.rindex(
'#')
675 uid = val[sep_ind+1:]
683 value = self.
av_u.split(
'#')
684 dn_str = self.
_app.display_dn(
688 if len(value) == 1
or not value[1]:
690 return web2ldapcnf.command_link_separator.join([
691 self.
_app.form.s2d(value[1]),
698 Plugin class for LDAP syntax
'Bit String'
699 (see https://datatracker.ietf.org/doc/html/rfc4517
701 oid: str = '1.3.6.1.4.1.1466.115.121.1.6'
702 desc: str =
'Bit String'
703 pattern = re.compile(
"^'[01]+'B$")
708 Plugin class for LDAP syntax
'IA5 String'
709 (see https://datatracker.ietf.org/doc/html/rfc4517
711 oid: str = '1.3.6.1.4.1.1466.115.121.1.26'
712 desc: str =
'IA5 String'
716 _ = attr_value.decode(
'ascii').encode(
'ascii')
724 Plugin class for LDAP syntax
'Generalized Time'
725 (see https://datatracker.ietf.org/doc/html/rfc4517
727 oid: str = '1.3.6.1.4.1.1466.115.121.1.24'
728 desc: str =
'Generalized Time'
731 pattern = re.compile(
r'^([0-9]){12,14}((\.|,)[0-9]+)*(Z|(\+|-)[0-9]{4})$')
735 form_value_fmt =
'%Y-%m-%dT%H:%M:%SZ'
738 '%Y-%m-%dT%H:%M:%SZ',
740 '%Y-%m-%dT%H:%M:%S+00:00',
741 '%Y-%m-%dT%H:%M:%S-00:00',
742 '%Y-%m-%d %H:%M:%SZ',
745 '%Y-%m-%d %H:%M:%S+00:00',
746 '%Y-%m-%d %H:%M:%S-00:00',
747 '%d.%m.%YT%H:%M:%SZ',
749 '%d.%m.%YT%H:%M:%S+00:00',
750 '%d.%m.%YT%H:%M:%S-00:00',
751 '%d.%m.%Y %H:%M:%SZ',
754 '%d.%m.%Y %H:%M:%S+00:00',
755 '%d.%m.%Y %H:%M:%S-00:00',
757 acceptable_formats = (
762 dt_display_format = (
763 '<time datetime="%Y-%m-%dT%H:%M:%SZ">'
764 '%A (%W. week) %Y-%m-%d %H:%M:%S+00:00'
770 d_t = utc_strptime(attr_value)
782 d_t = datetime.datetime.strptime(self.
av_u,
r'%Y%m%d%H%M%SZ')
784 result = IA5String.form_value(self)
790 av_u = self.
_app.ls.uc_decode(attr_value.strip().upper())[0]
792 if av_u
in {
'N',
'NOW',
'0'}:
793 return datetime.datetime.strftime(
794 datetime.datetime.utcnow(),
799 float_val = float(av_u)
803 return datetime.datetime.strftime(
804 datetime.datetime.utcnow()+datetime.timedelta(seconds=float_val),
809 if av_u
in (
'T',
'TODAY'):
810 return datetime.datetime.strftime(
811 datetime.datetime.utcnow(),
814 if av_u
in (
'Y',
'YESTERDAY'):
815 return datetime.datetime.strftime(
816 datetime.datetime.today()-datetime.timedelta(days=1),
819 if av_u
in (
'T',
'TOMORROW'):
820 return datetime.datetime.strftime(
821 datetime.datetime.today()+datetime.timedelta(days=1),
827 d_t = datetime.datetime.strptime(av_u, time_format)
831 result = datetime.datetime.strftime(d_t,
r'%Y%m%d%H%M%SZ')
837 d_t = datetime.datetime.strptime(av_u, time_format)
841 result = datetime.datetime.strftime(d_t,
r'%Y%m%d'+self.
timeDefault+
'Z')
846 return IA5String.sanitize(self, attr_value)
847 return result.encode(
'ascii')
852 dt_utc = utc_strptime(self.
av_u)
854 return IA5String.display(self, vidx, links)
858 return IA5String.display(self, vidx, links)
861 current_time = datetime.datetime.utcnow()
862 time_span = (current_time - dt_utc).total_seconds()
863 return '{dt_utc} ({av})<br>{timespan_disp} {timespan_comment}'.format(
866 timespan_disp=self.
_app.form.s2d(
867 ts2repr(Timespan.time_divisors,
' ', abs(time_span))
879 Plugin class for attributes indicating start of a period
881 oid: str = 'NotBefore-oid'
882 desc: str =
'A not-before timestamp by default starting at 00:00:00'
883 timeDefault =
'000000'
888 Plugin class for attributes indicating end of a period
890 oid: str = 'NotAfter-oid'
891 desc: str =
'A not-after timestamp by default ending at 23:59:59'
892 timeDefault =
'235959'
897 Plugin class for LDAP syntax
'UTC Time'
898 (see https://datatracker.ietf.org/doc/html/rfc4517
900 oid: str = '1.3.6.1.4.1.1466.115.121.1.53'
901 desc: str =
'UTC Time'
906 Plugin class for strings terminated with null-byte
908 oid: str = 'NullTerminatedDirectoryString-oid'
909 desc: str =
'Directory String terminated by null-byte'
912 return attr_value + b
'\x00'
915 return attr_value.endswith(b
'\x00')
918 return self.
_app.ls.uc_decode((self.
_av or b
'\x00')[:-1])[0]
921 return self.
_app.form.s2d(
922 self.
_app.ls.uc_decode((self.
_av or b
'\x00')[:-1])[0]
928 Plugin class for LDAP syntax
'Other Mailbox'
929 (see https://datatracker.ietf.org/doc/html/rfc4517
931 oid: str = '1.3.6.1.4.1.1466.115.121.1.39'
932 desc: str =
'Other Mailbox'
938 Plugin class for LDAP syntax
'Integer'
939 (see https://datatracker.ietf.org/doc/html/rfc4517
941 oid: str = '1.3.6.1.4.1.1466.115.121.1.27'
942 desc: str =
'Integer'
947 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=
None):
948 IA5String.__init__(self, app, dn, schema, attrType, attr_value, entry)
953 min_value_len = max_value_len = fval_len = 0
959 fval_len = len(fval.encode(self.
_app.ls.charset))
960 return max(self.input_size, fval_len, min_value_len, max_value_len)
964 val =
int(attr_value)
969 (min_value
is None or val >= min_value)
and
970 (max_value
is None or val <= max_value)
975 return str(
int(attr_value)).encode(
'ascii')
982 input_field = web_forms.Input(
984 ': '.join([self.
_at, self.desc]),
989 size=min(self.input_size, max_len),
991 input_field.input_type =
'number'
997 Plugin class for string representation of IPv4 or IPv6 host address
999 oid: str = 'IPHostAddress-oid'
1000 desc: str =
'string representation of IPv4 or IPv6 address'
1009 addr = ipaddress.ip_address(attr_value.decode(
'ascii'))
1017 Plugin class for string representation of IPv4 host address
1019 oid: str = 'IPv4HostAddress-oid'
1020 desc: str =
'string representation of IPv4 address'
1021 addr_class = ipaddress.IPv4Address
1026 Plugin class for string representation of IPv6 host address
1028 oid: str = 'IPv6HostAddress-oid'
1029 desc: str =
'string representation of IPv6 address'
1030 addr_class = ipaddress.IPv6Address
1035 Plugin class for string representation of IPv4 or IPv6 network address
1037 oid: str = 'IPNetworkAddress-oid'
1038 desc: str =
'string representation of IPv4 or IPv6 network address/mask'
1042 addr = ipaddress.ip_network(attr_value.decode(
'ascii'), strict=
False)
1050 Plugin class for string representation of IPv4 network address
1052 oid: str = 'IPv4NetworkAddress-oid'
1053 desc: str =
'string representation of IPv4 network address/mask'
1054 addr_class = ipaddress.IPv4Network
1059 Plugin class for string representation of IPv6 network address
1061 oid: str = 'IPv6NetworkAddress-oid'
1062 desc: str =
'string representation of IPv6 network address/mask'
1063 addr_class = ipaddress.IPv6Network
1068 Plugin class for service port number (see /etc/services)
1070 oid: str = 'IPServicePortNumber-oid'
1071 desc: str =
'Port number for an UDP- or TCP-based service'
1078 Plugin class for IEEEE MAC addresses of network devices
1080 oid: str = 'MacAddress-oid'
1081 desc: str =
'MAC address in hex-colon notation'
1084 pattern = re.compile(
r'^([0-9a-f]{2}\:){5}[0-9a-f]{2}$')
1087 attr_value = attr_value.translate(
None, b
'.-: ').lower().strip()
1088 if len(attr_value) == 12:
1089 return b
':'.join([attr_value[i*2:i*2+2]
for i
in range(6)])
1095 Plugin class for Uniform Resource Identifiers (URIs, see RFC 2079)
1097 oid: str = 'Uri-OID'
1099 pattern = re.compile(
r'^(ftp|http|https|news|snews|ldap|ldaps|mailto):(|//)[^ ]*')
1105 attr_value = self.
av_u
1107 url, label = attr_value.split(
' ', 1)
1109 url, label = attr_value, attr_value
1112 display_url =
' (%s)' % (url)
1113 if ldap0.ldapurl.is_ldapurl(url):
1114 return '<a href="%s?%s">%s%s</a>' % (
1115 self.
_app.form.script_name,
1116 self.
_app.form.s2d(url),
1117 self.
_app.form.s2d(label),
1118 self.
_app.form.s2d(display_url),
1120 if url.lower().find(
'javascript:') >= 0:
1121 return '<code>%s</code>' % (
1122 DirectoryString.display(self, vidx=
False, links=
False)
1124 return '<a href="%s?%s">%s%s</a>' % (
1125 self.
_app.form.action_url(
'urlredirect', self.
_app.sid),
1126 self.
_app.form.s2d(url),
1127 self.
_app.form.s2d(label),
1128 self.
_app.form.s2d(display_url),
1134 Plugin base class for attributes containing image data.
1136 oid: str = 'Image-OID'
1137 desc: str =
'Image base class'
1138 mime_type: str =
'application/octet-stream'
1139 file_ext: str =
'bin'
1144 return imghdr.what(
None, attr_value) == self.
imageFormat.lower()
1149 with BytesIO(attr_value)
as imgfile:
1150 img = PILImage.open(imgfile)
1153 attr_value = imgfile.getvalue()
1154 except Exception
as err:
1156 'Error converting image data (%d bytes) to %s: %r',
1164 maxwidth, maxheight = 100, 150
1165 width, height =
None,
None
1169 with BytesIO(self.
_av)
as imgfile:
1170 img = PILImage.open(imgfile)
1174 width, height = img.size
1175 if width > maxwidth:
1176 size_attr_html =
'width="%d" height="%d"' % (
1178 int(float(maxwidth)/width*height),
1180 elif height > maxheight:
1181 size_attr_html =
'width="%d" height="%d"' % (
1182 int(float(maxheight)/height*width),
1186 size_attr_html =
'width="%d" height="%d"' % (width, height)
1187 attr_value_len = len(self.
_av)
1188 img_link =
'%s?dn=%s&read_attr=%s&read_attrindex=%d' % (
1189 self.
_app.form.action_url(
'read', self.
_app.sid),
1190 urllib.parse.quote(self.
_dn),
1191 urllib.parse.quote(self.
_at),
1197 '<img src="data:%s;base64,\n%s" alt="%d bytes of image data" %s>'
1202 self.
_av.encode(
'base64'),
1206 return '<a href="%s"><img src="%s" alt="%d bytes of image data" %s></a>' % (
1216 Plugin class for LDAP syntax
'JPEG'
1217 (see https://datatracker.ietf.org/doc/html/rfc4517
1219 oid: str = '1.3.6.1.4.1.1466.115.121.1.28'
1220 desc: str =
'JPEG image'
1221 mime_type: str =
'image/jpeg'
1222 file_ext: str =
'jpg'
1223 imageFormat =
'JPEG'
1228 Plugin class for LDAP syntax
'Fax'
1229 (see https://datatracker.ietf.org/doc/html/rfc4517
1231 oid: str = '1.3.6.1.4.1.1466.115.121.1.23'
1232 desc: str =
'Photo (G3 fax)'
1233 mime_type: str =
'image/g3fax'
1234 file_ext: str =
'tif'
1239 Plugin class for LDAP syntax
'OID'
1240 (see https://datatracker.ietf.org/doc/html/rfc4517
1242 oid: str = '1.3.6.1.4.1.1466.115.121.1.38'
1244 pattern = re.compile(
r'^([a-zA-Z]+[a-zA-Z0-9;-]*|[0-2]?\.([0-9]+\.)*[0-9]+)$')
1245 no_val_button_attrs = frozenset((
1247 'structuralobjectclass',
1255 return IA5String.value_button(self, command, row, mode, link_text=link_text)
1258 return attr_value.strip()
1262 name, description, reference = OID_REG[self.
av_u]
1263 except (KeyError, ValueError):
1265 se_obj = self.
_schema.get_obj(
1272 se_obj = self.
_schema.get_obj(
1278 return IA5String.display(self, vidx, links)
1283 name_template=
'{name}\n{anchor}',
1284 link_text=
'»',
1286 if self.
_at.lower() ==
'structuralobjectclass':
1287 name_template =
'{name}\n{anchor}'
1289 name_template =
'{name}\n (%s){anchor}' % (OBJECTCLASS_KIND_STR[se_obj.kind],)
1295 name_template=name_template,
1296 link_text=
'»',
1298 return '<strong>%s</strong> (%s):<br>%s (see %s)' % (
1299 self.
_app.form.s2d(name),
1300 IA5String.display(self, vidx, links),
1301 self.
_app.form.s2d(description),
1302 self.
_app.form.s2d(reference)
1308 Plugin class for attributes containing LDAP URLs
1310 oid: str = 'LDAPUrl-oid'
1311 desc: str =
'LDAP URL'
1319 linksstr = self.
_app.ldap_url_anchor(
1325 return '<strong>Not a valid LDAP URL:</strong> %s' % (
1326 self.
_app.form.s2d(repr(self.
_av))
1328 return '<table><tr><td>%s</td><td><a href="%s">%s</a></td></tr></table>' % (
1337 Plugin class for LDAP syntax
'Octet String'
1338 (see https://datatracker.ietf.org/doc/html/rfc4517
1340 oid: str = '1.3.6.1.4.1.1466.115.121.1.40'
1341 desc: str =
'Octet String'
1342 editable: bool =
True
1348 attr_value = attr_value.translate(
None, b
': ,\r\n')
1350 res = binascii.unhexlify(attr_value)
1351 except binascii.Error:
1359 '<td><code>%0.6X</code></td>'
1360 '<td><code>%s</code></td>'
1361 '<td><code>%s</code></td>'
1365 ':'.join(c[j:j+1].hex().upper()
for j
in range(len(c))),
1370 return '\n<table class="HexDump">\n%s\n</table>\n' % (
'\n'.join(lines))
1373 hex_av = (self.
_av or b
'').hex().upper()
1374 hex_range = range(0, len(hex_av), 2)
1375 return str(
'\r\n'.join(
1377 ':'.join([hex_av[i:i+2]
for i
in hex_range]),
1384 return web_forms.Textarea(
1386 ': '.join([self.
_at, self.desc]),
1397 Plugin base class for multi-line text.
1399 oid: str = 'MultilineText-oid'
1400 desc: str =
'Multiple lines of text'
1401 pattern = re.compile(
'^.*$', re.S+re.M)
1403 mime_type: str =
'text/plain'
1410 return value.split(self.
lineSep)
1414 return attr_value.replace(
1421 return '<br>'.join([
1422 self.
_app.form.s2d(self.
_app.ls.uc_decode(line_b)[0])
1428 self.
_app.ls.uc_decode(line_b)[0]
1431 return '\r\n'.join(splitted_lines)
1435 return web_forms.Textarea(
1437 ': '.join([self.
_at, self.desc]),
1438 self.max_len, self.max_values,
1448 Plugin base class for multi-line text displayed
with mono-spaced font,
1449 e.g. program code, XML, JSON etc.
1451 oid: str = 'PreformattedMultilineText-oid'
1453 tab_identiation =
' '
1458 self.
_app.ls.uc_decode(line_b)[0],
1463 return '<code>%s</code>' %
'<br>'.join(lines)
1468 Plugin class for LDAP syntax
'Postal Address'
1469 (see https://datatracker.ietf.org/doc/html/rfc4517
1471 oid: str = '1.3.6.1.4.1.1466.115.121.1.41'
1472 desc: str =
'Postal Address'
1483 return attr_value.replace(b
'\r', b
'').replace(b
'\n', self.
lineSeplineSep)
1488 Plugin class for LDAP syntax
'Printable String'
1489 (see https://datatracker.ietf.org/doc/html/rfc4517
1491 oid: str = '1.3.6.1.4.1.1466.115.121.1.44'
1492 desc: str =
'Printable String'
1493 pattern = re.compile(
"^[a-zA-Z0-9'()+,.=/:? -]*$")
1499 Plugin class for LDAP syntax
'Numeric String'
1500 (see https://datatracker.ietf.org/doc/html/rfc4517
1502 oid: str = '1.3.6.1.4.1.1466.115.121.1.36'
1503 desc: str =
'Numeric String'
1504 pattern = re.compile(
'^[ 0-9]+$')
1509 Plugin class for LDAP syntax
'Enhanced Guide'
1510 (see https://datatracker.ietf.org/doc/html/rfc4517
1512 oid: str = '1.3.6.1.4.1.1466.115.121.1.21'
1513 desc: str =
'Enhanced Search Guide'
1518 Plugin class for LDAP syntax
'Search Guide'
1519 (see https://datatracker.ietf.org/doc/html/rfc4517
1521 oid: str = '1.3.6.1.4.1.1466.115.121.1.25'
1522 desc: str =
'Search Guide'
1527 Plugin class for LDAP syntax
''
1528 (see https://datatracker.ietf.org/doc/html/rfc4517
1530 oid: str = '1.3.6.1.4.1.1466.115.121.1.50'
1531 desc: str =
'Telephone Number'
1532 pattern = re.compile(
'^[0-9+x(). /-]+$')
1535 if PHONENUMBERS_AVAIL:
1537 attr_value = phonenumbers.format_number(
1539 attr_value.decode(
'ascii'),
1541 self.
_entry[
'c'][0].decode(
'ascii')
1546 phonenumbers.PhoneNumberFormat.INTERNATIONAL,
1551 phonenumbers.phonenumberutil.NumberParseException,
1553 attr_value = PrintableString.sanitize(self, attr_value)
1555 attr_value = PrintableString.sanitize(self, attr_value)
1561 Plugin class for LDAP syntax
'Facsimile Telephone Number'
1562 (see https://datatracker.ietf.org/doc/html/rfc4517
1564 oid: str = '1.3.6.1.4.1.1466.115.121.1.22'
1565 desc: str =
'Facsimile Number'
1566 pattern = re.compile(
1569 r'(twoDimensional|fineResolution|unlimitedLength|b4Length|a3Width|b4Width|uncompressed)'
1576 Plugin class for LDAP syntax
'Telex Number'
1577 (see https://datatracker.ietf.org/doc/html/rfc4517
1579 oid: str = '1.3.6.1.4.1.1466.115.121.1.52'
1580 desc: str =
'Telex Number'
1581 pattern = re.compile(
"^[a-zA-Z0-9'()+,.=/:?$ -]*$")
1586 Plugin class for LDAP syntax
'Teletex Terminal Identifier'
1587 (see https://datatracker.ietf.org/doc/html/rfc4517
1589 oid: str = '1.3.6.1.4.1.1466.115.121.1.51'
1590 desc: str =
'Teletex Terminal Identifier'
1594 oid: str =
'ObjectGUID-oid'
1595 desc: str =
'Object GUID'
1599 objectguid_str =
''.join([
1603 return ldap0.ldapurl.LDAPUrl(
1604 ldapUrl=self.
_app.ls.uri,
1605 dn=
'GUID=%s' % (objectguid_str),
1608 hrefText=objectguid_str,
1615 Plugin base class for a date without(!) time component.
1617 oid: str = 'Date-oid'
1618 desc: str =
'Date in syntax specified by class attribute storage_format'
1620 storage_format =
'%Y-%m-%d'
1621 acceptable_formats = (
1629 datetime.datetime.strptime(
1630 self.
_app.ls.uc_decode(attr_value)[0],
1633 except (UnicodeDecodeError, ValueError):
1638 av_u = attr_value.strip().decode(self.
_app.ls.charset)
1642 time_tuple = datetime.datetime.strptime(av_u, time_format)
1646 result = datetime.datetime.strftime(time_tuple, self.
storage_format).encode(
'ascii')
1653 Plugin class for a date using syntax YYYYMMDD typically
1654 using LDAP syntax Numstring.
1656 oid: str = 'NumstringDate-oid'
1657 desc: str =
'Date in syntax YYYYMMDD'
1658 pattern = re.compile(
'^[0-9]{4}[0-1][0-9][0-3][0-9]$')
1659 storage_format =
'%Y%m%d'
1664 Plugin class for a date using syntax YYYY-MM-DD (see ISO 8601).
1666 oid: str = 'ISO8601Date-oid'
1667 desc: str =
'Date in syntax YYYY-MM-DD (see ISO 8601)'
1668 pattern = re.compile(
'^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$')
1669 storage_format =
'%Y-%m-%d'
1674 Plugin class for date of birth syntax YYYY-MM-DD (see ISO 8601).
1676 Displays the age based at current time.
1678 oid: str = 'DateOfBirth-oid'
1679 desc: str =
'Date of birth: syntax YYYY-MM-DD (see ISO 8601)'
1683 birth_date = datetime.date(
1685 month=birth_dt.month,
1688 current_date = datetime.date.today()
1689 age = current_date.year - birth_date.year
1690 if birth_date.month > current_date.month
or \
1691 (birth_date.month == current_date.month
and birth_date.day > current_date.day):
1697 birth_dt = datetime.datetime.strptime(
1698 self.
_app.ls.uc_decode(attr_value)[0],
1703 return self.
_age(birth_dt) >= 0
1706 raw_date = ISO8601Date.display(self, vidx, links)
1711 return '%s (%s years old)' % (raw_date, self.
_age(birth_dt))
1716 Plugin class for seconds since epoch (1970-01-01 00:00:00).
1718 oid: str = 'SecondsSinceEpoch-oid'
1719 desc: str =
'Seconds since epoch (1970-01-01 00:00:00)'
1723 int_str = Integer.display(self, vidx, links)
1725 return '%s (%s)' % (
1735 Plugin class for days since epoch (1970-01-01).
1737 oid: str = 'DaysSinceEpoch-oid'
1738 desc: str =
'Days since epoch (1970-01-01)'
1742 int_str = Integer.display(self, vidx, links)
1744 return '%s (%s)' % (
1753 oid: str =
'Timespan-oid'
1754 desc: str =
'Time span in seconds'
1755 input_size: int = LDAPSyntax.input_size
1773 attr_value.decode(
'ascii')
1776 result = Integer.sanitize(self, attr_value)
1785 result = Integer.form_value(self)
1789 return IA5String.input_field(self)
1793 result = self.
_app.form.s2d(
'%s (%s)' % (
1795 Integer.display(self, vidx, links)
1798 result = Integer.display(self, vidx, links)
1804 Base class for dictionary based select lists which
1805 should
not be used directly
1807 oid: str = 'SelectList-oid'
1808 attr_value_dict: Dict[str, str] = {}
1809 input_fallback: bool =
True
1811 tag_tmpl: Dict[bool, str] = {
1812 False:
'{attr_text}: {attr_value}',
1813 True:
'<span title="{attr_title}">{attr_text}:{sep}{attr_value}</span>',
1818 attr_value_dict: Dict[str, str] = {
'':
'-/-'}
1819 attr_value_dict.update(self.attr_value_dict)
1820 return attr_value_dict
1824 fval = DirectoryString.form_value(self)
1828 for val
in self.
_entry.get(self.
_at, []):
1829 val = self.
_app.ls.uc_decode(val)[0]
1836 if fval
not in vdict:
1840 for key, val
in vdict.items():
1841 if isinstance(val, str):
1842 result.append((key, val,
None))
1843 elif isinstance(val, tuple):
1844 result.append((key, val[0], val[1]))
1847 key=
lambda x: x[1].lower(),
1852 return self.
_app.ls.uc_decode(attr_value)[0]
in attr_value_dict
1855 attr_value_str = DirectoryString.display(self, vidx, links)
1858 attr_value_desc = attr_value_dict[self.
av_u]
1860 return attr_value_str
1862 attr_text, attr_title = attr_value_desc
1864 attr_text, attr_title = attr_value_desc,
None
1865 if attr_text == attr_value_str:
1866 return attr_value_str
1867 return self.tag_tmpl[
bool(attr_title)].format(
1868 attr_value=attr_value_str,
1870 attr_text=self.
_app.form.s2d(attr_text),
1871 attr_title=self.
_app.form.s2d(attr_title
or '')
1876 if self.input_fallback
and \
1877 (
not attr_value_dict
or not list(filter(
None, attr_value_dict.keys()))):
1878 return DirectoryString.input_field(self)
1879 field = web_forms.Select(
1881 ': '.join([self.
_at, self.desc]),
1887 field.charset = self.
_app.form.accept_charset
1893 Plugin base class for attribute value select lists of LDAP syntax
DirectoryString
1894 constructed
and validated by reading a properties file.
1896 oid: str = 'PropertiesSelectList-oid'
1897 properties_pathname: Optional[str] =
None
1898 properties_charset: str =
'utf-8'
1899 properties_delimiter: str =
'='
1902 attr_value_dict: Dict[str, str] = SelectList.get_attr_value_dict(self)
1904 self.properties_pathname,
1905 self.
_app.form.accept_language
1907 with open(real_path_name,
'rb')
as prop_file:
1908 for line
in prop_file.readlines():
1909 line = line.decode(self.properties_charset).strip()
1910 if line
and not line.startswith(
'#'):
1911 key, value = line.split(self.properties_delimiter, 1)
1912 attr_value_dict[key.strip()] = value.strip()
1913 return attr_value_dict
1919 Plugin base class for attribute value select lists of LDAP syntax
DirectoryString
1920 constructed
and validated by internal LDAP search.
1922 oid: str = 'DynamicValueSelectList-oid'
1923 ldap_url: Optional[str] =
None
1924 value_prefix: str =
''
1925 value_suffix: str =
''
1927 ldap0.NO_SUCH_OBJECT,
1928 ldap0.SIZELIMIT_EXCEEDED,
1929 ldap0.TIMELIMIT_EXCEEDED,
1930 ldap0.PARTIAL_RESULTS,
1931 ldap0.INSUFFICIENT_ACCESS,
1932 ldap0.CONSTRAINT_VIOLATION,
1936 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=
None):
1937 self.
lu_obj = ldap0.ldapurl.LDAPUrl(self.ldap_url)
1938 self.
min_len = len(self.value_prefix)+len(self.value_suffix)
1939 SelectList.__init__(self, app, dn, schema, attrType, attr_value, entry)
1942 return self.
lu_obj.filterstr
or '(objectClass=*)'
1945 attr_value = attr_value[len(self.value_prefix):-len(self.value_suffix)
or None]
1946 search_filter =
'(&%s(%s=%s))' % (
1952 ldap_result = self.
_app.ls.l.search_s(
1956 attrlist=self.
lu_obj.attrs,
1960 ldap0.NO_SUCH_OBJECT,
1961 ldap0.CONSTRAINT_VIOLATION,
1962 ldap0.INSUFFICIENT_ACCESS,
1964 ldap0.SIZELIMIT_EXCEEDED,
1965 ldap0.TIMELIMIT_EXCEEDED,
1970 (sre.dn_s, sre.entry_s)
1971 for sre
in ldap_result
1972 if isinstance(sre, SearchResultEntry)
1974 if ldap_result
and len(ldap_result) == 1:
1975 return ldap_result[0]
1979 av_u = self.
_app.ls.uc_decode(attr_value)[0]
1981 not av_u.startswith(self.value_prefix)
or
1982 not av_u.endswith(self.value_suffix)
or
1984 (self.max_len
is not None and len(av_u) > self.max_len)
1990 if links
and self.
lu_obj.attrs:
1993 ref_dn, ref_entry = ref_result
1995 attr_value_desc = ref_entry[self.
lu_obj.attrs[1]][0]
1996 except (KeyError, IndexError):
1997 display_text, link_html =
'',
''
1999 if self.
lu_obj.attrs[0].lower() == self.
lu_obj.attrs[1].lower():
2002 display_text = self.
_app.form.s2d(attr_value_desc+
':')
2004 link_html = self.
_app.anchor(
2011 display_text, link_html =
'',
''
2013 display_text, link_html =
'',
''
2016 DirectoryString.display(self, vidx, links),
2021 ldap_url_dn = self.
lu_obj.dn
2022 if ldap_url_dn ==
'_':
2023 result_dn =
str(self.
_app.naming_context)
2024 elif ldap_url_dn ==
'.':
2025 result_dn = self.
_dn
2026 elif ldap_url_dn ==
'..':
2027 result_dn =
str(self.
dn.parent())
2028 elif ldap_url_dn.endswith(
',_'):
2029 result_dn =
','.join((ldap_url_dn[:-2],
str(self.
_app.naming_context)))
2030 elif ldap_url_dn.endswith(
',.'):
2031 result_dn =
','.join((ldap_url_dn[:-2], self.
_dn))
2032 elif ldap_url_dn.endswith(
',..'):
2033 result_dn =
','.join((ldap_url_dn[:-3],
str(self.
dn.parent())))
2035 result_dn = ldap_url_dn
2036 if result_dn.endswith(
','):
2037 result_dn = result_dn[:-1]
2042 attr_value_dict: Dict[str, str] = SelectList.get_attr_value_dict(self)
2045 'Connecting to other server not supported! hostport attribute was %r' % (
2049 search_scope = self.
lu_obj.scope
or ldap0.SCOPE_BASE
2050 search_attrs = (self.
lu_obj.attrs
or []) + [
'description',
'info']
2053 ldap_result = self.
_app.ls.l.search_s(
2057 attrlist=search_attrs,
2061 if search_scope == ldap0.SCOPE_BASE:
2063 assert len(self.
lu_obj.attrs
or []) == 1, ValueError(
2064 'attrlist in ldap_url must be of length 1 if scope is base, got %r' % (
2068 list_attr = self.
lu_obj.attrs[0]
2075 for attr_value
in ldap_result[0].entry_s[list_attr]
2077 attr_value_dict: Dict[str, str] = {
2079 for u
in attr_values_u
2082 if not self.
lu_obj.attrs:
2083 option_value_map, option_text_map = (
None,
None)
2084 elif len(self.
lu_obj.attrs) == 1:
2085 option_value_map, option_text_map = (
None, self.
lu_obj.attrs[0])
2086 elif len(self.
lu_obj.attrs) >= 2:
2087 option_value_map, option_text_map = self.
lu_obj.attrs[:2]
2088 for sre
in ldap_result:
2090 if not isinstance(sre, SearchResultEntry):
2092 sre.entry_s[
None] = [sre.dn_s]
2094 option_value =
''.join((
2096 sre.entry_s[option_value_map][0],
2103 option_text = sre.entry_s[option_text_map][0]
2105 option_text = option_value
2106 option_title = sre.entry_s.get(
'description', sre.entry_s.get(
'info', [
'']))[0]
2108 attr_value_dict[option_value] = (option_text, option_title)
2110 attr_value_dict[option_value] = option_text
2111 return attr_value_dict
2117 Plugin base class for attribute value select lists of LDAP syntax DN
2118 constructed
and validated by internal LDAP search.
2120 oid: str = 'DynamicDNSelectList-oid'
2124 sre = self.
_app.ls.l.read_s(
2126 attrlist=attrlist
or self.
lu_obj.attrs,
2130 ldap0.NO_SUCH_OBJECT,
2131 ldap0.CONSTRAINT_VIOLATION,
2132 ldap0.INSUFFICIENT_ACCESS,
2133 ldap0.INVALID_DN_SYNTAX,
2142 return SelectList._validate(self, attr_value)
2145 if links
and self.
lu_obj.attrs:
2148 attr_value_desc = ref_entry[self.
lu_obj.attrs[0]][0]
2149 except (KeyError, IndexError):
2152 display_text = self.
_app.form.s2d(attr_value_desc+
': ')
2155 return self.desc_sep.join((
2157 DistinguishedName.display(self, vidx, links)
2163 Plugin base class for attribute value select lists of LDAP syntax DN
2164 constructed
and validated by internal LDAP search.
2166 Same
as DynamicDNSelectList
except that Dereference extended control
is used.
2168 oid: str = 'DerefDynamicDNSelectList-oid'
2171 deref_crtl = DereferenceControl(
2173 {self.
_at: self.
lu_obj.attrs
or [
'entryDN']}
2176 ldap_result = self.
_app.ls.l.search_s(
2179 filterstr=
'(objectClass=*)',
2181 req_ctrls=[deref_crtl],
2184 ldap0.NO_SUCH_OBJECT,
2185 ldap0.CONSTRAINT_VIOLATION,
2186 ldap0.INSUFFICIENT_ACCESS,
2187 ldap0.INVALID_DN_SYNTAX,
2191 if ldap_result
is None or not ldap_result.ctrls:
2193 for ref
in ldap_result.ctrls[0].derefRes[self.
_at]:
2201 Plugin class for LDAP syntax
'Boolean'
2202 (see https://datatracker.ietf.org/doc/html/rfc4517
2204 oid: str = '1.3.6.1.4.1.1466.115.121.1.7'
2205 desc: str =
'Boolean'
2206 attr_value_dict: Dict[str, str] = {
2212 attr_value_dict: Dict[str, str] = SelectList.get_attr_value_dict(self)
2213 if self.
_av and self.
_av.lower() == self.
_av:
2214 for key, val
in attr_value_dict.items():
2215 del attr_value_dict[key]
2216 attr_value_dict[key.lower()] = val.lower()
2217 return attr_value_dict
2220 if not self.
_av and attr_value.lower() == attr_value:
2221 return SelectList._validate(self, attr_value.upper())
2222 return SelectList._validate(self, attr_value)
2225 return IA5String.display(self, vidx, links)
2230 Plugin class for LDAP syntax
'Country String'
2231 (see https://datatracker.ietf.org/doc/html/rfc4517
2233 oid: str = '1.3.6.1.4.1.1466.115.121.1.11'
2234 desc: str =
'Two letter country string as listed in ISO 3166-2'
2241 attr_value_dict: Dict[str, str] = {
'':
'-/-'}
2242 attr_value_dict.update({
2243 alpha2: cty.name
for alpha2, cty
in iso3166.countries_by_alpha2.items()
2245 return attr_value_dict
2250 Plugin class for LDAP syntax
'Delivery Method'
2251 (see https://datatracker.ietf.org/doc/html/rfc4517
2253 oid: str = '1.3.6.1.4.1.1466.115.121.1.14'
2254 desc: str =
'Delivery Method'
2255 pdm =
'(any|mhs|physical|telex|teletex|g3fax|g4fax|ia5|videotex|telephone)'
2256 pattern = re.compile(
'^%s[ $]*%s$' % (pdm, pdm))
2261 Plugin class for attributes with
Integer syntax where the integer
2262 value
is interpreted
as binary flags
2264 oid: str = 'BitArrayInteger-oid'
2265 flag_desc_table: Sequence[Tuple[str, int]] = tuple()
2266 true_false_desc: Dict[bool, str] = {
2271 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=
None):
2272 Integer.__init__(self, app, dn, schema, attrType, attr_value, entry)
2276 for i, j
in self.flag_desc_table
2283 av_u = attr_value.decode(
'ascii')
2284 except UnicodeDecodeError:
2290 for row
in av_u.split(
'\n'):
2293 flag_set, flag_desc = row[0:1], row[1:]
2302 return str(result).encode(
'ascii')
2305 attr_value_int =
int(self.
av_u or 0)
2308 self.true_false_desc[
int((attr_value_int & flag_int) > 0)],
2311 for flag_desc, flag_int
in self.flag_desc_table
2313 return '\r\n'.join(flag_lines)
2317 return web_forms.Textarea(
2319 ': '.join([self.
_at, self.desc]),
2320 self.
max_len, self.max_values,
2324 cols=max([len(desc)
for desc, _
in self.flag_desc_table])+1
2331 '<table summary="Flags">'
2332 '<tr><th>Property flag</th><th>Value</th><th>Status</th></tr>'
2336 Integer.display(self, vidx, links),
2338 '<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % (
2339 self.
_app.form.s2d(desc),
2341 {
False:
'-',
True:
'on'}[(av_i & flag_value) > 0]
2343 for desc, flag_value
in self.flag_desc_table
2350 Generic String Encoding Rules (GSER) for ASN.1 Types (see RFC 3641)
2352 oid: str = 'GSER-oid'
2353 desc: str =
'GSER syntax (see RFC 3641)'
2358 Plugin class for Universally Unique IDentifier (
UUID), see RFC 4122
2360 oid: str = '1.3.6.1.1.16.1'
2362 pattern = re.compile(
2363 '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$'
2368 return str(uuid.UUID(attr_value.decode(
'ascii').replace(
':',
''))).encode(
'ascii')
2375 Plugin class for fully-qualified DNS domain names
2377 oid: str = 'DNSDomain-oid'
2378 desc: str =
'DNS domain name (see RFC 1035)'
2379 pattern = re.compile(
r'^(\*|[a-zA-Z0-9_-]+)(\.[a-zA-Z0-9_-]+)*$')
2381 max_len: int = min(255, IA5String.max_len)
2388 attr_value = IA5String.sanitize(self, attr_value)
2391 for dc
in attr_value.decode(self.
_app.form.accept_charset).split(
'.')
2398 for dc
in (self.
_av or b
'').split(b
'.')
2400 except UnicodeDecodeError:
2401 result =
'!!!snipped because of UnicodeDecodeError!!!'
2405 if self.
av_u != self.
_av.decode(
'idna'):
2406 return '%s (%s)' % (
2407 IA5String.display(self, vidx, links),
2410 return IA5String.display(self, vidx, links)
2415 Plugin class for RFC 822 addresses
2417 oid: str = 'RFC822Address-oid'
2418 desc: str =
'RFC 822 mail address'
2419 pattern = re.compile(
r'^[\w@.+=/_ ()-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$')
2420 html_tmpl =
'<a href="mailto:{av}">{av}</a>'
2422 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=
None):
2423 IA5String.__init__(self, app, dn, schema, attrType, attr_value, entry)
2427 return IA5String.form_value(self)
2429 localpart, domainpart = self.
_av.rsplit(b
'@')
2431 return IA5String.form_value(self)
2434 localpart.decode(self.
_app.ls.charset),
2435 dns_domain.form_value()
2440 localpart, domainpart = attr_value.rsplit(b
'@')
2446 DNSDomain.sanitize(self, domainpart)
2452 Plugin class for a single DNS label
2453 (see https://datatracker.ietf.org/doc/html/rfc2181
2455 oid: str = 'DomainComponent-oid'
2456 desc: str =
'DNS domain name component'
2457 pattern = re.compile(
r'^(\*|[a-zA-Z0-9_-]+)$')
2458 max_len: int = min(63, DNSDomain.max_len)
2463 Plugin class used for JSON data (see RFC 8259)
2465 oid: str = 'JSONValue-oid'
2466 desc: str =
'JSON data'
2468 mime_type: str =
'application/json'
2472 json.loads(attr_value)
2479 obj = json.loads(value)
2481 return PreformattedMultilineText._split_lines(self, value)
2482 return PreformattedMultilineText._split_lines(
2487 separators=(
',',
': ')
2493 obj = json.loads(attr_value)
2495 return PreformattedMultilineText.sanitize(self, attr_value)
2498 separators=(
',',
':')
2504 Plugin class used for XML data
2506 oid: str = 'XmlValue-oid'
2507 desc: str =
'XML data'
2509 mime_type: str =
'text/xml'
2512 if not DEFUSEDXML_AVAIL:
2513 return PreformattedMultilineText._validate(self, attr_value)
2515 defusedxml.ElementTree.XML(attr_value)
2516 except defusedxml.ElementTree.ParseError:
2523 Plugin class used for BER-encoded ASN.1 data
2525 oid: str = 'ASN1Object-oid'
2526 desc: str =
'BER-encoded ASN.1 data'
2531 This base-class class is used for OIDs of cryptographic algorithms
2533 oid: str = 'AlgorithmOID-oid'
2538 Plugin class for selection of OIDs for hash algorithms
2539 (see https://www.iana.org/assignments/hash-function-text-names/).
2541 oid: str = 'HashAlgorithmOID-oid'
2542 desc: str =
'values from https://www.iana.org/assignments/hash-function-text-names/'
2543 attr_value_dict: Dict[str, str] = {
2544 '1.2.840.113549.2.2':
'md2',
2545 '1.2.840.113549.2.5':
'md5',
2546 '1.3.14.3.2.26':
'sha-1',
2547 '2.16.840.1.101.3.4.2.4':
'sha-224',
2548 '2.16.840.1.101.3.4.2.1':
'sha-256',
2549 '2.16.840.1.101.3.4.2.2':
'sha-384',
2550 '2.16.840.1.101.3.4.2.3':
'sha-512',
2556 Plugin class for selection of OIDs for HMAC algorithms (see RFC 8018).
2558 oid: str = 'HMACAlgorithmOID-oid'
2559 desc: str =
'values from RFC 8018'
2560 attr_value_dict: Dict[str, str] = {
2562 '1.2.840.113549.2.7':
'hmacWithSHA1',
2563 '1.2.840.113549.2.8':
'hmacWithSHA224',
2564 '1.2.840.113549.2.9':
'hmacWithSHA256',
2565 '1.2.840.113549.2.10':
'hmacWithSHA384',
2566 '1.2.840.113549.2.11':
'hmacWithSHA512',
2572 This mix-in plugin
class composes attribute values from other attribute values.
2574 One can define an ordered sequence of string templates
in class
2575 attribute ComposedDirectoryString.compose_templates.
2578 Obviously this only works
for single-valued attributes,
2579 more precisely only the
"first" attribute value
is used.
2581 oid: str = 'ComposedDirectoryString-oid'
2582 compose_templates: Sequence[str] = ()
2586 dictionary-like class which only stores and returns the
2587 first value of an attribute value list
2594 for key, val
in entry.items():
2599 dict.__setitem__(self, key, val[0].decode(self.
_encoding))
2603 Return a dummy value that attribute is returned
from input form
and
2608 def transmute(self, attr_values: List[bytes]) -> List[bytes]:
2610 always returns a list with a single value based on the first
2611 successfully applied compose template
2614 for template
in self.compose_templates:
2616 attr_values = [template.format(**entry).encode(self.
_app.ls.charset)]
2627 composed attributes must only have hidden input field
2629 input_field = web_forms.HiddenInput(
2631 ': '.join([self.
_at, self.desc]),
2637 input_field.charset = self.
_app.form.accept_charset
2643 Plugin base class for attributes with
Integer syntax
2644 constrained to valid LDAP result code.
2646 oid: str = 'LDAPResultCode-oid'
2647 desc: str =
'LDAPv3 declaration of resultCode in (see RFC 4511)'
2648 attr_value_dict: Dict[str, str] = {
2650 '1':
'operationsError',
2651 '2':
'protocolError',
2652 '3':
'timeLimitExceeded',
2653 '4':
'sizeLimitExceeded',
2654 '5':
'compareFalse',
2656 '7':
'authMethodNotSupported',
2657 '8':
'strongerAuthRequired',
2660 '11':
'adminLimitExceeded',
2661 '12':
'unavailableCriticalExtension',
2662 '13':
'confidentialityRequired',
2663 '14':
'saslBindInProgress',
2664 '16':
'noSuchAttribute',
2665 '17':
'undefinedAttributeType',
2666 '18':
'inappropriateMatching',
2667 '19':
'constraintViolation',
2668 '20':
'attributeOrValueExists',
2669 '21':
'invalidAttributeSyntax',
2670 '32':
'noSuchObject',
2671 '33':
'aliasProblem',
2672 '34':
'invalidDNSyntax',
2673 '35':
'reserved for undefined isLeaf',
2674 '36':
'aliasDereferencingProblem',
2675 '48':
'inappropriateAuthentication',
2676 '49':
'invalidCredentials',
2677 '50':
'insufficientAccessRights',
2679 '52':
'unavailable',
2680 '53':
'unwillingToPerform',
2682 '64':
'namingViolation',
2683 '65':
'objectClassViolation',
2684 '66':
'notAllowedOnNonLeaf',
2685 '67':
'notAllowedOnRDN',
2686 '68':
'entryAlreadyExists',
2687 '69':
'objectClassModsProhibited',
2688 '70':
'reserved for CLDAP',
2689 '71':
'affectsMultipleDSAs',
2695 oid: str =
'SchemaDescription-oid'
2703 return DirectoryString._validate(self, attr_value)
2706 except (IndexError, ValueError):
2712 oid: str =
'1.3.6.1.4.1.1466.115.121.1.37'
2713 schema_cls = ldap0.schema.models.ObjectClass
2717 oid: str =
'1.3.6.1.4.1.1466.115.121.1.3'
2718 schema_cls = ldap0.schema.models.AttributeType
2722 oid: str =
'1.3.6.1.4.1.1466.115.121.1.30'
2723 schema_cls = ldap0.schema.models.MatchingRule
2727 oid: str =
'1.3.6.1.4.1.1466.115.121.1.31'
2728 schema_cls = ldap0.schema.models.MatchingRuleUse
2732 oid: str =
'1.3.6.1.4.1.1466.115.121.1.54'
2733 schema_cls = ldap0.schema.models.LDAPSyntax
2737 oid: str =
'1.3.6.1.4.1.1466.115.121.1.16'
2738 schema_cls = ldap0.schema.models.DITContentRule
2742 oid: str =
'1.3.6.1.4.1.1466.115.121.1.17'
2743 schema_cls = ldap0.schema.models.DITStructureRule
2747 oid: str =
'1.3.6.1.4.1.1466.115.121.1.35'
2748 schema_cls = ldap0.schema.models.NameForm
2755syntax_registry.reg_syntaxes(__name__)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
str display(self, vidx, links)
web_forms.Field input_field(self)
str display(self, vidx, links)
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
bytes sanitize(self, bytes attr_value)
str display(self, vidx, links)
web_forms.Field input_field(self)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
Dict[str, str] get_attr_value_dict(self)
def __setitem__(self, key, val)
def __init__(self, entry, encoding)
List[bytes] transmute(self, List[bytes] attr_values)
web_forms.Field input_field(self)
Dict[str, str] get_attr_value_dict(self)
str display(self, vidx, links)
bytes sanitize(self, bytes attr_value)
str display(self, vidx, links)
bool _validate(self, bytes attr_value)
bytes sanitize(self, bytes attr_value)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
dict _get_ref_entry(self, str dn, attrlist=None)
str display(self, vidx, links)
bool _validate(self, bytes attr_value)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
def _additional_links(self)
dict _get_ref_entry(self, str dn, attrlist=None)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
bool _validate(self, bytes attr_value)
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
str display(self, vidx, links)
def _search_ref(self, str attr_value)
Dict[str, str] get_attr_value_dict(self)
bytes sanitize(self, bytes attr_value)
str display(self, vidx, links)
bool _validate(self, bytes attr_value)
bool _validate(self, bytes attr_value)
bool _validate(self, bytes attr_value)
bool _validate(self, bytes attr_value)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
bytes sanitize(self, bytes attr_value)
bytes sanitize(self, bytes attr_value)
web_forms.Field input_field(self)
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
bool _validate(self, bytes attr_value)
bool _validate(self, bytes attr_value)
def _split_lines(self, value)
bytes sanitize(self, bytes attr_value)
bytes sanitize(self, bytes attr_value)
web_forms.Field input_field(self)
def __init__(self, app, Optional[str] dn, SubSchema schema, Optional[str] attrType, Optional[bytes] attr_value, entry=None)
def validate(self, bytes attr_value)
List[bytes] transmute(self, List[bytes] attr_values)
str display(self, vidx, links)
str value_button(self, command, row, mode, link_text=None)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
def _command_ldap_url(self, ldap_url)
bytes sanitize(self, bytes attr_value)
web_forms.Field input_field(self)
def _split_lines(self, value)
str display(self, vidx, links)
bytes sanitize(self, bytes attr_value)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
Tuple[str, Optional[str]] _split_dn_and_uid(str val)
str display(self, vidx, links)
bytes sanitize(self, bytes attr_value)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
str value_button(self, command, row, mode, link_text=None)
bytes sanitize(self, bytes attr_value)
str display(self, vidx, links)
str display(self, vidx, links)
bytes sanitize(self, bytes attr_value)
web_forms.Field input_field(self)
bytes sanitize(self, bytes attr_value)
def _split_lines(self, value)
str display(self, vidx, links)
Dict[str, str] get_attr_value_dict(self)
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
bytes sanitize(self, bytes attr_value)
bool _validate(self, bytes attr_value)
str display(self, vidx, links)
bool _validate(self, bytes attr_value)
def _sorted_select_options(self)
str display(self, vidx, links)
web_forms.Field input_field(self)
Dict[str, str] get_attr_value_dict(self)
def reg_syntax(self, cls)
def reg_at(self, str syntax_oid, attr_types, structural_oc_oids=None)
def reg_syntaxes(self, modulename)
def get_at(self, app, dn, schema, attr_type, attr_value, entry=None)
def get_syntax(self, schema, attrtype_nameoroid, structural_oc)
bytes sanitize(self, bytes attr_value)
web_forms.Field input_field(self)
bytes sanitize(self, bytes attr_value)
str display(self, vidx, links)
bytes sanitize(self, bytes attr_value)
str display(self, vidx, links)
bool _validate(self, bytes attr_value)
def schema_anchor(app, se_nameoroid, se_class, name_template='{name}\n{anchor}', link_text=None)
def get_variant_filename(pathname, variantlist)
str ascii_dump(bytes buf, str repl='.')
str ts2repr(Sequence[Tuple[str, int]] time_divisors, str ts_sep, Union[str, bytes] ts_value)
def repr2ts(time_divisors, ts_sep, value)