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