diff --git a/0007-man-page-update-ipa-server-upgrade.1.patch b/0007-man-page-update-ipa-server-upgrade.1.patch new file mode 100644 index 0000000..df25002 --- /dev/null +++ b/0007-man-page-update-ipa-server-upgrade.1.patch @@ -0,0 +1,35 @@ +From ecb407864fde4d917dabe0aae95881561ed384ab Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Wed, 7 Jul 2021 14:11:40 +0200 +Subject: [PATCH] man page: update ipa-server-upgrade.1 + +The man page needs to clarify in which case the command needs +to be run. + +Fixes: https://pagure.io/freeipa/issue/8913 +Reviewed-By: Francois Cami +--- + install/tools/man/ipa-server-upgrade.1 | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/install/tools/man/ipa-server-upgrade.1 b/install/tools/man/ipa-server-upgrade.1 +index 3db19b0f13da1f5a36bd6e8df23fc916d0401a6d..f01e21c6b599499c4c6dbbcf120b19a3431fb3ed 100644 +--- a/install/tools/man/ipa-server-upgrade.1 ++++ b/install/tools/man/ipa-server-upgrade.1 +@@ -8,7 +8,12 @@ ipa\-server\-upgrade \- upgrade IPA server + .SH "SYNOPSIS" + ipa\-server\-upgrade [options] + .SH "DESCRIPTION" +-ipa\-server\-upgrade is used to upgrade IPA server when the IPA packages are being updated. It is not intended to be executed by end\-users. ++ipa\-server\-upgrade is executed automatically to upgrade IPA server when ++the IPA packages are being updated. It is not intended to be executed by ++end\-users, unless the automatic execution reports an error. In this case, ++the administrator needs to identify and fix the issue that is causing the ++upgrade failure (with the help of /var/log/ipaupgrade.log) ++and manually re\-run ipa\-server\-upgrade. + + ipa\-server\-upgrade will: + +-- +2.26.3 + diff --git a/0008-Add-basic-support-for-subordinate-user-group-ids.patch b/0008-Add-basic-support-for-subordinate-user-group-ids.patch new file mode 100644 index 0000000..6b52566 --- /dev/null +++ b/0008-Add-basic-support-for-subordinate-user-group-ids.patch @@ -0,0 +1,2324 @@ +From 69cd05bf635d19b9844f65d83dace05136a40326 Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Fri, 19 Mar 2021 11:48:38 +0100 +Subject: [PATCH] Add basic support for subordinate user/group ids + +New LDAP object class "ipaUserSubordinate" with four new fields: +- ipasubuidnumber / ipasubuidcount +- ipasubgidnumber / ipasgbuidcount + +New self-service permission to add subids. + +New command user-auto-subid to auto-assign subid + +The code hard-codes counts to 65536, sets subgid equal to subuid, and +does not allow removal of subids. There is also a hack that emulates a +DNA plugin with step interval 65536 for testing. + +Work around problem with older SSSD clients that fail with unknown +idrange type "ipa-local-subid", see: https://github.com/SSSD/sssd/issues/5571 + +Related: https://pagure.io/freeipa/issue/8361 +Signed-off-by: Christian Heimes +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +--- + ACI.txt | 2 +- + API.txt | 47 ++- + Makefile.am | 2 +- + VERSION.m4 | 4 +- + doc/designs/index.rst | 1 + + doc/designs/subordinate-ids.md | 468 ++++++++++++++++++++++ + freeipa.spec.in | 1 + + install/share/60basev2.ldif | 1 + + install/share/60basev4.ldif | 19 + + install/share/Makefile.am | 1 + + install/share/bootstrap-template.ldif | 22 + + install/share/dna.ldif | 20 + + install/tools/Makefile.am | 2 + + install/tools/ipa-subids.in | 8 + + install/ui/src/freeipa/user.js | 53 ++- + install/updates/20-indices.update | 18 + + install/updates/73-subid.update | 102 +++++ + install/updates/Makefile.am | 1 + + ipalib/constants.py | 13 + + ipaserver/install/adtrustinstance.py | 29 +- + ipaserver/install/dsinstance.py | 43 +- + ipaserver/install/ipa_subids.py | 154 +++++++ + ipaserver/install/ldapupdate.py | 95 +++-- + ipaserver/plugins/baseuser.py | 274 ++++++++++++- + ipaserver/plugins/idrange.py | 10 +- + ipaserver/plugins/internal.py | 12 + + ipaserver/plugins/user.py | 17 +- + ipatests/prci_definitions/gating.yaml | 12 + + ipatests/test_integration/test_subids.py | 201 ++++++++++ + ipatests/test_xmlrpc/test_range_plugin.py | 7 + + 31 files changed, 1565 insertions(+), 75 deletions(-) + create mode 100644 doc/designs/subordinate-ids.md + create mode 100644 install/share/60basev4.ldif + create mode 100644 install/tools/ipa-subids.in + create mode 100644 install/updates/73-subid.update + create mode 100644 ipaserver/install/ipa_subids.py + create mode 100644 ipatests/test_integration/test_subids.py + +diff --git a/ACI.txt b/ACI.txt +index 05852cf6c0150db7d8de99a5f7a44e538df29e5e..fce02a333b212de9b61f920515eed3e356b1391b 100644 +--- a/ACI.txt ++++ b/ACI.txt +@@ -375,7 +375,7 @@ aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber + dn: dc=ipa,dc=example + aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=compat,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read User Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";) + dn: cn=users,cn=accounts,dc=ipa,dc=example +-aci: (targetattr = "ipasshpubkey || ipauniqueid || ipauserauthtype || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User IPA Attributes";allow (compare,read,search) userdn = "ldap:///all";) ++aci: (targetattr = "ipasshpubkey || ipasubgidcount || ipasubgidnumber || ipasubuidcount || ipasubuidnumber || ipauniqueid || ipauserauthtype || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User IPA Attributes";allow (compare,read,search) userdn = "ldap:///all";) + dn: cn=users,cn=accounts,dc=ipa,dc=example + aci: (targetattr = "krbcanonicalname || krblastpwdchange || krbpasswordexpiration || krbprincipalaliases || krbprincipalexpiration || krbprincipalname || krbprincipaltype || nsaccountlock")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Kerberos Attributes";allow (compare,read,search) userdn = "ldap:///all";) + dn: cn=users,cn=accounts,dc=ipa,dc=example +diff --git a/API.txt b/API.txt +index 212ef807c771794dc2f89eb89e03b669eb49295b..262b4d6a72c7d7032a7027116f7a4f65aa620615 100644 +--- a/API.txt ++++ b/API.txt +@@ -4974,7 +4974,7 @@ output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') + command: stageuser_add/1 +-args: 1,45,3 ++args: 1,46,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -4992,6 +4992,7 @@ option: Str('givenname', cli_name='first') + option: Str('homedirectory?', cli_name='homedir') + option: Str('initials?', autofill=True) + option: Str('ipasshpubkey*', cli_name='sshpubkey') ++option: Int('ipasubuidnumber?', cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', cli_name='radius') + option: Str('ipatokenradiususername?', cli_name='radius_username') + option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -5080,7 +5081,7 @@ output: Output('result', type=[]) + output: Output('summary', type=[, ]) + output: ListOfPrimaryKeys('value') + command: stageuser_find/1 +-args: 1,58,4 ++args: 1,60,4 + arg: Str('criteria?') + option: Flag('all', autofill=True, cli_name='all', default=False) + option: Str('carlicense*', autofill=False) +@@ -5104,6 +5105,8 @@ option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') + option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') ++option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid') ++option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -5145,7 +5148,7 @@ output: ListOfEntries('result') + output: Output('summary', type=[, ]) + output: Output('truncated', type=[]) + command: stageuser_mod/1 +-args: 1,51,3 ++args: 1,52,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -5167,6 +5170,7 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') + option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') ++option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -6058,7 +6062,7 @@ output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') + command: user_add/1 +-args: 1,46,3 ++args: 1,47,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -6075,6 +6079,7 @@ option: Str('givenname', cli_name='first') + option: Str('homedirectory?', cli_name='homedir') + option: Str('initials?', autofill=True) + option: Str('ipasshpubkey*', cli_name='sshpubkey') ++option: Int('ipasubuidnumber?', cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', cli_name='radius') + option: Str('ipatokenradiususername?', cli_name='radius_username') + option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -6156,6 +6161,16 @@ option: Str('version?') + output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') ++command: user_auto_subid/1 ++args: 1,4,3 ++arg: Str('uid', cli_name='login') ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Flag('no_members', autofill=True, default=False) ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Str('version?') ++output: Entry('result') ++output: Output('summary', type=[, ]) ++output: PrimaryKey('value') + command: user_del/1 + args: 1,3,3 + arg: Str('uid+', cli_name='login') +@@ -6180,7 +6195,7 @@ output: Output('result', type=[]) + output: Output('summary', type=[, ]) + output: PrimaryKey('value') + command: user_find/1 +-args: 1,61,4 ++args: 1,63,4 + arg: Str('criteria?') + option: Flag('all', autofill=True, cli_name='all', default=False) + option: Str('carlicense*', autofill=False) +@@ -6204,6 +6219,8 @@ option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') + option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') ++option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid') ++option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -6247,8 +6264,23 @@ output: Output('count', type=[]) + output: ListOfEntries('result') + output: Output('summary', type=[, ]) + output: Output('truncated', type=[]) ++command: user_match_subid/1 ++args: 1,8,4 ++arg: Str('criteria?') ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Int('ipasubuidnumber', autofill=False, cli_name='subuid') ++option: Flag('no_members', autofill=True, default=True) ++option: Flag('pkey_only?', autofill=True, default=False) ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Int('sizelimit?', autofill=False) ++option: Int('timelimit?', autofill=False) ++option: Str('version?') ++output: Output('count', type=[]) ++output: ListOfEntries('result') ++output: Output('summary', type=[, ]) ++output: Output('truncated', type=[]) + command: user_mod/1 +-args: 1,52,3 ++args: 1,53,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -6270,6 +6302,7 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') + option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') ++option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -7183,10 +7216,12 @@ default: user_add_cert/1 + default: user_add_certmapdata/1 + default: user_add_manager/1 + default: user_add_principal/1 ++default: user_auto_subid/1 + default: user_del/1 + default: user_disable/1 + default: user_enable/1 + default: user_find/1 ++default: user_match_subid/1 + default: user_mod/1 + default: user_remove_cert/1 + default: user_remove_certmapdata/1 +diff --git a/Makefile.am b/Makefile.am +index c5a33e67f56b2c6f9efb5b4c6af3f7a44ccbdb3c..321df05a7c44f32929a2c5ec45341a42105a8e2f 100644 +--- a/Makefile.am ++++ b/Makefile.am +@@ -229,7 +229,7 @@ fasttest: $(GENERATED_PYTHON_FILES) ipasetup.py + --ignore $(abspath $(top_srcdir))/ipatests/test_integration \ + --ignore $(abspath $(top_srcdir))/ipatests/test_xmlrpc + +-fastlint: $(GENERATED_PYTHON_FILES) ipasetup.py ++fastlint: $(GENERATED_PYTHON_FILES) ipasetup.py acilint apilint + if ! WITH_PYLINT + @echo "ERROR: pylint not available"; exit 1 + endif +diff --git a/VERSION.m4 b/VERSION.m4 +index 9f024675f905a1ee771b6ff293c25b2ac46d92df..1c1e0d56c0eb5c15be0887fae9f90e399757acc7 100644 +--- a/VERSION.m4 ++++ b/VERSION.m4 +@@ -86,8 +86,8 @@ define(IPA_DATA_VERSION, 20100614120000) + # # + ######################################################## + define(IPA_API_VERSION_MAJOR, 2) +-define(IPA_API_VERSION_MINOR, 242) +-# Last change: add status options for cert-find ++# Last change: add subordinate id feature ++define(IPA_API_VERSION_MINOR, 243) + + + ######################################################## +diff --git a/doc/designs/index.rst b/doc/designs/index.rst +index cbec1096c363c9c31656b05f22c50321cd45e073..6dd0edff3004fd0d19208f0c063d4156bde3bf91 100644 +--- a/doc/designs/index.rst ++++ b/doc/designs/index.rst +@@ -17,3 +17,4 @@ FreeIPA design documentation + membermanager.md + hidden-replicas.md + disable-stale-users.md ++ subordinate-ids.md +diff --git a/doc/designs/subordinate-ids.md b/doc/designs/subordinate-ids.md +new file mode 100644 +index 0000000000000000000000000000000000000000..1b578667a8cfdda223af38a14d142c72a5d5c073 +--- /dev/null ++++ b/doc/designs/subordinate-ids.md +@@ -0,0 +1,468 @@ ++# Central management of subordinate user and group ids ++ ++Subordinate ids are a Linux Kernel feature to grant a user additional ++user and group id ranges. Amongst others the feature can be used ++by container runtime engies to implement rootless containers. ++Traditionally subordinate id ranges are configured in ``/etc/subuid`` ++and ``/etc/subgid``. ++ ++To make rootless containers in a large environment as easy as pie, IPA ++gains the ability to centrally manage and assign subordinate id ranges. ++SSSD and shadow-util are extended to read subordinate ids from IPA and ++provide them to userspace tools. ++ ++## Overview ++ ++Feature requests ++ ++* [FreeIPA feature request #8361](https://pagure.io/freeipa/issue/8361) ++* [SSSD feature request #5197](https://github.com/SSSD/sssd/issues/5197) ++* [shadow-util feature request #154](https://github.com/shadow-maint/shadow/issues/154) ++* [389-DS RFE for DNA plugin rhbz#1938239](https://bugzilla.redhat.com/show_bug.cgi?id=1938239) ++ ++Man pages ++ ++* [man subuid(5)](https://man7.org/linux/man-pages/man5/subuid.5.html) ++* [man subgid(5)](https://man7.org/linux/man-pages/man5/subgid.5.html) ++* [man user_namespaces(7)](https://man7.org/linux/man-pages/man7/user_namespaces.7.html) ++* [man newuidmap(1)](https://man7.org/linux/man-pages/man1/newuidmap.1.html) ++ ++Articles / blog posts ++* [Basic Setup and Use of Podman in a Rootless environment](https://github.com/containers/podman/blob/master/docs/tutorials/rootless_tutorial.md) ++* [How does rootless Podman work](https://opensource.com/article/19/2/how-does-rootless-podman-work) ++ ++## Design choices ++ ++Some design choices are owed to the circumstance that uids and gids ++are limited datatypes. The Linux Kernel and userland defines ++``uid_t`` and ``gid_t`` as unsigned 32bit integers (``uint32_t``), which ++limits possible values for numeric user and group ids to ++``0 .. 2^32-2``. ``(uid_t)-1`` is reserved for error reporting. On the ++other hand the user ``nobody`` typically has uid 65534 / gid 65534. This ++means we need to assign 65,536 subordinate ids to every user. The ++theoretical maximum amount of subordinate ranges is less than 65,536 ++(``65536 * 65536 == 2^32``). [``logins.def``](https://man7.org/linux/man-pages/man5/login.defs.5.html) ++also uses 65536 as default setting for ``SUB_UID_COUNT``. ++ ++The practical limit is far smaller. Subordinate ids should not overlap ++with system accounts, local user accounts, IPA user accounts, and ++mapped accounts from Active Directory. Therefore IPA uses the upper ++half of the uid_t range (>= 2^31 == 2,147,483,648) for subordinate ids. ++The high bit is rarely used. IPA limits general numeric ids ++(``uidNumber``, ``gidNumber``, ID ranges) to maximum values of signed ++32bit integer (2^31-1) for backwards compatibility with XML-RPC. ++``logins.def`` defaults to ``SUB_UID_MAX`` 600,100,000. ++ ++A default subordinate id count of 65,536 and a total range of approx. ++2.1 billion limits IPA to slightly more than 32,000 possible ranges. It ++may sound like a lot of users, but there are much bigger installations ++of IPA. For comparison Fedora Accounts has over 120,000 users stored in ++IPA. ++ ++For that reason we treat subordinate id space as premium real estate ++and don't auto-map or auto-assign subordinate ids by default. Instead ++we give the admin several options to assign them manually, semi-manual, ++or automatically. ++ ++### Revision 1 limitation ++ ++The first revision of the feature is deliberately limited and ++restricted. We are aiming for a simple implementation that covers ++basic use cases. Some restrictions may be lifted in the future. ++ ++* subuid and subgids cannot be set independently. They are always set ++ to the same value. ++* counts are hard-coded to value 65536 ++* once assigned subids cannot be removed ++* IPA does not support multiple subordinate id ranges. Contrary to ++ ``/etc/subuid``, users are limited to one set of subordinate ids. ++* subids are auto-assigned. Auto-assignment is currently emulated ++ until 389-DS has been extended to support DNA with step interval. ++* subids are allocated from hard-coded range ++ ``[2147483648..4294901767]`` (``2^31`` to ``2^32-1-65536``), which ++ is the upper 2.1 billion uids of ``uid_t`` (``uint32_t``). The range ++ can hold little 32,767 subordinate id ranges. ++* Active Directory support is out of scope and may be provided in the ++ future. ++ ++### Subid assignment example ++ ++``` ++>>> import itertools ++>>> def subids(): ++... for n in itertools.count(start=0): ++... start = SUBID_RANGE_START + (n * SUBID_COUNT) ++... last = start + SUBID_COUNT - 1 ++... yield (start, last) ++... ++>>> gen = subids() ++>>> next(gen) ++(2147483648, 2147549183) ++>>> next(gen) ++(2147549184, 2147614719) ++>>> next(gen) ++(2147614720, 2147680255) ++``` ++ ++The first user has 65565 subordinate ids from uid/gid ``2147483648`` ++to ``2147549183``, the next user has ``2147549184`` to ``2147614719``, ++and so on. The range count includes the start value. ++ ++An installation with multiple servers, 389-DS' ++[DNA](https://directory.fedoraproject.org/docs/389ds/design/dna-plugin.html) ++plug-in takes care of delegating and assigning chunks of subid ranges ++to servers. The DNA plug-in guarantees uniqueness across servers. ++ ++## LDAP ++ ++### LDAP schema extension ++ ++The subordinate id feature introduces a new auxiliar object class ++``ipaSubordinateId`` with four required attributes ``ipaSubUidNumber``, ++``ipaSubUidCount``, ``ipaSubGidNumber``, and ``ipaSubGidCount``. The ++attributes with ``number`` suffix store the start value of the interval. ++The ``count`` attributes contain the size of the interval including the ++start value. The maximum subid is ++``ipaSubUidNumber + ipaSubUidCount - 1``. ++ ++All four attributes are single-value ``INTEGER`` type with standard ++integer matching rules. OIDs ``2.16.840.1.113730.3.8.23.8`` and ++``2.16.840.1.113730.3.8.23.11`` are reserved for future use. ++ ++```raw ++attributeTypes: ( ++ 2.16.840.1.113730.3.8.23.6 ++ NAME 'ipaSubUidNumber' ++ DESC 'Numerical subordinate user ID (range start value)' ++ EQUALITY integerMatch ORDERING integerOrderingMatch ++ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ++ X-ORIGIN 'IPA v4.9' ++) ++attributeTypes: ( ++ 2.16.840.1.113730.3.8.23.7 ++ NAME 'ipaSubUidCount' ++ DESC 'Subordinate user ID count (range size)' ++ EQUALITY integerMatch ORDERING integerOrderingMatch ++ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ++ X-ORIGIN 'IPA v4.9' ++) ++attributeTypes: ( ++ 2.16.840.1.113730.3.8.23.9 ++ NAME 'ipaSubGidNumber' ++ DESC 'Numerical subordinate group ID (range start value)' ++ EQUALITY integerMatch ORDERING integerOrderingMatch ++ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ++ X-ORIGIN 'IPA v4.9' ++) ++attributeTypes: ( ++ 2.16.840.1.113730.3.8.23.10 ++ NAME 'ipaSubGidCount' ++ DESC 'Subordinate group ID count (range size)' ++ EQUALITY integerMatch ORDERING integerOrderingMatch ++ SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ++ X-ORIGIN 'IPA v4.9' ++) ++``` ++ ++The ``ipaSubordinateId`` object class is an auxiliar subclass of ++``top`` and requires all four subordinate id attributes as well as ++``uidNumber``. It does not subclass ``posixAccount`` to make ++the class reusable in idview overrides later. ++ ++```raw ++objectClasses: ( ++ 2.16.840.1.113730.3.8.24.4 ++ NAME 'ipaSubordinateId' ++ DESC 'Subordinate uid and gid for users' ++ SUP top AUXILIARY ++ MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) ++ X-ORIGIN 'IPA v4.9' ++) ++``` ++ ++The ``ipaSubordinateGid`` and ``ipaSubordinateUid`` are defined for ++future use. IPA always assumes the presence of ``ipaSubordinateId`` and ++does not use these object classes. ++ ++```raw ++objectClasses: ( ++ 2.16.840.1.113730.3.8.24.2 ++ NAME 'ipaSubordinateUid' ++ DESC 'Subordinate uids for users, see subuid(5)' ++ SUP top AUXILIARY ++ MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount ) ++ X-ORIGIN 'IPA v4.9' ++ ) ++objectClasses: ( ++ 2.16.840.1.113730.3.8.24.3 ++ NAME 'ipaSubordinateGid' ++ DESC 'Subordinate gids for users, see subgid(5)' ++ SUP top AUXILIARY ++ MUST ( uidNumber $ ipaSubGidNumber $ ipaSubGidCount ) ++ X-ORIGIN 'IPA v4.9' ++) ++``` ++ ++### Index ++ ++The attributes ``ipaSubUidNumber`` and ``ipaSubGidNumber`` are index ++for ``pres`` and ``eq`` with ``nsMatchingRule: integerOrderingMatch`` ++to enable efficient ``=``, ``>=``, and ``<=`` searches. ++ ++### Distributed numeric assignment (DNA) plug-in extension ++ ++Subordinate id auto-assignment requires an extension of 389-DS' ++[DNA](https://directory.fedoraproject.org/docs/389ds/design/dna-plugin.html) ++plug-in. The DNA plug-in is responsible for safely assigning unique ++numeric ids across all replicas. ++ ++Currently the DNA plug-in only supports a step size of ``1``. A new ++option ``dnaStepAttr`` (name is tentative) will tell the DNA plug-in ++to use the value of entry attributes as step size. ++ ++ ++## Permissions, Privileges, Roles ++ ++### Self-servive RBAC ++ ++The self-service permission enables users to request auto-assignment ++of subordinate uid and gid ranges for themselves. Subordinate ids cannot ++be modified or deleted. ++ ++* ACI: *selfservice: Add subordinate id* ++* Permission: *Self-service subordinate ID* ++* Privilege: *Subordinate ID Selfservice User* ++* Role: *Subordinate ID Selfservice Users* ++* role default member: n/a ++ ++### Administrator RBAC ++ ++The administrator permission allows privileged users to auto-assign ++subordinate ids to users. Once assigned subordinate ids cannot ++be modified or deleted. ++ ++* ACI: *Add subordinate ids to any user* ++* Permission: *Manage subordinate ID* ++* Privilege: *Subordinate ID Administrators* ++* default privilege role: *User Administrator* ++ ++ ++## Workflows ++ ++In the default configuration of IPA, neither existing users nor new ++users will have subordinate ids assigned. There are a couple of ways ++to assign subordinate ids to users. ++ ++### User administrator ++ ++Users with *User Administrator* role and members of the *admins* group ++have permission to auto-assign new subordinate ids to any user. Auto ++assignment can be performed with new ``user-auto-subid`` command on the ++command line or with the *Auto assign subordinate ids* action in the ++*Actions* drop-down menu in the web UI. ++ ++```shell ++$ ipa user-auto-subid someusername ++``` ++ ++### Self-service for group members ++ ++Ordinary users cannot self-service subordinate ids by default. Admins ++can assign the new *Subordinate ID Selfservice User* to users group to ++enable self-service for members of the group. ++ ++For example to enable self-service for all members of the default user ++group ``ipausers``, do: ++ ++```shell ++$ ipa role-add-member "Subordinate ID Selfservice User" --groups=ipausers ++``` ++ ++This allows members of ``ipausers`` to request subordinate ids with ++the ``user-auto-subid`` command or the *Auto assign subordinate ids* ++action in the web UI. ++ ++```shell ++$ ipa user-auto-subid myusername ++``` ++ ++### Auto assignment with user default object class ++ ++Admins can also enable auto-assignment of subordinate ids for all new ++users by adding ``ipasubordinateid`` as a default user objectclass. ++This can be accomplished in the web UI under "IPA Server" / ++"Configuration" / "Default user objectclasses" or on the command line ++with: ++ ++```shell ++$ ipa config-mod --addattr="ipaUserObjectClasses=ipasubordinateid" ++``` ++ ++**NOTE:** The objectclass must be written all lower case. ++ ++### ipa-subid tool ++ ++Finally IPA includes a new tool for mass-assignment of subordinate ids. ++The command uses automatic LDAPI EXTERNAL bind when it's executed as ++root user. Other it requires valid Kerberos TGT of an admin or user ++administrator. ++ ++```raw ++ ++# /usr/libexec/ipa/ipa-subids --help ++Usage: ipa-subids ++ ++Mass-assign subordinate ids ++ ++Options: ++ --version show program's version number and exit ++ -h, --help show this help message and exit ++ --group=GROUP Filter by group membership ++ --filter=USER_FILTER Raw LDAP filter ++ --dry-run Dry run mode. ++ --all-users All users ++ ++ Logging and output options: ++ -v, --verbose print debugging information ++ -d, --debug alias for --verbose (deprecated) ++ -q, --quiet output only errors ++ --log-file=FILE log to the given file ++ ++# # /usr/libexec/ipa/ipa-subids --group ipausers ++Processing user 'testsubordinated1' (1/15) ++Processing user 'testsubordinated2' (2/15) ++Processing user 'testsubordinated3' (3/15) ++Processing user 'testsubordinated4' (4/15) ++Processing user 'testsubordinated5' (5/15) ++Processing user 'testsubordinated6' (6/15) ++Processing user 'testsubordinated7' (7/15) ++Processing user 'testsubordinated8' (8/15) ++Processing user 'testsubordinated9' (9/15) ++Processing user 'testsubordinated10' (10/15) ++Processing user 'testsubordinated11' (11/15) ++Processing user 'testsubordinated12' (12/15) ++Processing user 'testsubordinated13' (13/15) ++Processing user 'testsubordinated14' (14/15) ++Processing user 'testsubordinated15' (15/15) ++Processed 15 user(s) ++The ipa-subids command was successful ++``` ++ ++### Find and match users by any subordinate id ++ ++The ``user-find`` command search by start value of subordinate uid and ++gid range. The new command ``user-match-subid`` can be used to find a ++user by any subordinate id in their range. ++ ++```raw ++$ ipa user-match-subid --subuid=2153185287 ++ User login: asmith ++ First name: Alice ++ Last name: Smith ++ ... ++ SubUID range start: 2153185280 ++ SubUID range size: 65536 ++ SubGID range start: 2153185280 ++ SubGID range size: 65536 ++---------------------------- ++Number of entries returned 1 ++---------------------------- ++$ ipa user-match-subid --subuid=2153185279 ++ User login: bjones ++ First name: Bob ++ Last name: Jones ++ ... ++ SubUID range start: 2153119744 ++ SubUID range size: 65536 ++ SubGID range start: 2153119744 ++ SubGID range size: 65536 ++---------------------------- ++Number of entries returned 1 ++---------------------------- ++``` ++ ++## SSSD integration ++ ++* base: ``cn=accounts,$SUFFIX`` / ``cn=users,cn=accounts,$SUFFIX`` ++* scope: ``SCOPE_SUBTREE`` (2) / ``SCOPE_ONELEVEL`` (1) ++* user filter: should include ``(objectClass=posixAccount)`` ++* attributes: ``uidNumber ipaSubUidNumber ipaSubUidCount ipaSubGidNumber ipaSubGidCount`` ++ ++SSSD can safely assume that only *user accounts* of type ``posixAccount`` ++have subordinate ids. In the first revision there are no other entries ++with subordinate ids. The ``posixAccount`` object class has ``uid`` ++(user login name) and ``uidNumber`` (numeric user id) as mandatory ++attributes. The ``uid`` attribute is guaranteed to be unique across ++all user accounts in an IPA domain. ++ ++The ``uidNumber`` attribute is commonly unique, too. However it's ++technically possible that an administrator has assigned the same ++numeric user id to multiple users. Automatically assigned uid numbers ++don't conflict. SSSD should treat multiple users with same numeric ++user id as an error. ++ ++The attribute ``ipaSubUidNumber`` is always accompanied by ++``ipaSubUidCount`` and ``ipaSubGidNumber`` is always accompanied ++by ``ipaSubGidCount``. In revision 1 the presence of ++``ipaSubUidNumber`` implies presence of the other three attributes. ++All four subordinate id attributes and ``uidNumber`` are single-value ++``INTEGER`` types. Any value outside of range of ``uint32_t`` must ++treated as invalid. SSSD will never see the DNA magic value ``-1`` ++in ``cn=accounts,$SUFFIX`` subtree. ++ ++IPA recommends that SSSD simply extends its existing query for user ++accounts and requests the four subordinate attributes additionally to ++RFC 2307 attributes ``rfc2307_user_map``. SSSD can directly take the ++values and return them without further processing, e.g. ++``uidNumber:ipaSubUidNumber:ipaSubUidCount`` for ``/etc/subuid``. ++ ++Filters for additional cases: ++ ++* subuid filter (find user with subuid by numeric uid): ++ ``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=$UID))``, ++ ``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar ++* subuid enumeration filter: ++ ``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=*))``, ++ ``(objectClass=ipaSubordinateId)``, or similar ++* subgid filter (find user with subgid by numeric uid): ++ ``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=$UID))``, ++ ``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar ++* subgid enumeration filter: ++ ``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=*))``, ++ ``(objectClass=ipaSubordinateId)``, or similar ++ ++## Implementation details ++ ++* The four subid attributes are not included in ++ ``baseuser.default_attributes`` on purpose. The ``config-mod`` ++ command does not permit removal of a user default objectclasses ++ when the class is the last provider of an attribute in ++ ``default_attributes``. ++* ``ipaSubordinateId`` object class does not subclass the other two ++ object classes. LDAP supports ++ ``SUP ( ipaSubordinateGid $ ipaSubordinateUid )`` but 389-DS only ++ auto-inherits from first object class. ++* The idrange entry ``$REALM_subid_range`` has preconfigured base RIDs ++ and SID so idrange plug-in and sidgen task ignore the entry. It's the ++ simplest approach to ensure backwards compatibility with older IPA ++ server versions that don't know how to handle the new range. ++ The SID is ``S-1-5-21-738065-838566-$DOMAIN_HASH``. ``S-1-5-21`` ++ is the well-known SID prefix for domain SIDs. ``738065-838566`` is ++ the decimal representation of the string ``IPA-SUB``. ``DOMAIN_HASH`` ++ is the MURMUR-3 hash of the domain name for key ``0xdeadbeef``. SSSD ++ rejects SIDs unless they are prefixed with ``S-1-5-21`` (see ++ ``sss_idmap.c:is_domain_sid()``). ++* The new ``$REALM_subid_range`` entry uses range type ``ipa-ad-trust`` ++ instead of range type ``ipa-local-subid`` for backwards compatibility ++ with older SSSD clients, see ++ [SSSD #5571](https://github.com/SSSD/sssd/issues/5571). ++* Shared DNA configuration entries in ``cn=dna,cn=ipa,cn=etc,$SUFFIX`` ++ are automatically removed by existing code. Server and replication ++ plug-ins search and delete entries by ``dnaHostname`` attribute. ++ ++### TODO ++ ++* enable configuration for ``dnaStepAttr`` ++* remove ``fake_dna_plugin`` hack from ``baseuser`` plug-in. ++* add custom range type for idranges and teach AD trust, sidgen, and ++ range overlap check code to deal with new range type. +diff --git a/freeipa.spec.in b/freeipa.spec.in +index ae4af099f39641a9f5163d61cfb37e1c3afb6f4b..044e3559975c399f6697d4da94b5a059eb5b407c 100755 +--- a/freeipa.spec.in ++++ b/freeipa.spec.in +@@ -1361,6 +1361,7 @@ fi + %{_libexecdir}/ipa/ipa-pki-wait-running + %{_libexecdir}/ipa/ipa-otpd + %{_libexecdir}/ipa/ipa-print-pac ++%{_libexecdir}/ipa/ipa-subids + %dir %{_libexecdir}/ipa/custodia + %attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-dmldap + %attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat +diff --git a/install/share/60basev2.ldif b/install/share/60basev2.ldif +index f253f30c91350c1358b24986806efea7768ea9ce..952755309d13d7df1806a52af351df250185b16d 100644 +--- a/install/share/60basev2.ldif ++++ b/install/share/60basev2.ldif +@@ -3,6 +3,7 @@ + ## Attributes: 2.16.840.1.113730.3.8.3 - V2 base attributres + ## ObjectClasses: 2.16.840.1.113730.3.8.4 - V2 base objectclasses + ## Attributes: 2.16.840.1.113730.3.8.23 - V4 base attributes ++## ObjectClasses: 2.16.840.1.113730.3.8.24 - V4 base objectclasses + ## + dn: cn=schema + attributeTypes: (2.16.840.1.113730.3.8.3.1 NAME 'ipaUniqueID' DESC 'Unique identifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' ) +diff --git a/install/share/60basev4.ldif b/install/share/60basev4.ldif +new file mode 100644 +index 0000000000000000000000000000000000000000..7f5173e593ff68a03d4005957b1dc9b9eb489dc5 +--- /dev/null ++++ b/install/share/60basev4.ldif +@@ -0,0 +1,19 @@ ++## IPA Base OID: 2.16.840.1.113730.3.8 ++## ++## Attributes: 2.16.840.1.113730.3.8.23 - V4 base attributes ++## ObjectClasses: 2.16.840.1.113730.3.8.24 - V4 base objectclasses ++## ++dn: cn=schema ++# subordinate ids ++# range ceiling OIDs are reserved for future use (operational attribute?) ++# object class requires uidNumber but does not subclass posixAccount so we ++# can re-use the object class in idview overrides later. ++attributeTypes: ( 2.16.840.1.113730.3.8.23.6 NAME 'ipaSubUidNumber' DESC 'Numerical subordinate user ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.7 NAME 'ipaSubUidCount' DESC 'Subordinate user ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++# attributeTypes: ( 2.16.840.1.113730.3.8.23.8 NAME 'ipaSubUidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.9 NAME 'ipaSubGidNumber' DESC 'Numerical subordinate group ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.10 NAME 'ipaSubGidCount' DESC 'Subordinate group ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++# attributeTypes: ( 2.16.840.1.113730.3.8.23.11 NAME 'ipaSubGidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++objectClasses: (2.16.840.1.113730.3.8.24.2 NAME 'ipaSubordinateUid' DESC 'Subordinate uids for users, see subuid(5)' SUP top AUXILIARY MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount ) X-ORIGIN 'IPA v4.9') ++objectClasses: (2.16.840.1.113730.3.8.24.3 NAME 'ipaSubordinateGid' DESC 'Subordinate gids for users, see subgid(5)' SUP top AUXILIARY MUST ( uidNumber $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9') ++objectClasses: (2.16.840.1.113730.3.8.24.4 NAME 'ipaSubordinateId' DESC 'Subordinate uid and gid for users' SUP top AUXILIARY MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9') +diff --git a/install/share/Makefile.am b/install/share/Makefile.am +index 0f1a6975fc3394316769295e67ac3c2e05ee9cee..e0fe4b7d1756bd05f060a92ab52f910b4bd3adc8 100644 +--- a/install/share/Makefile.am ++++ b/install/share/Makefile.am +@@ -16,6 +16,7 @@ dist_app_DATA = \ + 60ipaconfig.ldif \ + 60basev2.ldif \ + 60basev3.ldif \ ++ 60basev4.ldif \ + 60ipadns.ldif \ + 60ipapk11.ldif \ + 60certificate-profiles.ldif \ +diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif +index 6a689798451e8cc072284065849f9a95635f8069..16f2ef822eaf56dd68d4140b22a607539645b151 100644 +--- a/install/share/bootstrap-template.ldif ++++ b/install/share/bootstrap-template.ldif +@@ -167,6 +167,12 @@ objectClass: nsContainer + objectClass: top + cn: posix-ids + ++dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX ++changetype: add ++objectClass: nsContainer ++objectClass: top ++cn: subordinate-ids ++ + dn: cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX + changetype: add + objectClass: nsContainer +@@ -476,6 +482,22 @@ ipaBaseID: $IDSTART + ipaIDRangeSize: $IDRANGE_SIZE + ipaRangeType: ipa-local + ++dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX ++changetype: add ++objectClass: top ++objectClass: ipaIDrange ++objectClass: ipaTrustedADDomainRange ++cn: ${REALM}_subid_range ++ipaBaseID: eval($SUBID_RANGE_START) ++ipaIDRangeSize: eval($SUBID_RANGE_SIZE) ++# HACK: RIDs to work around adtrust sidgen issue ++ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE) ++# 738065-838566 = IPA-SUB ++ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH ++# HACK: "ipa-local-subid" range type causes issues with older SSSD clients ++# see https://github.com/SSSD/sssd/issues/5571 ++ipaRangeType: ipa-ad-trust ++ + dn: cn=ca,$SUFFIX + changetype: add + objectClass: nsContainer +diff --git a/install/share/dna.ldif b/install/share/dna.ldif +index f4bff3691570eb1fe028b13b69d2cc175c7df174..649313e72fc58112865e5901125923b3704276b1 100644 +--- a/install/share/dna.ldif ++++ b/install/share/dna.ldif +@@ -16,6 +16,26 @@ dnaThreshold: 500 + dnaSharedCfgDN: cn=posix-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX + dnaExcludeScope: cn=provisioning,$SUFFIX + ++dn: cn=Subordinate IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config ++changetype: add ++objectclass: top ++objectclass: extensibleObject ++cn: Subordinate IDs ++dnaType: ipasubuidnumber ++dnaType: ipasubgidnumber ++dnaNextValue: eval($SUBID_RANGE_START) ++dnaMaxValue: eval($SUBID_RANGE_MAX) ++dnaMagicRegen: -1 ++dnaFilter: (objectClass=ipaSubordinateId) ++dnaScope: $SUFFIX ++dnaThreshold: eval($SUBID_DNA_THRESHOLD) ++# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr ++# dnaStepAttr: ipaSubUidCount ++# dnaStepAttr: ipaSubGidCount ++# dnaStepAllowedValues: eval($SUBID_COUNT) ++dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX ++dnaExcludeScope: cn=provisioning,$SUFFIX ++ + # Enable the DNA plugin + dn: cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config + changetype: modify +diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am +index d6fbf9e3bc84bc475d7a797ff663df40da0a0efa..5f36742957505f6d695097c8aab6c73f9d59e146 100644 +--- a/install/tools/Makefile.am ++++ b/install/tools/Makefile.am +@@ -38,6 +38,7 @@ dist_noinst_DATA = \ + ipa-pki-retrieve-key.in \ + ipa-pki-wait-running.in \ + ipa-acme-manage.in \ ++ ipa-subids.in \ + $(NULL) + + nodist_sbin_SCRIPTS = \ +@@ -78,6 +79,7 @@ nodist_app_SCRIPTS = \ + ipa-httpd-pwdreader \ + ipa-pki-retrieve-key \ + ipa-pki-wait-running \ ++ ipa-subids \ + $(NULL) + + PYTHON_SHEBANG = \ +diff --git a/install/tools/ipa-subids.in b/install/tools/ipa-subids.in +new file mode 100644 +index 0000000000000000000000000000000000000000..5c7b9f8f788e3c230253e86151cff8234161909b +--- /dev/null ++++ b/install/tools/ipa-subids.in +@@ -0,0 +1,8 @@ ++#!/usr/bin/python3 ++# ++# Copyright (C) 2021 FreeIPA Contributors see COPYING for license ++# ++ ++from ipaserver.install.ipa_subids import IPASubids ++ ++IPASubids.run_cli() +diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js +index a4eb390b7d9ca0fb8f50245cfedec27ca2607cdd..5b49b0f6edbbbb6c802afb803a6406a0ab796c44 100644 +--- a/install/ui/src/freeipa/user.js ++++ b/install/ui/src/freeipa/user.js +@@ -259,6 +259,33 @@ return { + } + ] + }, ++ { ++ name: 'subordinate', ++ label: '@i18n:objects.subordinate.identity', ++ fields: [ ++ { ++ name: 'ipasubuidnumber', ++ label: '@i18n:objects.subordinate.subuidnumber', ++ read_only: true ++ }, ++ { ++ name: 'ipasubuidcount', ++ label: '@i18n:objects.subordinate.subuidcount', ++ read_only: true ++ ++ }, ++ { ++ name: 'ipasubgidnumber', ++ label: '@i18n:objects.subordinate.subgidnumber', ++ read_only: true ++ }, ++ { ++ name: 'ipasubgidcount', ++ label: '@i18n:objects.subordinate.subgidcount', ++ read_only: true ++ } ++ ] ++ }, + { + name: 'pwpolicy', + label: '@i18n:objects.pwpolicy.identity', +@@ -451,6 +478,16 @@ return { + enable_cond: ['is-locked'], + confirm_msg: '@i18n:objects.user.unlock_confirm' + }, ++ { ++ $factory: IPA.object_action, ++ name: 'auto_subid', ++ method: 'auto_subid', ++ label: '@i18n:objects.user.auto_subid', ++ needs_confirm: true, ++ hide_cond: ['preserved-user'], ++ enable_cond: ['no-subid'], ++ confirm_msg: '@i18n:objects.user.auto_subid_confirm' ++ }, + { + $type: 'automember_rebuild', + name: 'automember_rebuild', +@@ -461,12 +498,22 @@ return { + $type: 'cert_request', + hide_cond: ['preserved-user'], + title: '@i18n:objects.cert.issue_for_user' ++ }, ++ { ++ $factory: IPA.object_action, ++ name: 'auto_subid', ++ method: 'auto_subid', ++ label: '@i18n:objects.user.auto_subid', ++ needs_confirm: true, ++ hide_cond: ['preserved-user'], ++ enable_cond: ['no-subid'], ++ confirm_msg: '@i18n:objects.user.auto_subid_confirm' + } + ], + header_actions: [ + 'reset_password', 'enable', 'disable', 'stage', 'undel', + 'delete_active_user', 'delete', 'unlock', 'add_otptoken', +- 'automember_rebuild', 'request_cert' ++ 'automember_rebuild', 'request_cert', 'auto_subid' + ], + state: { + evaluators: [ +@@ -1159,6 +1206,10 @@ IPA.user.is_locked_evaluator = function(spec) { + } + } + ++ if (!user.ipasubuidnumber) { ++ that.state.push('no-subid'); ++ } ++ + that.notify_on_change(old_state); + }; + +diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update +index 6632f105a98276d0d7e63ce249ade15501c3b673..7f83ab9f04c565a59efdd2f41c3e7ee30f5da9c7 100644 +--- a/install/updates/20-indices.update ++++ b/install/updates/20-indices.update +@@ -272,6 +272,24 @@ add:nsIndexType: eq + add:nsIndexType: pres + add:nsIndexType: sub + ++dn: cn=ipaSubGidNumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config ++only:cn: ipaSubGidNumber ++default:objectClass: nsIndex ++default:objectClass: top ++default:nsSystemIndex: false ++add:nsIndexType: eq ++add:nsIndexType: pres ++add:nsMatchingRule: integerOrderingMatch ++ ++dn: cn=ipaSubUidNumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config ++only:cn: ipaSubUidNumber ++default:objectClass: nsIndex ++default:objectClass: top ++default:nsSystemIndex: false ++add:nsIndexType: eq ++add:nsIndexType: pres ++add:nsMatchingRule: integerOrderingMatch ++ + dn: cn=ipasudorunasgroup,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config + only:cn: ipasudorunasgroup + default:objectClass: nsIndex +diff --git a/install/updates/73-subid.update b/install/updates/73-subid.update +new file mode 100644 +index 0000000000000000000000000000000000000000..2aab3d445a33ae1663f81ca2d61b62ebc94aa37d +--- /dev/null ++++ b/install/updates/73-subid.update +@@ -0,0 +1,102 @@ ++# subordinate ids ++ ++# self-service RBAC ++dn: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX ++default:objectClass: groupofnames ++default:objectClass: nestedgroup ++default:objectClass: top ++default:cn: Subordinate ID Selfservice User ++default:description: User that can self-request subordiante ids ++# default: member: cn=ipausers,cn=groups,cn=accounts,$SUFFIX ++ ++dn: cn=Subordinate ID Selfservice Users,cn=privileges,cn=pbac,$SUFFIX ++default:objectClass: top ++default:objectClass: groupofnames ++default:objectClass: nestedgroup ++default:cn: Subordinate ID Selfservice Users ++default:description: Subordinate ID Selfservice User ++default:member: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX ++ ++dn: cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX ++default:objectClass: top ++default:objectClass: groupofnames ++default:objectClass: ipapermission ++default:cn: Self-service subordinate ID ++default:ipapermissiontype: SYSTEM ++default:member: cn=Subordinate ID Selfservice Users,cn=privileges,cn=pbac,$SUFFIX ++ ++# Administrator RBAC ++dn: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX ++default:objectClass: top ++default:objectClass: groupofnames ++default:objectClass: nestedgroup ++default:cn: Subordinate ID Administrators ++default:description: Subordinate ID Administrators ++default:member: cn=User Administrator,cn=roles,cn=accounts,$SUFFIX ++ ++dn: cn=Manage subordinate ID,cn=permissions,cn=pbac,$SUFFIX ++default:objectClass: top ++default:objectClass: groupofnames ++default:objectClass: ipapermission ++default:cn: Manage subordinate ID ++default:ipapermissiontype: SYSTEM ++default:member: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX ++ ++# ACIs (in domain database root so they also apply to staging area) ++# ++# - allow users to request new subid with DNA_MAGIC value, subid count=65536, ++# and subgid == subuid. ++# - allow user admins to set subids. count=65536 and subgid == subuid ++# properties are enforced as wel. ++# ++# The delete-when-empty check is required because IPA uses MOD_REPLACE to ++# set attributes, see https://github.com/389ds/389-ds-base/issues/4597. ++# ++# TODO: remove (ipasubuidnumber>=eval($SUBID_RANGE_START) from ++# self-service permission when 389-DS' DNA plugin supports dnaStepAttr and ++# fake_dna_plugin hack has been removed. ++# ++dn: $SUFFIX ++add: aci: (targetfilter = "(objectclass=posixaccount)")(targattrfilters = "add=objectClass:(|(objectClass=ipasubordinateid)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=eval($SUBID_RANGE_START))(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=eval($SUBID_RANGE_START))(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "selfservice: Add subordinate id";allow (write) userdn = "ldap:///self" and groupdn="ldap:///cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX";) ++add: aci: (targetfilter = "(objectclass=posixaccount)")(targattrfilters = "add=objectClass:(|(objectClass=ipasubordinateid)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=1)(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=1)(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "Add subordinate ids to any user";allow (write) groupdn="ldap:///cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX";) ++ ++# DNA plugin and idrange configuration ++dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX ++default: objectClass: nsContainer ++default: objectClass: top ++default: cn: subordinate-ids ++ ++dn: cn=Subordinate IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config ++default: objectclass: top ++default: objectclass: extensibleObject ++default: cn: Subordinate IDs ++default: dnaType: ipasubuidnumber ++default: dnaType: ipasubgidnumber ++default: dnaNextValue: eval($SUBID_RANGE_START) ++default: dnaMaxValue: eval($SUBID_RANGE_MAX) ++default: dnaMagicRegen: -1 ++default: dnaFilter: (objectClass=ipaSubordinateId) ++default: dnaScope: $SUFFIX ++default: dnaThreshold: eval($SUBID_DNA_THRESHOLD) ++# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr ++# default: dnaStepAttr: ipaSubUidCount ++# default: dnaStepAttr: ipaSubGidCount ++# default: dnaStepAllowedValues: eval($SUBID_COUNT) ++default: dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX ++default: dnaExcludeScope: cn=provisioning,$SUFFIX ++default: aci: (targetattr = "dnaNextRange || dnaNextValue || dnaMaxValue")(version 3.0;acl "permission:Modify DNA Range";allow (write) groupdn = "ldap:///cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX";) ++default: aci: (targetattr = "cn || dnaMaxValue || dnaNextRange || dnaNextValue || dnaThreshold || dnaType || objectclass")(version 3.0;acl "permission:Read DNA Range";allow (read, search, compare) groupdn = "ldap:///cn=Read DNA Range,cn=permissions,cn=pbac,$SUFFIX";) ++ ++dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX ++default: objectClass: top ++default: objectClass: ipaIDrange ++default: objectClass: ipaTrustedADDomainRange ++default: cn: ${REALM}_subid_range ++default: ipaBaseID: $SUBID_RANGE_START ++default: ipaIDRangeSize: $SUBID_RANGE_SIZE ++# HACK: RIDs to work around adtrust sidgen issue ++default: ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE) ++default: ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH ++# HACK: "ipa-local-subid" range type causes issues with older SSSD clients ++# see https://github.com/SSSD/sssd/issues/5571 ++default: ipaRangeType: ipa-ad-trust +diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am +index 5741805a65a09c4c00ea47bf437c8821373d1e80..d4f6acba0dc83e4692edd10b8a7617915bd49e84 100644 +--- a/install/updates/Makefile.am ++++ b/install/updates/Makefile.am +@@ -61,6 +61,7 @@ app_DATA = \ + 71-idviews-sasl-mapping.update \ + 72-domainlevels.update \ + 73-custodia.update \ ++ 73-subid.update \ + 73-winsync.update \ + 73-certmap.update \ + 75-user-trust-attributes.update \ +diff --git a/ipalib/constants.py b/ipalib/constants.py +index 79ea36f08cb0108a7434bc58cf0a764e9e15a7af..bee4c92fb39769d427e315116575f217924915be 100644 +--- a/ipalib/constants.py ++++ b/ipalib/constants.py +@@ -343,3 +343,16 @@ SOFTHSM_DNSSEC_TOKEN_LABEL = u'ipaDNSSEC' + # Apache's mod_ssl SSLVerifyDepth value (Maximum depth of CA + # Certificates in Client Certificate verification) + MOD_SSL_VERIFY_DEPTH = '5' ++ ++# subuid / subgid counts are hard-coded ++# An interval of 65536 uids/gids is required to map nobody (65534). ++SUBID_COUNT = 65536 ++ ++# upper half of uid_t (uint32_t) ++SUBID_RANGE_START = 2 ** 31 ++# theoretical max limit is UINT32_MAX-1 ((2 ** 32) - 2) ++# We use a smaller value to keep the topmost subid interval unused. ++SUBID_RANGE_MAX = (2 ** 32) - (2 * SUBID_COUNT) ++SUBID_RANGE_SIZE = SUBID_RANGE_MAX - SUBID_RANGE_START ++# threshold before DNA plugin requests a new range ++SUBID_DNA_THRESHOLD = 500 * SUBID_COUNT +diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py +index a7a403f37db13b7cccf74dff1b92b22529170b8a..24e90f3ecf5b4669f162e1bc68a33ef9d6094514 100644 +--- a/ipaserver/install/adtrustinstance.py ++++ b/ipaserver/install/adtrustinstance.py +@@ -36,6 +36,7 @@ from ipaserver.install import service + from ipaserver.install import installutils + from ipaserver.install.replication import wait_for_task + from ipalib import errors, api ++from ipalib.constants import SUBID_RANGE_START + from ipalib.util import normalize_zone + from ipapython.dn import DN + from ipapython import ipachangeconf +@@ -352,12 +353,19 @@ class ADTRUSTInstance(service.Service): + DN(api.env.container_ranges, self.suffix), + ldap.SCOPE_ONELEVEL, "(objectclass=ipaDomainIDRange)") + +- # Filter out ranges where RID base is already set +- no_rid_base_set = lambda r: not any(( +- r.single_value.get('ipaBaseRID'), +- r.single_value.get('ipaSecondaryBaseRID'))) ++ ranges_with_no_rid_base = [] ++ for entry in ranges: ++ sv = entry.single_value ++ if sv.get('ipaBaseRID') or sv.get('ipaSecondaryBaseRID'): ++ # skip range where RID base is already set ++ continue ++ if sv.get('ipaRangeType') == 'ipa-local-subid': ++ # ignore subid ranges ++ continue ++ ranges_with_no_rid_base.append(entry) + +- ranges_with_no_rid_base = [r for r in ranges if no_rid_base_set(r)] ++ logger.debug(repr(ranges)) ++ logger.debug(repr(ranges_with_no_rid_base)) + + # Return if no range is without RID base + if len(ranges_with_no_rid_base) == 0: +@@ -384,6 +392,17 @@ class ADTRUSTInstance(service.Service): + "They have to differ at least by %d." % size) + raise RuntimeError("RID bases too close.\n") + ++ # values above ++ if any( ++ v + size >= SUBID_RANGE_START ++ for v in (self.rid_base, self.secondary_rid_base) ++ ): ++ self.print_msg( ++ "Ceiling of primary or secondary base is larger than " ++ f"start of subordinate id range {SUBID_RANGE_START}." ++ ) ++ raise RuntimeError("RID bases overlap with SUBID range.\n") ++ + # Modify the range + # If the RID bases would cause overlap with some other range, + # this will be detected by ipa-range-check DS plugin +diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py +index 6033c04109f6278cb7b6015becd507b2b4699e02..ac9e131bb1b8c6ff8aff911cb257fbb03406d603 100644 +--- a/ipaserver/install/dsinstance.py ++++ b/ipaserver/install/dsinstance.py +@@ -23,7 +23,6 @@ from __future__ import print_function, absolute_import + import logging + import shutil + import os +-import time + import tempfile + import fnmatch + +@@ -46,6 +45,7 @@ from ipaserver.install import certs + from ipaserver.install import replication + from ipaserver.install import sysupgrade + from ipaserver.install import upgradeinstance ++from ipaserver.install import ldapupdate + from ipalib import api + from ipalib import errors + from ipalib import constants +@@ -66,6 +66,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif", + "60ipaconfig.ldif", + "60basev2.ldif", + "60basev3.ldif", ++ "60basev4.ldif", + "60ipapk11.ldif", + "60ipadns.ldif", + "60certificate-profiles.ldif", +@@ -214,6 +215,8 @@ class DsInstance(service.Service): + if realm_name: + self.suffix = ipautil.realm_to_suffix(self.realm) + self.serverid = ipaldap.realm_to_serverid(self.realm) ++ if self.domain is None: ++ self.domain = self.realm.lower() + self.__setup_sub_dict() + else: + self.suffix = DN() +@@ -497,34 +500,22 @@ class DsInstance(service.Service): + + def __setup_sub_dict(self): + server_root = find_server_root() +- try: +- idrange_size = self.idmax - self.idstart + 1 +- except TypeError: +- idrange_size = None +- self.sub_dict = dict( +- FQDN=self.fqdn, SERVERID=self.serverid, ++ self.sub_dict = ldapupdate.get_sub_dict( ++ realm=self.realm, ++ domain=self.domain, ++ suffix=self.suffix, ++ fqdn=self.fqdn, ++ idstart=self.idstart, ++ idmax=self.idmax, ++ ) ++ self.sub_dict.update( ++ DOMAIN_LEVEL=self.domainlevel, ++ SERVERID=self.serverid, + PASSWORD=self.dm_password, + RANDOM_PASSWORD=ipautil.ipa_generate_password(), +- SUFFIX=self.suffix, +- REALM=self.realm, USER=DS_USER, +- SERVER_ROOT=server_root, DOMAIN=self.domain, +- TIME=int(time.time()), IDSTART=self.idstart, +- IDMAX=self.idmax, HOST=self.fqdn, +- ESCAPED_SUFFIX=str(self.suffix), ++ USER=DS_USER, + GROUP=DS_GROUP, +- IDRANGE_SIZE=idrange_size, +- DOMAIN_LEVEL=self.domainlevel, +- MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL, +- MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL, +- STRIP_ATTRS=" ".join(replication.STRIP_ATTRS), +- EXCLUDES='(objectclass=*) $ EXCLUDE ' + +- ' '.join(replication.EXCLUDES), +- TOTAL_EXCLUDES='(objectclass=*) $ EXCLUDE ' + +- ' '.join(replication.TOTAL_EXCLUDES), +- DEFAULT_SHELL=platformconstants.DEFAULT_SHELL, +- DEFAULT_ADMIN_SHELL=platformconstants.DEFAULT_ADMIN_SHELL, +- SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT, +- SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER, ++ SERVER_ROOT=server_root, + ) + + def __create_instance(self): +diff --git a/ipaserver/install/ipa_subids.py b/ipaserver/install/ipa_subids.py +new file mode 100644 +index 0000000000000000000000000000000000000000..ac77a4008aec58d92c8b24df5e00b83c6998401f +--- /dev/null ++++ b/ipaserver/install/ipa_subids.py +@@ -0,0 +1,154 @@ ++# ++# Copyright (C) 2021 FreeIPA Contributors see COPYING for license ++# ++ ++import logging ++ ++from ipalib import api ++from ipalib import errors ++from ipalib.facts import is_ipa_configured ++from ipaplatform.paths import paths ++from ipapython.admintool import AdminTool, ScriptError ++from ipapython.dn import DN ++from ipaserver.plugins.baseldap import DNA_MAGIC ++ ++logger = logging.getLogger(__name__) ++ ++ ++class IPASubids(AdminTool): ++ command_name = "ipa-subids" ++ usage = "%prog [--group GROUP|--all-users]" ++ description = "Mass-assign subordinate ids to users" ++ ++ @classmethod ++ def add_options(cls, parser): ++ super(IPASubids, cls).add_options(parser, debug_option=True) ++ parser.add_option( ++ "--group", ++ dest="group", ++ action="store", ++ default=None, ++ help="Updates members of a user group.", ++ ) ++ parser.add_option( ++ "--all-users", ++ dest="all_users", ++ action="store_true", ++ default=False, ++ help="Update all users.", ++ ) ++ parser.add_option( ++ "--filter", ++ dest="user_filter", ++ action="store", ++ default="(!(nsaccountlock=TRUE))", ++ help="Additional raw LDAP filter (default: active users).", ++ ) ++ parser.add_option( ++ "--dry-run", ++ dest="dry_run", ++ action="store_true", ++ default=False, ++ help="Dry run mode.", ++ ) ++ ++ def validate_options(self, neends_root=False): ++ super().validate_options(needs_root=True) ++ opt = self.safe_options ++ ++ if opt.all_users and opt.group: ++ raise ScriptError("--group and --all-users are mutually exclusive") ++ if not opt.all_users and not opt.group: ++ raise ScriptError("Either --group or --all-users required") ++ ++ def get_group_info(self): ++ assert api.isdone("finalize") ++ group = self.safe_options.group ++ if group is None: ++ return None ++ try: ++ result = api.Command.group_show(group, no_members=True) ++ return result["result"] ++ except errors.NotFound: ++ raise ScriptError(f"Unknown users group '{group}'.") ++ ++ def make_filter(self, groupinfo, user_filter): ++ filters = [ ++ # only users with posixAccount ++ "(objectClass=posixAccount)", ++ # without subordinate ids ++ "(!(objectClass=ipaSubordinateId))", ++ ] ++ if groupinfo is not None: ++ filters.append( ++ self.ldap2.make_filter({"memberof": groupinfo["dn"]}) ++ ) ++ if user_filter: ++ filters.append(user_filter) ++ return self.ldap2.combine_filters(filters, self.ldap2.MATCH_ALL) ++ ++ def search_users(self, filters): ++ users_dn = DN(api.env.container_user, api.env.basedn) ++ attrs = ["objectclass", "uid", "uidnumber"] ++ ++ logger.debug("basedn: %s", users_dn) ++ logger.debug("attrs: %s", attrs) ++ logger.debug("filter: %s", filters) ++ ++ try: ++ entries = self.ldap2.get_entries( ++ base_dn=users_dn, ++ filter=filters, ++ attrs_list=attrs, ++ ) ++ except errors.NotFound: ++ logger.debug("No entries found") ++ return [] ++ else: ++ return entries ++ ++ def run(self): ++ if not is_ipa_configured(): ++ print("IPA is not configured.") ++ return 2 ++ ++ api.bootstrap(in_server=True, confdir=paths.ETC_IPA) ++ api.finalize() ++ api.Backend.ldap2.connect() ++ self.ldap2 = api.Backend.ldap2 ++ user_obj = api.Object["user"] ++ ++ dry_run = self.safe_options.dry_run ++ group_info = self.get_group_info() ++ filters = self.make_filter( ++ group_info, self.safe_options.user_filter ++ ) ++ ++ entries = self.search_users(filters) ++ total = len(entries) ++ logger.info("Found %i user(s) without subordinate ids", total) ++ ++ total = len(entries) ++ for i, entry in enumerate(entries, start=1): ++ logger.info( ++ " Processing user '%s' (%i/%i)", ++ entry.single_value["uid"], ++ i, ++ total ++ ) ++ user_obj.set_subordinate_ids( ++ self.ldap2, entry.dn, entry, DNA_MAGIC ++ ) ++ if not dry_run: ++ self.ldap2.update_entry(entry) ++ ++ if dry_run: ++ logger.info("Dry run mode, no user was modified") ++ else: ++ logger.info("Updated %s user(s)", total) ++ ++ return 0 ++ ++ ++if __name__ == "__main__": ++ IPASubids.run_cli() +diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py +index f21e5a5af465be37541b9fbdddaf800b73f80b71..d0516dc3028366df5d03a960866abe72601aa4b6 100644 +--- a/ipaserver/install/ldapupdate.py ++++ b/ipaserver/install/ldapupdate.py +@@ -32,9 +32,9 @@ import os + import fnmatch + import warnings + ++from pysss_murmur import murmurhash3 # pylint: disable=no-name-in-module + import six + +-from ipaserver.install import installutils + from ipapython import ipautil, ipaldap + from ipalib import errors + from ipalib import api, create_api +@@ -43,6 +43,7 @@ from ipaplatform.constants import constants as platformconstants + from ipaplatform.paths import paths + from ipaplatform.tasks import tasks + from ipapython.dn import DN ++from ipaserver.install import installutils, replication + + if six.PY3: + unicode = str +@@ -53,6 +54,54 @@ UPDATES_DIR=paths.UPDATES_DIR + UPDATE_SEARCH_TIME_LIMIT = 30 # seconds + + ++def get_sub_dict(realm, domain, suffix, fqdn, idstart=None, idmax=None): ++ """LDAP template substitution dict for installer and updater ++ """ ++ if idstart is None: ++ idrange_size = None ++ else: ++ idrange_size = idmax - idstart + 1 ++ ++ return dict( ++ REALM=realm, ++ DOMAIN=domain, ++ SUFFIX=suffix, ++ ESCAPED_SUFFIX=str(suffix), ++ FQDN=fqdn, ++ HOST=fqdn, ++ LIBARCH=paths.LIBARCH, ++ TIME=int(time.time()), ++ FIPS="#" if tasks.is_fips_enabled() else "", ++ # idstart, idmax, and idrange_size may be None ++ IDSTART=idstart, ++ IDMAX=idmax, ++ IDRANGE_SIZE=idrange_size, ++ SUBID_COUNT=constants.SUBID_COUNT, ++ SUBID_RANGE_START=constants.SUBID_RANGE_START, ++ SUBID_RANGE_SIZE=constants.SUBID_RANGE_SIZE, ++ SUBID_RANGE_MAX=constants.SUBID_RANGE_MAX, ++ SUBID_DNA_THRESHOLD=constants.SUBID_DNA_THRESHOLD, ++ DOMAIN_HASH=murmurhash3(domain, len(domain), 0xdeadbeef), ++ MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL, ++ MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL, ++ STRIP_ATTRS=" ".join(replication.STRIP_ATTRS), ++ EXCLUDES=( ++ '(objectclass=*) $ EXCLUDE ' + ' '.join(replication.EXCLUDES) ++ ), ++ TOTAL_EXCLUDES=( ++ '(objectclass=*) $ EXCLUDE ' ++ + ' '.join(replication.TOTAL_EXCLUDES) ++ ), ++ DEFAULT_SHELL=platformconstants.DEFAULT_SHELL, ++ DEFAULT_ADMIN_SHELL=platformconstants.DEFAULT_ADMIN_SHELL, ++ SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT, ++ SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER, ++ # uid / gid for autobind ++ NAMED_UID=platformconstants.NAMED_USER.uid, ++ NAMED_GID=platformconstants.NAMED_GROUP.gid, ++ ) ++ ++ + def connect(ldapi=False, realm=None, fqdn=None): + """Create a connection for updates""" + if ldapi: +@@ -284,35 +333,33 @@ class LDAPUpdate: + ldap_uri=self.ldapuri + ) + self.api.finalize() +- + self.create_connection() + ++ # get ipa-local domain idrange settings ++ domain_range = f"{self.api.env.realm}_id_range" ++ try: ++ result = self.api.Command.idrange_show(domain_range)["result"] ++ except errors.NotFound: ++ idstart = None ++ idmax = None ++ else: ++ idstart = int(result['ipabaseid'][0]) ++ idrange_size = int(result['ipaidrangesize'][0]) ++ idmax = idstart + idrange_size - 1 ++ ++ default_sub = get_sub_dict( ++ realm=api.env.realm, ++ domain=api.env.domain, ++ suffix=api.env.basedn, ++ fqdn=api.env.host, ++ idstart=idstart, ++ idmax=idmax, ++ ) + replication_plugin = ( + installutils.get_replication_plugin_name(self.conn.get_entry) + ) ++ default_sub["REPLICATION_PLUGIN"] = replication_plugin + +- default_sub = dict( +- REALM=api.env.realm, +- DOMAIN=api.env.domain, +- SUFFIX=api.env.basedn, +- ESCAPED_SUFFIX=str(api.env.basedn), +- FQDN=api.env.host, +- LIBARCH=paths.LIBARCH, +- TIME=int(time.time()), +- MIN_DOMAIN_LEVEL=str(constants.MIN_DOMAIN_LEVEL), +- MAX_DOMAIN_LEVEL=str(constants.MAX_DOMAIN_LEVEL), +- STRIP_ATTRS=" ".join(constants.REPL_AGMT_STRIP_ATTRS), +- EXCLUDES="(objectclass=*) $ EXCLUDE %s" % ( +- " ".join(constants.REPL_AGMT_EXCLUDES) +- ), +- TOTAL_EXCLUDES="(objectclass=*) $ EXCLUDE %s" % ( +- " ".join(constants.REPL_AGMT_TOTAL_EXCLUDES) +- ), +- SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT, +- SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER, +- FIPS="#" if tasks.is_fips_enabled() else "", +- REPLICATION_PLUGIN=replication_plugin, +- ) + for k, v in default_sub.items(): + self.sub_dict.setdefault(k, v) + +diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py +index 6035228f19ef8acaf4992490d5512c126881816d..12ff03c2302ff08aabb9369306965e0c125724f8 100644 +--- a/ipaserver/plugins/baseuser.py ++++ b/ipaserver/plugins/baseuser.py +@@ -17,9 +17,10 @@ + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + ++import random + import six + +-from ipalib import api, errors ++from ipalib import api, errors, output, constants + from ipalib import ( + Flag, Int, Password, Str, Bool, StrEnum, DateTime, DNParam) + from ipalib.parameters import Principal, Certificate +@@ -27,13 +28,13 @@ from ipalib.plugable import Registry + from .baseldap import ( + DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, + LDAPRetrieve, LDAPAddAttribute, LDAPModAttribute, LDAPRemoveAttribute, +- LDAPAddMember, LDAPRemoveMember, ++ LDAPQuery, LDAPAddMember, LDAPRemoveMember, + LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption, +- add_missing_object_class) ++ add_missing_object_class, DNA_MAGIC, pkey_to_value, entry_to_dict ++) + from ipaserver.plugins.service import (validate_realm, normalize_principal) + from ipalib.request import context + from ipalib import _ +-from ipalib.constants import PATTERN_GROUPUSER_NAME + from ipapython import kerberos + from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS + from ipapython.ipavalidate import Email +@@ -161,7 +162,7 @@ class baseuser(LDAPObject): + possible_objectclasses = [ + 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', + 'ipatokenradiusproxyuser', 'ipacertmapobject', +- 'ipantuserattrs' ++ 'ipantuserattrs', 'ipasubordinateid', + ] + disallow_object_classes = ['krbticketpolicyaux'] + permission_filter_objectclasses = ['posixaccount'] +@@ -175,13 +176,15 @@ class baseuser(LDAPObject): + 'krbprincipalexpiration', 'usercertificate;binary', + 'krbprincipalname', 'krbcanonicalname', + 'ipacertmapdata', 'ipantlogonscript', 'ipantprofilepath', +- 'ipanthomedirectory', 'ipanthomedirectorydrive' ++ 'ipanthomedirectory', 'ipanthomedirectorydrive', + ] + search_display_attributes = [ + 'uid', 'givenname', 'sn', 'homedirectory', 'krbcanonicalname', + 'krbprincipalname', 'loginshell', + 'mail', 'telephonenumber', 'title', 'nsaccountlock', + 'uidnumber', 'gidnumber', 'sshpubkeyfp', ++ 'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber', ++ 'ipasubgidcount', + ] + uuid_attribute = 'ipauniqueid' + attribute_members = { +@@ -198,7 +201,7 @@ class baseuser(LDAPObject): + + takes_params = ( + Str('uid', +- pattern=PATTERN_GROUPUSER_NAME, ++ pattern=constants.PATTERN_GROUPUSER_NAME, + pattern_errmsg='may only include letters, numbers, _, -, . and $', + maxlength=255, + cli_name='login', +@@ -429,6 +432,41 @@ class baseuser(LDAPObject): + 'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:', + 'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:'), + ), ++ Int( ++ 'ipasubuidnumber?', ++ label=_('SubUID range start'), ++ cli_name='subuid', ++ doc=_('Start value for subordinate user ID (subuid) range'), ++ minvalue=constants.SUBID_RANGE_START, ++ maxvalue=constants.SUBID_RANGE_MAX, ++ ), ++ Int( ++ 'ipasubuidcount?', ++ label=_('SubUID range size'), ++ cli_name='subuidcount', ++ doc=_('Subordinate user ID count'), ++ flags={'no_create', 'no_update', 'no_search'}, ++ minvalue=constants.SUBID_COUNT, ++ maxvalue=constants.SUBID_COUNT, ++ ), ++ Int( ++ 'ipasubgidnumber?', ++ label=_('SubGID range start'), ++ cli_name='subgid', ++ doc=_('Start value for subordinate group ID (subgid) range'), ++ flags={'no_create', 'no_update'}, ++ minvalue=constants.SUBID_RANGE_START, ++ maxvalue=constants.SUBID_RANGE_MAX, ++ ), ++ Int( ++ 'ipasubgidcount?', ++ label=_('SubGID range size'), ++ cli_name='subgidcount', ++ doc=_('Subordinate group ID count'), ++ flags={'no_create', 'no_update', 'no_search'}, ++ minvalue=constants.SUBID_COUNT, ++ maxvalue=constants.SUBID_COUNT, ++ ), + ) + + def normalize_and_validate_email(self, email, config=None): +@@ -526,6 +564,131 @@ class baseuser(LDAPObject): + except KeyError: + pass + ++ def handle_subordinate_ids(self, ldap, dn, entry_attrs): ++ """Handle ipaSubordinateId object class ++ """ ++ obj_classes = entry_attrs.get("objectclass") ++ new_subuid = entry_attrs.single_value.get("ipasubuidnumber") ++ new_subgid = entry_attrs.single_value.get("ipasubgidnumber") ++ ++ # entry has object class ipaSubordinateId ++ # default to auto-assigment of subuids ++ if ( ++ new_subuid is None ++ and obj_classes is not None ++ and self.has_objectclass(obj_classes, "ipasubordinateid") ++ ): ++ new_subuid = DNA_MAGIC ++ ++ # neither auto-assignment nor explicit assignment ++ if new_subuid is None: ++ # nothing to do ++ return False ++ ++ # enforce subuid == subgid ++ if new_subgid is not None and new_subgid != new_subuid: ++ raise errors.ValidationError( ++ name="ipasubgidnumber", ++ error=_("subgidnumber must be equal to subuidnumber") ++ ) ++ ++ self.set_subordinate_ids(ldap, dn, entry_attrs, new_subuid) ++ return True ++ ++ def set_subordinate_ids(self, ldap, dn, entry_attrs, subuid): ++ """Set subuid value of an entry ++ ++ Takes care of objectclass and sibbling attributes ++ """ ++ if "objectclass" in entry_attrs: ++ obj_classes = entry_attrs["objectclass"] ++ else: ++ _entry_attrs = ldap.get_entry(dn, ["objectclass"]) ++ entry_attrs["objectclass"] = _entry_attrs["objectclass"] ++ obj_classes = entry_attrs["objectclass"] ++ ++ if not self.has_objectclass(obj_classes, "ipasubordinateid"): ++ # could append ipasubordinategid and ipasubordinateuid, too ++ obj_classes.append("ipasubordinateid") ++ ++ # XXX HACK, remove later ++ if subuid == DNA_MAGIC: ++ subuid = self._fake_dna_plugin(ldap, dn, entry_attrs) ++ ++ entry_attrs["ipasubuidnumber"] = subuid ++ # enforice subuid == subgid for now ++ entry_attrs["ipasubgidnumber"] = subuid ++ # hard-coded constants ++ entry_attrs["ipasubuidcount"] = constants.SUBID_COUNT ++ entry_attrs["ipasubgidcount"] = constants.SUBID_COUNT ++ ++ def get_subid_match_candidate_filter( ++ self, ldap, *, subuid, subgid, extra_filters=(), offset=None, ++ ): ++ """Create LDAP filter to locate matching/overlapping subids ++ """ ++ if subuid is None and subgid is None: ++ raise ValueError("subuid and subgid are both None") ++ if offset is None: ++ # assumes that no subordinate count is larger than SUBID_COUNT ++ offset = constants.SUBID_COUNT - 1 ++ ++ class_filters = "(objectclass=ipasubordinateid)" ++ subid_filters = [] ++ if subuid is not None: ++ subid_filters.append( ++ ldap.combine_filters( ++ [ ++ f"(ipasubuidnumber>={subuid - offset})", ++ f"(ipasubuidnumber<={subuid + offset})", ++ ], ++ rules=ldap.MATCH_ALL ++ ) ++ ) ++ if subgid is not None: ++ subid_filters.append( ++ ldap.combine_filters( ++ [ ++ f"(ipasubgidnumber>={subgid - offset})", ++ f"(ipasubgidnumber<={subgid + offset})", ++ ], ++ rules=ldap.MATCH_ALL ++ ) ++ ) ++ ++ subid_filters = ldap.combine_filters( ++ subid_filters, rules=ldap.MATCH_ANY ++ ) ++ filters = [class_filters, subid_filters] ++ filters.extend(extra_filters) ++ return ldap.combine_filters(filters, rules=ldap.MATCH_ALL) ++ ++ def _fake_dna_plugin(self, ldap, dn, entry_attrs): ++ """XXX HACK, remove when 389-DS DNA plugin supports steps""" ++ uidnumber = entry_attrs.single_value.get("uidnumber") ++ if uidnumber is None: ++ entry = ldap.get_entry(dn, ["uidnumber"]) ++ uidnumber = entry.single_value["uidnumber"] ++ uidnumber = int(uidnumber) ++ ++ if uidnumber == DNA_MAGIC: ++ return ( ++ 3221225472 ++ + random.randint(1, 16382) * constants.SUBID_COUNT ++ ) ++ ++ if not hasattr(context, "idrange_ipabaseid"): ++ range_name = f"{self.api.env.realm}_id_range" ++ range = self.api.Command.idrange_show(range_name)["result"] ++ context.idrange_ipabaseid = int(range["ipabaseid"][0]) ++ ++ range_start = context.idrange_ipabaseid ++ ++ assert uidnumber >= range_start ++ assert uidnumber < range_start + 2**14 ++ ++ return (uidnumber - range_start) * constants.SUBID_COUNT + 2**31 ++ + + class baseuser_add(LDAPCreate): + """ +@@ -536,6 +699,7 @@ class baseuser_add(LDAPCreate): + assert isinstance(dn, DN) + set_krbcanonicalname(entry_attrs) + self.obj.convert_usercertificate_pre(entry_attrs) ++ self.obj.handle_subordinate_ids(ldap, dn, entry_attrs) + if entry_attrs.get('ipatokenradiususername', None): + add_missing_object_class(ldap, u'ipatokenradiusproxyuser', dn, + entry_attrs, update=False) +@@ -688,6 +852,7 @@ class baseuser_mod(LDAPUpdate): + + self.check_objectclass(ldap, dn, entry_attrs) + self.obj.convert_usercertificate_pre(entry_attrs) ++ self.obj.handle_subordinate_ids(ldap, dn, entry_attrs) + self.preserve_krbprincipalname_pre(ldap, entry_attrs, *keys, **options) + update_samba_attrs(ldap, dn, entry_attrs, **options) + +@@ -968,3 +1133,98 @@ class baseuser_remove_certmapdata(ModCertMapData, + LDAPRemoveAttribute): + __doc__ = _("Remove one or more certificate mappings from the user entry.") + msg_summary = _('Removed certificate mappings from user "%(value)s"') ++ ++ ++class baseuser_auto_subid(LDAPQuery): ++ __doc__ = _("Auto-assign subuid and subgid range to user entry") ++ ++ has_output = output.standard_entry ++ ++ def execute(self, cn, **options): ++ ldap = self.obj.backend ++ dn = self.obj.get_dn(cn) ++ ++ try: ++ entry_attrs = ldap.get_entry( ++ dn, ["objectclass", "ipasubuidnumber"] ++ ) ++ except errors.NotFound: ++ raise self.obj.handle_not_found(cn) ++ ++ if "ipasubuidnumber" in entry_attrs: ++ raise errors.AlreadyContainsValueError(attr="ipasubuidnumber") ++ ++ self.obj.set_subordinate_ids(ldap, dn, entry_attrs, subuid=DNA_MAGIC) ++ ldap.update_entry(entry_attrs) ++ ++ # fetch updated entry (use search display attribute to show subids) ++ if options.get('all', False): ++ attrs_list = ['*'] + self.obj.search_display_attributes ++ else: ++ attrs_list = set(self.obj.search_display_attributes) ++ attrs_list.update(entry_attrs.keys()) ++ if options.get('no_members', False): ++ attrs_list.difference_update(self.obj.attribute_members) ++ attrs_list = list(attrs_list) ++ ++ entry = self._exc_wrapper((cn,), options, ldap.get_entry)( ++ dn, attrs_list ++ ) ++ entry_attrs = entry_to_dict(entry, **options) ++ entry_attrs['dn'] = dn ++ ++ return dict(result=entry_attrs, value=pkey_to_value(cn, options)) ++ ++ ++class baseuser_match_subid(baseuser_find): ++ __doc__ = _("Match users by any subordinate uid in their range") ++ ++ _subid_attrs = { ++ "ipasubuidnumber", ++ "ipasubuidcount", ++ "ipasubgidnumber", ++ "ipasubgidcount" ++ } ++ ++ def get_options(self): ++ base_options = {p.name for p in self.obj.takes_params} ++ for option in super().get_options(): ++ if option.name == "ipasubuidnumber": ++ yield option.clone( ++ label=_('SubUID match'), ++ doc=_('Match value for subordinate user ID'), ++ required=True, ++ ) ++ elif option.name not in base_options: ++ # raw, version ++ yield option.clone() ++ ++ def pre_callback( ++ self, ldap, filters, attrs_list, base_dn, scope, *args, **options ++ ): ++ # search for candidates in range ++ # Code assumes that no subordinate count is larger than SUBID_COUNT ++ filters = self.obj.get_subid_match_candidate_filter( ++ ldap, subuid=options["ipasubuidnumber"], subgid=None, ++ ) ++ # always include subid attributes ++ for missing in self._subid_attrs.difference(attrs_list): ++ attrs_list.append(missing) ++ ++ return filters, base_dn, scope ++ ++ def post_callback(self, ldap, entries, truncated, *args, **options): ++ # filter out mismatches manually ++ osubuid = options["ipasubuidnumber"] ++ new_entries = [] ++ for entry in entries: ++ esubuid = int(entry.single_value["ipasubuidnumber"]) ++ esubcount = int(entry.single_value["ipasubuidcount"]) ++ minsubuid = esubuid ++ maxsubuid = esubuid + esubcount - 1 ++ if minsubuid <= osubuid <= maxsubuid: ++ new_entries.append(entry) ++ ++ entries[:] = new_entries ++ ++ return truncated +diff --git a/ipaserver/plugins/idrange.py b/ipaserver/plugins/idrange.py +index 32b9c0c2d01b616d76505fc06fa9b6e5e209b234..3e486b8e27cfb12f2e4732fc1ee113f25dfbac5b 100644 +--- a/ipaserver/plugins/idrange.py ++++ b/ipaserver/plugins/idrange.py +@@ -205,6 +205,7 @@ class idrange(LDAPObject): + # The commented range types are planned but not yet supported + range_types = { + u'ipa-local': unicode(_('local domain range')), ++ # u'ipa-local-subid': unicode(_('local domain subid range')), + # u'ipa-ad-winsync': unicode(_('Active Directory winsync range')), + u'ipa-ad-trust': unicode(_('Active Directory domain range')), + u'ipa-ad-trust-posix': unicode(_('Active Directory trust range with ' +@@ -221,10 +222,14 @@ class idrange(LDAPObject): + Int('ipabaseid', + cli_name='base_id', + label=_("First Posix ID of the range"), ++ minvalue=1, ++ maxvalue=Int.MAX_UINT32 + ), + Int('ipaidrangesize', + cli_name='range_size', + label=_("Number of IDs in the range"), ++ minvalue=1, ++ maxvalue=Int.MAX_UINT32 + ), + Int('ipabaserid?', + cli_name='rid_base', +@@ -669,7 +674,10 @@ class idrange_mod(LDAPUpdate): + except errors.NotFound: + raise self.obj.handle_not_found(*keys) + +- if old_attrs['iparangetype'][0] == 'ipa-local': ++ if ( ++ old_attrs['iparangetype'][0] in {'ipa-local', 'ipa-local-subid'} ++ or old_attrs['cn'][0] == f'{self.api.env.realm}_subid_range' ++ ): + raise errors.ExecutionError( + message=_('This command can not be used to change ID ' + 'allocation for local IPA domain. Run ' +diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py +index 70164eb8654d211523c98722a02b77ee13eb0009..199838b199eb4cdabf597bd34d571d05547fd32e 100644 +--- a/ipaserver/plugins/internal.py ++++ b/ipaserver/plugins/internal.py +@@ -1547,6 +1547,13 @@ class i18n_messages(Command): + "Drive to mount a home directory" + ), + }, ++ "subordinate": { ++ "identity": _("Subordinate user and group id"), ++ "subuidnumber": _("Subordinate user id"), ++ "subuidcount": _("Subordinate user id count"), ++ "subgidnumber": _("Subordinate group id"), ++ "subgidcount": _("Subordinate group id count"), ++ }, + "trustconfig": { + "options": _("Options"), + }, +@@ -1570,6 +1577,11 @@ class i18n_messages(Command): + "add_into_sudo": _( + "Add user '${primary_key}' into sudo rules" + ), ++ "auto_subid": _("Auto assign subordinate ids"), ++ "auto_subid_confirm": _( ++ "Are you sure you want to auto-assign a subordinate id " ++ "to user ${object}?" ++ ), + "contact": _("Contact Settings"), + "delete_mode": _("Delete mode"), + "employee": _("Employee Information"), +diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py +index e4ee572b236c288fd7dcf1d44c5adf1f836f63aa..f89b3ad5d9c994fe1ceb3da560fde7cc5bf5155a 100644 +--- a/ipaserver/plugins/user.py ++++ b/ipaserver/plugins/user.py +@@ -50,7 +50,10 @@ from .baseuser import ( + baseuser_add_principal, + baseuser_remove_principal, + baseuser_add_certmapdata, +- baseuser_remove_certmapdata) ++ baseuser_remove_certmapdata, ++ baseuser_auto_subid, ++ baseuser_match_subid, ++) + from .idviews import remove_ipaobject_overrides + from ipalib.plugable import Registry + from .baseldap import ( +@@ -202,6 +205,8 @@ class user(baseuser): + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass', ++ 'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber', ++ 'ipasubgidcount', + }, + 'fixup_function': fix_addressbook_permission_bindrule, + }, +@@ -1306,3 +1311,13 @@ class user_add_principal(baseuser_add_principal): + class user_remove_principal(baseuser_remove_principal): + __doc__ = _('Remove principal alias from the user entry') + msg_summary = _('Removed aliases from user "%(value)s"') ++ ++ ++@register() ++class user_auto_subid(baseuser_auto_subid): ++ __doc__ = baseuser_auto_subid.__doc__ ++ ++ ++@register() ++class user_match_subid(baseuser_match_subid): ++ __doc__ = baseuser_match_subid.__doc__ +diff --git a/ipatests/prci_definitions/gating.yaml b/ipatests/prci_definitions/gating.yaml +index a66b56ad8f62a458e9cc240440e7d222c32c599f..6ddd155c9967fa248581a59c68dfe547a34be623 100644 +--- a/ipatests/prci_definitions/gating.yaml ++++ b/ipatests/prci_definitions/gating.yaml +@@ -298,3 +298,15 @@ jobs: + template: *ci-ipa-4-9-latest + timeout: 3600 + topology: *master_1repl ++ ++ fedora-latest-ipa-4-9/test_subids: ++ requires: [fedora-latest-ipa-4-9/build] ++ priority: 100 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-9/build_url}' ++ test_suite: test_integration/test_subids.py ++ template: *ci-ipa-4-9-latest ++ timeout: 3600 ++ topology: *master_1repl +diff --git a/ipatests/test_integration/test_subids.py b/ipatests/test_integration/test_subids.py +new file mode 100644 +index 0000000000000000000000000000000000000000..b462f22ac067f3e1e97ef3f6d63d4e14e4ae79af +--- /dev/null ++++ b/ipatests/test_integration/test_subids.py +@@ -0,0 +1,201 @@ ++# ++# Copyright (C) 2021 FreeIPA Contributors see COPYING for license ++# ++ ++"""Tests for subordinate ids ++""" ++import os ++ ++from ipalib.constants import SUBID_COUNT, SUBID_RANGE_START, SUBID_RANGE_MAX ++from ipaplatform.paths import paths ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++ ++ ++class TestSubordinateId(IntegrationTest): ++ num_replicas = 0 ++ topology = "star" ++ ++ def _parse_result(self, result): ++ info = {} ++ for line in result.stdout_text.split("\n"): ++ line = line.strip() ++ if line: ++ if ":" not in line: ++ continue ++ k, v = line.split(":", 1) ++ k = k.strip() ++ v = v.strip() ++ try: ++ v = int(v, 10) ++ except ValueError: ++ if v == "FALSE": ++ v = False ++ elif v == "TRUE": ++ v = True ++ info.setdefault(k.lower(), []).append(v) ++ ++ for k, v in info.items(): ++ if len(v) == 1: ++ info[k] = v[0] ++ else: ++ info[k] = set(v) ++ return info ++ ++ def get_user(self, uid): ++ cmd = ["ipa", "user-show", "--all", "--raw", uid] ++ result = self.master.run_command(cmd) ++ return self._parse_result(result) ++ ++ def user_auto_subid(self, uid, **kwargs): ++ cmd = ["ipa", "user-auto-subid", uid] ++ return self.master.run_command(cmd, **kwargs) ++ ++ def test_auto_subid(self): ++ tasks.kinit_admin(self.master) ++ uid = "testuser_auto1" ++ tasks.user_add(self.master, uid) ++ info = self.get_user(uid) ++ assert "ipasubuidcount" not in info ++ ++ self.user_auto_subid(uid) ++ info = self.get_user(uid) ++ assert "ipasubuidcount" in info ++ ++ subuid = info["ipasubuidnumber"] ++ result = self.master.run_command( ++ ["ipa", "user-match-subid", f"--subuid={subuid}", "--raw"] ++ ) ++ match = self._parse_result(result) ++ assert match["uid"] == uid ++ assert match["ipasubuidnumber"] == info["ipasubuidnumber"] ++ assert match["ipasubuidnumber"] >= SUBID_RANGE_START ++ assert match["ipasubuidnumber"] <= SUBID_RANGE_MAX ++ assert match["ipasubuidcount"] == SUBID_COUNT ++ assert match["ipasubgidnumber"] == info["ipasubgidnumber"] ++ assert match["ipasubgidnumber"] == match["ipasubuidnumber"] ++ assert match["ipasubgidcount"] == SUBID_COUNT ++ ++ def test_ipa_subid_script(self): ++ tasks.kinit_admin(self.master) ++ ++ tool = os.path.join(paths.LIBEXEC_IPA_DIR, "ipa-subids") ++ users = [] ++ for i in range(1, 11): ++ uid = f"testuser_script{i}" ++ users.append(uid) ++ tasks.user_add(self.master, uid) ++ info = self.get_user(uid) ++ assert "ipasubuidcount" not in info ++ ++ cmd = [tool, "--verbose", "--group", "ipausers"] ++ self.master.run_command(cmd) ++ ++ for uid in users: ++ info = self.get_user(uid) ++ assert info["ipasubuidnumber"] >= SUBID_RANGE_START ++ assert info["ipasubuidnumber"] <= SUBID_RANGE_MAX ++ assert info["ipasubuidnumber"] == info["ipasubgidnumber"] ++ assert info["ipasubuidcount"] == SUBID_COUNT ++ assert info["ipasubuidcount"] == info["ipasubgidcount"] ++ ++ def test_subid_selfservice(self): ++ tasks.kinit_admin(self.master) ++ ++ uid = "testuser_selfservice1" ++ password = "Secret123" ++ role = "Subordinate ID Selfservice User" ++ ++ tasks.user_add(self.master, uid, password=password) ++ tasks.kinit_user( ++ self.master, uid, f"{password}\n{password}\n{password}\n" ++ ) ++ info = self.get_user(uid) ++ assert "ipasubuidcount" not in info ++ result = self.user_auto_subid(uid, raiseonerr=False) ++ assert result.returncode > 0 ++ ++ tasks.kinit_admin(self.master) ++ self.master.run_command( ++ ["ipa", "role-add-member", role, "--groups=ipausers"] ++ ) ++ ++ try: ++ tasks.kinit_user(self.master, uid, password) ++ self.user_auto_subid(uid) ++ info = self.get_user(uid) ++ assert "ipasubuidcount" in info ++ finally: ++ tasks.kinit_admin(self.master) ++ self.master.run_command( ++ ["ipa", "role-remove-member", role, "--groups=ipausers"] ++ ) ++ ++ def test_subid_useradmin(self): ++ tasks.kinit_admin(self.master) ++ ++ uid_useradmin = "testuser_usermgr_mgr1" ++ role = "User Administrator" ++ uid = "testuser_usermgr_user1" ++ password = "Secret123" ++ ++ # create user administrator ++ tasks.user_add(self.master, uid_useradmin, password=password) ++ # add user to user admin group ++ tasks.kinit_admin(self.master) ++ self.master.run_command( ++ ["ipa", "role-add-member", role, f"--users={uid_useradmin}"], ++ ) ++ # kinit as user admin ++ tasks.kinit_user( ++ self.master, ++ uid_useradmin, ++ f"{password}\n{password}\n{password}\n", ++ ) ++ # create new user as user admin ++ tasks.user_add(self.master, uid) ++ # assign new subid to user (with useradmin credentials) ++ self.user_auto_subid(uid) ++ ++ def test_subordinate_default_objclass(self): ++ tasks.kinit_admin(self.master) ++ ++ result = self.master.run_command( ++ ["ipa", "config-show", "--raw", "--all"] ++ ) ++ info = self._parse_result(result) ++ usercls = info["ipauserobjectclasses"] ++ assert "ipasubordinateid" not in usercls ++ ++ cmd = [ ++ "ipa", ++ "config-mod", ++ "--addattr", ++ "ipaUserObjectClasses=ipasubordinateid", ++ ] ++ self.master.run_command(cmd) ++ ++ uid = "testuser_usercls1" ++ tasks.user_add(self.master, uid) ++ info = self.get_user(uid) ++ assert "ipasubuidcount" in info ++ ++ def test_idrange_subid(self): ++ tasks.kinit_admin(self.master) ++ ++ range_name = f"{self.master.domain.realm}_subid_range" ++ ++ result = self.master.run_command( ++ ["ipa", "idrange-show", range_name, "--raw"] ++ ) ++ info = self._parse_result(result) ++ ++ # see https://github.com/SSSD/sssd/issues/5571 ++ assert info["iparangetype"] == "ipa-ad-trust" ++ assert info["ipabaseid"] == SUBID_RANGE_START ++ assert info["ipaidrangesize"] == SUBID_RANGE_MAX - SUBID_RANGE_START ++ assert info["ipabaserid"] < SUBID_RANGE_START ++ assert "ipasecondarybaserid" not in info ++ assert info["ipanttrusteddomainsid"].startswith( ++ "S-1-5-21-738065-838566-" ++ ) +diff --git a/ipatests/test_xmlrpc/test_range_plugin.py b/ipatests/test_xmlrpc/test_range_plugin.py +index c756bb7941d6c2acae89d44d6c89abc6b80ef5f7..ef683f84e97cbba61972f580e84e3587fda8c63a 100644 +--- a/ipatests/test_xmlrpc/test_range_plugin.py ++++ b/ipatests/test_xmlrpc/test_range_plugin.py +@@ -24,6 +24,7 @@ Test the `ipaserver/plugins/idrange.py` module, and XML-RPC in general. + import six + + from ipalib import api, errors, messages ++from ipalib import constants + from ipaplatform import services + from ipatests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid + from ipatests.test_xmlrpc import objectclasses +@@ -46,6 +47,12 @@ rid_shift = 0 + for idrange in api.Command['idrange_find']()['result']: + size = int(idrange['ipaidrangesize'][0]) + base_id = int(idrange['ipabaseid'][0]) ++ rtype = idrange['iparangetype'][0] ++ ++ if rtype == 'ipa-local-subid' or base_id == constants.SUBID_RANGE_START: ++ # ignore subordinate id range. It would push values beyond uint32_t. ++ # There is plenty of space below SUBUID_RANGE_START. ++ continue + + id_end = base_id + size + rid_end = 0 +-- +2.26.3 + diff --git a/0009-Redesign-subid-feature.patch b/0009-Redesign-subid-feature.patch new file mode 100644 index 0000000..da0b878 --- /dev/null +++ b/0009-Redesign-subid-feature.patch @@ -0,0 +1,2906 @@ +From f6fd0abeaa64d927f2d993235e97ac3009e64f2c Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Wed, 14 Apr 2021 15:21:18 +0200 +Subject: [PATCH] Redesign subid feature + +Subordinate ids are now handled by a new plugin class and stored in +separate entries in the cn=subids,cn=accounts subtree. + +Signed-off-by: Christian Heimes +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +--- + ACI.txt | 12 +- + API.txt | 153 +++-- + doc/designs/subordinate-ids.md | 358 ++++++++--- + install/share/60basev4.ldif | 25 +- + install/share/60ipaconfig.ldif | 5 +- + install/share/dna.ldif | 8 +- + install/share/memberof-conf.ldif | 4 +- + install/ui/src/freeipa/app.js | 1 + + .../ui/src/freeipa/navigation/menu_spec.js | 3 +- + install/ui/src/freeipa/serverconfig.js | 4 + + install/ui/src/freeipa/subid.js | 92 +++ + install/ui/src/freeipa/user.js | 108 ++-- + install/updates/10-uniqueness.update | 19 + + install/updates/20-indices.update | 12 + + install/updates/25-referint.update | 1 + + install/updates/73-subid.update | 28 +- + ipalib/constants.py | 6 +- + ipaserver/install/ipa_subids.py | 18 +- + .../plugins/update_dna_shared_config.py | 2 +- + ipaserver/plugins/baseldap.py | 10 +- + ipaserver/plugins/baseuser.py | 272 +------- + ipaserver/plugins/config.py | 8 +- + ipaserver/plugins/internal.py | 2 +- + ipaserver/plugins/subid.py | 608 ++++++++++++++++++ + ipaserver/plugins/user.py | 39 +- + ipatests/test_integration/test_subids.py | 182 +++--- + 26 files changed, 1389 insertions(+), 591 deletions(-) + create mode 100644 install/ui/src/freeipa/subid.js + create mode 100644 ipaserver/plugins/subid.py + +diff --git a/ACI.txt b/ACI.txt +index fce02a333b212de9b61f920515eed3e356b1391b..e985461cd1c10cc98d1080daa81cfd90e2433dbb 100644 +--- a/ACI.txt ++++ b/ACI.txt +@@ -61,7 +61,7 @@ aci: (targetattr = "cn || description || ipacertprofilestoreissued")(targetfilte + dn: cn=certprofiles,cn=ca,dc=ipa,dc=example + aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacertprofilestoreissued || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Read Certificate Profiles";allow (compare,read,search) userdn = "ldap:///all";) + dn: cn=ipaconfig,cn=etc,dc=ipa,dc=example +-aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipadomainresolutionorder || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxhostnamelength || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";) ++aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipadomainresolutionorder || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxhostnamelength || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserdefaultsubordinateid || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";) + dn: cn=costemplates,cn=accounts,dc=ipa,dc=example + aci: (targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Add Group Password Policy costemplate";allow (add) groupdn = "ldap:///cn=System: Add Group Password Policy costemplate,cn=permissions,cn=pbac,dc=ipa,dc=example";) + dn: cn=costemplates,cn=accounts,dc=ipa,dc=example +@@ -318,6 +318,14 @@ dn: cn=deleted users,cn=accounts,cn=provisioning,dc=ipa,dc=example + aci: (targetattr = "krblastpwdchange || krbpasswordexpiration || krbprincipalkey || userpassword")(target = "ldap:///uid=*,cn=deleted users,cn=accounts,cn=provisioning,dc=ipa,dc=example")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Reset Preserved User password";allow (read,search,write) groupdn = "ldap:///cn=System: Reset Preserved User password,cn=permissions,cn=pbac,dc=ipa,dc=example";) + dn: dc=ipa,dc=example + aci: (target_to = "ldap:///cn=users,cn=accounts,dc=ipa,dc=example")(target_from = "ldap:///cn=deleted users,cn=accounts,cn=provisioning,dc=ipa,dc=example")(targetfilter = "(objectclass=nsContainer)")(version 3.0;acl "permission:System: Undelete User";allow (moddn) groupdn = "ldap:///cn=System: Undelete User,cn=permissions,cn=pbac,dc=ipa,dc=example";) ++dn: cn=subids,cn=accounts,dc=ipa,dc=example ++aci: (targetattr = "description || ipaowner")(targetfilter = "(objectclass=ipasubordinateidentry)")(version 3.0;acl "permission:System: Manage Subordinate Ids";allow (write) groupdn = "ldap:///cn=System: Manage Subordinate Ids,cn=permissions,cn=pbac,dc=ipa,dc=example";) ++dn: cn=subids,cn=accounts,dc=ipa,dc=example ++aci: (targetattr = "createtimestamp || description || entryusn || ipaowner || ipasubgidcount || ipasubgidnumber || ipasubuidcount || ipasubuidnumber || ipauniqueid || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipasubordinateidentry)")(version 3.0;acl "permission:System: Read Subordinate Id Attributes";allow (compare,read,search) userdn = "ldap:///all";) ++dn: cn=subids,cn=accounts,dc=ipa,dc=example ++aci: (targetattr = "numsubordinates")(target = "ldap:///cn=subids,cn=accounts,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read Subordinate Id Count";allow (compare,read,search) userdn = "ldap:///all";) ++dn: cn=subids,cn=accounts,dc=ipa,dc=example ++aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(version 3.0;acl "permission:System: Remove Subordinate Ids";allow (delete) groupdn = "ldap:///cn=System: Remove Subordinate Ids,cn=permissions,cn=pbac,dc=ipa,dc=example";) + dn: cn=sudocmds,cn=sudo,dc=ipa,dc=example + aci: (targetfilter = "(objectclass=ipasudocmd)")(version 3.0;acl "permission:System: Add Sudo Command";allow (add) groupdn = "ldap:///cn=System: Add Sudo Command,cn=permissions,cn=pbac,dc=ipa,dc=example";) + dn: cn=sudocmds,cn=sudo,dc=ipa,dc=example +@@ -375,7 +383,7 @@ aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber + dn: dc=ipa,dc=example + aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=compat,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read User Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";) + dn: cn=users,cn=accounts,dc=ipa,dc=example +-aci: (targetattr = "ipasshpubkey || ipasubgidcount || ipasubgidnumber || ipasubuidcount || ipasubuidnumber || ipauniqueid || ipauserauthtype || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User IPA Attributes";allow (compare,read,search) userdn = "ldap:///all";) ++aci: (targetattr = "ipasshpubkey || ipauniqueid || ipauserauthtype || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User IPA Attributes";allow (compare,read,search) userdn = "ldap:///all";) + dn: cn=users,cn=accounts,dc=ipa,dc=example + aci: (targetattr = "krbcanonicalname || krblastpwdchange || krbpasswordexpiration || krbprincipalaliases || krbprincipalexpiration || krbprincipalname || krbprincipaltype || nsaccountlock")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Kerberos Attributes";allow (compare,read,search) userdn = "ldap:///all";) + dn: cn=users,cn=accounts,dc=ipa,dc=example +diff --git a/API.txt b/API.txt +index 262b4d6a72c7d7032a7027116f7a4f65aa620615..6c80028bfe8e9b739637fa11e015441efbf984b5 100644 +--- a/API.txt ++++ b/API.txt +@@ -1076,7 +1076,7 @@ args: 0,1,1 + option: Str('version?') + output: Output('result') + command: config_mod/1 +-args: 0,28,3 ++args: 0,29,3 + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) + option: Str('ca_renewal_master_server?', autofill=False) +@@ -1099,6 +1099,7 @@ option: Int('ipasearchtimelimit?', autofill=False, cli_name='searchtimelimit') + option: Str('ipaselinuxusermapdefault?', autofill=False) + option: Str('ipaselinuxusermaporder?', autofill=False) + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'disabled']) ++option: Bool('ipauserdefaultsubordinateid?', autofill=False, cli_name='user_default_subid') + option: Str('ipauserobjectclasses*', autofill=False, cli_name='userobjectclasses') + option: IA5Str('ipausersearchfields?', autofill=False, cli_name='usersearch') + option: Flag('raw', autofill=True, cli_name='raw', default=False) +@@ -4974,7 +4975,7 @@ output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') + command: stageuser_add/1 +-args: 1,46,3 ++args: 1,45,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -4992,7 +4993,6 @@ option: Str('givenname', cli_name='first') + option: Str('homedirectory?', cli_name='homedir') + option: Str('initials?', autofill=True) + option: Str('ipasshpubkey*', cli_name='sshpubkey') +-option: Int('ipasubuidnumber?', cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', cli_name='radius') + option: Str('ipatokenradiususername?', cli_name='radius_username') + option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -5099,14 +5099,13 @@ option: Str('in_group*', cli_name='in_groups') + option: Str('in_hbacrule*', cli_name='in_hbacrules') + option: Str('in_netgroup*', cli_name='in_netgroups') + option: Str('in_role*', cli_name='in_roles') ++option: Str('in_subid*', cli_name='in_subids') + option: Str('in_sudorule*', cli_name='in_sudorules') + option: Str('initials?', autofill=False) + option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') + option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') +-option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid') +-option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -5123,6 +5122,7 @@ option: Str('not_in_group*', cli_name='not_in_groups') + option: Str('not_in_hbacrule*', cli_name='not_in_hbacrules') + option: Str('not_in_netgroup*', cli_name='not_in_netgroups') + option: Str('not_in_role*', cli_name='not_in_roles') ++option: Str('not_in_subid*', cli_name='not_in_subids') + option: Str('not_in_sudorule*', cli_name='not_in_sudorules') + option: Str('ou?', autofill=False, cli_name='orgunit') + option: Str('pager*', autofill=False) +@@ -5148,7 +5148,7 @@ output: ListOfEntries('result') + output: Output('summary', type=[, ]) + output: Output('truncated', type=[]) + command: stageuser_mod/1 +-args: 1,52,3 ++args: 1,51,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -5170,7 +5170,6 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') + option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') +-option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -5263,6 +5262,100 @@ option: Str('version?') + output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') ++command: subid_add/1 ++args: 1,8,3 ++arg: Str('ipauniqueid?', cli_name='id') ++option: Str('addattr*', cli_name='addattr') ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Str('description?', cli_name='desc') ++option: Str('ipaowner', cli_name='owner') ++option: Int('ipasubuidnumber?', cli_name='subuid') ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Str('setattr*', cli_name='setattr') ++option: Str('version?') ++output: Entry('result') ++output: Output('summary', type=[, ]) ++output: PrimaryKey('value') ++command: subid_del/1 ++args: 1,2,3 ++arg: Str('ipauniqueid+', cli_name='id') ++option: Flag('continue', autofill=True, cli_name='continue', default=False) ++option: Str('version?') ++output: Output('result', type=[]) ++output: Output('summary', type=[, ]) ++output: ListOfPrimaryKeys('value') ++command: subid_find/1 ++args: 1,11,4 ++arg: Str('criteria?') ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Str('description?', autofill=False, cli_name='desc') ++option: Str('ipaowner?', autofill=False, cli_name='owner') ++option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid') ++option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') ++option: Str('ipauniqueid?', autofill=False, cli_name='id') ++option: Flag('pkey_only?', autofill=True, default=False) ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Int('sizelimit?', autofill=False) ++option: Int('timelimit?', autofill=False) ++option: Str('version?') ++output: Output('count', type=[]) ++output: ListOfEntries('result') ++output: Output('summary', type=[, ]) ++output: Output('truncated', type=[]) ++command: subid_generate/1 ++args: 0,4,3 ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Str('ipaowner?', cli_name='owner') ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Str('version?') ++output: Entry('result') ++output: Output('summary', type=[, ]) ++output: PrimaryKey('value') ++command: subid_match/1 ++args: 1,7,4 ++arg: Str('criteria?') ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Int('ipasubuidnumber', autofill=False, cli_name='subuid') ++option: Flag('pkey_only?', autofill=True, default=False) ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Int('sizelimit?', autofill=False) ++option: Int('timelimit?', autofill=False) ++option: Str('version?') ++output: Output('count', type=[]) ++output: ListOfEntries('result') ++output: Output('summary', type=[, ]) ++output: Output('truncated', type=[]) ++command: subid_mod/1 ++args: 1,8,3 ++arg: Str('ipauniqueid', cli_name='id') ++option: Str('addattr*', cli_name='addattr') ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Str('delattr*', cli_name='delattr') ++option: Str('description?', autofill=False, cli_name='desc') ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Flag('rights', autofill=True, default=False) ++option: Str('setattr*', cli_name='setattr') ++option: Str('version?') ++output: Entry('result') ++output: Output('summary', type=[, ]) ++output: PrimaryKey('value') ++command: subid_show/1 ++args: 1,4,3 ++arg: Str('ipauniqueid', cli_name='id') ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Flag('rights', autofill=True, default=False) ++option: Str('version?') ++output: Entry('result') ++output: Output('summary', type=[, ]) ++output: PrimaryKey('value') ++command: subid_stats/1 ++args: 0,3,2 ++option: Flag('all', autofill=True, cli_name='all', default=False) ++option: Flag('raw', autofill=True, cli_name='raw', default=False) ++option: Str('version?') ++output: Entry('result') ++output: Output('summary', type=[, ]) + command: sudocmd_add/1 + args: 1,7,3 + arg: Str('sudocmd', cli_name='command') +@@ -6062,7 +6155,7 @@ output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') + command: user_add/1 +-args: 1,47,3 ++args: 1,46,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -6079,7 +6172,6 @@ option: Str('givenname', cli_name='first') + option: Str('homedirectory?', cli_name='homedir') + option: Str('initials?', autofill=True) + option: Str('ipasshpubkey*', cli_name='sshpubkey') +-option: Int('ipasubuidnumber?', cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', cli_name='radius') + option: Str('ipatokenradiususername?', cli_name='radius_username') + option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -6161,16 +6253,6 @@ option: Str('version?') + output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') +-command: user_auto_subid/1 +-args: 1,4,3 +-arg: Str('uid', cli_name='login') +-option: Flag('all', autofill=True, cli_name='all', default=False) +-option: Flag('no_members', autofill=True, default=False) +-option: Flag('raw', autofill=True, cli_name='raw', default=False) +-option: Str('version?') +-output: Entry('result') +-output: Output('summary', type=[, ]) +-output: PrimaryKey('value') + command: user_del/1 + args: 1,3,3 + arg: Str('uid+', cli_name='login') +@@ -6213,14 +6295,13 @@ option: Str('in_group*', cli_name='in_groups') + option: Str('in_hbacrule*', cli_name='in_hbacrules') + option: Str('in_netgroup*', cli_name='in_netgroups') + option: Str('in_role*', cli_name='in_roles') ++option: Str('in_subid*', cli_name='in_subids') + option: Str('in_sudorule*', cli_name='in_sudorules') + option: Str('initials?', autofill=False) + option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir') + option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:']) + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') +-option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid') +-option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -6237,6 +6318,7 @@ option: Str('not_in_group*', cli_name='not_in_groups') + option: Str('not_in_hbacrule*', cli_name='not_in_hbacrules') + option: Str('not_in_netgroup*', cli_name='not_in_netgroups') + option: Str('not_in_role*', cli_name='not_in_roles') ++option: Str('not_in_subid*', cli_name='not_in_subids') + option: Str('not_in_sudorule*', cli_name='not_in_sudorules') + option: Bool('nsaccountlock?', autofill=False, cli_name='disabled', default=False) + option: Str('ou?', autofill=False, cli_name='orgunit') +@@ -6264,23 +6346,8 @@ output: Output('count', type=[]) + output: ListOfEntries('result') + output: Output('summary', type=[, ]) + output: Output('truncated', type=[]) +-command: user_match_subid/1 +-args: 1,8,4 +-arg: Str('criteria?') +-option: Flag('all', autofill=True, cli_name='all', default=False) +-option: Int('ipasubuidnumber', autofill=False, cli_name='subuid') +-option: Flag('no_members', autofill=True, default=True) +-option: Flag('pkey_only?', autofill=True, default=False) +-option: Flag('raw', autofill=True, cli_name='raw', default=False) +-option: Int('sizelimit?', autofill=False) +-option: Int('timelimit?', autofill=False) +-option: Str('version?') +-output: Output('count', type=[]) +-output: ListOfEntries('result') +-output: Output('summary', type=[, ]) +-output: Output('truncated', type=[]) + command: user_mod/1 +-args: 1,53,3 ++args: 1,52,3 + arg: Str('uid', cli_name='login') + option: Str('addattr*', cli_name='addattr') + option: Flag('all', autofill=True, cli_name='all', default=False) +@@ -6302,7 +6369,6 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d + option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script') + option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path') + option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey') +-option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid') + option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius') + option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username') + option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened']) +@@ -7138,6 +7204,15 @@ default: stageuser_remove_certmapdata/1 + default: stageuser_remove_manager/1 + default: stageuser_remove_principal/1 + default: stageuser_show/1 ++default: subid/1 ++default: subid_add/1 ++default: subid_del/1 ++default: subid_find/1 ++default: subid_generate/1 ++default: subid_match/1 ++default: subid_mod/1 ++default: subid_show/1 ++default: subid_stats/1 + default: sudocmd/1 + default: sudocmd_add/1 + default: sudocmd_del/1 +@@ -7216,12 +7291,10 @@ default: user_add_cert/1 + default: user_add_certmapdata/1 + default: user_add_manager/1 + default: user_add_principal/1 +-default: user_auto_subid/1 + default: user_del/1 + default: user_disable/1 + default: user_enable/1 + default: user_find/1 +-default: user_match_subid/1 + default: user_mod/1 + default: user_remove_cert/1 + default: user_remove_certmapdata/1 +diff --git a/doc/designs/subordinate-ids.md b/doc/designs/subordinate-ids.md +index 1b578667a8cfdda223af38a14d142c72a5d5c073..b3be3bcfa275e836e777f807d5210a4db6be0f79 100644 +--- a/doc/designs/subordinate-ids.md ++++ b/doc/designs/subordinate-ids.md +@@ -1,5 +1,9 @@ + # Central management of subordinate user and group ids + ++## OUTDATED ++ ++**The design document does not reflect new implementation yet!** ++ + Subordinate ids are a Linux Kernel feature to grant a user additional + user and group id ranges. Amongst others the feature can be used + by container runtime engies to implement rootless containers. +@@ -74,8 +78,10 @@ basic use cases. Some restrictions may be lifted in the future. + to the same value. + * counts are hard-coded to value 65536 + * once assigned subids cannot be removed +-* IPA does not support multiple subordinate id ranges. Contrary to +- ``/etc/subuid``, users are limited to one set of subordinate ids. ++* IPA does not support multiple subordinate id ranges, yet. Contrary to ++ ``/etc/subuid``, users are limited to one set of subordinate ids. The ++ limitation is implemented with a unique index on owner reference and ++ can be lifted in the future. + * subids are auto-assigned. Auto-assignment is currently emulated + until 389-DS has been extended to support DNA with step interval. + * subids are allocated from hard-coded range +@@ -118,20 +124,21 @@ to servers. The DNA plug-in guarantees uniqueness across servers. + ### LDAP schema extension + + The subordinate id feature introduces a new auxiliar object class +-``ipaSubordinateId`` with four required attributes ``ipaSubUidNumber``, +-``ipaSubUidCount``, ``ipaSubGidNumber``, and ``ipaSubGidCount``. The +-attributes with ``number`` suffix store the start value of the interval. +-The ``count`` attributes contain the size of the interval including the +-start value. The maximum subid is +-``ipaSubUidNumber + ipaSubUidCount - 1``. +- +-All four attributes are single-value ``INTEGER`` type with standard +-integer matching rules. OIDs ``2.16.840.1.113730.3.8.23.8`` and ++``ipaSubordinateId`` with five required attributes ``ipaOwner``, ++``ipaSubUidNumber``, ``ipaSubUidCount``, ``ipaSubGidNumber``, and ++``ipaSubGidCount``. The attributes with ``number`` suffix store the ++start value of the interval. The ``count`` attributes contain the ++size of the interval including the start value. The maximum subid is ++``ipaSubUidNumber + ipaSubUidCount - 1``. The ``ipaOwner`` attribute ++is a reference to the owning user. ++ ++All count and number attributes are single-value ``INTEGER`` type with ++standard integer matching rules. OIDs ``2.16.840.1.113730.3.8.23.8`` and + ``2.16.840.1.113730.3.8.23.11`` are reserved for future use. + + ```raw + attributeTypes: ( +- 2.16.840.1.113730.3.8.23.6 ++ 2.16.840.1.113730.3.8.23.7 + NAME 'ipaSubUidNumber' + DESC 'Numerical subordinate user ID (range start value)' + EQUALITY integerMatch ORDERING integerOrderingMatch +@@ -139,7 +146,7 @@ attributeTypes: ( + X-ORIGIN 'IPA v4.9' + ) + attributeTypes: ( +- 2.16.840.1.113730.3.8.23.7 ++ 2.16.840.1.113730.3.8.23.8 + NAME 'ipaSubUidCount' + DESC 'Subordinate user ID count (range size)' + EQUALITY integerMatch ORDERING integerOrderingMatch +@@ -147,7 +154,7 @@ attributeTypes: ( + X-ORIGIN 'IPA v4.9' + ) + attributeTypes: ( +- 2.16.840.1.113730.3.8.23.9 ++ 2.16.840.1.113730.3.8.23.10 + NAME 'ipaSubGidNumber' + DESC 'Numerical subordinate group ID (range start value)' + EQUALITY integerMatch ORDERING integerOrderingMatch +@@ -155,7 +162,7 @@ attributeTypes: ( + X-ORIGIN 'IPA v4.9' + ) + attributeTypes: ( +- 2.16.840.1.113730.3.8.23.10 ++ 2.16.840.1.113730.3.8.23.11 + NAME 'ipaSubGidCount' + DESC 'Subordinate group ID count (range size)' + EQUALITY integerMatch ORDERING integerOrderingMatch +@@ -164,51 +171,96 @@ attributeTypes: ( + ) + ``` + +-The ``ipaSubordinateId`` object class is an auxiliar subclass of ++The ``ipaOwner`` attribute is a single-value DN attribute that refers ++to user entry that owns the subordinate ID entry. The proposal does not ++reuse any of the existing attributes like ``owner`` or ``member``, ++because they are all multi-valued. ++ ++``` ++attributeTypes: ( ++ 2.16.840.1.113730.3.8.23.13 ++ NAME 'ipaOwner' ++ DESC 'Owner of an entry' ++ SUP distinguishedName ++ EQUALITY distinguishedNameMatch ++ SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ++ SINGLE-VALUE ++ X-ORIGIN 'IPA v4.9') ++``` ++ ++The ``ipaSubordinateId`` object class is an auxiliary subclass of + ``top`` and requires all four subordinate id attributes as well as +-``uidNumber``. It does not subclass ``posixAccount`` to make +-the class reusable in idview overrides later. ++``ipaOwner`` attribute`` + + ```raw + objectClasses: ( + 2.16.840.1.113730.3.8.24.4 + NAME 'ipaSubordinateId' + DESC 'Subordinate uid and gid for users' +- SUP top AUXILIARY +- MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) ++ SUP top ++ AUXILIARY ++ MUST ( ipaOwner $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) + X-ORIGIN 'IPA v4.9' + ) + ``` + + The ``ipaSubordinateGid`` and ``ipaSubordinateUid`` are defined for +-future use. IPA always assumes the presence of ``ipaSubordinateId`` and +-does not use these object classes. ++future use. IPA always assumes the presence of ``ipaSubordinateId``. + + ```raw + objectClasses: ( + 2.16.840.1.113730.3.8.24.2 + NAME 'ipaSubordinateUid' + DESC 'Subordinate uids for users, see subuid(5)' +- SUP top AUXILIARY +- MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount ) ++ SUP top ++ AUXILIARY ++ MUST ( ipaOwner $ ipaSubUidNumber $ ipaSubUidCount ) + X-ORIGIN 'IPA v4.9' + ) + objectClasses: ( + 2.16.840.1.113730.3.8.24.3 + NAME 'ipaSubordinateGid' + DESC 'Subordinate gids for users, see subgid(5)' +- SUP top AUXILIARY +- MUST ( uidNumber $ ipaSubGidNumber $ ipaSubGidCount ) ++ SUP top ++ AUXILIARY ++ MUST ( ipaOwner $ ipaSubGidNumber $ ipaSubGidCount ) + X-ORIGIN 'IPA v4.9' + ) + ``` + +-### Index ++Subordinate id entries have the structural object class ++``ipaSubordinateIdEntry`` and one or more of the auxiliary object ++classes ``ipaSubordinateId``, ``ipaSubordinateGid``, or ++``ipaSubordinateUid``. ``ipaUniqueId`` is used as a primary key (RDN). ++ ++```raw ++objectClasses: ( ++ 2.16.840.1.113730.3.8.24.5 ++ NAME 'ipaSubordinateIdEntry' ++ DESC 'Subordinate uid and gid entry' ++ SUP top ++ STRUCTURAL ++ MUST ( ipaUniqueId ) MAY ( description ) X-ORIGIN 'IPA v4.9' ++) ++``` ++ ++### cn=subids,cn=accounts,$SUFFIX ++ ++Subordiante ids and ACIs are stored in the new subtree ++``cn=subids,cn=accounts,$SUFFIX``. ++ ++### Index, integrity, memberOf + + The attributes ``ipaSubUidNumber`` and ``ipaSubGidNumber`` are index + for ``pres`` and ``eq`` with ``nsMatchingRule: integerOrderingMatch`` + to enable efficient ``=``, ``>=``, and ``<=`` searches. + ++The attribute ``ipaOwner`` is indexed for ``pres`` and ``eq``. This DN ++attribute is also checked for referential integrity and uniqueness ++within the ``cn=subids,cn=accounts,$SUFFIX`` subtree. The memberOf ++plugin creates back-references for ``ipaOwner`` references. ++ ++ + ### Distributed numeric assignment (DNA) plug-in extension + + Subordinate id auto-assignment requires an extension of 389-DS' +@@ -221,6 +273,85 @@ option ``dnaStepAttr`` (name is tentative) will tell the DNA plug-in + to use the value of entry attributes as step size. + + ++## IPA plugins and commands ++ ++The config plugin has a new option to enable or disable generation of ++subordinate id entries for new users: ++ ++```raw ++$ ipa config-mod --user-default-subid=true ++``` ++ ++Subordinate ids are managed by a new plugin class. The ``subid-add`` ++and ``subid-del`` commands are hidden from command line. New subordinate ++ids are generated and auto-assigned with ``subid-generate``. ++ ++```raw ++$ ipa help subid ++Topic commands: ++ subid-find Search for subordinate id. ++ subid-generate Generate and auto-assign subuid and subgid range to user entry ++ subid-match Match users by any subordinate uid in their range ++ subid-mod Modify a subordinate id. ++ subid-show Display information about a subordinate id. ++ subid-stats Subordinate id statistics ++``` ++ ++```raw ++$ ipa subid-generate --owner testuser9 ++----------------------------------------------------------- ++Added subordinate id "aa28f132-457c-488b-82e1-d123727e4f81" ++----------------------------------------------------------- ++ Unique ID: aa28f132-457c-488b-82e1-d123727e4f81 ++ Description: auto-assigned subid ++ Owner: testuser9 ++ SubUID range start: 3922132992 ++ SubUID range size: 65536 ++ SubGID range start: 3922132992 ++ SubGID range size: 65536 ++``` ++ ++ ++```raw ++$ ipa subid-find --owner testuser9 ++------------------------ ++1 subordinate id matched ++------------------------ ++ Unique ID: aa28f132-457c-488b-82e1-d123727e4f81 ++ Owner: testuser9 ++ SubUID range start: 3922132992 ++ SubUID range size: 65536 ++ SubGID range start: 3922132992 ++ SubGID range size: 65536 ++---------------------------- ++Number of entries returned 1 ++---------------------------- ++``` ++ ++```raw ++$ ipa -vv subid-stats ++... ++ipa: INFO: Response: { ++ "error": null, ++ "id": 0, ++ "principal": "admin@IPASUBID.TEST", ++ "result": { ++ "result": { ++ "assigned_subids": 20, ++ "baseid": 2147483648, ++ "dna_remaining": 4293394434, ++ "rangesize": 2147352576, ++ "remaining_subids": 65512 ++ }, ++ "summary": "65532 remaining subordinate id ranges" ++ }, ++ "version": "4.10.0.dev" ++} ++------------------------------------- ++65532 remaining subordinate id ranges ++------------------------------------- ++``` ++ + ## Permissions, Privileges, Roles + + ### Self-servive RBAC +@@ -246,6 +377,13 @@ be modified or deleted. + * Privilege: *Subordinate ID Administrators* + * default privilege role: *User Administrator* + ++### Managed permissions ++ ++* *System: Read Subordinate Id Attributes* (all authenticated users) ++* *System: Read Subordinate Id Count* (all authenticated usrs) ++* *System: Manage Subordinate Ids* (User Administrators) ++* *System: Remove Subordinate Ids* (User Administrators) ++ + + ## Workflows + +@@ -257,12 +395,12 @@ to assign subordinate ids to users. + + Users with *User Administrator* role and members of the *admins* group + have permission to auto-assign new subordinate ids to any user. Auto +-assignment can be performed with new ``user-auto-subid`` command on the ++assignment can be performed with new ``subid-generate`` command on the + command line or with the *Auto assign subordinate ids* action in the + *Actions* drop-down menu in the web UI. + + ```shell +-$ ipa user-auto-subid someusername ++$ ipa subid-generate --owner myusername + ``` + + ### Self-service for group members +@@ -279,27 +417,14 @@ $ ipa role-add-member "Subordinate ID Selfservice User" --groups=ipausers + ``` + + This allows members of ``ipausers`` to request subordinate ids with +-the ``user-auto-subid`` command or the *Auto assign subordinate ids* +-action in the web UI. +- +-```shell +-$ ipa user-auto-subid myusername +-``` +- +-### Auto assignment with user default object class +- +-Admins can also enable auto-assignment of subordinate ids for all new +-users by adding ``ipasubordinateid`` as a default user objectclass. +-This can be accomplished in the web UI under "IPA Server" / +-"Configuration" / "Default user objectclasses" or on the command line +-with: ++the ``subid-generate`` command or the *Auto assign subordinate ids* ++action in the web UI (**TODO** not implemented yet). The command picks ++the name of the current user principal automatically. + + ```shell +-$ ipa config-mod --addattr="ipaUserObjectClasses=ipasubordinateid" ++$ ipa subid-generate + ``` + +-**NOTE:** The objectclass must be written all lower case. +- + ### ipa-subid tool + + Finally IPA includes a new tool for mass-assignment of subordinate ids. +@@ -355,26 +480,25 @@ gid range. The new command ``user-match-subid`` can be used to find a + user by any subordinate id in their range. + + ```raw +-$ ipa user-match-subid --subuid=2153185287 +- User login: asmith +- First name: Alice +- Last name: Smith +- ... +- SubUID range start: 2153185280 ++$ ipa subid-match --subuid=2147549183 ++------------------------ ++1 subordinate id matched ++------------------------ ++ Name: asmith-auto ++ Owner: asmith ++ SubUID range start: 2147483648 + SubUID range size: 65536 +- SubGID range start: 2153185280 ++ SubGID range start: 2147483648 + SubGID range size: 65536 + ---------------------------- + Number of entries returned 1 + ---------------------------- +-$ ipa user-match-subid --subuid=2153185279 +- User login: bjones +- First name: Bob +- Last name: Jones +- ... +- SubUID range start: 2153119744 ++$ ipa user-match-subid --subuid=2147549184 ++ Name: bjones-auto ++ Owner: bjones ++ SubUID range start: 2147549184 + SubUID range size: 65536 +- SubGID range start: 2153119744 ++ SubGID range start: 2147549184 + SubGID range size: 65536 + ---------------------------- + Number of entries returned 1 +@@ -383,61 +507,54 @@ Number of entries returned 1 + + ## SSSD integration + +-* base: ``cn=accounts,$SUFFIX`` / ``cn=users,cn=accounts,$SUFFIX`` +-* scope: ``SCOPE_SUBTREE`` (2) / ``SCOPE_ONELEVEL`` (1) +-* user filter: should include ``(objectClass=posixAccount)`` +-* attributes: ``uidNumber ipaSubUidNumber ipaSubUidCount ipaSubGidNumber ipaSubGidCount`` +- +-SSSD can safely assume that only *user accounts* of type ``posixAccount`` +-have subordinate ids. In the first revision there are no other entries +-with subordinate ids. The ``posixAccount`` object class has ``uid`` +-(user login name) and ``uidNumber`` (numeric user id) as mandatory +-attributes. The ``uid`` attribute is guaranteed to be unique across +-all user accounts in an IPA domain. +- +-The ``uidNumber`` attribute is commonly unique, too. However it's +-technically possible that an administrator has assigned the same +-numeric user id to multiple users. Automatically assigned uid numbers +-don't conflict. SSSD should treat multiple users with same numeric +-user id as an error. ++* search base: ``cn=subids,cn=accounts,$SUFFIX`` ++* scope: ``SCOPE_ONELEVEL`` (1) ++* filter: ``(objectClass=ipaSubordinateId)`` ++* attributes: ``ipaOwner ipaSubUidNumber ipaSubUidCount ipaSubGidNumber ipaSubGidCount`` + + The attribute ``ipaSubUidNumber`` is always accompanied by + ``ipaSubUidCount`` and ``ipaSubGidNumber`` is always accompanied + by ``ipaSubGidCount``. In revision 1 the presence of + ``ipaSubUidNumber`` implies presence of the other three attributes. +-All four subordinate id attributes and ``uidNumber`` are single-value +-``INTEGER`` types. Any value outside of range of ``uint32_t`` must +-treated as invalid. SSSD will never see the DNA magic value ``-1`` +-in ``cn=accounts,$SUFFIX`` subtree. +- +-IPA recommends that SSSD simply extends its existing query for user +-accounts and requests the four subordinate attributes additionally to +-RFC 2307 attributes ``rfc2307_user_map``. SSSD can directly take the +-values and return them without further processing, e.g. +-``uidNumber:ipaSubUidNumber:ipaSubUidCount`` for ``/etc/subuid``. +- +-Filters for additional cases: +- +-* subuid filter (find user with subuid by numeric uid): +- ``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=$UID))``, +- ``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar +-* subuid enumeration filter: +- ``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=*))``, +- ``(objectClass=ipaSubordinateId)``, or similar +-* subgid filter (find user with subgid by numeric uid): +- ``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=$UID))``, +- ``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar +-* subgid enumeration filter: +- ``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=*))``, +- ``(objectClass=ipaSubordinateId)``, or similar ++All four subordinate id attributes are single-value ``INTEGER`` types. ++Any value outside of range of ``uint32_t`` must treated as invalid. ++SSSD will never see the DNA magic value ``-1`` in ++``cn=accounts,$SUFFIX`` subtree. In revision 1 each user subordinate ++id entry is assigned to exactly one user and each user has either 0 ++or 1 subid. ++ ++IPA recommends that SSSD uses LDAP deref controls for ``ipaOwner`` ++attribute to fetch ``uidNumber`` from the user object. ++ ++### Deref control example ++ ++```raw ++$ ldapsearch -L -E '!deref=ipaOwner:uid,uidNumber' \ ++ -b 'cn=subids,cn=accounts,dc=ipasubid,dc=test' \ ++ '(ipaOwner=uid=testuser10,cn=users,cn=accounts,dc=ipasubid,dc=test)' ++ ++dn: ipauniqueid=35c02c93-3799-4551-a355-ebbf042e431c,cn=subids,cn=accounts,dc=ipasubid,dc=test ++# control: 1.3.6.1.4.1.4203.666.5.16 false MIQAAABxMIQAAABrBAhpcGFPd25lcgQ3dWlk ++ PXRlc3R1c2VyMTAsY249dXNlcnMsY249YWNjb3VudHMsZGM9aXBhc3ViaWQsZGM9dGVzdKCEAAAAIj ++ CEAAAAHAQJdWlkTnVtYmVyMYQAAAALBAk2MjgwMDAwMTE= ++# ipaOwner: ;uid=testuser10,cn=users,cn=accounts,dc=ipasubid,dc=test ++ ++ipaOwner: uid=testuser10,cn=users,cn=accounts,dc=ipasubid,dc=test ++ipaUniqueID: 35c02c93-3799-4551-a355-ebbf042e431c ++description: auto-assigned subid ++objectClass: ipasubordinateidentry ++objectClass: ipasubordinategid ++objectClass: ipasubordinateuid ++objectClass: ipasubordinateid ++objectClass: top ++ipaSubUidNumber: 3434020864 ++ipaSubGidNumber: 3434020864 ++ipaSubUidCount: 65536 ++ipaSubGidCount: 65536 ++``` + + ## Implementation details + +-* The four subid attributes are not included in +- ``baseuser.default_attributes`` on purpose. The ``config-mod`` +- command does not permit removal of a user default objectclasses +- when the class is the last provider of an attribute in +- ``default_attributes``. + * ``ipaSubordinateId`` object class does not subclass the other two + object classes. LDAP supports + ``SUP ( ipaSubordinateGid $ ipaSubordinateUid )`` but 389-DS only +@@ -459,6 +576,13 @@ Filters for additional cases: + * Shared DNA configuration entries in ``cn=dna,cn=ipa,cn=etc,$SUFFIX`` + are automatically removed by existing code. Server and replication + plug-ins search and delete entries by ``dnaHostname`` attribute. ++* ``ipaSubordinateId`` entries no longer contains ``uidNumber`` ++ attribute. I considered to use CoS plugin to provide ``uidNumber`` ++ as virtual attribute. However it's not possible to ++ ``objectClass: cosIndirectDefinition`` with ++ ``cosIndirectSpecifier: ipaOwner`` and ++ ``cosAttribute: uidNumber override`` for the task. Indexes and ++ searches don't work with virtual attributes. + + ### TODO + +@@ -466,3 +590,23 @@ Filters for additional cases: + * remove ``fake_dna_plugin`` hack from ``baseuser`` plug-in. + * add custom range type for idranges and teach AD trust, sidgen, and + range overlap check code to deal with new range type. ++ ++#### user-del --preserve ++ ++Preserving a user with ``ipa user-del --preserve`` currently fails with ++an ObjectclassViolation error (err=65). The problem is caused by ++configuration of referential integrity postoperation plug-in. The ++plug-in excludes the subtree ++``nsslapd-pluginexcludeentryscope: cn=provisioning,$SUFFX``. Preserved ++users are moved into the staging area of the provisioning subtree. ++Since the ``ipaOwner`` DN target is now out of scope, the plug-in ++attempts to delete references. However ``ipaOwner`` is a required ++attribute, which triggers the objectclass violation. ++ ++Possible solutions ++ ++* Don't preserve subid entries ++* Implement preserve feature for subid entries and move subids of ++ preserved users into ++ ``cn=deleted subids,cn=accounts,cn=provisioning,$SUFFIX`` subtree. ++* Change ``nsslapd-pluginexcludeentryscope`` setting +diff --git a/install/share/60basev4.ldif b/install/share/60basev4.ldif +index 7f5173e593ff68a03d4005957b1dc9b9eb489dc5..c48b0c36a3012d86a74e12a77695e29cceafb698 100644 +--- a/install/share/60basev4.ldif ++++ b/install/share/60basev4.ldif +@@ -5,15 +5,16 @@ + ## + dn: cn=schema + # subordinate ids +-# range ceiling OIDs are reserved for future use (operational attribute?) +-# object class requires uidNumber but does not subclass posixAccount so we +-# can re-use the object class in idview overrides later. +-attributeTypes: ( 2.16.840.1.113730.3.8.23.6 NAME 'ipaSubUidNumber' DESC 'Numerical subordinate user ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') +-attributeTypes: ( 2.16.840.1.113730.3.8.23.7 NAME 'ipaSubUidCount' DESC 'Subordinate user ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') +-# attributeTypes: ( 2.16.840.1.113730.3.8.23.8 NAME 'ipaSubUidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') +-attributeTypes: ( 2.16.840.1.113730.3.8.23.9 NAME 'ipaSubGidNumber' DESC 'Numerical subordinate group ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') +-attributeTypes: ( 2.16.840.1.113730.3.8.23.10 NAME 'ipaSubGidCount' DESC 'Subordinate group ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') +-# attributeTypes: ( 2.16.840.1.113730.3.8.23.11 NAME 'ipaSubGidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') +-objectClasses: (2.16.840.1.113730.3.8.24.2 NAME 'ipaSubordinateUid' DESC 'Subordinate uids for users, see subuid(5)' SUP top AUXILIARY MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount ) X-ORIGIN 'IPA v4.9') +-objectClasses: (2.16.840.1.113730.3.8.24.3 NAME 'ipaSubordinateGid' DESC 'Subordinate gids for users, see subgid(5)' SUP top AUXILIARY MUST ( uidNumber $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9') +-objectClasses: (2.16.840.1.113730.3.8.24.4 NAME 'ipaSubordinateId' DESC 'Subordinate uid and gid for users' SUP top AUXILIARY MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9') ++# range ceiling OIDs are reserved for future use ++attributeTypes: ( 2.16.840.1.113730.3.8.23.7 NAME 'ipaSubUidNumber' DESC 'Numerical subordinate user ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.8 NAME 'ipaSubUidCount' DESC 'Subordinate user ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++# attributeTypes: ( 2.16.840.1.113730.3.8.23.9 NAME 'ipaSubUidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.10 NAME 'ipaSubGidNumber' DESC 'Numerical subordinate group ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.11 NAME 'ipaSubGidCount' DESC 'Subordinate group ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++# attributeTypes: ( 2.16.840.1.113730.3.8.23.12 NAME 'ipaSubGidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.13 NAME 'ipaOwner' DESC 'Owner of an entry' SUP distinguishedName EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++# attribute 2.16.840.1.113730.3.8.23.14 'ipaUserDefaultSubordinateId' is defined in 60ipaconfig.ldif ++objectClasses: (2.16.840.1.113730.3.8.24.2 NAME 'ipaSubordinateUid' DESC 'Subordinate uids for users, see subuid(5)' SUP top AUXILIARY MUST ( ipaOwner $ ipaSubUidNumber $ ipaSubUidCount ) X-ORIGIN 'IPA v4.9') ++objectClasses: (2.16.840.1.113730.3.8.24.3 NAME 'ipaSubordinateGid' DESC 'Subordinate gids for users, see subgid(5)' SUP top AUXILIARY MUST ( ipaOwner $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9') ++objectClasses: (2.16.840.1.113730.3.8.24.4 NAME 'ipaSubordinateId' DESC 'Subordinate uid and gid for users' SUP top AUXILIARY MUST ( ipaOwner $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9') ++objectClasses: (2.16.840.1.113730.3.8.24.5 NAME 'ipaSubordinateIdEntry' DESC 'Subordinate uid and gid entry' SUP top STRUCTURAL MUST ( ipaUniqueId ) MAY ( description ) X-ORIGIN 'IPA v4.9') +diff --git a/install/share/60ipaconfig.ldif b/install/share/60ipaconfig.ldif +index dbcf9ee603b361f6aac1413d0a53fff5561b6f89..f84b38ead1d70ff408f5669029f1517b0c98ecf1 100644 +--- a/install/share/60ipaconfig.ldif ++++ b/install/share/60ipaconfig.ldif +@@ -6,6 +6,7 @@ + ## ObjectClasses: 2.16.840.1.113730.3.8.2 - V1 + ## Attributes: 2.16.840.1.113730.3.8.3 - V2 + ## ObjectClasses: 2.16.840.1.113730.3.8.4 - V2 ++## Attributes: 2.16.840.1.113730.3.8.23 - V4 base attributes + dn: cn=schema + ############################################### + ## +@@ -45,11 +46,13 @@ attributeTypes: ( 2.16.840.1.113730.3.8.3.26 NAME 'ipaSELinuxUserMapDefault' DES + attributeTypes: ( 2.16.840.1.113730.3.8.3.27 NAME 'ipaSELinuxUserMapOrder' DESC 'Available SELinux user context ordering' EQUALITY caseIgnoreMatch ORDERING caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v3') + ## ipaMaxHostnameLength - maximum hostname length to allow + attributeTypes: ( 2.16.840.1.113730.3.8.1.28 NAME 'ipaMaxHostnameLength' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE) ++# ipaUserDefaultSubordinateId - if TRUE new user entries gain subordinate id by default ++attributeTypes: ( 2.16.840.1.113730.3.8.3.23.14 NAME 'ipaUserDefaultSubordinateId' DESC 'Enable adding user entries with subordinate id' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.9') + ############################################### + ## + ## ObjectClasses + ## + ## ipaGuiConfig - GUI config parameters objectclass +-objectClasses: ( 2.16.840.1.113730.3.8.2.1 NAME 'ipaGuiConfig' AUXILIARY MAY ( ipaUserSearchFields $ ipaGroupSearchFields $ ipaSearchTimeLimit $ ipaSearchRecordsLimit $ ipaCustomFields $ ipaHomesRootDir $ ipaDefaultLoginShell $ ipaDefaultPrimaryGroup $ ipaMaxUsernameLength $ ipaPwdExpAdvNotify $ ipaUserObjectClasses $ ipaGroupObjectClasses $ ipaDefaultEmailDomain $ ipaMigrationEnabled $ ipaCertificateSubjectBase $ ipaSELinuxUserMapDefault $ ipaSELinuxUserMapOrder $ ipaKrbAuthzData $ ipaMaxHostnameLength) ) ++objectClasses: ( 2.16.840.1.113730.3.8.2.1 NAME 'ipaGuiConfig' AUXILIARY MAY ( ipaUserSearchFields $ ipaGroupSearchFields $ ipaSearchTimeLimit $ ipaSearchRecordsLimit $ ipaCustomFields $ ipaHomesRootDir $ ipaDefaultLoginShell $ ipaDefaultPrimaryGroup $ ipaMaxUsernameLength $ ipaPwdExpAdvNotify $ ipaUserObjectClasses $ ipaGroupObjectClasses $ ipaDefaultEmailDomain $ ipaMigrationEnabled $ ipaCertificateSubjectBase $ ipaSELinuxUserMapDefault $ ipaSELinuxUserMapOrder $ ipaKrbAuthzData $ ipaMaxHostnameLength $ ipaUserDefaultSubordinateId) ) + ## ipaConfigObject - Generic config strings object holder + objectClasses: (2.16.840.1.113730.3.8.4.13 NAME 'ipaConfigObject' DESC 'generic config object for IPA' AUXILIARY MAY ( ipaConfigString ) X-ORIGIN 'IPA v2' ) +diff --git a/install/share/dna.ldif b/install/share/dna.ldif +index 649313e72fc58112865e5901125923b3704276b1..735faab8261feef59486f7c933b01c57ad511166 100644 +--- a/install/share/dna.ldif ++++ b/install/share/dna.ldif +@@ -29,12 +29,12 @@ dnaMagicRegen: -1 + dnaFilter: (objectClass=ipaSubordinateId) + dnaScope: $SUFFIX + dnaThreshold: eval($SUBID_DNA_THRESHOLD) +-# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr +-# dnaStepAttr: ipaSubUidCount +-# dnaStepAttr: ipaSubGidCount +-# dnaStepAllowedValues: eval($SUBID_COUNT) + dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX + dnaExcludeScope: cn=provisioning,$SUFFIX ++# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr ++# dnaIntervalAttr: ipasubuidcount ++# dnaIntervalAttr: ipasubgidcount ++# dnaMaxInterval: eval($SUBID_COUNT) + + # Enable the DNA plugin + dn: cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config +diff --git a/install/share/memberof-conf.ldif b/install/share/memberof-conf.ldif +index 79ad647e76feb6647524553a634c91c66ebd178e..3c22dfa9e52b8005bac88cd0962571c6fea18e7b 100644 +--- a/install/share/memberof-conf.ldif ++++ b/install/share/memberof-conf.ldif +@@ -8,4 +8,6 @@ memberofgroupattr: memberUser + - + add: memberofgroupattr + memberofgroupattr: memberHost +- ++- ++add: memberofgroupattr ++memberofgroupattr: ipaOwner +diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js +index 093737b8f923b41e8a1eabc90f66c6709991e239..9e0007528febb3d6641d152c72c51328d2d72cf6 100644 +--- a/install/ui/src/freeipa/app.js ++++ b/install/ui/src/freeipa/app.js +@@ -52,6 +52,7 @@ define([ + './serverconfig', + './service', + './stageuser', ++ './subid', + './sudo', + './trust', + './topology', +diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js +index 0c30459691d8f652dc35ccf74ed27fae7654020d..6ccd06919fbe04c7e8d2034ff7a1f644f373c607 100644 +--- a/install/ui/src/freeipa/navigation/menu_spec.js ++++ b/install/ui/src/freeipa/navigation/menu_spec.js +@@ -103,7 +103,8 @@ var nav = {}; + ] + } + ] +- } ++ }, ++ { entity: 'subid' } + ] + }, + { +diff --git a/install/ui/src/freeipa/serverconfig.js b/install/ui/src/freeipa/serverconfig.js +index bb26b107317ae12ee032fb767fcfc9060690c66e..9de0bab4b64abf1094e52dffda8add5349dc0e3c 100644 +--- a/install/ui/src/freeipa/serverconfig.js ++++ b/install/ui/src/freeipa/serverconfig.js +@@ -123,6 +123,10 @@ return { + $type: 'checkbox', + name: 'ipamigrationenabled' + }, ++ { ++ $type: 'checkbox', ++ name: 'ipauserdefaultsubordinateid' ++ }, + { + $type: 'multivalued', + name: 'ipauserobjectclasses' +diff --git a/install/ui/src/freeipa/subid.js b/install/ui/src/freeipa/subid.js +new file mode 100644 +index 0000000000000000000000000000000000000000..f286165070b08badf77cac6c30e93cab916c2acc +--- /dev/null ++++ b/install/ui/src/freeipa/subid.js +@@ -0,0 +1,92 @@ ++/* ++ * Copyright (C) 2021 FreeIPA Contributors see COPYING for license ++ */ ++ ++define([ ++ 'dojo/on', ++ './ipa', ++ './jquery', ++ './phases', ++ './reg', ++ './details', ++ './search', ++ './association', ++ './entity'], ++ function(on, IPA, $, phases, reg) { ++ ++var exp = IPA.subid = {}; ++ ++var make_spec = function() { ++return { ++ name: 'subid', ++ facets: [ ++ { ++ $type: 'search', ++ columns: [ ++ 'ipauniqueid', ++ 'ipaowner', ++ 'ipasubgidnumber', ++ 'ipasubuidnumber' ++ ] ++ }, ++ { ++ $type: 'details', ++ sections: [ ++ { ++ name: 'details', ++ fields: [ ++ 'ipauniqueid', ++ 'description', ++ { ++ name: 'ipaowner', ++ label: '@i18n:objects.subid.ipaowner', ++ title: '@mo-param:subid:ipaowner:label' ++ }, ++ { ++ name: 'ipasubgidnumber', ++ label: '@i18n:objects.subid.ipasubgidnumber', ++ title: '@mo-param:subid:ipasubgidnumber:label' ++ }, ++ { ++ name: 'ipasubgidcount', ++ label: '@i18n:objects.subid.ipasubgidcount', ++ title: '@mo-param:subid:ipasubgidcount:label' ++ }, ++ { ++ name: 'ipasubuidnumber', ++ label: '@i18n:objects.subid.ipasubuidnumber', ++ title: '@mo-param:subid:ipasubuidnumber:label' ++ }, ++ { ++ name: 'ipasubuidcount', ++ label: '@i18n:objects.subid.ipasubuidcount', ++ title: '@mo-param:subid:ipasubuidcount:label' ++ } ++ ] ++ } ++ ] ++ } ++ ], ++ adder_dialog: { ++ title: '@i18n:objects.subid.add', ++ method: 'generate', ++ fields: [ ++ { ++ $type: 'entity_select', ++ name: 'ipaowner', ++ other_entity: 'user', ++ other_field: 'uid' ++ } ++ ] ++ } ++};}; ++ ++exp.entity_spec = make_spec(); ++exp.register = function() { ++ var e = reg.entity; ++ e.register({type: 'subid', spec: exp.entity_spec}); ++}; ++phases.on('registration', exp.register); ++ ++return {}; ++}); +diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js +index 5b49b0f6edbbbb6c802afb803a6406a0ab796c44..56bb6f4feffb637d33a57aecf9a98f08d4639550 100644 +--- a/install/ui/src/freeipa/user.js ++++ b/install/ui/src/freeipa/user.js +@@ -259,33 +259,6 @@ return { + } + ] + }, +- { +- name: 'subordinate', +- label: '@i18n:objects.subordinate.identity', +- fields: [ +- { +- name: 'ipasubuidnumber', +- label: '@i18n:objects.subordinate.subuidnumber', +- read_only: true +- }, +- { +- name: 'ipasubuidcount', +- label: '@i18n:objects.subordinate.subuidcount', +- read_only: true +- +- }, +- { +- name: 'ipasubgidnumber', +- label: '@i18n:objects.subordinate.subgidnumber', +- read_only: true +- }, +- { +- name: 'ipasubgidcount', +- label: '@i18n:objects.subordinate.subgidcount', +- read_only: true +- } +- ] +- }, + { + name: 'pwpolicy', + label: '@i18n:objects.pwpolicy.identity', +@@ -478,16 +451,6 @@ return { + enable_cond: ['is-locked'], + confirm_msg: '@i18n:objects.user.unlock_confirm' + }, +- { +- $factory: IPA.object_action, +- name: 'auto_subid', +- method: 'auto_subid', +- label: '@i18n:objects.user.auto_subid', +- needs_confirm: true, +- hide_cond: ['preserved-user'], +- enable_cond: ['no-subid'], +- confirm_msg: '@i18n:objects.user.auto_subid_confirm' +- }, + { + $type: 'automember_rebuild', + name: 'automember_rebuild', +@@ -500,20 +463,15 @@ return { + title: '@i18n:objects.cert.issue_for_user' + }, + { +- $factory: IPA.object_action, +- name: 'auto_subid', +- method: 'auto_subid', +- label: '@i18n:objects.user.auto_subid', +- needs_confirm: true, ++ $type: 'subid_generate', + hide_cond: ['preserved-user'], +- enable_cond: ['no-subid'], +- confirm_msg: '@i18n:objects.user.auto_subid_confirm' ++ enable_cond: ['no-subid'] + } + ], + header_actions: [ + 'reset_password', 'enable', 'disable', 'stage', 'undel', + 'delete_active_user', 'delete', 'unlock', 'add_otptoken', +- 'automember_rebuild', 'request_cert', 'auto_subid' ++ 'automember_rebuild', 'request_cert', 'subid_generate' + ], + state: { + evaluators: [ +@@ -532,7 +490,8 @@ return { + IPA.user.self_service_other_user_evaluator, + IPA.user.preserved_user_evaluator, + IPA.user.is_locked_evaluator, +- IPA.cert.certificate_evaluator ++ IPA.cert.certificate_evaluator, ++ IPA.user.has_subid_evaluator + ], + summary_conditions: [ + { +@@ -593,6 +552,12 @@ return { + add_title: '@i18n:objects.user.add_into_sudo', + remove_method: 'remove_user', + remove_title: '@i18n:objects.user.remove_from_sudo' ++ }, ++ { ++ $type: 'association', ++ name: 'memberof_subid', ++ associator: IPA.serial_associator, ++ read_only: true + } + ], + standard_association_facets: { +@@ -1206,7 +1171,31 @@ IPA.user.is_locked_evaluator = function(spec) { + } + } + +- if (!user.ipasubuidnumber) { ++ that.notify_on_change(old_state); ++ }; ++ ++ return that; ++}; ++ ++IPA.user.has_subid_evaluator = function(spec) { ++ ++ spec = spec || {}; ++ spec.event = spec.event || 'post_load'; ++ ++ var that = IPA.state_evaluator(spec); ++ that.name = spec.name || 'has_subid_evaluator'; ++ that.param = spec.param || 'memberof_subid'; ++ ++ /** ++ * Evaluates if user already has a subid ++ */ ++ that.on_event = function(data) { ++ ++ var old_state = that.state; ++ that.state = []; ++ ++ var value = that.adapter.load(data); ++ if (value.length === 0) { + that.state.push('no-subid'); + } + +@@ -1216,6 +1205,30 @@ IPA.user.is_locked_evaluator = function(spec) { + return that; + }; + ++IPA.user.subid_generate_action = function(spec) { ++ ++ spec = spec || {}; ++ spec.name = spec.name || 'subid_generate'; ++ spec.label = spec.label || '@i18n:objects.user.auto_subid'; ++ spec.hide_cond = spec.hide_cond || ['preserved-user']; ++ spec.confirm_msg = spec.confirm_msg || '@i18n:objects.user.auto_subid_confirm'; ++ ++ var that = IPA.action(spec); ++ ++ that.execute_action = function(facet) { ++ ++ var subid_e = reg.entity.get('subid'); ++ var dialog = subid_e.get_dialog('add'); ++ dialog.open(); ++ if (!IPA.is_selfservice) { ++ var owner = facet.get_pkey(); ++ dialog.get_field('ipaowner').set_value([owner]); ++ } ++ }; ++ ++ return that; ++}; ++ + exp.entity_spec = make_spec(); + exp.register = function() { + var e = reg.entity; +@@ -1225,6 +1238,7 @@ exp.register = function() { + a.register('reset_password', IPA.user.reset_password_action); + a.register('add_otptoken', IPA.user.add_otptoken_action); + a.register('delete_active_user', IPA.user.delete_active_user_action); ++ a.register('subid_generate', IPA.user.subid_generate_action); + d.copy('password', 'user_password', { + factory: IPA.user.password_dialog, + pre_ops: [IPA.user.password_dialog_pre_op] +diff --git a/install/updates/10-uniqueness.update b/install/updates/10-uniqueness.update +index 77facba195cb5a1564818010f97afdd15d65a274..699de3b4d3305def5d81aeb945106b80eef0ef40 100644 +--- a/install/updates/10-uniqueness.update ++++ b/install/updates/10-uniqueness.update +@@ -109,3 +109,22 @@ default:nsslapd-plugin-depends-on-type: database + default:nsslapd-pluginId: NSUniqueAttr + default:nsslapd-pluginVersion: 1.1.0 + default:nsslapd-pluginVendor: Fedora Project ++ ++dn: cn=ipaSubordinateIdEntry ipaOwner uniqueness,cn=plugins,cn=config ++default:objectClass: top ++default:objectClass: nsSlapdPlugin ++default:objectClass: extensibleObject ++default:cn: ipaSubordinateIdEntry ipaOwner uniqueness ++default:nsslapd-pluginDescription: Enforce unique attribute values of ipaOwner ++default:nsslapd-pluginPath: libattr-unique-plugin ++default:nsslapd-pluginInitfunc: NSUniqueAttr_Init ++default:nsslapd-pluginType: preoperation ++default:nsslapd-pluginEnabled: on ++default:uniqueness-attribute-name: ipaOwner ++default:uniqueness-subtrees: cn=subids,cn=accounts,$SUFFIX ++default:uniqueness-across-all-subtrees: on ++default:uniqueness-subtree-entries-oc: ipaSubordinateIdEntry ++default:nsslapd-plugin-depends-on-type: database ++default:nsslapd-pluginId: NSUniqueAttr ++default:nsslapd-pluginVersion: 1.1.0 ++default:nsslapd-pluginVendor: Fedora Project +diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update +index 7f83ab9f04c565a59efdd2f41c3e7ee30f5da9c7..d6df5b37d0a9092e936d2c6002bce71ed876ec27 100644 +--- a/install/updates/20-indices.update ++++ b/install/updates/20-indices.update +@@ -263,6 +263,14 @@ default:nsSystemIndex: false + add:nsIndexType: eq + add:nsIndexType: pres + ++dn: cn=ipaOwner,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config ++only:cn: ipaOwner ++default:objectClass: nsIndex ++default:objectClass: top ++default:nsSystemIndex: false ++add:nsIndexType: eq ++add:nsIndexType: pres ++ + dn: cn=ipasudorunas,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config + only:cn: ipasudorunas + default:objectClass: nsIndex +@@ -425,6 +433,10 @@ default:nsSystemIndex: false + add:nsIndexType: eq + add:nsIndexType: pres + ++dn: cn=memberOf,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config ++only:cn: member ++add:nsIndexType: sub ++ + dn: cn=memberPrincipal,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config + only:cn: memberPrincipal + default:objectClass: nsIndex +diff --git a/install/updates/25-referint.update b/install/updates/25-referint.update +index 89bc5ef91b2f5c85ee3a4a2c7d112a0549d4e1da..b29926a4e3bd410115cde4ede3ed7886a412d300 100644 +--- a/install/updates/25-referint.update ++++ b/install/updates/25-referint.update +@@ -21,3 +21,4 @@ add: referint-membership-attr: ipamemberca + add: referint-membership-attr: ipamembercertprofile + add: referint-membership-attr: ipalocation + add: referint-membership-attr: membermanager ++add: referint-membership-attr: ipaowner +diff --git a/install/updates/73-subid.update b/install/updates/73-subid.update +index 2aab3d445a33ae1663f81ca2d61b62ebc94aa37d..1aa43822a8b8c220583b81e08d70b648ca594363 100644 +--- a/install/updates/73-subid.update ++++ b/install/updates/73-subid.update +@@ -1,5 +1,15 @@ + # subordinate ids + ++# create memberOf attributes for ipaOwner ++dn: cn=MemberOf Plugin,cn=plugins,cn=config ++add: memberofgroupattr: ipaOwner ++ ++# container ++dn: cn=subids,cn=accounts,$SUFFIX ++default: objectClass: top ++default: objectClass: nsContainer ++default: cn: subids ++ + # self-service RBAC + dn: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX + default:objectClass: groupofnames +@@ -56,9 +66,9 @@ default:member: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX + # self-service permission when 389-DS' DNA plugin supports dnaStepAttr and + # fake_dna_plugin hack has been removed. + # +-dn: $SUFFIX +-add: aci: (targetfilter = "(objectclass=posixaccount)")(targattrfilters = "add=objectClass:(|(objectClass=ipasubordinateid)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=eval($SUBID_RANGE_START))(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=eval($SUBID_RANGE_START))(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "selfservice: Add subordinate id";allow (write) userdn = "ldap:///self" and groupdn="ldap:///cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX";) +-add: aci: (targetfilter = "(objectclass=posixaccount)")(targattrfilters = "add=objectClass:(|(objectClass=ipasubordinateid)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=1)(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=1)(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "Add subordinate ids to any user";allow (write) groupdn="ldap:///cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX";) ++dn: cn=subids,cn=accounts,$SUFFIX ++add: aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(targetattr="description || ipaowner || ipauniqueid")(targattrfilters = "add=objectClass:(|(objectClass=top)(objectClass=ipasubordinateid)(objectClass=ipasubordinateidentry)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=eval($SUBID_RANGE_START))(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=eval($SUBID_RANGE_START))(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "selfservice: Add subordinate id";allow (add, write) userattr = "ipaowner#SELFDN" and groupdn="ldap:///cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX";) ++add: aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(targetattr="description || ipaowner || ipauniqueid")(targattrfilters = "add=objectClass:(|(objectClass=top)(objectClass=ipasubordinateid)(objectClass=ipasubordinateidentry)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=1)(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=1)(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "Add subordinate ids to any user";allow (add, write) groupdn="ldap:///cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX";) + + # DNA plugin and idrange configuration + dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX +@@ -78,14 +88,14 @@ default: dnaMagicRegen: -1 + default: dnaFilter: (objectClass=ipaSubordinateId) + default: dnaScope: $SUFFIX + default: dnaThreshold: eval($SUBID_DNA_THRESHOLD) +-# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr +-# default: dnaStepAttr: ipaSubUidCount +-# default: dnaStepAttr: ipaSubGidCount +-# default: dnaStepAllowedValues: eval($SUBID_COUNT) + default: dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX + default: dnaExcludeScope: cn=provisioning,$SUFFIX +-default: aci: (targetattr = "dnaNextRange || dnaNextValue || dnaMaxValue")(version 3.0;acl "permission:Modify DNA Range";allow (write) groupdn = "ldap:///cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX";) +-default: aci: (targetattr = "cn || dnaMaxValue || dnaNextRange || dnaNextValue || dnaThreshold || dnaType || objectclass")(version 3.0;acl "permission:Read DNA Range";allow (read, search, compare) groupdn = "ldap:///cn=Read DNA Range,cn=permissions,cn=pbac,$SUFFIX";) ++# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr ++# add: dnaIntervalAttr: ipasubuidcount ++# add: dnaIntervalAttr: ipasubgidcount ++# addifnew: dnaMaxInterval: eval($SUBID_COUNT) ++add: aci: (targetattr = "dnaNextRange || dnaNextValue || dnaMaxValue")(version 3.0;acl "permission:Modify DNA Range";allow (write) groupdn = "ldap:///cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX";) ++add: aci: (targetattr = "cn || dnaMaxValue || dnaNextRange || dnaNextValue || dnaThreshold || dnaType || objectclass")(version 3.0;acl "permission:Read DNA Range";allow (read, search, compare) groupdn = "ldap:///cn=Read DNA Range,cn=permissions,cn=pbac,$SUFFIX";) + + dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX + default: objectClass: top +diff --git a/ipalib/constants.py b/ipalib/constants.py +index bee4c92fb39769d427e315116575f217924915be..bff899ba64c75832e2037870ccbca4587458d97b 100644 +--- a/ipalib/constants.py ++++ b/ipalib/constants.py +@@ -131,6 +131,9 @@ DEFAULT_CONFIG = ( + ('container_ranges', DN(('cn', 'ranges'), ('cn', 'etc'))), + ('container_dna', DN(('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))), + ('container_dna_posix_ids', DN(('cn', 'posix-ids'), ('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))), ++ ('container_dna_subordinate_ids', DN( ++ ('cn', 'subordinate-ids'), ('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc') ++ )), + ('container_realm_domains', DN(('cn', 'Realm Domains'), ('cn', 'ipa'), ('cn', 'etc'))), + ('container_otp', DN(('cn', 'otp'))), + ('container_radiusproxy', DN(('cn', 'radiusproxy'))), +@@ -148,6 +151,7 @@ DEFAULT_CONFIG = ( + ('container_certmaprules', DN(('cn', 'certmaprules'), ('cn', 'certmap'))), + ('container_ca_renewal', + DN(('cn', 'ca_renewal'), ('cn', 'ipa'), ('cn', 'etc'))), ++ ('container_subids', DN(('cn', 'subids'), ('cn', 'accounts'))), + + # Ports, hosts, and URIs: + # Following values do not have any reasonable default. +@@ -355,4 +359,4 @@ SUBID_RANGE_START = 2 ** 31 + SUBID_RANGE_MAX = (2 ** 32) - (2 * SUBID_COUNT) + SUBID_RANGE_SIZE = SUBID_RANGE_MAX - SUBID_RANGE_START + # threshold before DNA plugin requests a new range +-SUBID_DNA_THRESHOLD = 500 * SUBID_COUNT ++SUBID_DNA_THRESHOLD = 500 +diff --git a/ipaserver/install/ipa_subids.py b/ipaserver/install/ipa_subids.py +index ac77a4008aec58d92c8b24df5e00b83c6998401f..2b33b667084f529fa50e2f11eeefda8a8927f68c 100644 +--- a/ipaserver/install/ipa_subids.py ++++ b/ipaserver/install/ipa_subids.py +@@ -10,7 +10,7 @@ from ipalib.facts import is_ipa_configured + from ipaplatform.paths import paths + from ipapython.admintool import AdminTool, ScriptError + from ipapython.dn import DN +-from ipaserver.plugins.baseldap import DNA_MAGIC ++from ipapython.version import API_VERSION + + logger = logging.getLogger(__name__) + +@@ -77,7 +77,7 @@ class IPASubids(AdminTool): + # only users with posixAccount + "(objectClass=posixAccount)", + # without subordinate ids +- "(!(objectClass=ipaSubordinateId))", ++ f"(!(memberOf=*,cn=subids,cn=accounts,{api.env.basedn}))", + ] + if groupinfo is not None: + filters.append( +@@ -89,7 +89,7 @@ class IPASubids(AdminTool): + + def search_users(self, filters): + users_dn = DN(api.env.container_user, api.env.basedn) +- attrs = ["objectclass", "uid", "uidnumber"] ++ attrs = ["objectclass", "uid"] + + logger.debug("basedn: %s", users_dn) + logger.debug("attrs: %s", attrs) +@@ -116,7 +116,7 @@ class IPASubids(AdminTool): + api.finalize() + api.Backend.ldap2.connect() + self.ldap2 = api.Backend.ldap2 +- user_obj = api.Object["user"] ++ subid_generate = api.Command.subid_generate + + dry_run = self.safe_options.dry_run + group_info = self.get_group_info() +@@ -136,11 +136,13 @@ class IPASubids(AdminTool): + i, + total + ) +- user_obj.set_subordinate_ids( +- self.ldap2, entry.dn, entry, DNA_MAGIC +- ) + if not dry_run: +- self.ldap2.update_entry(entry) ++ # TODO: check for duplicate entry (race condition) ++ # TODO: log new subid ++ subid_generate( ++ ipaowner=entry.single_value["uid"], ++ version=API_VERSION ++ ) + + if dry_run: + logger.info("Dry run mode, no user was modified") +diff --git a/ipaserver/install/plugins/update_dna_shared_config.py b/ipaserver/install/plugins/update_dna_shared_config.py +index 0f704c68a0551a7b7db6d42275498d02885b70fc..955bee5dd830f0dcad3f0810e7e2f1a1c725a0aa 100644 +--- a/ipaserver/install/plugins/update_dna_shared_config.py ++++ b/ipaserver/install/plugins/update_dna_shared_config.py +@@ -17,7 +17,7 @@ register = Registry() + + @register() + class update_dna_shared_config(Updater): +- dna_plugin_names = ('posix IDs',) ++ dna_plugin_names = ('posix IDs', 'Subordinate IDs') + + dna_plugin_dn = DN( + ('cn', 'Distributed Numeric Assignment Plugin'), +diff --git a/ipaserver/plugins/baseldap.py b/ipaserver/plugins/baseldap.py +index 3ccd2e38a254e274ba3685b9233f23b2313f8eec..0b7839536f66740a60377460c7432ade7c0654c2 100644 +--- a/ipaserver/plugins/baseldap.py ++++ b/ipaserver/plugins/baseldap.py +@@ -121,6 +121,8 @@ global_output_params = ( + Str('memberof_hbacrule?', + label='Member of HBAC rule', + ), ++ Str('memberof_subid?', ++ label='Subordinate ids',), + Str('member_idoverrideuser?', + label=_('Member ID user overrides'),), + Str('memberindirect_idoverrideuser?', +@@ -795,7 +797,13 @@ class LDAPObject(Object): + + dn = entry.dn + filter = self.backend.make_filter( +- {'member': dn, 'memberuser': dn, 'memberhost': dn}) ++ { ++ 'member': dn, ++ 'memberuser': dn, ++ 'memberhost': dn, ++ 'ipaowner': dn ++ } ++ ) + try: + result = self.backend.get_entries( + self.api.env.basedn, +diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py +index 12ff03c2302ff08aabb9369306965e0c125724f8..14a71b2217d3370701662b35c43f4207c1ab9b95 100644 +--- a/ipaserver/plugins/baseuser.py ++++ b/ipaserver/plugins/baseuser.py +@@ -17,10 +17,9 @@ + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + +-import random + import six + +-from ipalib import api, errors, output, constants ++from ipalib import api, errors, constants + from ipalib import ( + Flag, Int, Password, Str, Bool, StrEnum, DateTime, DNParam) + from ipalib.parameters import Principal, Certificate +@@ -28,9 +27,9 @@ from ipalib.plugable import Registry + from .baseldap import ( + DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, + LDAPRetrieve, LDAPAddAttribute, LDAPModAttribute, LDAPRemoveAttribute, +- LDAPQuery, LDAPAddMember, LDAPRemoveMember, ++ LDAPAddMember, LDAPRemoveMember, + LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption, +- add_missing_object_class, DNA_MAGIC, pkey_to_value, entry_to_dict ++ add_missing_object_class + ) + from ipaserver.plugins.service import (validate_realm, normalize_principal) + from ipalib.request import context +@@ -162,7 +161,7 @@ class baseuser(LDAPObject): + possible_objectclasses = [ + 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', + 'ipatokenradiusproxyuser', 'ipacertmapobject', +- 'ipantuserattrs', 'ipasubordinateid', ++ 'ipantuserattrs', + ] + disallow_object_classes = ['krbticketpolicyaux'] + permission_filter_objectclasses = ['posixaccount'] +@@ -183,13 +182,13 @@ class baseuser(LDAPObject): + 'krbprincipalname', 'loginshell', + 'mail', 'telephonenumber', 'title', 'nsaccountlock', + 'uidnumber', 'gidnumber', 'sshpubkeyfp', +- 'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber', +- 'ipasubgidcount', + ] + uuid_attribute = 'ipauniqueid' + attribute_members = { + 'manager': ['user'], +- 'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], ++ 'memberof': [ ++ 'group', 'netgroup', 'role', 'hbacrule', 'sudorule', 'subid' ++ ], + 'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], + } + allow_rename = True +@@ -432,41 +431,6 @@ class baseuser(LDAPObject): + 'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:', + 'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:'), + ), +- Int( +- 'ipasubuidnumber?', +- label=_('SubUID range start'), +- cli_name='subuid', +- doc=_('Start value for subordinate user ID (subuid) range'), +- minvalue=constants.SUBID_RANGE_START, +- maxvalue=constants.SUBID_RANGE_MAX, +- ), +- Int( +- 'ipasubuidcount?', +- label=_('SubUID range size'), +- cli_name='subuidcount', +- doc=_('Subordinate user ID count'), +- flags={'no_create', 'no_update', 'no_search'}, +- minvalue=constants.SUBID_COUNT, +- maxvalue=constants.SUBID_COUNT, +- ), +- Int( +- 'ipasubgidnumber?', +- label=_('SubGID range start'), +- cli_name='subgid', +- doc=_('Start value for subordinate group ID (subgid) range'), +- flags={'no_create', 'no_update'}, +- minvalue=constants.SUBID_RANGE_START, +- maxvalue=constants.SUBID_RANGE_MAX, +- ), +- Int( +- 'ipasubgidcount?', +- label=_('SubGID range size'), +- cli_name='subgidcount', +- doc=_('Subordinate group ID count'), +- flags={'no_create', 'no_update', 'no_search'}, +- minvalue=constants.SUBID_COUNT, +- maxvalue=constants.SUBID_COUNT, +- ), + ) + + def normalize_and_validate_email(self, email, config=None): +@@ -564,131 +528,6 @@ class baseuser(LDAPObject): + except KeyError: + pass + +- def handle_subordinate_ids(self, ldap, dn, entry_attrs): +- """Handle ipaSubordinateId object class +- """ +- obj_classes = entry_attrs.get("objectclass") +- new_subuid = entry_attrs.single_value.get("ipasubuidnumber") +- new_subgid = entry_attrs.single_value.get("ipasubgidnumber") +- +- # entry has object class ipaSubordinateId +- # default to auto-assigment of subuids +- if ( +- new_subuid is None +- and obj_classes is not None +- and self.has_objectclass(obj_classes, "ipasubordinateid") +- ): +- new_subuid = DNA_MAGIC +- +- # neither auto-assignment nor explicit assignment +- if new_subuid is None: +- # nothing to do +- return False +- +- # enforce subuid == subgid +- if new_subgid is not None and new_subgid != new_subuid: +- raise errors.ValidationError( +- name="ipasubgidnumber", +- error=_("subgidnumber must be equal to subuidnumber") +- ) +- +- self.set_subordinate_ids(ldap, dn, entry_attrs, new_subuid) +- return True +- +- def set_subordinate_ids(self, ldap, dn, entry_attrs, subuid): +- """Set subuid value of an entry +- +- Takes care of objectclass and sibbling attributes +- """ +- if "objectclass" in entry_attrs: +- obj_classes = entry_attrs["objectclass"] +- else: +- _entry_attrs = ldap.get_entry(dn, ["objectclass"]) +- entry_attrs["objectclass"] = _entry_attrs["objectclass"] +- obj_classes = entry_attrs["objectclass"] +- +- if not self.has_objectclass(obj_classes, "ipasubordinateid"): +- # could append ipasubordinategid and ipasubordinateuid, too +- obj_classes.append("ipasubordinateid") +- +- # XXX HACK, remove later +- if subuid == DNA_MAGIC: +- subuid = self._fake_dna_plugin(ldap, dn, entry_attrs) +- +- entry_attrs["ipasubuidnumber"] = subuid +- # enforice subuid == subgid for now +- entry_attrs["ipasubgidnumber"] = subuid +- # hard-coded constants +- entry_attrs["ipasubuidcount"] = constants.SUBID_COUNT +- entry_attrs["ipasubgidcount"] = constants.SUBID_COUNT +- +- def get_subid_match_candidate_filter( +- self, ldap, *, subuid, subgid, extra_filters=(), offset=None, +- ): +- """Create LDAP filter to locate matching/overlapping subids +- """ +- if subuid is None and subgid is None: +- raise ValueError("subuid and subgid are both None") +- if offset is None: +- # assumes that no subordinate count is larger than SUBID_COUNT +- offset = constants.SUBID_COUNT - 1 +- +- class_filters = "(objectclass=ipasubordinateid)" +- subid_filters = [] +- if subuid is not None: +- subid_filters.append( +- ldap.combine_filters( +- [ +- f"(ipasubuidnumber>={subuid - offset})", +- f"(ipasubuidnumber<={subuid + offset})", +- ], +- rules=ldap.MATCH_ALL +- ) +- ) +- if subgid is not None: +- subid_filters.append( +- ldap.combine_filters( +- [ +- f"(ipasubgidnumber>={subgid - offset})", +- f"(ipasubgidnumber<={subgid + offset})", +- ], +- rules=ldap.MATCH_ALL +- ) +- ) +- +- subid_filters = ldap.combine_filters( +- subid_filters, rules=ldap.MATCH_ANY +- ) +- filters = [class_filters, subid_filters] +- filters.extend(extra_filters) +- return ldap.combine_filters(filters, rules=ldap.MATCH_ALL) +- +- def _fake_dna_plugin(self, ldap, dn, entry_attrs): +- """XXX HACK, remove when 389-DS DNA plugin supports steps""" +- uidnumber = entry_attrs.single_value.get("uidnumber") +- if uidnumber is None: +- entry = ldap.get_entry(dn, ["uidnumber"]) +- uidnumber = entry.single_value["uidnumber"] +- uidnumber = int(uidnumber) +- +- if uidnumber == DNA_MAGIC: +- return ( +- 3221225472 +- + random.randint(1, 16382) * constants.SUBID_COUNT +- ) +- +- if not hasattr(context, "idrange_ipabaseid"): +- range_name = f"{self.api.env.realm}_id_range" +- range = self.api.Command.idrange_show(range_name)["result"] +- context.idrange_ipabaseid = int(range["ipabaseid"][0]) +- +- range_start = context.idrange_ipabaseid +- +- assert uidnumber >= range_start +- assert uidnumber < range_start + 2**14 +- +- return (uidnumber - range_start) * constants.SUBID_COUNT + 2**31 +- + + class baseuser_add(LDAPCreate): + """ +@@ -699,7 +538,6 @@ class baseuser_add(LDAPCreate): + assert isinstance(dn, DN) + set_krbcanonicalname(entry_attrs) + self.obj.convert_usercertificate_pre(entry_attrs) +- self.obj.handle_subordinate_ids(ldap, dn, entry_attrs) + if entry_attrs.get('ipatokenradiususername', None): + add_missing_object_class(ldap, u'ipatokenradiusproxyuser', dn, + entry_attrs, update=False) +@@ -852,7 +690,6 @@ class baseuser_mod(LDAPUpdate): + + self.check_objectclass(ldap, dn, entry_attrs) + self.obj.convert_usercertificate_pre(entry_attrs) +- self.obj.handle_subordinate_ids(ldap, dn, entry_attrs) + self.preserve_krbprincipalname_pre(ldap, entry_attrs, *keys, **options) + update_samba_attrs(ldap, dn, entry_attrs, **options) + +@@ -1133,98 +970,3 @@ class baseuser_remove_certmapdata(ModCertMapData, + LDAPRemoveAttribute): + __doc__ = _("Remove one or more certificate mappings from the user entry.") + msg_summary = _('Removed certificate mappings from user "%(value)s"') +- +- +-class baseuser_auto_subid(LDAPQuery): +- __doc__ = _("Auto-assign subuid and subgid range to user entry") +- +- has_output = output.standard_entry +- +- def execute(self, cn, **options): +- ldap = self.obj.backend +- dn = self.obj.get_dn(cn) +- +- try: +- entry_attrs = ldap.get_entry( +- dn, ["objectclass", "ipasubuidnumber"] +- ) +- except errors.NotFound: +- raise self.obj.handle_not_found(cn) +- +- if "ipasubuidnumber" in entry_attrs: +- raise errors.AlreadyContainsValueError(attr="ipasubuidnumber") +- +- self.obj.set_subordinate_ids(ldap, dn, entry_attrs, subuid=DNA_MAGIC) +- ldap.update_entry(entry_attrs) +- +- # fetch updated entry (use search display attribute to show subids) +- if options.get('all', False): +- attrs_list = ['*'] + self.obj.search_display_attributes +- else: +- attrs_list = set(self.obj.search_display_attributes) +- attrs_list.update(entry_attrs.keys()) +- if options.get('no_members', False): +- attrs_list.difference_update(self.obj.attribute_members) +- attrs_list = list(attrs_list) +- +- entry = self._exc_wrapper((cn,), options, ldap.get_entry)( +- dn, attrs_list +- ) +- entry_attrs = entry_to_dict(entry, **options) +- entry_attrs['dn'] = dn +- +- return dict(result=entry_attrs, value=pkey_to_value(cn, options)) +- +- +-class baseuser_match_subid(baseuser_find): +- __doc__ = _("Match users by any subordinate uid in their range") +- +- _subid_attrs = { +- "ipasubuidnumber", +- "ipasubuidcount", +- "ipasubgidnumber", +- "ipasubgidcount" +- } +- +- def get_options(self): +- base_options = {p.name for p in self.obj.takes_params} +- for option in super().get_options(): +- if option.name == "ipasubuidnumber": +- yield option.clone( +- label=_('SubUID match'), +- doc=_('Match value for subordinate user ID'), +- required=True, +- ) +- elif option.name not in base_options: +- # raw, version +- yield option.clone() +- +- def pre_callback( +- self, ldap, filters, attrs_list, base_dn, scope, *args, **options +- ): +- # search for candidates in range +- # Code assumes that no subordinate count is larger than SUBID_COUNT +- filters = self.obj.get_subid_match_candidate_filter( +- ldap, subuid=options["ipasubuidnumber"], subgid=None, +- ) +- # always include subid attributes +- for missing in self._subid_attrs.difference(attrs_list): +- attrs_list.append(missing) +- +- return filters, base_dn, scope +- +- def post_callback(self, ldap, entries, truncated, *args, **options): +- # filter out mismatches manually +- osubuid = options["ipasubuidnumber"] +- new_entries = [] +- for entry in entries: +- esubuid = int(entry.single_value["ipasubuidnumber"]) +- esubcount = int(entry.single_value["ipasubuidcount"]) +- minsubuid = esubuid +- maxsubuid = esubuid + esubcount - 1 +- if minsubuid <= osubuid <= maxsubuid: +- new_entries.append(entry) +- +- entries[:] = new_entries +- +- return truncated +diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py +index ace66e589e50dac098aefd6b393b5e835cac9d7f..3526153ec117a05846daca7d42447ff50b5b7934 100644 +--- a/ipaserver/plugins/config.py ++++ b/ipaserver/plugins/config.py +@@ -121,6 +121,7 @@ class config(LDAPObject): + 'ipapwdexpadvnotify', 'ipaselinuxusermaporder', + 'ipaselinuxusermapdefault', 'ipaconfigstring', 'ipakrbauthzdata', + 'ipauserauthtype', 'ipadomainresolutionorder', 'ipamaxhostnamelength', ++ 'ipauserdefaultsubordinateid', + ] + container_dn = DN(('cn', 'ipaconfig'), ('cn', 'etc')) + permission_filter_objectclasses = ['ipaguiconfig'] +@@ -142,7 +143,7 @@ class config(LDAPObject): + 'ipasearchrecordslimit', 'ipasearchtimelimit', + 'ipauserauthtype', 'ipauserobjectclasses', + 'ipausersearchfields', 'ipacustomfields', +- 'ipamaxhostnamelength', ++ 'ipamaxhostnamelength', 'ipauserdefaultsubordinateid', + }, + }, + } +@@ -261,6 +262,11 @@ class config(LDAPObject): + values=(u'password', u'radius', u'otp', + u'pkinit', u'hardened', u'disabled'), + ), ++ Bool('ipauserdefaultsubordinateid?', ++ cli_name='user_default_subid', ++ label=_('Enable adding subids to new users'), ++ doc=_('Enable adding subids to new users'), ++ ), + Str( + 'ipa_master_server*', + label=_('IPA masters'), +diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py +index 199838b199eb4cdabf597bd34d571d05547fd32e..5ef940c2b88cc2b132a15d619772349b30731306 100644 +--- a/ipaserver/plugins/internal.py ++++ b/ipaserver/plugins/internal.py +@@ -1547,7 +1547,7 @@ class i18n_messages(Command): + "Drive to mount a home directory" + ), + }, +- "subordinate": { ++ "subid": { + "identity": _("Subordinate user and group id"), + "subuidnumber": _("Subordinate user id"), + "subuidcount": _("Subordinate user id count"), +diff --git a/ipaserver/plugins/subid.py b/ipaserver/plugins/subid.py +new file mode 100644 +index 0000000000000000000000000000000000000000..7d9a2f33e84bc7cdf17900346343e49d5eda0d8c +--- /dev/null ++++ b/ipaserver/plugins/subid.py +@@ -0,0 +1,608 @@ ++# ++# Copyright (C) 2021 FreeIPA Contributors see COPYING for license ++# ++ ++import random ++import uuid ++ ++from ipalib import api ++from ipalib import constants ++from ipalib import errors ++from ipalib import output ++from ipalib.plugable import Registry ++from ipalib.parameters import Int, Str ++from ipalib.request import context ++from ipalib.text import _, ngettext ++from ipapython.dn import DN ++ ++from .baseldap import ( ++ LDAPObject, ++ LDAPCreate, ++ LDAPDelete, ++ LDAPUpdate, ++ LDAPSearch, ++ LDAPRetrieve, ++ LDAPQuery, ++ DNA_MAGIC, ++) ++ ++__doc__ = _( ++ """ ++Subordinate ids ++ ++Manage subordinate user and group ids for users ++ ++EXAMPLES: ++ ++ Auto-assign a subordinate id range to current user ++ ipa subid-generate ++ ++ Auto-assign a subordinate id range to user alice: ++ ipa subid-generate --owner=alice ++ ++ Find subordinate ids for user alice: ++ ipa subid-find --owner=alice ++ ++ Match entry by any subordinate uid in range: ++ ipa subid-match --subuid=2147483649 ++""" ++) ++ ++register = Registry() ++ ++ ++@register() ++class subid(LDAPObject): ++ """Subordinate id object.""" ++ ++ container_dn = api.env.container_subids ++ ++ object_name = _("Subordinate id") ++ object_name_plural = _("Subordinate ids") ++ label = _("Subordinate ids") ++ label_singular = _("Subordinate id") ++ ++ object_class = ["ipasubordinateidentry"] ++ possible_objectclasses = [ ++ "ipasubordinategid", ++ "ipasubordinateuid", ++ "ipasubordinateid", ++ ] ++ default_attributes = [ ++ "ipauniqueid", ++ "ipaowner", ++ "ipasubuidnumber", ++ "ipasubuidcount", ++ "ipasubgidnumber", ++ "ipasubgidcount", ++ ] ++ allow_rename = False ++ ++ permission_filter_objectclasses_string = ( ++ "(objectclass=ipasubordinateidentry)" ++ ) ++ managed_permissions = { ++ # all authenticated principals can read subordinate id information ++ "System: Read Subordinate Id Attributes": { ++ "ipapermbindruletype": "all", ++ "ipapermright": {"read", "search", "compare"}, ++ "ipapermtargetfilter": [ ++ permission_filter_objectclasses_string, ++ ], ++ "ipapermdefaultattr": { ++ "objectclass", ++ "ipauniqueid", ++ "description", ++ "ipaowner", ++ "ipasubuidnumber", ++ "ipasubuidcount", ++ "ipasubgidnumber", ++ "ipasubgidcount", ++ }, ++ }, ++ "System: Read Subordinate Id Count": { ++ "ipapermbindruletype": "all", ++ "ipapermright": {"read", "search", "compare"}, ++ "ipapermtargetfilter": [], ++ "ipapermtarget": DN(container_dn, api.env.basedn), ++ "ipapermdefaultattr": {"numSubordinates"}, ++ }, ++ # user administrators can remove subordinate ids or update the ++ # ipaowner attribute. This enables user admins to remove users ++ # with assigned subids or move them to staging area (--preserve). ++ "System: Manage Subordinate Ids": { ++ "ipapermright": {"write"}, ++ "ipapermtargetfilter": [ ++ permission_filter_objectclasses_string, ++ ], ++ "ipapermdefaultattr": { ++ "description", ++ "ipaowner", # allow user admins to preserve users ++ }, ++ "default_privileges": {"User Administrators"}, ++ }, ++ "System: Remove Subordinate Ids": { ++ "ipapermright": {"delete"}, ++ "ipapermtargetfilter": [ ++ permission_filter_objectclasses_string, ++ ], ++ "default_privileges": {"User Administrators"}, ++ }, ++ } ++ ++ takes_params = ( ++ Str( ++ "ipauniqueid", ++ cli_name="id", ++ label=_("Unique ID"), ++ primary_key=True, ++ flags={"optional_create"}, ++ ), ++ Str( ++ "description?", ++ cli_name="desc", ++ label=_("Description"), ++ doc=_("Subordinate id description"), ++ ), ++ Str( ++ "ipaowner", ++ cli_name="owner", ++ label=_("Owner"), ++ doc=_("Owning user of subordinate id entry"), ++ flags={"no_update"}, ++ ), ++ Int( ++ "ipasubuidnumber?", ++ label=_("SubUID range start"), ++ cli_name="subuid", ++ doc=_("Start value for subordinate user ID (subuid) range"), ++ flags={"no_update"}, ++ minvalue=constants.SUBID_RANGE_START, ++ maxvalue=constants.SUBID_RANGE_MAX, ++ ), ++ Int( ++ "ipasubuidcount?", ++ label=_("SubUID range size"), ++ cli_name="subuidcount", ++ doc=_("Subordinate user ID count"), ++ flags={"no_create", "no_update", "no_search"}, # auto-assigned ++ minvalue=constants.SUBID_COUNT, ++ maxvalue=constants.SUBID_COUNT, ++ ), ++ Int( ++ "ipasubgidnumber?", ++ label=_("SubGID range start"), ++ cli_name="subgid", ++ doc=_("Start value for subordinate group ID (subgid) range"), ++ flags={"no_create", "no_update"}, # auto-assigned ++ minvalue=constants.SUBID_RANGE_START, ++ maxvalue=constants.SUBID_RANGE_MAX, ++ ), ++ Int( ++ "ipasubgidcount?", ++ label=_("SubGID range size"), ++ cli_name="subgidcount", ++ doc=_("Subordinate group ID count"), ++ flags={"no_create", "no_update", "no_search"}, # auto-assigned ++ minvalue=constants.SUBID_COUNT, ++ maxvalue=constants.SUBID_COUNT, ++ ), ++ ) ++ ++ def fixup_objectclass(self, entry_attrs): ++ """Add missing object classes to entry""" ++ has_subuid = "ipasubuidnumber" in entry_attrs ++ has_subgid = "ipasubgidnumber" in entry_attrs ++ ++ candicates = set(self.object_class) ++ if has_subgid: ++ candicates.add("ipasubordinategid") ++ if has_subuid: ++ candicates.add("ipasubordinateuid") ++ if has_subgid and has_subuid: ++ candicates.add("ipasubordinateid") ++ ++ entry_oc = entry_attrs.setdefault("objectclass", []) ++ current_oc = {x.lower() for x in entry_oc} ++ for oc in candicates.difference(current_oc): ++ entry_oc.append(oc) ++ ++ def handle_duplicate_entry(self, *keys): ++ if hasattr(context, "subid_owner_dn"): ++ uid = context.subid_owner_dn[0].value ++ msg = _( ++ '%(oname)s with with name "%(pkey)s" or for user "%(uid)s" ' ++ "already exists." ++ ) % { ++ "uid": uid, ++ "pkey": keys[-1] if keys else "", ++ "oname": self.object_name, ++ } ++ raise errors.DuplicateEntry(message=msg) from None ++ else: ++ super().handle_duplicate_entry(*keys) ++ ++ def convert_owner(self, entry_attrs, options): ++ """Change owner from DN to uid string""" ++ if not options.get("raw", False) and "ipaowner" in entry_attrs: ++ userobj = self.api.Object.user ++ entry_attrs["ipaowner"] = [ ++ userobj.get_primary_key_from_dn(entry_attrs["ipaowner"][0]) ++ ] ++ ++ def get_owner_dn(self, *keys, **options): ++ """Get owning user entry entry (username or DN)""" ++ owner = keys[-1] ++ userobj = self.api.Object.user ++ if isinstance(owner, DN): ++ # it's already a DN, validate it's either an active or preserved ++ # user. Ref integrity plugin checks that it's not a dangling DN. ++ user_dns = ( ++ DN(userobj.active_container_dn, self.api.env.basedn), ++ DN(userobj.delete_container_dn, self.api.env.basedn), ++ ) ++ if not owner.endswith(user_dns): ++ raise errors.ValidationError( ++ name="ipaowner", ++ error=_("'%(dn)s is not a valid user") % {"dn": owner}, ++ ) ++ return owner ++ ++ # similar to user.get_either_dn() but with error reporting and ++ # returns an entry ++ ldap = self.backend ++ try: ++ active_dn = userobj.get_dn(owner, **options) ++ entry = ldap.get_entry(active_dn, attrs_list=[]) ++ return entry.dn ++ except errors.NotFound: ++ # fall back to deleted user ++ try: ++ delete_dn = userobj.get_delete_dn(owner, **options) ++ entry = ldap.get_entry(delete_dn, attrs_list=[]) ++ return entry.dn ++ except errors.NotFound: ++ raise userobj.handle_not_found(owner) ++ ++ def handle_subordinate_ids(self, ldap, dn, entry_attrs): ++ """Handle ipaSubordinateId object class""" ++ new_subuid = entry_attrs.single_value.get("ipasubuidnumber") ++ new_subgid = entry_attrs.single_value.get("ipasubgidnumber") ++ ++ if new_subuid is None: ++ new_subuid = DNA_MAGIC ++ ++ # enforce subuid == subgid ++ if new_subgid is not None and new_subgid != new_subuid: ++ raise errors.ValidationError( ++ name="ipasubgidnumber", ++ error=_("subgidnumber must be equal to subuidnumber"), ++ ) ++ ++ self.set_subordinate_ids(ldap, dn, entry_attrs, new_subuid) ++ return True ++ ++ def set_subordinate_ids(self, ldap, dn, entry_attrs, subuid): ++ """Set subuid value of an entry ++ ++ Takes care of objectclass and sibbling attributes ++ """ ++ if "objectclass" not in entry_attrs: ++ _entry_attrs = ldap.get_entry(dn, ["objectclass"]) ++ entry_attrs["objectclass"] = _entry_attrs["objectclass"] ++ ++ # XXX HACK, remove later ++ if subuid == DNA_MAGIC: ++ subuid = self._fake_dna_plugin(ldap, dn, entry_attrs) ++ ++ entry_attrs["ipasubuidnumber"] = subuid ++ # enforice subuid == subgid for now ++ entry_attrs["ipasubgidnumber"] = subuid ++ # hard-coded constants ++ entry_attrs["ipasubuidcount"] = constants.SUBID_COUNT ++ entry_attrs["ipasubgidcount"] = constants.SUBID_COUNT ++ ++ self.fixup_objectclass(entry_attrs) ++ ++ def get_subid_match_candidate_filter( ++ self, ++ ldap, ++ *, ++ subuid, ++ subgid, ++ extra_filters=(), ++ offset=None, ++ ): ++ """Create LDAP filter to locate matching/overlapping subids""" ++ if subuid is None and subgid is None: ++ raise ValueError("subuid and subgid are both None") ++ if offset is None: ++ # assumes that no subordinate count is larger than SUBID_COUNT ++ offset = constants.SUBID_COUNT - 1 ++ ++ class_filters = "(objectclass=ipasubordinateid)" ++ subid_filters = [] ++ if subuid is not None: ++ subid_filters.append( ++ ldap.combine_filters( ++ [ ++ f"(ipasubuidnumber>={subuid - offset})", ++ f"(ipasubuidnumber<={subuid + offset})", ++ ], ++ rules=ldap.MATCH_ALL, ++ ) ++ ) ++ if subgid is not None: ++ subid_filters.append( ++ ldap.combine_filters( ++ [ ++ f"(ipasubgidnumber>={subgid - offset})", ++ f"(ipasubgidnumber<={subgid + offset})", ++ ], ++ rules=ldap.MATCH_ALL, ++ ) ++ ) ++ ++ subid_filters = ldap.combine_filters( ++ subid_filters, rules=ldap.MATCH_ANY ++ ) ++ filters = [class_filters, subid_filters] ++ filters.extend(extra_filters) ++ return ldap.combine_filters(filters, rules=ldap.MATCH_ALL) ++ ++ def _fake_dna_plugin(self, ldap, dn, entry_attrs): ++ """XXX HACK, remove when 389-DS DNA plugin supports steps""" ++ return ( ++ constants.SUBID_RANGE_START ++ + random.randint(1, 32764 - 2) * constants.SUBID_COUNT ++ ) ++ ++ ++@register() ++class subid_add(LDAPCreate): ++ __doc__ = _("Add a new subordinate id.") ++ msg_summary = _('Added subordinate id "%(value)s"') ++ ++ # internal command, use subid-auto to auto-assign subids ++ NO_CLI = True ++ ++ def pre_callback( ++ self, ldap, dn, entry_attrs, attrs_list, *keys, **options ++ ): ++ # XXX let ref integrity plugin validate DN? ++ owner_dn = self.obj.get_owner_dn(entry_attrs["ipaowner"], **options) ++ context.subid_owner_dn = owner_dn ++ entry_attrs["ipaowner"] = owner_dn ++ ++ self.obj.handle_subordinate_ids(ldap, dn, entry_attrs) ++ attrs_list.append("objectclass") ++ ++ return dn ++ ++ def execute(self, ipauniqueid=None, **options): ++ if ipauniqueid is None: ++ ipauniqueid = str(uuid.uuid4()) ++ return super().execute(ipauniqueid, **options) ++ ++ def post_callback(self, ldap, dn, entry_attrs, *keys, **options): ++ self.obj.convert_owner(entry_attrs, options) ++ return super(subid_add, self).post_callback( ++ ldap, dn, entry_attrs, *keys, **options ++ ) ++ ++ ++@register() ++class subid_del(LDAPDelete): ++ __doc__ = _("Delete a subordinate id.") ++ msg_summary = _('Deleted subordinate id "%(value)s"') ++ ++ # internal command, subids cannot be removed ++ NO_CLI = True ++ ++ ++@register() ++class subid_mod(LDAPUpdate): ++ __doc__ = _("Modify a subordinate id.") ++ msg_summary = _('Modified subordinate id "%(value)s"') ++ ++ def post_callback(self, ldap, dn, entry_attrs, *keys, **options): ++ self.obj.convert_owner(entry_attrs, options) ++ return super(subid_mod, self).post_callback( ++ ldap, dn, entry_attrs, *keys, **options ++ ) ++ ++ ++@register() ++class subid_find(LDAPSearch): ++ __doc__ = _("Search for subordinate id.") ++ msg_summary = ngettext( ++ "%(count)d subordinate id matched", ++ "%(count)d subordinate ids matched", ++ 0, ++ ) ++ ++ def pre_callback( ++ self, ldap, filters, attrs_list, base_dn, scope, *args, **options ++ ): ++ attrs_list.append("objectclass") ++ return super(subid_find, self).pre_callback( ++ ldap, filters, attrs_list, base_dn, scope, *args, **options ++ ) ++ ++ def args_options_2_entry(self, *args, **options): ++ entry_attrs = super(subid_find, self).args_options_2_entry( ++ *args, **options ++ ) ++ owner = entry_attrs.get("ipaowner") ++ if owner is not None: ++ owner_dn = self.obj.get_owner_dn(owner, **options) ++ entry_attrs["ipaowner"] = owner_dn ++ return entry_attrs ++ ++ def post_callback(self, ldap, entries, truncated, *args, **options): ++ for entry in entries: ++ self.obj.convert_owner(entry, options) ++ return super(subid_find, self).post_callback( ++ ldap, entries, truncated, *args, **options ++ ) ++ ++ ++@register() ++class subid_show(LDAPRetrieve): ++ __doc__ = _("Display information about a subordinate id.") ++ ++ def pre_callback(self, ldap, dn, attrs_list, *keys, **options): ++ attrs_list.append("objectclass") ++ return super(subid_show, self).pre_callback( ++ ldap, dn, attrs_list, *keys, **options ++ ) ++ ++ def post_callback(self, ldap, dn, entry_attrs, *keys, **options): ++ self.obj.convert_owner(entry_attrs, options) ++ return super(subid_show, self).post_callback( ++ ldap, dn, entry_attrs, *keys, **options ++ ) ++ ++ ++@register() ++class subid_generate(LDAPQuery): ++ __doc__ = _( ++ "Generate and auto-assign subuid and subgid range to user entry" ++ ) ++ ++ has_output = output.standard_entry ++ ++ takes_options = LDAPQuery.takes_options + ( ++ Str( ++ "ipaowner?", ++ cli_name="owner", ++ label=_("Owner"), ++ doc=_("Owning user of subordinate id entry"), ++ ), ++ ) ++ ++ def get_args(self): ++ return [] ++ ++ def execute(self, *keys, **options): ++ owner_uid = options.get("ipaowner") ++ # default to current user ++ if owner_uid is None: ++ owner_dn = DN(self.api.Backend.ldap2.conn.whoami_s()[4:]) ++ # validate it's a user and not a service or host ++ owner_dn = self.obj.get_owner_dn(owner_dn) ++ owner_uid = owner_dn[0].value ++ ++ return self.api.Command.subid_add( ++ description="auto-assigned subid", ++ ipaowner=owner_uid, ++ version=options["version"], ++ ) ++ ++ ++@register() ++class subid_match(subid_find): ++ __doc__ = _("Match users by any subordinate uid in their range") ++ ++ def get_options(self): ++ base_options = {p.name for p in self.obj.takes_params} ++ for option in super().get_options(): ++ if option.name == "ipasubuidnumber": ++ yield option.clone( ++ label=_("SubUID match"), ++ doc=_("Match value for subordinate user ID"), ++ required=True, ++ ) ++ elif option.name not in base_options: ++ # raw, version ++ yield option.clone() ++ ++ def pre_callback( ++ self, ldap, filters, attrs_list, base_dn, scope, *args, **options ++ ): ++ # search for candidates in range ++ # Code assumes that no subordinate count is larger than SUBID_COUNT ++ filters = self.obj.get_subid_match_candidate_filter( ++ ldap, ++ subuid=options["ipasubuidnumber"], ++ subgid=None, ++ ) ++ attrs_list.extend(self.obj.default_attributes) ++ ++ return filters, base_dn, scope ++ ++ def post_callback(self, ldap, entries, truncated, *args, **options): ++ # filter out mismatches manually ++ osubuid = options["ipasubuidnumber"] ++ new_entries = [] ++ for entry in entries: ++ esubuid = int(entry.single_value["ipasubuidnumber"]) ++ esubcount = int(entry.single_value["ipasubuidcount"]) ++ minsubuid = esubuid ++ maxsubuid = esubuid + esubcount - 1 ++ if minsubuid <= osubuid <= maxsubuid: ++ new_entries.append(entry) ++ ++ entries[:] = new_entries ++ ++ return truncated ++ ++ ++@register() ++class subid_stats(LDAPQuery): ++ __doc__ = _("Subordinate id statistics") ++ ++ takes_options = () ++ has_output = ( ++ output.summary, ++ output.Entry("result"), ++ ) ++ ++ def get_args(self): ++ return () ++ ++ def get_remaining_dna(self, ldap, **options): ++ base_dn = DN( ++ self.api.env.container_dna_subordinate_ids, self.api.env.basedn ++ ) ++ entries, _truncated = ldap.find_entries( ++ "(objectClass=dnaSharedConfig)", ++ attrs_list=["dnaRemainingValues"], ++ base_dn=base_dn, ++ scope=ldap.SCOPE_ONELEVEL, ++ ) ++ return sum( ++ int(entry.single_value["dnaRemainingValues"]) for entry in entries ++ ) ++ ++ def get_idrange(self, ldap, **options): ++ cn = f"{self.api.env.realm}_subid_range" ++ result = self.api.Command.idrange_show(cn, version=options["version"]) ++ baseid = int(result["result"]["ipabaseid"][0]) ++ rangesize = int(result["result"]["ipaidrangesize"][0]) ++ return baseid, rangesize ++ ++ def get_subid_assigned(self, ldap, **options): ++ dn = DN(self.api.env.container_subids, self.api.env.basedn) ++ entry = ldap.get_entry(dn=dn, attrs_list=["numSubordinates"]) ++ return int(entry.single_value["numSubordinates"]) ++ ++ def execute(self, *keys, **options): ++ ldap = self.obj.backend ++ dna_remaining = self.get_remaining_dna(ldap, **options) ++ baseid, rangesize = self.get_idrange(ldap, **options) ++ assigned_subids = self.get_subid_assigned(ldap, **options) ++ remaining_subids = dna_remaining // constants.SUBID_COUNT ++ return dict( ++ summary=_("%(remaining)i remaining subordinate id ranges") ++ % { ++ "remaining": remaining_subids, ++ }, ++ result=dict( ++ baseid=baseid, ++ rangesize=rangesize, ++ dna_remaining=dna_remaining, ++ assigned_subids=assigned_subids, ++ remaining_subids=remaining_subids, ++ ), ++ ) +diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py +index f89b3ad5d9c994fe1ceb3da560fde7cc5bf5155a..19d07e6d61a451a0b1177adf2cf8ae1b7fceeb67 100644 +--- a/ipaserver/plugins/user.py ++++ b/ipaserver/plugins/user.py +@@ -51,8 +51,6 @@ from .baseuser import ( + baseuser_remove_principal, + baseuser_add_certmapdata, + baseuser_remove_certmapdata, +- baseuser_auto_subid, +- baseuser_match_subid, + ) + from .idviews import remove_ipaobject_overrides + from ipalib.plugable import Registry +@@ -205,8 +203,6 @@ class user(baseuser): + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass', +- 'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber', +- 'ipasubgidcount', + }, + 'fixup_function': fix_addressbook_permission_bindrule, + }, +@@ -670,6 +666,17 @@ class user_add(baseuser_add): + # if both randompassword and userpassword options were used + pass + ++ # generate subid ++ default_subid = config.single_value.get( ++ 'ipaUserDefaultSubordinateId', 'FALSE' ++ ) ++ if default_subid == 'TRUE': ++ result = self.api.Command.subid_generate( ++ ipaowner=entry_attrs.single_value['uid'], ++ version=options['version'] ++ ) ++ entry_attrs["memberOf"].append(result['result']['dn']) ++ + self.obj.get_preserved_attribute(entry_attrs, options) + + self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) +@@ -757,7 +764,9 @@ class user_del(baseuser_del): + # of OTP tokens. + check_protected_member(keys[-1]) + +- if not options.get('preserve', False): ++ preserve = options.get('preserve', False) ++ ++ if not preserve: + # Remove any ID overrides tied with this user + try: + remove_ipaobject_overrides(self.obj.backend, self.obj.api, dn) +@@ -780,6 +789,15 @@ class user_del(baseuser_del): + else: + self.api.Command.otptoken_del(token) + ++ # XXX: preserving doesn't work yet, see subordinate-ids.md ++ # Delete all subid entries owned by this user. ++ results = self.api.Command.subid_find(ipaowner=owner)["result"] ++ for subid_entry in results: ++ subid_pkey = self.api.Object.subid.get_primary_key_from_dn( ++ subid_entry["dn"] ++ ) ++ self.api.Command.subid_del(subid_pkey) ++ + return dn + + def execute(self, *keys, **options): +@@ -829,6 +847,7 @@ class user_mod(baseuser_mod): + self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, + **options) + validate_nsaccountlock(entry_attrs) ++ # TODO: forward uidNumber changes and rename to subids + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): +@@ -1311,13 +1330,3 @@ class user_add_principal(baseuser_add_principal): + class user_remove_principal(baseuser_remove_principal): + __doc__ = _('Remove principal alias from the user entry') + msg_summary = _('Removed aliases from user "%(value)s"') +- +- +-@register() +-class user_auto_subid(baseuser_auto_subid): +- __doc__ = baseuser_auto_subid.__doc__ +- +- +-@register() +-class user_match_subid(baseuser_match_subid): +- __doc__ = baseuser_match_subid.__doc__ +diff --git a/ipatests/test_integration/test_subids.py b/ipatests/test_integration/test_subids.py +index b462f22ac067f3e1e97ef3f6d63d4e14e4ae79af..48e58c26464f52605438afe865575e5ca4c8f1f8 100644 +--- a/ipatests/test_integration/test_subids.py ++++ b/ipatests/test_integration/test_subids.py +@@ -17,6 +17,7 @@ class TestSubordinateId(IntegrationTest): + topology = "star" + + def _parse_result(self, result): ++ # ipa CLI should get an --outform json option + info = {} + for line in result.stdout_text.split("\n"): + line = line.strip() +@@ -42,39 +43,69 @@ class TestSubordinateId(IntegrationTest): + info[k] = set(v) + return info + +- def get_user(self, uid): +- cmd = ["ipa", "user-show", "--all", "--raw", uid] +- result = self.master.run_command(cmd) +- return self._parse_result(result) ++ def assert_subid_info(self, uid, info): ++ assert info["ipauniqueid"] ++ basedn = self.master.domain.basedn ++ assert info["ipaowner"] == f"uid={uid},cn=users,cn=accounts,{basedn}" ++ assert info["ipasubuidnumber"] == info["ipasubuidnumber"] ++ assert info["ipasubuidnumber"] >= SUBID_RANGE_START ++ assert info["ipasubuidnumber"] <= SUBID_RANGE_MAX ++ assert info["ipasubuidcount"] == SUBID_COUNT ++ assert info["ipasubgidnumber"] == info["ipasubgidnumber"] ++ assert info["ipasubgidnumber"] == info["ipasubuidnumber"] ++ assert info["ipasubgidcount"] == SUBID_COUNT ++ ++ def assert_subid(self, uid, *, match): ++ cmd = ["ipa", "subid-find", "--raw", "--owner", uid] ++ result = self.master.run_command(cmd, raiseonerr=False) ++ if not match: ++ assert result.returncode >= 1 ++ if result.returncode == 1: ++ assert "0 subordinate ids matched" in result.stdout_text ++ elif result.returncode == 2: ++ assert "user not found" in result.stderr_text ++ return None ++ else: ++ assert result.returncode == 0 ++ assert "1 subordinate id matched" in result.stdout_text ++ info = self._parse_result(result) ++ self.assert_subid_info(uid, info) ++ self.master.run_command( ++ ["ipa", "subid-show", info["ipauniqueid"]] ++ ) ++ return info + +- def user_auto_subid(self, uid, **kwargs): +- cmd = ["ipa", "user-auto-subid", uid] ++ def subid_generate(self, uid, **kwargs): ++ cmd = ["ipa", "subid-generate"] ++ if uid is not None: ++ cmd.extend(("--owner", uid)) + return self.master.run_command(cmd, **kwargs) + +- def test_auto_subid(self): +- tasks.kinit_admin(self.master) ++ def test_auto_generate_subid(self): + uid = "testuser_auto1" +- tasks.user_add(self.master, uid) +- info = self.get_user(uid) +- assert "ipasubuidcount" not in info ++ passwd = "Secret123" ++ tasks.create_active_user(self.master, uid, password=passwd) + +- self.user_auto_subid(uid) +- info = self.get_user(uid) +- assert "ipasubuidcount" in info ++ tasks.kinit_admin(self.master) ++ self.assert_subid(uid, match=False) ++ ++ # add subid by name ++ self.subid_generate(uid) ++ info = self.assert_subid(uid, match=True) ++ ++ # second generate fails due to unique index on ipaowner ++ result = self.subid_generate(uid, raiseonerr=False) ++ assert result.returncode > 0 ++ assert f'for user "{uid}" already exists' in result.stderr_text + ++ # check matching + subuid = info["ipasubuidnumber"] +- result = self.master.run_command( +- ["ipa", "user-match-subid", f"--subuid={subuid}", "--raw"] +- ) +- match = self._parse_result(result) +- assert match["uid"] == uid +- assert match["ipasubuidnumber"] == info["ipasubuidnumber"] +- assert match["ipasubuidnumber"] >= SUBID_RANGE_START +- assert match["ipasubuidnumber"] <= SUBID_RANGE_MAX +- assert match["ipasubuidcount"] == SUBID_COUNT +- assert match["ipasubgidnumber"] == info["ipasubgidnumber"] +- assert match["ipasubgidnumber"] == match["ipasubuidnumber"] +- assert match["ipasubgidcount"] == SUBID_COUNT ++ for offset in (0, 1, 65535): ++ result = self.master.run_command( ++ ["ipa", "subid-match", f"--subuid={subuid + offset}", "--raw"] ++ ) ++ match = self._parse_result(result) ++ self.assert_subid_info(uid, match) + + def test_ipa_subid_script(self): + tasks.kinit_admin(self.master) +@@ -85,34 +116,28 @@ class TestSubordinateId(IntegrationTest): + uid = f"testuser_script{i}" + users.append(uid) + tasks.user_add(self.master, uid) +- info = self.get_user(uid) +- assert "ipasubuidcount" not in info ++ self.assert_subid(uid, match=False) + + cmd = [tool, "--verbose", "--group", "ipausers"] + self.master.run_command(cmd) + + for uid in users: +- info = self.get_user(uid) +- assert info["ipasubuidnumber"] >= SUBID_RANGE_START +- assert info["ipasubuidnumber"] <= SUBID_RANGE_MAX +- assert info["ipasubuidnumber"] == info["ipasubgidnumber"] +- assert info["ipasubuidcount"] == SUBID_COUNT +- assert info["ipasubuidcount"] == info["ipasubgidcount"] ++ self.assert_subid(uid, match=True) + + def test_subid_selfservice(self): +- tasks.kinit_admin(self.master) +- +- uid = "testuser_selfservice1" ++ uid1 = "testuser_selfservice1" ++ uid2 = "testuser_selfservice2" + password = "Secret123" + role = "Subordinate ID Selfservice User" + +- tasks.user_add(self.master, uid, password=password) +- tasks.kinit_user( +- self.master, uid, f"{password}\n{password}\n{password}\n" +- ) +- info = self.get_user(uid) +- assert "ipasubuidcount" not in info +- result = self.user_auto_subid(uid, raiseonerr=False) ++ tasks.create_active_user(self.master, uid1, password=password) ++ tasks.create_active_user(self.master, uid2, password=password) ++ ++ tasks.kinit_user(self.master, uid1, password=password) ++ self.assert_subid(uid1, match=False) ++ result = self.subid_generate(uid1, raiseonerr=False) ++ assert result.returncode > 0 ++ result = self.subid_generate(None, raiseonerr=False) + assert result.returncode > 0 + + tasks.kinit_admin(self.master) +@@ -121,10 +146,14 @@ class TestSubordinateId(IntegrationTest): + ) + + try: +- tasks.kinit_user(self.master, uid, password) +- self.user_auto_subid(uid) +- info = self.get_user(uid) +- assert "ipasubuidcount" in info ++ tasks.kinit_user(self.master, uid1, password) ++ self.subid_generate(uid1) ++ self.assert_subid(uid1, match=True) ++ ++ # add subid from whoami ++ tasks.kinit_as_user(self.master, uid2, password=password) ++ self.subid_generate(None) ++ self.assert_subid(uid2, match=True) + finally: + tasks.kinit_admin(self.master) + self.master.run_command( +@@ -140,45 +169,46 @@ class TestSubordinateId(IntegrationTest): + password = "Secret123" + + # create user administrator +- tasks.user_add(self.master, uid_useradmin, password=password) ++ tasks.create_active_user( ++ self.master, uid_useradmin, password=password ++ ) + # add user to user admin group + tasks.kinit_admin(self.master) + self.master.run_command( + ["ipa", "role-add-member", role, f"--users={uid_useradmin}"], + ) + # kinit as user admin +- tasks.kinit_user( +- self.master, +- uid_useradmin, +- f"{password}\n{password}\n{password}\n", +- ) ++ tasks.kinit_user(self.master, uid_useradmin, password) ++ + # create new user as user admin + tasks.user_add(self.master, uid) + # assign new subid to user (with useradmin credentials) +- self.user_auto_subid(uid) +- +- def test_subordinate_default_objclass(self): ++ self.subid_generate(uid) ++ ++ # test that user admin can preserve and delete users with subids ++ self.master.run_command(["ipa", "user-del", "--preserve", uid]) ++ # XXX does not work, see subordinate-ids.md ++ # subid should still exist ++ # self.assert_subid(uid, match=True) ++ # final delete should remove the user and subid ++ self.master.run_command(["ipa", "user-del", uid]) ++ self.assert_subid(uid, match=False) ++ ++ def tset_subid_auto_assign(self): + tasks.kinit_admin(self.master) ++ uid = "testuser_autoassign_user1" + +- result = self.master.run_command( +- ["ipa", "config-show", "--raw", "--all"] ++ self.master.run_command( ++ ["ipa", "config-mod", "--user-default-subid=true"] + ) +- info = self._parse_result(result) +- usercls = info["ipauserobjectclasses"] +- assert "ipasubordinateid" not in usercls +- +- cmd = [ +- "ipa", +- "config-mod", +- "--addattr", +- "ipaUserObjectClasses=ipasubordinateid", +- ] +- self.master.run_command(cmd) + +- uid = "testuser_usercls1" +- tasks.user_add(self.master, uid) +- info = self.get_user(uid) +- assert "ipasubuidcount" in info ++ try: ++ tasks.user_add(self.master, uid) ++ self.assert_subid(uid, match=True) ++ finally: ++ self.master.run_command( ++ ["ipa", "config-mod", "--user-default-subid=false"] ++ ) + + def test_idrange_subid(self): + tasks.kinit_admin(self.master) +@@ -199,3 +229,7 @@ class TestSubordinateId(IntegrationTest): + assert info["ipanttrusteddomainsid"].startswith( + "S-1-5-21-738065-838566-" + ) ++ ++ def test_subid_stats(self): ++ tasks.kinit_admin(self.master) ++ self.master.run_command(["ipa", "subid-stats"]) +-- +2.26.3 + diff --git a/0010-Use-389-DS-dnaInterval-setting-to-assign-intervals.patch b/0010-Use-389-DS-dnaInterval-setting-to-assign-intervals.patch new file mode 100644 index 0000000..33c9237 --- /dev/null +++ b/0010-Use-389-DS-dnaInterval-setting-to-assign-intervals.patch @@ -0,0 +1,113 @@ +From c9bae715b24df0f5476bdb70a2209d5f55e46a93 Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Fri, 21 May 2021 09:26:33 +0200 +Subject: [PATCH] Use 389-DS' dnaInterval setting to assign intervals + +Signed-off-by: Christian Heimes +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +--- + freeipa.spec.in | 3 ++- + install/share/dna.ldif | 1 + + install/updates/73-subid.update | 7 ++----- + ipaserver/plugins/subid.py | 14 +------------- + 4 files changed, 6 insertions(+), 19 deletions(-) + +diff --git a/freeipa.spec.in b/freeipa.spec.in +index 044e3559975c399f6697d4da94b5a059eb5b407c..fa649cf4e1abe8e9928ef340a66d48d78f7e3521 100755 +--- a/freeipa.spec.in ++++ b/freeipa.spec.in +@@ -106,8 +106,9 @@ + %global python_ldap_version 3.1.0-1 + + # Make sure to use 389-ds-base versions that fix https://github.com/389ds/389-ds-base/issues/4700 ++# and has DNA interval enabled + %if 0%{?fedora} < 34 +-%global ds_version %{lua: local v={}; v['32']='1.4.3.20-2'; v['33']='1.4.4.16-1'; print(v[rpm.expand('%{fedora}')])} ++%global ds_version 1.4.4.16-1 + %else + %global ds_version 2.0.5-1 + %endif +diff --git a/install/share/dna.ldif b/install/share/dna.ldif +index 735faab8261feef59486f7c933b01c57ad511166..9023fcd7db5a2c121c493559e2546c85c0daf69a 100644 +--- a/install/share/dna.ldif ++++ b/install/share/dna.ldif +@@ -31,6 +31,7 @@ dnaScope: $SUFFIX + dnaThreshold: eval($SUBID_DNA_THRESHOLD) + dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX + dnaExcludeScope: cn=provisioning,$SUFFIX ++dnaInterval: eval($SUBID_COUNT) + # TODO: enable when 389-DS' DNA plugin supports dnaStepAttr + # dnaIntervalAttr: ipasubuidcount + # dnaIntervalAttr: ipasubgidcount +diff --git a/install/updates/73-subid.update b/install/updates/73-subid.update +index 1aa43822a8b8c220583b81e08d70b648ca594363..e10703aa3f9528751233ddebe00b8c8c8fc5ed3f 100644 +--- a/install/updates/73-subid.update ++++ b/install/updates/73-subid.update +@@ -62,12 +62,8 @@ default:member: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX + # The delete-when-empty check is required because IPA uses MOD_REPLACE to + # set attributes, see https://github.com/389ds/389-ds-base/issues/4597. + # +-# TODO: remove (ipasubuidnumber>=eval($SUBID_RANGE_START) from +-# self-service permission when 389-DS' DNA plugin supports dnaStepAttr and +-# fake_dna_plugin hack has been removed. +-# + dn: cn=subids,cn=accounts,$SUFFIX +-add: aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(targetattr="description || ipaowner || ipauniqueid")(targattrfilters = "add=objectClass:(|(objectClass=top)(objectClass=ipasubordinateid)(objectClass=ipasubordinateidentry)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=eval($SUBID_RANGE_START))(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=eval($SUBID_RANGE_START))(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "selfservice: Add subordinate id";allow (add, write) userattr = "ipaowner#SELFDN" and groupdn="ldap:///cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX";) ++add: aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(targetattr="description || ipaowner || ipauniqueid")(targattrfilters = "add=objectClass:(|(objectClass=top)(objectClass=ipasubordinateid)(objectClass=ipasubordinateidentry)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(ipasubuidnumber=-1) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(ipasubgidnumber=-1) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "selfservice: Add subordinate id";allow (add, write) userattr = "ipaowner#SELFDN" and groupdn="ldap:///cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX";) + add: aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(targetattr="description || ipaowner || ipauniqueid")(targattrfilters = "add=objectClass:(|(objectClass=top)(objectClass=ipasubordinateid)(objectClass=ipasubordinateidentry)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=1)(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=1)(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "Add subordinate ids to any user";allow (add, write) groupdn="ldap:///cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX";) + + # DNA plugin and idrange configuration +@@ -90,6 +86,7 @@ default: dnaScope: $SUFFIX + default: dnaThreshold: eval($SUBID_DNA_THRESHOLD) + default: dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX + default: dnaExcludeScope: cn=provisioning,$SUFFIX ++default: dnaInterval: eval($SUBID_COUNT) + # TODO: enable when 389-DS' DNA plugin supports dnaStepAttr + # add: dnaIntervalAttr: ipasubuidcount + # add: dnaIntervalAttr: ipasubgidcount +diff --git a/ipaserver/plugins/subid.py b/ipaserver/plugins/subid.py +index 7d9a2f33e84bc7cdf17900346343e49d5eda0d8c..440f24ee627f0736100f63026158c564b04520c2 100644 +--- a/ipaserver/plugins/subid.py ++++ b/ipaserver/plugins/subid.py +@@ -2,7 +2,6 @@ + # Copyright (C) 2021 FreeIPA Contributors see COPYING for license + # + +-import random + import uuid + + from ipalib import api +@@ -291,12 +290,8 @@ class subid(LDAPObject): + _entry_attrs = ldap.get_entry(dn, ["objectclass"]) + entry_attrs["objectclass"] = _entry_attrs["objectclass"] + +- # XXX HACK, remove later +- if subuid == DNA_MAGIC: +- subuid = self._fake_dna_plugin(ldap, dn, entry_attrs) +- + entry_attrs["ipasubuidnumber"] = subuid +- # enforice subuid == subgid for now ++ # enforce subuid == subgid for now + entry_attrs["ipasubgidnumber"] = subuid + # hard-coded constants + entry_attrs["ipasubuidcount"] = constants.SUBID_COUNT +@@ -350,13 +345,6 @@ class subid(LDAPObject): + filters.extend(extra_filters) + return ldap.combine_filters(filters, rules=ldap.MATCH_ALL) + +- def _fake_dna_plugin(self, ldap, dn, entry_attrs): +- """XXX HACK, remove when 389-DS DNA plugin supports steps""" +- return ( +- constants.SUBID_RANGE_START +- + random.randint(1, 32764 - 2) * constants.SUBID_COUNT +- ) +- + + @register() + class subid_add(LDAPCreate): +-- +2.26.3 + diff --git a/0011-Fix-ipa-server-upgrade.patch b/0011-Fix-ipa-server-upgrade.patch new file mode 100644 index 0000000..055275c --- /dev/null +++ b/0011-Fix-ipa-server-upgrade.patch @@ -0,0 +1,68 @@ +From 21574b261cf0d346da48e34c0a5383736ca8798b Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Fri, 21 May 2021 14:56:32 +0200 +Subject: [PATCH] Fix ipa-server-upgrade + +Signed-off-by: Christian Heimes +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +--- + install/share/bootstrap-template.ldif | 2 +- + install/updates/73-subid.update | 2 +- + ipaserver/install/ldapupdate.py | 3 +++ + 3 files changed, 5 insertions(+), 2 deletions(-) + +diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif +index 16f2ef822eaf56dd68d4140b22a607539645b151..325eb8450c786899e7b5e4ae2ef8978f42a8425b 100644 +--- a/install/share/bootstrap-template.ldif ++++ b/install/share/bootstrap-template.ldif +@@ -491,7 +491,7 @@ cn: ${REALM}_subid_range + ipaBaseID: eval($SUBID_RANGE_START) + ipaIDRangeSize: eval($SUBID_RANGE_SIZE) + # HACK: RIDs to work around adtrust sidgen issue +-ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE) ++ipaBaseRID: eval($SUBID_BASE_RID) + # 738065-838566 = IPA-SUB + ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH + # HACK: "ipa-local-subid" range type causes issues with older SSSD clients +diff --git a/install/updates/73-subid.update b/install/updates/73-subid.update +index e10703aa3f9528751233ddebe00b8c8c8fc5ed3f..890eb7f1f6f261af977f26b3457e765ee8e9791f 100644 +--- a/install/updates/73-subid.update ++++ b/install/updates/73-subid.update +@@ -102,7 +102,7 @@ default: cn: ${REALM}_subid_range + default: ipaBaseID: $SUBID_RANGE_START + default: ipaIDRangeSize: $SUBID_RANGE_SIZE + # HACK: RIDs to work around adtrust sidgen issue +-default: ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE) ++default: ipaBaseRID: eval($SUBID_BASE_RID) + default: ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH + # HACK: "ipa-local-subid" range type causes issues with older SSSD clients + # see https://github.com/SSSD/sssd/issues/5571 +diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py +index d0516dc3028366df5d03a960866abe72601aa4b6..06cb78e0b7dc2c82f0339c43228045d93b922288 100644 +--- a/ipaserver/install/ldapupdate.py ++++ b/ipaserver/install/ldapupdate.py +@@ -59,8 +59,10 @@ def get_sub_dict(realm, domain, suffix, fqdn, idstart=None, idmax=None): + """ + if idstart is None: + idrange_size = None ++ subid_base_rid = None + else: + idrange_size = idmax - idstart + 1 ++ subid_base_rid = constants.SUBID_RANGE_START - idrange_size + + return dict( + REALM=realm, +@@ -81,6 +83,7 @@ def get_sub_dict(realm, domain, suffix, fqdn, idstart=None, idmax=None): + SUBID_RANGE_SIZE=constants.SUBID_RANGE_SIZE, + SUBID_RANGE_MAX=constants.SUBID_RANGE_MAX, + SUBID_DNA_THRESHOLD=constants.SUBID_DNA_THRESHOLD, ++ SUBID_BASE_RID=subid_base_rid, + DOMAIN_HASH=murmurhash3(domain, len(domain), 0xdeadbeef), + MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL, + MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL, +-- +2.26.3 + diff --git a/0012-Fix-oid-of-ipaUserDefaultSubordinateId.patch b/0012-Fix-oid-of-ipaUserDefaultSubordinateId.patch new file mode 100644 index 0000000..c964120 --- /dev/null +++ b/0012-Fix-oid-of-ipaUserDefaultSubordinateId.patch @@ -0,0 +1,29 @@ +From c8b4fd5bb773a73116350bf8e853246916fe87c2 Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Tue, 15 Jun 2021 13:25:18 +0200 +Subject: [PATCH] Fix oid of ipaUserDefaultSubordinateId + +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +--- + install/share/60ipaconfig.ldif | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/install/share/60ipaconfig.ldif b/install/share/60ipaconfig.ldif +index f84b38ead1d70ff408f5669029f1517b0c98ecf1..005c1dd11e37039132620f1d97f9662ffb8c8c59 100644 +--- a/install/share/60ipaconfig.ldif ++++ b/install/share/60ipaconfig.ldif +@@ -47,7 +47,7 @@ attributeTypes: ( 2.16.840.1.113730.3.8.3.27 NAME 'ipaSELinuxUserMapOrder' DESC + ## ipaMaxHostnameLength - maximum hostname length to allow + attributeTypes: ( 2.16.840.1.113730.3.8.1.28 NAME 'ipaMaxHostnameLength' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE) + # ipaUserDefaultSubordinateId - if TRUE new user entries gain subordinate id by default +-attributeTypes: ( 2.16.840.1.113730.3.8.3.23.14 NAME 'ipaUserDefaultSubordinateId' DESC 'Enable adding user entries with subordinate id' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.9') ++attributeTypes: ( 2.16.840.1.113730.3.8.23.14 NAME 'ipaUserDefaultSubordinateId' DESC 'Enable adding user entries with subordinate id' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.9') + ############################################### + ## + ## ObjectClasses +-- +2.26.3 + diff --git a/0013-WebUI-Improve-subordinate-ids-user-workflow.patch b/0013-WebUI-Improve-subordinate-ids-user-workflow.patch new file mode 100644 index 0000000..f1b78ec --- /dev/null +++ b/0013-WebUI-Improve-subordinate-ids-user-workflow.patch @@ -0,0 +1,275 @@ +From 10418b7f3ea8c682961fc201545169663d507bf6 Mon Sep 17 00:00:00 2001 +From: Serhii Tsymbaliuk +Date: Thu, 17 Jun 2021 13:56:19 +0200 +Subject: [PATCH] WebUI: Improve subordinate ids user workflow + +- add "Subordinate ID Statistics" page +- add button for generating subid in "Subordinate ids" tab of user details page +- allow to navigate directly to owner details from subordinate id page +- adjust i18n strings + +Ticket: https://pagure.io/freeipa/issue/8361 +Signed-off-by: Serhii Tsymbaliuk +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +--- + install/ui/src/freeipa/details.js | 8 ++- + .../ui/src/freeipa/navigation/menu_spec.js | 19 ++++++- + install/ui/src/freeipa/subid.js | 43 +++++++++++++++- + install/ui/src/freeipa/user.js | 49 +++++++++++++++---- + ipaserver/plugins/internal.py | 22 ++++++--- + 5 files changed, 121 insertions(+), 20 deletions(-) + +diff --git a/install/ui/src/freeipa/details.js b/install/ui/src/freeipa/details.js +index b557bbcef9a427a87eee3216f4345fc853cbaaff..2704cbd0ba98efa877cf5ec8a878e688ee6807e9 100644 +--- a/install/ui/src/freeipa/details.js ++++ b/install/ui/src/freeipa/details.js +@@ -602,6 +602,12 @@ exp.details_facet = IPA.details_facet = function(spec, no_init) { + */ + that.facet_group = spec.facet_group || 'settings'; + ++ /** ++ * Indicates if the details facet depends on pkey ++ * @property {boolean} ++ */ ++ that.require_pkey = spec.require_pkey !== undefined ? spec.require_pkey : true; ++ + /** + * Widgets + * @property {IPA.widget_container} +@@ -1105,7 +1111,7 @@ exp.details_facet = IPA.details_facet = function(spec, no_init) { + */ + that.refresh = function(on_success, on_error) { + +- if (!that.get_pkey() && that.entity.redirect_facet) { ++ if (that.require_pkey && !that.get_pkey() && that.entity.redirect_facet) { + that.redirect(); + return; + } +diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js +index 6ccd06919fbe04c7e8d2034ff7a1f644f373c607..a205dfade2f9508edbdc23ee6f7247508cc0479c 100644 +--- a/install/ui/src/freeipa/navigation/menu_spec.js ++++ b/install/ui/src/freeipa/navigation/menu_spec.js +@@ -104,7 +104,24 @@ var nav = {}; + } + ] + }, +- { entity: 'subid' } ++ { ++ name: 'subid', ++ label: '@i18n:tabs.subid', ++ children: [ ++ { ++ name: 'subid', ++ entity: 'subid', ++ facet: 'search', ++ label: '@i18n:tabs.subid' ++ }, ++ { ++ name: 'subid-stats', ++ entity: 'subid', ++ facet: 'stats', ++ label: '@i18n:objects.subid.stats' ++ } ++ ] ++ } + ] + }, + { +diff --git a/install/ui/src/freeipa/subid.js b/install/ui/src/freeipa/subid.js +index f286165070b08badf77cac6c30e93cab916c2acc..32f75bb7854cd3e84417a66870e99d34d49617e3 100644 +--- a/install/ui/src/freeipa/subid.js ++++ b/install/ui/src/freeipa/subid.js +@@ -31,6 +31,7 @@ return { + }, + { + $type: 'details', ++ disable_facet_tabs: true, + sections: [ + { + name: 'details', +@@ -38,9 +39,11 @@ return { + 'ipauniqueid', + 'description', + { ++ $type: 'link', + name: 'ipaowner', + label: '@i18n:objects.subid.ipaowner', +- title: '@mo-param:subid:ipaowner:label' ++ title: '@mo-param:subid:ipaowner:label', ++ other_entity: 'user' + }, + { + name: 'ipasubgidnumber', +@@ -65,6 +68,44 @@ return { + ] + } + ] ++ }, ++ { ++ $type: 'details', ++ name: 'stats', ++ label: '@i18n:objects.subid.stats', ++ refresh_command_name: 'stats', ++ check_rights: false, ++ no_update: true, ++ disable_facet_tabs: true, ++ disable_breadcrumb: true, ++ require_pkey: false, ++ fields: [ ++ { ++ name: 'assigned_subids', ++ label: '@i18n:objects.subid.assigned_subids', ++ read_only: true ++ }, ++ { ++ name: 'baseid', ++ label: '@i18n:objects.subid.baseid', ++ read_only: true ++ }, ++ { ++ name: 'dna_remaining', ++ label: '@i18n:objects.subid.dna_remaining', ++ read_only: true ++ }, ++ { ++ name: 'rangesize', ++ label: '@i18n:objects.subid.rangesize', ++ read_only: true ++ }, ++ { ++ name: 'remaining_subids', ++ label: '@i18n:objects.subid.remaining_subids', ++ read_only: true ++ } ++ ] + } + ], + adder_dialog: { +diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js +index 56bb6f4feffb637d33a57aecf9a98f08d4639550..6a56320c580f58a1aba84e598736631986421113 100644 +--- a/install/ui/src/freeipa/user.js ++++ b/install/ui/src/freeipa/user.js +@@ -464,7 +464,7 @@ return { + }, + { + $type: 'subid_generate', +- hide_cond: ['preserved-user'], ++ hide_cond: ['preserved-user', 'self-service-other'], + enable_cond: ['no-subid'] + } + ], +@@ -556,8 +556,35 @@ return { + { + $type: 'association', + name: 'memberof_subid', ++ columns: [ ++ 'ipauniqueid', ++ 'ipasubuidnumber', ++ 'ipasubgidnumber' ++ ], + associator: IPA.serial_associator, +- read_only: true ++ read_only: true, ++ state: { ++ evaluators: [ ++ IPA.user.self_service_other_user_evaluator, ++ IPA.user.preserved_user_evaluator, ++ IPA.user.has_subid_evaluator ++ ] ++ }, ++ actions: [ ++ { ++ $type: 'subid_generate', ++ name: 'subid_generate', ++ hide_cond: ['preserved-user', 'self-service-other'], ++ enable_cond: ['no-subid'] ++ } ++ ], ++ control_buttons: [ ++ { ++ name: 'subid_generate', ++ label: '@i18n:objects.user.auto_subid', ++ icon: 'fa-plus' ++ } ++ ] + } + ], + standard_association_facets: { +@@ -1216,14 +1243,16 @@ IPA.user.subid_generate_action = function(spec) { + var that = IPA.action(spec); + + that.execute_action = function(facet) { +- +- var subid_e = reg.entity.get('subid'); +- var dialog = subid_e.get_dialog('add'); +- dialog.open(); +- if (!IPA.is_selfservice) { +- var owner = facet.get_pkey(); +- dialog.get_field('ipaowner').set_value([owner]); +- } ++ var owner = facet.get_pkey(); ++ var command = rpc.command({ ++ entity: 'subid', ++ method: 'generate' ++ }); ++ command.set_option('ipaowner', owner); ++ command.on_success = function(data, text_status, xhr) { ++ facet.refresh(); ++ }; ++ command.execute(); + }; + + return that; +diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py +index 5ef940c2b88cc2b132a15d619772349b30731306..29e09f0067ec60d014e61c49313455d64478ef22 100644 +--- a/ipaserver/plugins/internal.py ++++ b/ipaserver/plugins/internal.py +@@ -1364,6 +1364,20 @@ class i18n_messages(Command): + "undel_success": _("${count} user(s) restored"), + "user_categories": _("User categories"), + }, ++ "subid": { ++ "add": _("Add subid"), ++ "assigned_subids": _("Assigned subids"), ++ "baseid": _("Base ID"), ++ "dna_remaining": _("DNA remaining"), ++ "ipaowner": _("Owner"), ++ "ipasubgidcount": _("SubGID range size"), ++ "ipasubgidnumber": _("SubGID range start"), ++ "ipasubuidcount": _("SubUID range size"), ++ "ipasubuidnumber": _("SubUID range start"), ++ "rangesize": _("Range size"), ++ "remaining_subids": _("Remaining subids"), ++ "stats": _("Subordinate ID Statistics"), ++ }, + "sudocmd": { + "add": _("Add sudo command"), + "add_into_sudocmdgroups": _( +@@ -1547,13 +1561,6 @@ class i18n_messages(Command): + "Drive to mount a home directory" + ), + }, +- "subid": { +- "identity": _("Subordinate user and group id"), +- "subuidnumber": _("Subordinate user id"), +- "subuidcount": _("Subordinate user id count"), +- "subgidnumber": _("Subordinate group id"), +- "subgidcount": _("Subordinate group id count"), +- }, + "trustconfig": { + "options": _("Options"), + }, +@@ -1942,6 +1949,7 @@ class i18n_messages(Command): + "network_services": _("Network Services"), + "policy": _("Policy"), + "role": _("Role-Based Access Control"), ++ "subid": _("Subordinate IDs"), + "sudo": _("Sudo"), + "topology": _("Topology"), + "trust": _("Trusts"), +-- +2.26.3 + diff --git a/0014-Test-DNA-plugin-configuration.patch b/0014-Test-DNA-plugin-configuration.patch new file mode 100644 index 0000000..75244fd --- /dev/null +++ b/0014-Test-DNA-plugin-configuration.patch @@ -0,0 +1,57 @@ +From b6ab27acdb07c21f43e9dcc9b777f8fd6a8925e1 Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Fri, 18 Jun 2021 10:51:54 +0200 +Subject: [PATCH] Test DNA plugin configuration + +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +Reviewed-By: Francois Cami +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_subids.py | 22 +++++++++++++++++++++- + 1 file changed, 21 insertions(+), 1 deletion(-) + +diff --git a/ipatests/test_integration/test_subids.py b/ipatests/test_integration/test_subids.py +index 48e58c26464f52605438afe865575e5ca4c8f1f8..28cd1f765cd63af944bce83f4676a2b1998f5f5d 100644 +--- a/ipatests/test_integration/test_subids.py ++++ b/ipatests/test_integration/test_subids.py +@@ -6,8 +6,11 @@ + """ + import os + +-from ipalib.constants import SUBID_COUNT, SUBID_RANGE_START, SUBID_RANGE_MAX ++from ipalib.constants import ( ++ SUBID_COUNT, SUBID_RANGE_START, SUBID_RANGE_MAX, SUBID_DNA_THRESHOLD ++) + from ipaplatform.paths import paths ++from ipapython.dn import DN + from ipatests.pytest_ipa.integration import tasks + from ipatests.test_integration.base import IntegrationTest + +@@ -81,6 +84,23 @@ class TestSubordinateId(IntegrationTest): + cmd.extend(("--owner", uid)) + return self.master.run_command(cmd, **kwargs) + ++ def test_dna_config(self): ++ conn = self.master.ldap_connect() ++ dna_cfg = DN( ++ "cn=Subordinate IDs,cn=Distributed Numeric Assignment Plugin," ++ "cn=plugins,cn=config" ++ ) ++ entry = conn.get_entry(dna_cfg) ++ ++ def single_int(key): ++ return int(entry.single_value[key]) ++ ++ assert single_int("dnaInterval") == SUBID_COUNT ++ assert single_int("dnaThreshold") == SUBID_DNA_THRESHOLD ++ assert single_int("dnaMagicRegen") == -1 ++ assert single_int("dnaMaxValue") == SUBID_RANGE_MAX ++ assert set(entry["dnaType"]) == {"ipasubgidnumber", "ipasubuidnumber"} ++ + def test_auto_generate_subid(self): + uid = "testuser_auto1" + passwd = "Secret123" +-- +2.26.3 + diff --git a/0015-Fall-back-to-krbprincipalname-when-validating-host-a.patch b/0015-Fall-back-to-krbprincipalname-when-validating-host-a.patch new file mode 100644 index 0000000..780d75d --- /dev/null +++ b/0015-Fall-back-to-krbprincipalname-when-validating-host-a.patch @@ -0,0 +1,69 @@ +From 3b7f537dd3022ecb758b2f0f8b2aba530e74bff7 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 12 Jul 2021 11:02:10 -0400 +Subject: [PATCH] Fall back to krbprincipalname when validating host auth + indicators + +When adding a new host the principal cannot be determined because it +relies on either: + +a) an entry to already exist +b) krbprincipalname be a component of the dn + +As a result the full dn is being passed into ipapython.Kerberos +which can't parse it. + +Look into the entry in validate_validate_auth_indicator() for +krbprincipalname in this case. + +https://pagure.io/freeipa/issue/8206 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/plugins/service.py | 5 +++++ + ipatests/test_xmlrpc/test_host_plugin.py | 11 +++++++++++ + 2 files changed, 16 insertions(+) + +diff --git a/ipaserver/plugins/service.py b/ipaserver/plugins/service.py +index cfbbff3c69c6a92535df58c51767c3d0952c7b0b..498f5e444364c6330e053d1057b727fb5181f70b 100644 +--- a/ipaserver/plugins/service.py ++++ b/ipaserver/plugins/service.py +@@ -209,6 +209,11 @@ def validate_auth_indicator(entry): + # and shouldn't be allowed to have auth indicators. + # https://pagure.io/freeipa/issue/8206 + pkey = api.Object['service'].get_primary_key_from_dn(entry.dn) ++ if pkey == str(entry.dn): ++ # krbcanonicalname may not be set yet if this is a host entry, ++ # try krbprincipalname ++ if 'krbprincipalname' in entry: ++ pkey = entry['krbprincipalname'] + principal = kerberos.Principal(pkey) + server = api.Command.server_find(principal.hostname)['result'] + if server: +diff --git a/ipatests/test_xmlrpc/test_host_plugin.py b/ipatests/test_xmlrpc/test_host_plugin.py +index 9cfde3565d48e103a0549e2bfb7579e07668f41b..ff50e796cd19fca2c7b6c87d73940779db8daa0b 100644 +--- a/ipatests/test_xmlrpc/test_host_plugin.py ++++ b/ipatests/test_xmlrpc/test_host_plugin.py +@@ -615,6 +615,17 @@ class TestProtectedMaster(XMLRPC_test): + )): + command() + ++ def test_add_non_master_with_auth_ind(self, host5): ++ host5.ensure_missing() ++ command = host5.make_command( ++ 'host_add', host5.fqdn, krbprincipalauthind=['radius'], ++ force=True ++ ) ++ result = command() ++ # The fact that the command succeeds exercises the change but ++ # let's check the indicator as well. ++ assert result['result']['krbprincipalauthind'] == ('radius',) ++ + + @pytest.mark.tier1 + class TestValidation(XMLRPC_test): +-- +2.26.3 + diff --git a/0016-spec-file-Trust-controller-role-should-pull-sssd-win.patch b/0016-spec-file-Trust-controller-role-should-pull-sssd-win.patch new file mode 100644 index 0000000..cdb9c35 --- /dev/null +++ b/0016-spec-file-Trust-controller-role-should-pull-sssd-win.patch @@ -0,0 +1,30 @@ +From aa07f41769765e55c1531b52ad9ef5876e97e0e9 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Thu, 15 Jul 2021 10:06:56 +0200 +Subject: [PATCH] spec file: Trust controller role should pull + sssd-winbind-idmap package + +ipa-server-trust-ad subpackage need to pull in sssd-winbind-idmap +Fixes: https://pagure.io/freeipa/issue/8923 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +--- + freeipa.spec.in | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/freeipa.spec.in b/freeipa.spec.in +index fa649cf4e1abe8e9928ef340a66d48d78f7e3521..c33d2e216e5b0f13ae4fd3f9f506d4983493f03a 100755 +--- a/freeipa.spec.in ++++ b/freeipa.spec.in +@@ -597,6 +597,7 @@ Requires: %{name}-common = %{version}-%{release} + + Requires: samba >= %{samba_version} + Requires: samba-winbind ++Requires: sssd-winbind-idmap + Requires: libsss_idmap + %if 0%{?rhel} + Obsoletes: ipa-idoverride-memberof-plugin <= 0.1 +-- +2.26.3 + diff --git a/freeipa.spec b/freeipa.spec index ae07ba5..374e5b0 100644 --- a/freeipa.spec +++ b/freeipa.spec @@ -77,7 +77,8 @@ # Bug 1929067 - PKI instance creation failed with new 389-ds-base build %global ds_version 1.4.3.16-12 %else -%global ds_version 2.0.3-3 +# DNA interval enabled +%global ds_version 2.0.5-1 %endif # Fix for TLS 1.3 PHA, RHBZ#1775158 @@ -106,8 +107,9 @@ %global python_ldap_version 3.1.0-1 # Make sure to use 389-ds-base versions that fix https://github.com/389ds/389-ds-base/issues/4700 +# and has DNA interval enabled %if 0%{?fedora} < 34 -%global ds_version %{lua: local v={}; v['32']='1.4.3.20-2'; v['33']='1.4.4.16-1'; print(v[rpm.expand('%{fedora}')])} +%global ds_version 1.4.4.16-1 %else %global ds_version 2.0.5-1 %endif @@ -194,7 +196,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 2%{?rc_version:.%rc_version}%{?dist} +Release: 3%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPLv3+ @@ -220,6 +222,16 @@ Patch0003: 0003-ipatests-ensure-auth-indicators-can-t-be-added-to-in.patch Patch0004: 0004-stageuser-add-ipauserauthtypeclass-when-required.patch Patch0005: 0005-XMLRPC-test-add-a-test-for-stageuser-add-user-auth-t.patch Patch0006: 0006-augeas-bump-version-for-rhel9.patch +Patch0007: 0007-man-page-update-ipa-server-upgrade.1.patch +Patch0008: 0008-Add-basic-support-for-subordinate-user-group-ids.patch +Patch0009: 0009-Redesign-subid-feature.patch +Patch0010: 0010-Use-389-DS-dnaInterval-setting-to-assign-intervals.patch +Patch0011: 0011-Fix-ipa-server-upgrade.patch +Patch0012: 0012-Fix-oid-of-ipaUserDefaultSubordinateId.patch +Patch0013: 0013-WebUI-Improve-subordinate-ids-user-workflow.patch +Patch0014: 0014-Test-DNA-plugin-configuration.patch +Patch0015: 0015-Fall-back-to-krbprincipalname-when-validating-host-a.patch +Patch0016: 0016-spec-file-Trust-controller-role-should-pull-sssd-win.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif @@ -597,6 +609,7 @@ Requires: %{name}-common = %{version}-%{release} Requires: samba >= %{samba_version} Requires: samba-winbind Requires: libsss_idmap +Requires: sssd-winbind-idmap %if 0%{?rhel} Obsoletes: ipa-idoverride-memberof-plugin <= 0.1 %endif @@ -1361,6 +1374,7 @@ fi %{_libexecdir}/ipa/ipa-pki-wait-running %{_libexecdir}/ipa/ipa-otpd %{_libexecdir}/ipa/ipa-print-pac +%{_libexecdir}/ipa/ipa-subids %dir %{_libexecdir}/ipa/custodia %attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-dmldap %attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat @@ -1698,6 +1712,12 @@ fi %endif %changelog +* Thu Jul 15 2021 Florence Blanc-Renaud - 4.9.6-3 +- Resolves: rhbz#1979629 Add checks to prevent assigning authentication indicators to internal IPA services +- Resolves: rhbz#1982212 ipa-trust-add fails with "not enough quota" +- Resolves: rhbz#1952028 [RFE] Add support for managing subuids and subgids in FreeIPA +- Resolves: rhbz#1981789 [man page] contradiction in ipa-server-upgrade command's man page and usage + * Fri Jul 9 2021 Florence Blanc-Renaud - 4.9.6-2 - Resolves: rhbz#1955440 ipa installation fails to configure chrony - Resolves: rhbz#1976761 Package python3-ipatests (from CRB repo) Requires python3-coverage