475 lines
20 KiB
Diff
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
|
|
|