web2ldap  1.7.7
About: web2ldap is a full-featured web-based LDAPv3 client.
  Fossies Dox: web2ldap-1.7.7.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

entry.py
Go to the documentation of this file.
1# -*- coding: ascii -*-
2"""
3web2ldap.app.entry - schema-aware Entry classes
4
5web2ldap - a web-based LDAP Client,
6see https://www.web2ldap.de for details
7
8(C) 1998-2022 by Michael Stroeder <michael@stroeder.com>
9
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
13"""
14
15from collections import UserDict
16from io import BytesIO
17
18import ldap0.schema.models
19from ldap0.cidict import CIDict
20from ldap0.schema.models import (
21 AttributeType,
22 ObjectClass,
23 SchemaElementOIDSet,
24)
25from ldap0.schema.subentry import SubSchema
26from ldap0.dn import DNObj
27from ldap0.ldif import LDIFWriter
28
29import web2ldapcnf
30
31from ..log import logger
32from ..msbase import GrabKeys
33
34from .tmpl import get_variant_filename
35from .gui import HIDDEN_FIELD
36from .schema import (
37 NEEDS_BINARY_TAG,
38 no_userapp_attr,
39 object_class_categories,
40)
41from .schema.viewer import schema_anchor
42from .schema.syntaxes import (
43 LDAPSyntaxValueError,
44 OctetString,
45 syntax_registry,
46)
47
48
49INPUT_FORM_LDIF_TMPL = """
50<fieldset>
51 <legend>Raw LDIF data</legend>
52 <textarea name="in_ldif" rows="50" cols="80" wrap="off">{value_ldif}</textarea>
53 <p>
54 Notes:
55 </p>
56 <ul>
57 <li>Lines containing "dn:" will be ignored</li>
58 <li>Only the first entry (until first empty line) will be accepted</li>
59 <li>Maximum length is set to {value_ldifmaxbytes} bytes</li>
60 <li>Allowed URL schemes: {text_ldifurlschemes}</li>
61 </ul>
62</fieldset>
63"""
64
65
66class DisplayEntry(UserDict):
67
68 def __init__(self, app, dn, schema, entry, sep_attr, links):
69 assert isinstance(dn, str), TypeError("Argument 'dn' must be str, was %r" % (dn))
70 assert isinstance(schema, SubSchema), \
71 TypeError('Expected schema to be instance of SubSchema, was %r' % (schema))
72 self._app = app
73 self.schema = schema
74 self.dn = dn
75 if isinstance(entry, dict):
76 self.entry = ldap0.schema.models.Entry(schema, dn, entry)
77 elif isinstance(entry, ldap0.schema.models.Entry):
78 self.entry = entry
79 else:
80 raise TypeError(
81 'Invalid type of argument entry, was %s.%s %r' % (
82 entry.__class__.__module__,
83 entry.__class__.__name__,
84 entry,
85 )
86 )
87 self.soc = self.entry.get_structural_oc()
88 self.invalid_attrs = set()
89 self.sep_attr = sep_attr
90 self.links = links
91
92 def __getitem__(self, nameoroid):
93 try:
94 values = self.entry.__getitem__(nameoroid)
95 except KeyError:
96 return ''
97 result = []
98 syntax_se = syntax_registry.get_syntax(self.entry._s, nameoroid, self.soc)
99 for i, value in enumerate(values):
100 attr_instance = syntax_se(
101 self._app,
102 self.dn,
103 self.entry._s,
104 nameoroid,
105 value,
106 self.entry,
107 )
108 try:
109 attr_value_html = attr_instance.display(i, self.links)
110 except UnicodeError:
111 # Fall back to hex-dump output
112 attr_instance = OctetString(
113 self._app,
114 self.dn,
115 self.schema,
116 nameoroid,
117 value,
118 self.entry,
119 )
120 attr_value_html = attr_instance.display(i, self.links)
121 try:
122 attr_instance.validate(value)
123 except LDAPSyntaxValueError:
124 attr_value_html = '<s>%s</s>' % (attr_value_html)
125 self.invalid_attrs.add(nameoroid)
126 result.append(attr_value_html)
127 if self.sep_attr is not None:
128 value_sep = getattr(attr_instance, self.sep_attr)
129 return value_sep.join(result)
130 return result
131
132 @property
133 def rdn_dict(self):
134 return DNObj.from_str(self.dn).rdn_attrs()
135
136 def get_html_templates(self, cnf_key):
137 read_template_dict = CIDict(self._app.cfg_param(cnf_key, {}))
138 # This gets all object classes no matter what
139 all_object_class_oid_set = self.entry.object_class_oid_set()
140 # Initialize the set with only the STRUCTURAL object class of the entry
141 object_class_oid_set = SchemaElementOIDSet(
142 self.entry._s, ldap0.schema.models.ObjectClass, []
143 )
144 structural_oc = self.entry.get_structural_oc()
145 if structural_oc:
146 object_class_oid_set.add(structural_oc)
147 # Now add the other AUXILIARY and ABSTRACT object classes
148 for ocl in all_object_class_oid_set:
149 ocl_obj = self.entry._s.get_obj(ldap0.schema.models.ObjectClass, ocl)
150 if ocl_obj is None or ocl_obj.kind != 0:
151 object_class_oid_set.add(ocl)
152 template_oc = object_class_oid_set.intersection(read_template_dict.data.keys())
153 return template_oc.names, read_template_dict
154 # end of get_html_templates()
155
156 def template_output(self, cnf_key, display_duplicate_attrs=True):
157 # Determine relevant HTML templates
158 template_oc, read_template_dict = self.get_html_templates(cnf_key)
159 # Sort the object classes by object class category
160 structural_oc, abstract_oc, auxiliary_oc = object_class_categories(
161 self.entry._s,
162 template_oc,
163 )
164 # Templates defined => display the entry with the help of the template
165 used_templates = set()
166 displayed_attrs = set()
167 for oc_set in (structural_oc, abstract_oc, auxiliary_oc):
168 for ocl in oc_set:
169 read_template_filename = read_template_dict[ocl]
170 logger.debug('Template file name %r defined for %r', read_template_dict[ocl], ocl)
171 if not read_template_filename:
172 logger.warning('Ignoring empty template file name for %r', ocl)
173 continue
174 read_template_filename = get_variant_filename(
175 read_template_filename,
176 self._app.form.accept_language,
177 )
178 if read_template_filename in used_templates:
179 # template already processed
180 logger.debug(
181 'Skipping already processed template file name %r for %r',
182 read_template_dict[ocl],
183 ocl,
184 )
185 continue
186 used_templates.add(read_template_filename)
187 try:
188 with open(read_template_filename, 'rb') as template_file:
189 template_str = template_file.read().decode('utf-8')
190 except IOError as err:
191 logger.error(
192 'Error reading template file %r for %r: %s',
193 read_template_dict[ocl],
194 ocl,
195 err,
196 )
197 continue
198 template_attr_oid_set = {
199 self.entry._s.get_oid(ldap0.schema.models.AttributeType, attr_type_name)
200 for attr_type_name in GrabKeys(template_str)()
201 }
202 if (
203 display_duplicate_attrs
204 or not displayed_attrs.intersection(template_attr_oid_set)
205 ):
206 self._app.outf.write(template_str % self)
207 displayed_attrs.update(template_attr_oid_set)
208 return displayed_attrs
209
210
212
214 self, app, dn, schema, entry,
215 readonly_attr_oids,
216 existing_object_classes=None,
217 invalid_attrs=None
218 ):
219 assert isinstance(dn, str), TypeError("Argument 'dn' must be str, was {!r}".format(dn))
220 DisplayEntry.__init__(self, app, dn, schema, entry, 'field_sep', False)
221 self.existing_object_classes = existing_object_classes
222 self.readonly_attr_oids = readonly_attr_oids
223 self.invalid_attrsinvalid_attrs = invalid_attrs or {}
224 new_object_classes = set(self.entry.object_class_oid_set()) - {
225 self.entry._s.get_oid(ObjectClass, oc_name)
226 for oc_name in existing_object_classes or []
227 }
228 new_attribute_types = self.entry._s.attribute_types(
229 new_object_classes,
230 raise_keyerror=0,
231 ignore_dit_content_rule=self._app.ls.relax_rules
232 )
233 old_attribute_types = self.entry._s.attribute_types(
234 existing_object_classes or [],
235 raise_keyerror=0,
236 ignore_dit_content_rule=self._app.ls.relax_rules
237 )
239 self.new_attribute_types_oids.update(new_attribute_types[0].keys())
240 self.new_attribute_types_oids.update(new_attribute_types[1].keys())
241 for at_oid in list(old_attribute_types[0].keys())+list(old_attribute_types[1].keys()):
242 try:
243 self.new_attribute_types_oids.remove(at_oid)
244 except KeyError:
245 pass
246
249 self.row_counter = 0
250 # end of _reset_input_counters()
251
252 def __getitem__(self, nameoroid):
253 """
254 Return HTML input field(s) for the attribute specified by nameoroid.
255 """
256 oid = self.entry.name2key(nameoroid)[0]
257 nameoroid_se = self.entry._s.get_obj(AttributeType, nameoroid)
258 syntax_class = syntax_registry.get_syntax(self.entry._s, nameoroid, self.soc)
259 try:
260 attr_values = self.entry.__getitem__(nameoroid)
261 except KeyError:
262 attr_values = []
263 # Attribute value list must contain at least one element to display an input field
264 attr_values = attr_values or [None]
265
266 result = []
267
268 # Eliminate binary attribute values from input form
269 if not syntax_class.editable:
270 attr_values = [b'']
271
272 invalid_attr_indexes = set(self.invalid_attrsinvalid_attrs.get(nameoroid, []))
273
274 for attr_index, attr_value in enumerate(attr_values):
275
276 attr_inst = syntax_class(
277 self._app, self.dn, self.entry._s, nameoroid, attr_value, self.entry,
278 )
279 highlight_invalid = attr_index in invalid_attr_indexes
280
281 if (
282 # Attribute type 'objectClass' always read-only here
283 oid == '2.5.4.0'
284 ) or (
285 # Attribute type 'structuralObjectClass' always read-only no matter what
286 oid == '2.5.21.9'
287 ) or (
288 # Check whether the server indicated this attribute
289 # not to be writeable by bound identity
290 not self.readonly_attr_oids is None and
291 oid in self.readonly_attr_oids and
292 not oid in self.new_attribute_types_oids
293 ) or (
294 # Check whether attribute type/value is used in the RDN => not writeable
296 attr_value and
297 nameoroid in self.rdn_dict and
298 self.rdn_dict[nameoroid].encode('utf-8') == attr_value
299 ) or (
300 # Set to writeable if relax rules control is in effect
301 # and attribute is NO-USER-APP in subschema
302 not self._app.ls.relax_rules and
303 no_userapp_attr(self.entry._s, oid)
304 ):
305 result.append('\n'.join((
306 '<span class="InvalidInput">'*highlight_invalid,
307 self._app.form.hidden_field_html('in_at', nameoroid, ''),
308 HIDDEN_FIELD % ('in_avi', str(self.attr_counter), ''),
309 HIDDEN_FIELD % (
310 'in_av',
311 self._app.form.s2d(attr_inst.form_value(), sp_entity=' '),
312 self._app.form.s2d(attr_inst.form_value(), sp_entity='&nbsp;&nbsp;')
313 ),
314 '</span>'*highlight_invalid,
315 )))
316 self.row_counter += 1
317
318 else:
319 attr_title = ''
320 attr_type_tags = []
321 attr_type_name = str(nameoroid).split(';')[0]
322 if nameoroid_se:
323 attr_type_name = (nameoroid_se.names or [nameoroid_se.oid])[0]
324 try:
325 attr_title = (nameoroid_se.desc or '')
326 except UnicodeError:
327 # This happens sometimes because of wrongly encoded schema files
328 attr_title = ''
329 # Determine whether transfer syntax has to be specified with ;binary
330 if (
331 nameoroid.endswith(';binary') or
332 oid in NEEDS_BINARY_TAG or
333 nameoroid_se.syntax in NEEDS_BINARY_TAG
334 ):
335 attr_type_tags.append('binary')
336 input_fields = attr_inst.input_fields()
337 for input_field in input_fields:
338 input_field.name = 'in_av'
339 input_field.charset = self._app.form.accept_charset
340 result.append('\n'.join([
341 '<span class="InvalidInput">'*highlight_invalid,
342 HIDDEN_FIELD % (
343 'in_at',
344 ';'.join([attr_type_name]+attr_type_tags),
345 ''
346 ),
347
348 HIDDEN_FIELD % ('in_avi', str(self.attr_counter), ''),
349 input_field.input_html(
350 id_value='_'.join((
351 'inputattr', attr_type_name, str(attr_index)
352 )),
353 title=attr_title
354 ),
355 attr_inst.value_button(self._app.command, self.row_counter, '+'),
356 attr_inst.value_button(self._app.command, self.row_counter, '-'),
357 '</span>'*highlight_invalid,
358 ]))
359 self.row_counter += 1
360
361 self.attr_counter += 1
362
363 return '<span id="in_a_%s">%s</span>' % (
364 self._app.form.s2d(nameoroid),
365 '\n<br>\n'.join(result),
366 )
367
369 # Initialize a list of assertions for filtering attribute types
370 # displayed in the input form
371 attr_type_filter = [
372 ('no_user_mod', [0]),
373 #('usage', range(2)),
374 ('collective', [0]),
375 ]
376 # Check whether Manage DIT control is in effect,
377 # filter out OBSOLETE attribute types otherwise
378 if not self._app.ls.relax_rules:
379 attr_type_filter.append(('obsolete', [0]))
380
381 # Filter out extensibleObject
382 object_class_oids = self.entry.object_class_oid_set()
383 try:
384 object_class_oids.remove('1.3.6.1.4.1.1466.101.120.111')
385 except KeyError:
386 pass
387 try:
388 object_class_oids.remove('extensibleObject')
389 except KeyError:
390 pass
391
392 required_attrs_dict, allowed_attrs_dict = self.entry._s.attribute_types(
393 list(object_class_oids),
394 attr_type_filter=attr_type_filter,
395 raise_keyerror=0,
396 ignore_dit_content_rule=self._app.ls.relax_rules,
397 )
398
399 # Additional check whether to explicitly add object class attribute.
400 # This is a work-around for LDAP servers which mark the
401 # objectClass attribute as not modifiable (e.g. MS Active Directory)
402 if '2.5.4.0' not in required_attrs_dict and '2.5.4.0' not in allowed_attrs_dict:
403 required_attrs_dict['2.5.4.0'] = self.entry._s.get_obj(ObjectClass, '2.5.4.0')
404 return required_attrs_dict, allowed_attrs_dict
405
406 def fieldset_table(self, attr_types_dict, fieldset_title):
407 self._app.outf.write(
408 """<fieldset title="%s">
409 <legend>%s</legend>
410 <table summary="%s">
411 """ % (fieldset_title, fieldset_title, fieldset_title)
412 )
413 seen_attr_type_oids = ldap0.cidict.CIDict()
414 attr_type_names = ldap0.cidict.CIDict()
415 for atype in self.entry.keys():
416 at_oid = self.entry.name2key(atype)[0]
417 if at_oid in attr_types_dict:
418 seen_attr_type_oids[at_oid] = None
419 attr_type_names[atype] = None
420 for at_oid, at_se in attr_types_dict.items():
421 if (
422 at_se and
423 at_oid not in seen_attr_type_oids and
424 not no_userapp_attr(self.entry._s, at_oid)
425 ):
426 attr_type_names[(at_se.names or (at_se.oid,))[0]] = None
427 attr_types = list(attr_type_names.keys())
428 attr_types.sort(key=str.lower)
429 for attr_type in attr_types:
430 attr_type_name = schema_anchor(self._app, attr_type, AttributeType, link_text='&raquo;')
431 attr_value_field_html = self[attr_type]
432 self._app.outf.write(
433 '<tr>\n<td class="InputAttrType">\n%s\n</td>\n<td>\n%s\n</td>\n</tr>\n' % (
434 attr_type_name,
435 attr_value_field_html,
436 )
437 )
438 self._app.outf.write('</table>\n</fieldset>\n')
439 # end of fieldset_table()
440
441 def table_input(self, attrs_dict_list):
443 for attr_dict, fieldset_title in attrs_dict_list:
444 if attr_dict:
445 self.fieldset_table(attr_dict, fieldset_title)
446 # end of table_input()
447
448 def template_output(self, cnf_key, display_duplicate_attrs=True):
450 displayed_attrs = DisplayEntry.template_output(
451 self, cnf_key, display_duplicate_attrs=display_duplicate_attrs
452 )
453 # Output hidden fields for attributes not displayed in template-based input form
454 for attr_type, attr_values in self.entry.items():
455 at_oid = self.entry.name2key(attr_type)[0]
456 syntax_class = syntax_registry.get_syntax(self.entry._s, attr_type, self.soc)
457 if syntax_class.editable and \
458 not no_userapp_attr(self.entry._s, attr_type) and \
459 not at_oid in displayed_attrs:
460 for attr_value in attr_values:
461 attr_inst = syntax_class(
462 self._app, self.dn, self.entry._s, attr_type, attr_value, self.entry
463 )
464 self._app.outf.write(self._app.form.hidden_field_html('in_at', attr_type, ''))
465 self._app.outf.write(HIDDEN_FIELD % ('in_avi', str(self.attr_counter), ''))
466 try:
467 attr_value_html = self._app.form.s2d(attr_inst.form_value(), sp_entity=' ')
468 except UnicodeDecodeError:
469 # Simply display an empty string if anything goes wrong with Unicode
470 # decoding (e.g. with binary attributes)
471 attr_value_html = ''
472 self._app.outf.write(HIDDEN_FIELD % (
473 'in_av', attr_value_html, ''
474 ))
475 self.attr_counter += 1
476 return displayed_attrs # template_output()
477
478 def ldif_input(self):
479 bio = BytesIO()
480 ldif_writer = LDIFWriter(bio)
481 ldap_entry = {}
482 for attr_type in self.entry.keys():
483 attr_values = self.entry.__getitem__(attr_type)
484 if not no_userapp_attr(self.entry._s, attr_type):
485 ldap_entry[attr_type.encode('ascii')] = [
486 attr_value
487 for attr_value in attr_values
488 if attr_value
489 ]
490 ldif_writer.unparse(self.dn.encode(self._app.ls.charset), ldap_entry)
491 self._app.outf.write(
492 INPUT_FORM_LDIF_TMPL.format(
493 value_ldif=self._app.form.s2d(
494 bio.getvalue().decode('utf-8'),
495 sp_entity=' ',
496 lf_entity='\n',
497 ),
498 value_ldifmaxbytes=web2ldapcnf.ldif_maxbytes,
499 text_ldifurlschemes=', '.join(web2ldapcnf.ldif_url_schemes)
500 )
501 )
502 # end of ldif_input()
def get_html_templates(self, cnf_key)
Definition: entry.py:136
def template_output(self, cnf_key, display_duplicate_attrs=True)
Definition: entry.py:156
def __init__(self, app, dn, schema, entry, sep_attr, links)
Definition: entry.py:68
def __getitem__(self, nameoroid)
Definition: entry.py:92
def table_input(self, attrs_dict_list)
Definition: entry.py:441
def template_output(self, cnf_key, display_duplicate_attrs=True)
Definition: entry.py:448
def fieldset_table(self, attr_types_dict, fieldset_title)
Definition: entry.py:406
def __getitem__(self, nameoroid)
Definition: entry.py:252
def __init__(self, app, dn, schema, entry, readonly_attr_oids, existing_object_classes=None, invalid_attrs=None)
Definition: entry.py:218
def no_userapp_attr(schema, attr_type_name, relax_rules=False)
Definition: __init__.py:81
def object_class_categories(sub_schema, object_classes)
Definition: __init__.py:120
def schema_anchor(app, se_nameoroid, se_class, name_template='{name}\n{anchor}', link_text=None)
Definition: __init__.py:205
def get_variant_filename(pathname, variantlist)
Definition: tmpl.py:20