389-ds-base/0024-Issue-6700-CLI-UI-include-superior-objectclasses-all.patch

475 lines
20 KiB
Diff

From 16c11881c48b8e285eb3593971bcb4cdf998887d Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Sun, 30 Mar 2025 15:49:45 -0400
Subject: [PATCH] Issue 6700 - CLI/UI - include superior objectclasses' allowed
and requires attrs
Description:
When you get/list an objectclass it only lists its level of allowed and
required objectclasses, but it should also include all its superior
objectclasses' allowed and required attributes.
Added an option to the CLI to also include all the parent/superior
required and allowed attributes
Relates: https://github.com/389ds/389-ds-base/issues/6700
Reviewed by: spichugi & tbordaz(Thanks!)
---
.../suites/clu/dsconf_schema_superior_test.py | 122 ++++++++++++++++++
.../tests/suites/schema/schema_test.py | 9 +-
.../src/lib/ldap_editor/lib/utils.jsx | 3 +-
src/cockpit/389-console/src/schema.jsx | 6 +-
src/lib389/lib389/cli_conf/schema.py | 12 +-
src/lib389/lib389/schema.py | 105 +++++++++++----
6 files changed, 219 insertions(+), 38 deletions(-)
create mode 100644 dirsrvtests/tests/suites/clu/dsconf_schema_superior_test.py
diff --git a/dirsrvtests/tests/suites/clu/dsconf_schema_superior_test.py b/dirsrvtests/tests/suites/clu/dsconf_schema_superior_test.py
new file mode 100644
index 000000000..185e16af2
--- /dev/null
+++ b/dirsrvtests/tests/suites/clu/dsconf_schema_superior_test.py
@@ -0,0 +1,122 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import logging
+import json
+import os
+import subprocess
+import pytest
+from lib389.topologies import topology_st as topo
+
+log = logging.getLogger(__name__)
+
+
+def execute_dsconf_command(dsconf_cmd, subcommands):
+ """Execute dsconf command and return output and return code"""
+
+ cmdline = dsconf_cmd + subcommands
+ proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE)
+ out, _ = proc.communicate()
+ return out.decode('utf-8'), proc.returncode
+
+
+def get_dsconf_base_cmd(topo):
+ """Return base dsconf command list"""
+ return ['/usr/sbin/dsconf', topo.standalone.serverid,
+ '-j', 'schema']
+
+
+def test_schema_oc_superior(topo):
+ """Specify a test case purpose or name here
+
+ :id: d12aab4a-1436-43eb-802a-0661281a13d0
+ :setup: Standalone Instance
+ :steps:
+ 1. List all the schema
+ 2. List all the schema and include superior OC's attrs
+ 3. Get objectclass list
+ 4. Get objectclass list and include superior OC's attrs
+ 5. Get objectclass
+ 6. Get objectclass and include superior OC's attrs
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ """
+
+ dsconf_cmd = get_dsconf_base_cmd(topo)
+
+ # Test default schema list
+ output, rc = execute_dsconf_command(dsconf_cmd, ['list'])
+ assert rc == 0
+ json_result = json.loads(output)
+ for schema_item in json_result:
+ if 'name' in schema_item and schema_item['name'] == 'inetOrgPerson':
+ assert len(schema_item['must']) == 0
+ break
+
+ # Test including the OC superior attributes
+ output, rc = execute_dsconf_command(dsconf_cmd, ['list',
+ '--include-oc-sup'])
+ assert rc == 0
+ json_result = json.loads(output)
+ for schema_item in json_result:
+ if 'name' in schema_item and schema_item['name'] == 'inetOrgPerson':
+ assert len(schema_item['must']) > 0 and \
+ 'cn' in schema_item['must'] and 'sn' in schema_item['must']
+ break
+
+ # Test default objectclass list
+ output, rc = execute_dsconf_command(dsconf_cmd, ['objectclasses', 'list'])
+ assert rc == 0
+ json_result = json.loads(output)
+ for schema_item in json_result:
+ if 'name' in schema_item and schema_item['name'] == 'inetOrgPerson':
+ assert len(schema_item['must']) == 0
+ break
+
+ # Test objectclass list and inslude superior attributes
+ output, rc = execute_dsconf_command(dsconf_cmd, ['objectclasses', 'list',
+ '--include-sup'])
+ assert rc == 0
+ json_result = json.loads(output)
+ for schema_item in json_result:
+ if 'name' in schema_item and schema_item['name'] == 'inetOrgPerson':
+ assert len(schema_item['must']) > 0 and \
+ 'cn' in schema_item['must'] and 'sn' in schema_item['must']
+ break
+
+ # Test default objectclass query
+ output, rc = execute_dsconf_command(dsconf_cmd, ['objectclasses', 'query',
+ 'inetOrgPerson'])
+ assert rc == 0
+ result = json.loads(output)
+ schema_item = result['oc']
+ assert 'names' in schema_item
+ assert schema_item['names'][0] == 'inetOrgPerson'
+ assert len(schema_item['must']) == 0
+
+ # Test objectclass query and include superior attributes
+ output, rc = execute_dsconf_command(dsconf_cmd, ['objectclasses', 'query',
+ 'inetOrgPerson',
+ '--include-sup'])
+ assert rc == 0
+ result = json.loads(output)
+ schema_item = result['oc']
+ assert 'names' in schema_item
+ assert schema_item['names'][0] == 'inetOrgPerson'
+ assert len(schema_item['must']) > 0 and 'cn' in schema_item['must'] \
+ and 'sn' in schema_item['must']
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
diff --git a/dirsrvtests/tests/suites/schema/schema_test.py b/dirsrvtests/tests/suites/schema/schema_test.py
index afc9cc678..8ca15af70 100644
--- a/dirsrvtests/tests/suites/schema/schema_test.py
+++ b/dirsrvtests/tests/suites/schema/schema_test.py
@@ -232,7 +232,7 @@ def test_gecos_mixed_definition_topo(topo_m2, request):
repl = ReplicationManager(DEFAULT_SUFFIX)
m1 = topo_m2.ms["supplier1"]
m2 = topo_m2.ms["supplier2"]
-
+
# create a test user
testuser_dn = 'uid={},{}'.format('testuser', DEFAULT_SUFFIX)
@@ -343,7 +343,7 @@ def test_gecos_directoryString_wins_M1(topo_m2, request):
repl = ReplicationManager(DEFAULT_SUFFIX)
m1 = topo_m2.ms["supplier1"]
m2 = topo_m2.ms["supplier2"]
-
+
# create a test user
testuser_dn = 'uid={},{}'.format('testuser', DEFAULT_SUFFIX)
@@ -471,7 +471,7 @@ def test_gecos_directoryString_wins_M2(topo_m2, request):
repl = ReplicationManager(DEFAULT_SUFFIX)
m1 = topo_m2.ms["supplier1"]
m2 = topo_m2.ms["supplier2"]
-
+
# create a test user
testuser_dn = 'uid={},{}'.format('testuser', DEFAULT_SUFFIX)
@@ -623,11 +623,10 @@ def test_definition_with_sharp(topology_st, request):
# start the instances
inst.start()
- i# Check that server is really running.
+ # Check that server is really running.
assert inst.status()
-
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/src/cockpit/389-console/src/lib/ldap_editor/lib/utils.jsx b/src/cockpit/389-console/src/lib/ldap_editor/lib/utils.jsx
index fc9c898fa..cd94063ec 100644
--- a/src/cockpit/389-console/src/lib/ldap_editor/lib/utils.jsx
+++ b/src/cockpit/389-console/src/lib/ldap_editor/lib/utils.jsx
@@ -873,7 +873,8 @@ export function getAllObjectClasses (serverId, allOcCallback) {
'ldapi://%2fvar%2frun%2fslapd-' + serverId + '.socket',
'schema',
'objectclasses',
- 'list'
+ 'list',
+ '--include-sup'
];
const result = [];
log_cmd("getAllObjectClasses", "", cmd);
diff --git a/src/cockpit/389-console/src/schema.jsx b/src/cockpit/389-console/src/schema.jsx
index e39f9fef2..19854e785 100644
--- a/src/cockpit/389-console/src/schema.jsx
+++ b/src/cockpit/389-console/src/schema.jsx
@@ -440,7 +440,8 @@ export class Schema extends React.Component {
"-j",
"ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
"schema",
- "list"
+ "list",
+ "--include-oc-sup"
];
log_cmd("loadSchemaData", "Get schema objects in one batch", cmd);
cockpit
@@ -568,7 +569,8 @@ export class Schema extends React.Component {
"schema",
"objectclasses",
"query",
- name
+ name,
+ "--include-sup"
];
log_cmd("openObjectclassModal", "Fetch ObjectClass data from schema", cmd);
diff --git a/src/lib389/lib389/cli_conf/schema.py b/src/lib389/lib389/cli_conf/schema.py
index 7a06c91bc..7782aa5e5 100644
--- a/src/lib389/lib389/cli_conf/schema.py
+++ b/src/lib389/lib389/cli_conf/schema.py
@@ -31,7 +31,8 @@ def list_all(inst, basedn, log, args):
if args is not None and args.json:
json = True
- objectclass_elems = schema.get_objectclasses(json=json)
+ objectclass_elems = schema.get_objectclasses(include_sup=args.include_oc_sup,
+ json=json)
attributetype_elems = schema.get_attributetypes(json=json)
matchingrule_elems = schema.get_matchingrules(json=json)
@@ -67,7 +68,7 @@ def list_objectclasses(inst, basedn, log, args):
log = log.getChild('list_objectclasses')
schema = Schema(inst)
if args is not None and args.json:
- print(dump_json(schema.get_objectclasses(json=True), indent=4))
+ print(dump_json(schema.get_objectclasses(include_sup=args.include_sup, json=True), indent=4))
else:
for oc in schema.get_objectclasses():
print(oc)
@@ -108,7 +109,7 @@ def query_objectclass(inst, basedn, log, args):
schema = Schema(inst)
# Need the query type
oc = _get_arg(args.name, msg="Enter objectclass to query")
- result = schema.query_objectclass(oc, json=args.json)
+ result = schema.query_objectclass(oc, include_sup=args.include_sup, json=args.json)
if args.json:
print(dump_json(result, indent=4))
else:
@@ -339,6 +340,9 @@ def create_parser(subparsers):
schema_subcommands = schema_parser.add_subparsers(help='schema')
schema_list_parser = schema_subcommands.add_parser('list', help='List all schema objects on this system', formatter_class=CustomHelpFormatter)
schema_list_parser.set_defaults(func=list_all)
+ schema_list_parser.add_argument('--include-oc-sup', action='store_true',
+ default=False,
+ help="Include the superior objectclasses' \"may\" and \"must\" attributes")
attributetypes_parser = schema_subcommands.add_parser('attributetypes', help='Work with attribute types on this system', formatter_class=CustomHelpFormatter)
attributetypes_subcommands = attributetypes_parser.add_subparsers(help='schema')
@@ -365,9 +369,11 @@ def create_parser(subparsers):
objectclasses_subcommands = objectclasses_parser.add_subparsers(help='schema')
oc_list_parser = objectclasses_subcommands.add_parser('list', help='List available objectClasses on this system', formatter_class=CustomHelpFormatter)
oc_list_parser.set_defaults(func=list_objectclasses)
+ oc_list_parser.add_argument('--include-sup', action='store_true', default=False, help="Include the superior objectclasses' \"may\" and \"must\" attributes")
oc_query_parser = objectclasses_subcommands.add_parser('query', help='Query an objectClass', formatter_class=CustomHelpFormatter)
oc_query_parser.set_defaults(func=query_objectclass)
oc_query_parser.add_argument('name', nargs='?', help='ObjectClass to query')
+ oc_query_parser.add_argument('--include-sup', action='store_true', default=False, help="Include the superior objectclasses' \"may\" and \"must\" attributes")
oc_add_parser = objectclasses_subcommands.add_parser('add', help='Add an objectClass to this system', formatter_class=CustomHelpFormatter)
oc_add_parser.set_defaults(func=add_objectclass)
_add_parser_args(oc_add_parser, 'objectclasses')
diff --git a/src/lib389/lib389/schema.py b/src/lib389/lib389/schema.py
index a47e13db8..2e8aa3ed8 100755
--- a/src/lib389/lib389/schema.py
+++ b/src/lib389/lib389/schema.py
@@ -116,15 +116,66 @@ class Schema(DSLdapObject):
result = ATTR_SYNTAXES
return result
- def _get_schema_objects(self, object_model, json=False):
- """Get all the schema objects for a specific model: Attribute, Objectclass,
- or Matchingreule.
+ def gather_oc_sup_attrs(self, oc, sup_oc, ocs, processed_ocs=None):
+ """
+ Recursively build up all the objectclass superiors' may/must
+ attributes
+
+ @param oc - original objectclass we are building up
+ @param sup_oc - superior objectclass that we are gathering must/may
+ attributes from, and for following its superior
+ objectclass
+ @param ocs - all objectclasses
+ @param processed_ocs - list of all the superior objectclasees we have
+ already processed. Used for checking if we
+ somehow get into an infinite loop
+ """
+ if processed_ocs is None:
+ # First pass, init our values
+ sup_oc = oc
+ processed_ocs = [sup_oc['names'][0]]
+ elif sup_oc['names'][0] in processed_ocs:
+ # We're looping, need to abort. This should never happen because
+ # of how the schema is structured, but perhaps a bug was
+ # introduced in the server schema handling?
+ return
+
+ # update processed list to prevent loops
+ processed_ocs.append(sup_oc['names'][0])
+
+ for soc in sup_oc['sup']:
+ if soc.lower() == "top":
+ continue
+ # Get sup_oc
+ for obj in ocs:
+ oc_dict = vars(ObjectClass(obj))
+ name = oc_dict['names'][0]
+ if name.lower() == soc.lower():
+ # Found the superior, get it's attributes
+ for attr in oc_dict['may']:
+ if attr not in oc['may']:
+ oc['may'] = oc['may'] + (attr,)
+ for attr in oc_dict['must']:
+ if attr not in oc['must']:
+ oc['must'] = oc['must'] + (attr,)
+
+ # Sort the tuples
+ oc['may'] = tuple(sorted(oc['may']))
+ oc['must'] = tuple(sorted(oc['must']))
+
+ # Now recurse and check this objectclass
+ self.gather_oc_sup_attrs(oc, oc_dict, ocs, processed_ocs)
+
+ def _get_schema_objects(self, object_model, include_sup=False, json=False):
+ """Get all the schema objects for a specific model:
+
+ Attribute, ObjectClass, or MatchingRule.
"""
attr_name = self._get_attr_name_by_model(object_model)
results = self.get_attr_vals_utf8(attr_name)
+ object_insts = []
if json:
- object_insts = []
for obj in results:
obj_i = vars(object_model(obj))
if len(obj_i["names"]) == 1:
@@ -136,20 +187,9 @@ class Schema(DSLdapObject):
else:
obj_i['name'] = ""
- # Temporary workaround for X-ORIGIN in ObjectClass objects.
- # It should be removed after https://github.com/python-ldap/python-ldap/pull/247 is merged
- if " X-ORIGIN " in obj and obj_i['names'] == vars(object_model(obj))['names']:
- remainder = obj.split(" X-ORIGIN ")[1]
- if remainder[:1] == "(":
- # Have multiple values
- end = remainder.rfind(')')
- vals = remainder[1:end]
- vals = re.findall(X_ORIGIN_REGEX, vals)
- # For now use the first value, but this should be a set (another bug in python-ldap)
- obj_i['x_origin'] = vals[0]
- else:
- # Single X-ORIGIN value
- obj_i['x_origin'] = obj.split(" X-ORIGIN ")[1].split("'")[1]
+ if object_model is ObjectClass and include_sup:
+ self.gather_oc_sup_attrs(obj_i, None, results)
+
object_insts.append(obj_i)
object_insts = sorted(object_insts, key=itemgetter('name'))
@@ -161,11 +201,20 @@ class Schema(DSLdapObject):
return {'type': 'list', 'items': object_insts}
else:
- object_insts = [object_model(obj_i) for obj_i in results]
+ for obj_i in results:
+ obj_i = object_model(obj_i)
+ if object_model is ObjectClass and include_sup:
+ obj_ii = vars(obj_i)
+ self.gather_oc_sup_attrs(obj_ii, None, results)
+ obj_i.may = obj_ii['may']
+ obj_i.must = obj_ii['must']
+ object_insts.append(obj_i)
return sorted(object_insts, key=lambda x: x.names, reverse=False)
- def _get_schema_object(self, name, object_model, json=False):
- objects = self._get_schema_objects(object_model, json=json)
+ def _get_schema_object(self, name, object_model, include_sup=False, json=False):
+ objects = self._get_schema_objects(object_model,
+ include_sup=include_sup,
+ json=json)
if json:
schema_object = [obj_i for obj_i in objects["items"] if name.lower() in
list(map(str.lower, obj_i["names"]))]
@@ -227,7 +276,6 @@ class Schema(DSLdapObject):
def _remove_schema_object(self, name, object_model):
attr_name = self._get_attr_name_by_model(object_model)
schema_object = self._get_schema_object(name, object_model)
-
return self.remove(attr_name, str(schema_object))
def _edit_schema_object(self, name, parameters, object_model):
@@ -371,7 +419,6 @@ class Schema(DSLdapObject):
:param name: the name of the objectClass you want to remove.
:type name: str
"""
-
return self._remove_schema_object(name, ObjectClass)
def edit_attributetype(self, name, parameters):
@@ -396,7 +443,7 @@ class Schema(DSLdapObject):
return self._edit_schema_object(name, parameters, ObjectClass)
- def get_objectclasses(self, json=False):
+ def get_objectclasses(self, include_sup=False, json=False):
"""Returns a list of ldap.schema.models.ObjectClass objects for all
objectClasses supported by this instance.
@@ -404,7 +451,8 @@ class Schema(DSLdapObject):
:type json: bool
"""
- return self._get_schema_objects(ObjectClass, json=json)
+ return self._get_schema_objects(ObjectClass, include_sup=include_sup,
+ json=json)
def get_attributetypes(self, json=False):
"""Returns a list of ldap.schema.models.AttributeType objects for all
@@ -447,7 +495,8 @@ class Schema(DSLdapObject):
else:
return matching_rule
- def query_objectclass(self, objectclassname, json=False):
+ def query_objectclass(self, objectclassname, include_sup=False,
+ json=False):
"""Returns a single ObjectClass instance that matches objectclassname.
Returns None if the objectClass doesn't exist.
@@ -462,7 +511,9 @@ class Schema(DSLdapObject):
<ldap.schema.models.ObjectClass instance>
"""
- objectclass = self._get_schema_object(objectclassname, ObjectClass, json=json)
+ objectclass = self._get_schema_object(objectclassname, ObjectClass,
+ include_sup=include_sup,
+ json=json)
if json:
result = {'type': 'schema', 'oc': objectclass}
--
2.48.1