7e1b55
From 69cd05bf635d19b9844f65d83dace05136a40326 Mon Sep 17 00:00:00 2001
7e1b55
From: Christian Heimes <cheimes@redhat.com>
7e1b55
Date: Fri, 19 Mar 2021 11:48:38 +0100
7e1b55
Subject: [PATCH] Add basic support for subordinate user/group ids
7e1b55
7e1b55
New LDAP object class "ipaUserSubordinate" with four new fields:
7e1b55
- ipasubuidnumber / ipasubuidcount
7e1b55
- ipasubgidnumber / ipasgbuidcount
7e1b55
7e1b55
New self-service permission to add subids.
7e1b55
7e1b55
New command user-auto-subid to auto-assign subid
7e1b55
7e1b55
The code hard-codes counts to 65536, sets subgid equal to subuid, and
7e1b55
does not allow removal of subids. There is also a hack that emulates a
7e1b55
DNA plugin with step interval 65536 for testing.
7e1b55
7e1b55
Work around problem with older SSSD clients that fail with unknown
7e1b55
idrange type "ipa-local-subid", see: https://github.com/SSSD/sssd/issues/5571
7e1b55
7e1b55
Related: https://pagure.io/freeipa/issue/8361
7e1b55
Signed-off-by: Christian Heimes <cheimes@redhat.com>
7e1b55
Reviewed-By: Francois Cami <fcami@redhat.com>
7e1b55
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
7e1b55
Reviewed-By: Francois Cami <fcami@redhat.com>
7e1b55
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
7e1b55
---
7e1b55
 ACI.txt                                   |   2 +-
7e1b55
 API.txt                                   |  47 ++-
7e1b55
 Makefile.am                               |   2 +-
7e1b55
 VERSION.m4                                |   4 +-
7e1b55
 doc/designs/index.rst                     |   1 +
7e1b55
 doc/designs/subordinate-ids.md            | 468 ++++++++++++++++++++++
7e1b55
 freeipa.spec.in                           |   1 +
7e1b55
 install/share/60basev2.ldif               |   1 +
7e1b55
 install/share/60basev4.ldif               |  19 +
7e1b55
 install/share/Makefile.am                 |   1 +
7e1b55
 install/share/bootstrap-template.ldif     |  22 +
7e1b55
 install/share/dna.ldif                    |  20 +
7e1b55
 install/tools/Makefile.am                 |   2 +
7e1b55
 install/tools/ipa-subids.in               |   8 +
7e1b55
 install/ui/src/freeipa/user.js            |  53 ++-
7e1b55
 install/updates/20-indices.update         |  18 +
7e1b55
 install/updates/73-subid.update           | 102 +++++
7e1b55
 install/updates/Makefile.am               |   1 +
7e1b55
 ipalib/constants.py                       |  13 +
7e1b55
 ipaserver/install/adtrustinstance.py      |  29 +-
7e1b55
 ipaserver/install/dsinstance.py           |  43 +-
7e1b55
 ipaserver/install/ipa_subids.py           | 154 +++++++
7e1b55
 ipaserver/install/ldapupdate.py           |  95 +++--
7e1b55
 ipaserver/plugins/baseuser.py             | 274 ++++++++++++-
7e1b55
 ipaserver/plugins/idrange.py              |  10 +-
7e1b55
 ipaserver/plugins/internal.py             |  12 +
7e1b55
 ipaserver/plugins/user.py                 |  17 +-
7e1b55
 ipatests/prci_definitions/gating.yaml     |  12 +
7e1b55
 ipatests/test_integration/test_subids.py  | 201 ++++++++++
7e1b55
 ipatests/test_xmlrpc/test_range_plugin.py |   7 +
7e1b55
 31 files changed, 1565 insertions(+), 75 deletions(-)
7e1b55
 create mode 100644 doc/designs/subordinate-ids.md
7e1b55
 create mode 100644 install/share/60basev4.ldif
7e1b55
 create mode 100644 install/tools/ipa-subids.in
7e1b55
 create mode 100644 install/updates/73-subid.update
7e1b55
 create mode 100644 ipaserver/install/ipa_subids.py
7e1b55
 create mode 100644 ipatests/test_integration/test_subids.py
7e1b55
7e1b55
diff --git a/ACI.txt b/ACI.txt
7e1b55
index 05852cf6c0150db7d8de99a5f7a44e538df29e5e..fce02a333b212de9b61f920515eed3e356b1391b 100644
7e1b55
--- a/ACI.txt
7e1b55
+++ b/ACI.txt
7e1b55
@@ -375,7 +375,7 @@ aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber
7e1b55
 dn: dc=ipa,dc=example
7e1b55
 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";)
7e1b55
 dn: cn=users,cn=accounts,dc=ipa,dc=example
7e1b55
-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";)
7e1b55
+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";)
7e1b55
 dn: cn=users,cn=accounts,dc=ipa,dc=example
7e1b55
 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";)
7e1b55
 dn: cn=users,cn=accounts,dc=ipa,dc=example
7e1b55
diff --git a/API.txt b/API.txt
7e1b55
index 212ef807c771794dc2f89eb89e03b669eb49295b..262b4d6a72c7d7032a7027116f7a4f65aa620615 100644
7e1b55
--- a/API.txt
7e1b55
+++ b/API.txt
7e1b55
@@ -4974,7 +4974,7 @@ output: Entry('result')
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: PrimaryKey('value')
7e1b55
 command: stageuser_add/1
7e1b55
-args: 1,45,3
7e1b55
+args: 1,46,3
7e1b55
 arg: Str('uid', cli_name='login')
7e1b55
 option: Str('addattr*', cli_name='addattr')
7e1b55
 option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
@@ -4992,6 +4992,7 @@ option: Str('givenname', cli_name='first')
7e1b55
 option: Str('homedirectory?', cli_name='homedir')
7e1b55
 option: Str('initials?', autofill=True)
7e1b55
 option: Str('ipasshpubkey*', cli_name='sshpubkey')
7e1b55
+option: Int('ipasubuidnumber?', cli_name='subuid')
7e1b55
 option: Str('ipatokenradiusconfiglink?', cli_name='radius')
7e1b55
 option: Str('ipatokenradiususername?', cli_name='radius_username')
7e1b55
 option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
7e1b55
@@ -5080,7 +5081,7 @@ output: Output('result', type=[<type 'dict'>])
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: ListOfPrimaryKeys('value')
7e1b55
 command: stageuser_find/1
7e1b55
-args: 1,58,4
7e1b55
+args: 1,60,4
7e1b55
 arg: Str('criteria?')
7e1b55
 option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
 option: Str('carlicense*', autofill=False)
7e1b55
@@ -5104,6 +5105,8 @@ option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir')
7e1b55
 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:'])
7e1b55
 option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
7e1b55
 option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
7e1b55
+option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid')
7e1b55
+option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
7e1b55
 option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
7e1b55
 option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
7e1b55
 option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
7e1b55
@@ -5145,7 +5148,7 @@ output: ListOfEntries('result')
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: Output('truncated', type=[<type 'bool'>])
7e1b55
 command: stageuser_mod/1
7e1b55
-args: 1,51,3
7e1b55
+args: 1,52,3
7e1b55
 arg: Str('uid', cli_name='login')
7e1b55
 option: Str('addattr*', cli_name='addattr')
7e1b55
 option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
@@ -5167,6 +5170,7 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d
7e1b55
 option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
7e1b55
 option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
7e1b55
 option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey')
7e1b55
+option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
7e1b55
 option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
7e1b55
 option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
7e1b55
 option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
7e1b55
@@ -6058,7 +6062,7 @@ output: Entry('result')
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: PrimaryKey('value')
7e1b55
 command: user_add/1
7e1b55
-args: 1,46,3
7e1b55
+args: 1,47,3
7e1b55
 arg: Str('uid', cli_name='login')
7e1b55
 option: Str('addattr*', cli_name='addattr')
7e1b55
 option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
@@ -6075,6 +6079,7 @@ option: Str('givenname', cli_name='first')
7e1b55
 option: Str('homedirectory?', cli_name='homedir')
7e1b55
 option: Str('initials?', autofill=True)
7e1b55
 option: Str('ipasshpubkey*', cli_name='sshpubkey')
7e1b55
+option: Int('ipasubuidnumber?', cli_name='subuid')
7e1b55
 option: Str('ipatokenradiusconfiglink?', cli_name='radius')
7e1b55
 option: Str('ipatokenradiususername?', cli_name='radius_username')
7e1b55
 option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
7e1b55
@@ -6156,6 +6161,16 @@ option: Str('version?')
7e1b55
 output: Entry('result')
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: PrimaryKey('value')
7e1b55
+command: user_auto_subid/1
7e1b55
+args: 1,4,3
7e1b55
+arg: Str('uid', cli_name='login')
7e1b55
+option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
+option: Flag('no_members', autofill=True, default=False)
7e1b55
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
7e1b55
+option: Str('version?')
7e1b55
+output: Entry('result')
7e1b55
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
+output: PrimaryKey('value')
7e1b55
 command: user_del/1
7e1b55
 args: 1,3,3
7e1b55
 arg: Str('uid+', cli_name='login')
7e1b55
@@ -6180,7 +6195,7 @@ output: Output('result', type=[<type 'bool'>])
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: PrimaryKey('value')
7e1b55
 command: user_find/1
7e1b55
-args: 1,61,4
7e1b55
+args: 1,63,4
7e1b55
 arg: Str('criteria?')
7e1b55
 option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
 option: Str('carlicense*', autofill=False)
7e1b55
@@ -6204,6 +6219,8 @@ option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir')
7e1b55
 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:'])
7e1b55
 option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
7e1b55
 option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
7e1b55
+option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid')
7e1b55
+option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
7e1b55
 option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
7e1b55
 option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
7e1b55
 option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
7e1b55
@@ -6247,8 +6264,23 @@ output: Output('count', type=[<type 'int'>])
7e1b55
 output: ListOfEntries('result')
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: Output('truncated', type=[<type 'bool'>])
7e1b55
+command: user_match_subid/1
7e1b55
+args: 1,8,4
7e1b55
+arg: Str('criteria?')
7e1b55
+option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
+option: Int('ipasubuidnumber', autofill=False, cli_name='subuid')
7e1b55
+option: Flag('no_members', autofill=True, default=True)
7e1b55
+option: Flag('pkey_only?', autofill=True, default=False)
7e1b55
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
7e1b55
+option: Int('sizelimit?', autofill=False)
7e1b55
+option: Int('timelimit?', autofill=False)
7e1b55
+option: Str('version?')
7e1b55
+output: Output('count', type=[<type 'int'>])
7e1b55
+output: ListOfEntries('result')
7e1b55
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
+output: Output('truncated', type=[<type 'bool'>])
7e1b55
 command: user_mod/1
7e1b55
-args: 1,52,3
7e1b55
+args: 1,53,3
7e1b55
 arg: Str('uid', cli_name='login')
7e1b55
 option: Str('addattr*', cli_name='addattr')
7e1b55
 option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
@@ -6270,6 +6302,7 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d
7e1b55
 option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
7e1b55
 option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
7e1b55
 option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey')
7e1b55
+option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
7e1b55
 option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
7e1b55
 option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
7e1b55
 option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
7e1b55
@@ -7183,10 +7216,12 @@ default: user_add_cert/1
7e1b55
 default: user_add_certmapdata/1
7e1b55
 default: user_add_manager/1
7e1b55
 default: user_add_principal/1
7e1b55
+default: user_auto_subid/1
7e1b55
 default: user_del/1
7e1b55
 default: user_disable/1
7e1b55
 default: user_enable/1
7e1b55
 default: user_find/1
7e1b55
+default: user_match_subid/1
7e1b55
 default: user_mod/1
7e1b55
 default: user_remove_cert/1
7e1b55
 default: user_remove_certmapdata/1
7e1b55
diff --git a/Makefile.am b/Makefile.am
7e1b55
index c5a33e67f56b2c6f9efb5b4c6af3f7a44ccbdb3c..321df05a7c44f32929a2c5ec45341a42105a8e2f 100644
7e1b55
--- a/Makefile.am
7e1b55
+++ b/Makefile.am
7e1b55
@@ -229,7 +229,7 @@ fasttest: $(GENERATED_PYTHON_FILES) ipasetup.py
7e1b55
 	    --ignore $(abspath $(top_srcdir))/ipatests/test_integration \
7e1b55
 	    --ignore $(abspath $(top_srcdir))/ipatests/test_xmlrpc
7e1b55
 
7e1b55
-fastlint: $(GENERATED_PYTHON_FILES) ipasetup.py
7e1b55
+fastlint: $(GENERATED_PYTHON_FILES) ipasetup.py acilint apilint
7e1b55
 if ! WITH_PYLINT
7e1b55
 	@echo "ERROR: pylint not available"; exit 1
7e1b55
 endif
7e1b55
diff --git a/VERSION.m4 b/VERSION.m4
7e1b55
index 9f024675f905a1ee771b6ff293c25b2ac46d92df..1c1e0d56c0eb5c15be0887fae9f90e399757acc7 100644
7e1b55
--- a/VERSION.m4
7e1b55
+++ b/VERSION.m4
7e1b55
@@ -86,8 +86,8 @@ define(IPA_DATA_VERSION, 20100614120000)
7e1b55
 #                                                      #
7e1b55
 ########################################################
7e1b55
 define(IPA_API_VERSION_MAJOR, 2)
7e1b55
-define(IPA_API_VERSION_MINOR, 242)
7e1b55
-# Last change: add status options for cert-find
7e1b55
+# Last change: add subordinate id feature
7e1b55
+define(IPA_API_VERSION_MINOR, 243)
7e1b55
 
7e1b55
 
7e1b55
 ########################################################
7e1b55
diff --git a/doc/designs/index.rst b/doc/designs/index.rst
7e1b55
index cbec1096c363c9c31656b05f22c50321cd45e073..6dd0edff3004fd0d19208f0c063d4156bde3bf91 100644
7e1b55
--- a/doc/designs/index.rst
7e1b55
+++ b/doc/designs/index.rst
7e1b55
@@ -17,3 +17,4 @@ FreeIPA design documentation
7e1b55
    membermanager.md
7e1b55
    hidden-replicas.md
7e1b55
    disable-stale-users.md
7e1b55
+   subordinate-ids.md
7e1b55
diff --git a/doc/designs/subordinate-ids.md b/doc/designs/subordinate-ids.md
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..1b578667a8cfdda223af38a14d142c72a5d5c073
7e1b55
--- /dev/null
7e1b55
+++ b/doc/designs/subordinate-ids.md
7e1b55
@@ -0,0 +1,468 @@
7e1b55
+# Central management of subordinate user and group ids
7e1b55
+
7e1b55
+Subordinate ids are a Linux Kernel feature to grant a user additional
7e1b55
+user and group id ranges. Amongst others the feature can be used
7e1b55
+by container runtime engies to implement rootless containers.
7e1b55
+Traditionally subordinate id ranges are configured in ``/etc/subuid``
7e1b55
+and ``/etc/subgid``.
7e1b55
+
7e1b55
+To make rootless containers in a large environment as easy as pie, IPA
7e1b55
+gains the ability to centrally manage and assign subordinate id ranges.
7e1b55
+SSSD and shadow-util are extended to read subordinate ids from IPA and
7e1b55
+provide them to userspace tools.
7e1b55
+
7e1b55
+## Overview
7e1b55
+
7e1b55
+Feature requests
7e1b55
+
7e1b55
+* [FreeIPA feature request #8361](https://pagure.io/freeipa/issue/8361)
7e1b55
+* [SSSD feature request #5197](https://github.com/SSSD/sssd/issues/5197)
7e1b55
+* [shadow-util feature request #154](https://github.com/shadow-maint/shadow/issues/154)
7e1b55
+* [389-DS RFE for DNA plugin rhbz#1938239](https://bugzilla.redhat.com/show_bug.cgi?id=1938239)
7e1b55
+
7e1b55
+Man pages
7e1b55
+
7e1b55
+* [man subuid(5)](https://man7.org/linux/man-pages/man5/subuid.5.html)
7e1b55
+* [man subgid(5)](https://man7.org/linux/man-pages/man5/subgid.5.html)
7e1b55
+* [man user_namespaces(7)](https://man7.org/linux/man-pages/man7/user_namespaces.7.html)
7e1b55
+* [man newuidmap(1)](https://man7.org/linux/man-pages/man1/newuidmap.1.html)
7e1b55
+
7e1b55
+Articles / blog posts
7e1b55
+* [Basic Setup and Use of Podman in a Rootless environment](https://github.com/containers/podman/blob/master/docs/tutorials/rootless_tutorial.md)
7e1b55
+* [How does rootless Podman work](https://opensource.com/article/19/2/how-does-rootless-podman-work)
7e1b55
+
7e1b55
+## Design choices
7e1b55
+
7e1b55
+Some design choices are owed to the circumstance that uids and gids
7e1b55
+are limited datatypes. The Linux Kernel and userland defines
7e1b55
+``uid_t`` and ``gid_t`` as unsigned 32bit integers (``uint32_t``), which
7e1b55
+limits possible values for numeric user and group ids to
7e1b55
+``0 .. 2^32-2``. ``(uid_t)-1`` is reserved for error reporting. On the
7e1b55
+other hand the user ``nobody`` typically has uid 65534 / gid 65534. This
7e1b55
+means we need to assign 65,536 subordinate ids to every user. The
7e1b55
+theoretical maximum amount of subordinate ranges is less than 65,536
7e1b55
+(``65536 * 65536 == 2^32``). [``logins.def``](https://man7.org/linux/man-pages/man5/login.defs.5.html)
7e1b55
+also uses 65536 as default setting for ``SUB_UID_COUNT``.
7e1b55
+
7e1b55
+The practical limit is far smaller. Subordinate ids should not overlap
7e1b55
+with system accounts, local user accounts, IPA user accounts, and
7e1b55
+mapped accounts from Active Directory. Therefore IPA uses the upper
7e1b55
+half of the uid_t range (>= 2^31 == 2,147,483,648) for subordinate ids.
7e1b55
+The high bit is rarely used. IPA limits general numeric ids
7e1b55
+(``uidNumber``, ``gidNumber``, ID ranges) to maximum values of signed
7e1b55
+32bit integer (2^31-1) for backwards compatibility with XML-RPC.
7e1b55
+``logins.def`` defaults to ``SUB_UID_MAX`` 600,100,000.
7e1b55
+
7e1b55
+A default subordinate id count of 65,536 and a total range of approx.
7e1b55
+2.1 billion limits IPA to slightly more than 32,000 possible ranges. It
7e1b55
+may sound like a lot of users, but there are much bigger installations
7e1b55
+of IPA. For comparison Fedora Accounts has over 120,000 users stored in
7e1b55
+IPA.
7e1b55
+
7e1b55
+For that reason we treat subordinate id space as premium real estate
7e1b55
+and don't auto-map or auto-assign subordinate ids by default. Instead
7e1b55
+we give the admin several options to assign them manually, semi-manual,
7e1b55
+or automatically.
7e1b55
+
7e1b55
+### Revision 1 limitation
7e1b55
+
7e1b55
+The first revision of the feature is deliberately limited and
7e1b55
+restricted. We are aiming for a simple implementation that covers
7e1b55
+basic use cases. Some restrictions may be lifted in the future.
7e1b55
+
7e1b55
+* subuid and subgids cannot be set independently. They are always set
7e1b55
+  to the same value.
7e1b55
+* counts are hard-coded to value 65536
7e1b55
+* once assigned subids cannot be removed
7e1b55
+* IPA does not support multiple subordinate id ranges. Contrary to
7e1b55
+  ``/etc/subuid``, users are limited to one set of subordinate ids.
7e1b55
+* subids are auto-assigned. Auto-assignment is currently emulated
7e1b55
+  until 389-DS has been extended to support DNA with step interval.
7e1b55
+* subids are allocated from hard-coded range
7e1b55
+  ``[2147483648..4294901767]`` (``2^31`` to ``2^32-1-65536``), which
7e1b55
+  is the upper 2.1 billion uids of ``uid_t`` (``uint32_t``). The range
7e1b55
+  can hold little 32,767 subordinate id ranges.
7e1b55
+* Active Directory support is out of scope and may be provided in the
7e1b55
+  future.
7e1b55
+
7e1b55
+### Subid assignment example
7e1b55
+
7e1b55
+```
7e1b55
+>>> import itertools
7e1b55
+>>> def subids():
7e1b55
+...     for n in itertools.count(start=0):
7e1b55
+...         start = SUBID_RANGE_START + (n * SUBID_COUNT)
7e1b55
+...         last = start + SUBID_COUNT - 1
7e1b55
+...         yield (start, last)
7e1b55
+...
7e1b55
+>>> gen = subids()
7e1b55
+>>> next(gen)
7e1b55
+(2147483648, 2147549183)
7e1b55
+>>> next(gen)
7e1b55
+(2147549184, 2147614719)
7e1b55
+>>> next(gen)
7e1b55
+(2147614720, 2147680255)
7e1b55
+```
7e1b55
+
7e1b55
+The first user has 65565 subordinate ids from uid/gid ``2147483648``
7e1b55
+to ``2147549183``, the next user has ``2147549184`` to ``2147614719``,
7e1b55
+and so on. The range count includes the start value.
7e1b55
+
7e1b55
+An installation with multiple servers, 389-DS'
7e1b55
+[DNA](https://directory.fedoraproject.org/docs/389ds/design/dna-plugin.html)
7e1b55
+plug-in takes care of delegating and assigning chunks of subid ranges
7e1b55
+to servers. The DNA plug-in guarantees uniqueness across servers.
7e1b55
+
7e1b55
+## LDAP
7e1b55
+
7e1b55
+### LDAP schema extension
7e1b55
+
7e1b55
+The subordinate id feature introduces a new auxiliar object class
7e1b55
+``ipaSubordinateId`` with four required attributes ``ipaSubUidNumber``,
7e1b55
+``ipaSubUidCount``, ``ipaSubGidNumber``, and ``ipaSubGidCount``. The
7e1b55
+attributes with ``number`` suffix store the start value of the interval.
7e1b55
+The ``count`` attributes contain the size of the interval including the
7e1b55
+start value. The maximum subid is
7e1b55
+``ipaSubUidNumber + ipaSubUidCount - 1``.
7e1b55
+
7e1b55
+All four attributes are single-value ``INTEGER`` type with standard
7e1b55
+integer matching rules. OIDs ``2.16.840.1.113730.3.8.23.8`` and
7e1b55
+``2.16.840.1.113730.3.8.23.11`` are reserved for future use.
7e1b55
+
7e1b55
+```raw
7e1b55
+attributeTypes: (
7e1b55
+  2.16.840.1.113730.3.8.23.6
7e1b55
+  NAME 'ipaSubUidNumber'
7e1b55
+  DESC 'Numerical subordinate user ID (range start value)'
7e1b55
+  EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
7e1b55
+  X-ORIGIN 'IPA v4.9'
7e1b55
+)
7e1b55
+attributeTypes: (
7e1b55
+  2.16.840.1.113730.3.8.23.7
7e1b55
+  NAME 'ipaSubUidCount'
7e1b55
+  DESC 'Subordinate user ID count (range size)'
7e1b55
+  EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
7e1b55
+  X-ORIGIN 'IPA v4.9'
7e1b55
+)
7e1b55
+attributeTypes: (
7e1b55
+  2.16.840.1.113730.3.8.23.9
7e1b55
+  NAME 'ipaSubGidNumber'
7e1b55
+  DESC 'Numerical subordinate group ID (range start value)'
7e1b55
+  EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
7e1b55
+  X-ORIGIN 'IPA v4.9'
7e1b55
+)
7e1b55
+attributeTypes: (
7e1b55
+  2.16.840.1.113730.3.8.23.10
7e1b55
+  NAME 'ipaSubGidCount'
7e1b55
+  DESC 'Subordinate group ID count (range size)'
7e1b55
+  EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
7e1b55
+  X-ORIGIN 'IPA v4.9'
7e1b55
+)
7e1b55
+```
7e1b55
+
7e1b55
+The ``ipaSubordinateId`` object class is an auxiliar subclass of
7e1b55
+``top`` and requires all four subordinate id attributes as well as
7e1b55
+``uidNumber``. It does not subclass ``posixAccount`` to make
7e1b55
+the class reusable in idview overrides later.
7e1b55
+
7e1b55
+```raw
7e1b55
+objectClasses: (
7e1b55
+  2.16.840.1.113730.3.8.24.4
7e1b55
+  NAME 'ipaSubordinateId'
7e1b55
+  DESC 'Subordinate uid and gid for users'
7e1b55
+  SUP top AUXILIARY
7e1b55
+  MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount )
7e1b55
+  X-ORIGIN 'IPA v4.9'
7e1b55
+)
7e1b55
+```
7e1b55
+
7e1b55
+The ``ipaSubordinateGid`` and ``ipaSubordinateUid`` are defined for
7e1b55
+future use. IPA always assumes the presence of ``ipaSubordinateId`` and
7e1b55
+does not use these object classes.
7e1b55
+
7e1b55
+```raw
7e1b55
+objectClasses: (
7e1b55
+  2.16.840.1.113730.3.8.24.2
7e1b55
+  NAME 'ipaSubordinateUid'
7e1b55
+  DESC 'Subordinate uids for users, see subuid(5)'
7e1b55
+  SUP top AUXILIARY
7e1b55
+  MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount )
7e1b55
+  X-ORIGIN 'IPA v4.9'
7e1b55
+ )
7e1b55
+objectClasses: (
7e1b55
+  2.16.840.1.113730.3.8.24.3
7e1b55
+  NAME 'ipaSubordinateGid'
7e1b55
+  DESC 'Subordinate gids for users, see subgid(5)'
7e1b55
+  SUP top AUXILIARY
7e1b55
+  MUST ( uidNumber $ ipaSubGidNumber $ ipaSubGidCount )
7e1b55
+  X-ORIGIN 'IPA v4.9'
7e1b55
+)
7e1b55
+```
7e1b55
+
7e1b55
+### Index
7e1b55
+
7e1b55
+The attributes ``ipaSubUidNumber`` and ``ipaSubGidNumber`` are index
7e1b55
+for ``pres`` and ``eq`` with ``nsMatchingRule: integerOrderingMatch``
7e1b55
+to enable efficient ``=``, ``>=``, and ``<=`` searches.
7e1b55
+
7e1b55
+### Distributed numeric assignment (DNA) plug-in extension
7e1b55
+
7e1b55
+Subordinate id auto-assignment requires an extension of 389-DS'
7e1b55
+[DNA](https://directory.fedoraproject.org/docs/389ds/design/dna-plugin.html)
7e1b55
+plug-in. The DNA plug-in is responsible for safely assigning unique
7e1b55
+numeric ids across all replicas.
7e1b55
+
7e1b55
+Currently the DNA plug-in only supports a step size of ``1``. A new
7e1b55
+option ``dnaStepAttr`` (name is tentative) will tell the DNA plug-in
7e1b55
+to use the value of entry attributes as step size.
7e1b55
+
7e1b55
+
7e1b55
+## Permissions, Privileges, Roles
7e1b55
+
7e1b55
+### Self-servive RBAC
7e1b55
+
7e1b55
+The self-service permission enables users to request auto-assignment
7e1b55
+of subordinate uid and gid ranges for themselves. Subordinate ids cannot
7e1b55
+be modified or deleted.
7e1b55
+
7e1b55
+* ACI: *selfservice: Add subordinate id*
7e1b55
+* Permission: *Self-service subordinate ID*
7e1b55
+* Privilege: *Subordinate ID Selfservice User*
7e1b55
+* Role: *Subordinate ID Selfservice Users*
7e1b55
+* role default member: n/a
7e1b55
+
7e1b55
+### Administrator RBAC
7e1b55
+
7e1b55
+The administrator permission allows privileged users to auto-assign
7e1b55
+subordinate ids to users. Once assigned subordinate ids cannot
7e1b55
+be modified or deleted.
7e1b55
+
7e1b55
+* ACI: *Add subordinate ids to any user*
7e1b55
+* Permission: *Manage subordinate ID*
7e1b55
+* Privilege: *Subordinate ID Administrators*
7e1b55
+* default privilege role: *User Administrator*
7e1b55
+
7e1b55
+
7e1b55
+## Workflows
7e1b55
+
7e1b55
+In the default configuration of IPA, neither existing users nor new
7e1b55
+users will have subordinate ids assigned. There are a couple of ways
7e1b55
+to assign subordinate ids to users.
7e1b55
+
7e1b55
+### User administrator
7e1b55
+
7e1b55
+Users with *User Administrator* role and members of the *admins* group
7e1b55
+have permission to auto-assign new subordinate ids to any user. Auto
7e1b55
+assignment can be performed with new ``user-auto-subid`` command on the
7e1b55
+command line or with the *Auto assign subordinate ids* action in the
7e1b55
+*Actions* drop-down menu in the web UI.
7e1b55
+
7e1b55
+```shell
7e1b55
+$ ipa user-auto-subid someusername
7e1b55
+```
7e1b55
+
7e1b55
+### Self-service for group members
7e1b55
+
7e1b55
+Ordinary users cannot self-service subordinate ids by default. Admins
7e1b55
+can assign the new *Subordinate ID Selfservice User* to users group to
7e1b55
+enable self-service for members of the group.
7e1b55
+
7e1b55
+For example to enable self-service for all members of the default user
7e1b55
+group ``ipausers``, do:
7e1b55
+
7e1b55
+```shell
7e1b55
+$ ipa role-add-member "Subordinate ID Selfservice User" --groups=ipausers
7e1b55
+```
7e1b55
+
7e1b55
+This allows members of ``ipausers`` to request subordinate ids with
7e1b55
+the ``user-auto-subid`` command or the *Auto assign subordinate ids*
7e1b55
+action in the web UI.
7e1b55
+
7e1b55
+```shell
7e1b55
+$ ipa user-auto-subid myusername
7e1b55
+```
7e1b55
+
7e1b55
+### Auto assignment with user default object class
7e1b55
+
7e1b55
+Admins can also enable auto-assignment of subordinate ids for all new
7e1b55
+users by adding ``ipasubordinateid`` as a default user objectclass.
7e1b55
+This can be accomplished in the web UI under "IPA Server" /
7e1b55
+"Configuration" / "Default user objectclasses" or on the command line
7e1b55
+with:
7e1b55
+
7e1b55
+```shell
7e1b55
+$ ipa config-mod --addattr="ipaUserObjectClasses=ipasubordinateid"
7e1b55
+```
7e1b55
+
7e1b55
+**NOTE:** The objectclass must be written all lower case.
7e1b55
+
7e1b55
+### ipa-subid tool
7e1b55
+
7e1b55
+Finally IPA includes a new tool for mass-assignment of subordinate ids.
7e1b55
+The command uses automatic LDAPI EXTERNAL bind when it's executed as
7e1b55
+root user. Other it requires valid Kerberos TGT of an admin or user
7e1b55
+administrator.
7e1b55
+
7e1b55
+```raw
7e1b55
+
7e1b55
+# /usr/libexec/ipa/ipa-subids --help
7e1b55
+Usage: ipa-subids
7e1b55
+
7e1b55
+Mass-assign subordinate ids
7e1b55
+
7e1b55
+Options:
7e1b55
+  --version             show program's version number and exit
7e1b55
+  -h, --help            show this help message and exit
7e1b55
+  --group=GROUP         Filter by group membership
7e1b55
+  --filter=USER_FILTER  Raw LDAP filter
7e1b55
+  --dry-run             Dry run mode.
7e1b55
+  --all-users           All users
7e1b55
+
7e1b55
+  Logging and output options:
7e1b55
+    -v, --verbose       print debugging information
7e1b55
+    -d, --debug         alias for --verbose (deprecated)
7e1b55
+    -q, --quiet         output only errors
7e1b55
+    --log-file=FILE     log to the given file
7e1b55
+
7e1b55
+# # /usr/libexec/ipa/ipa-subids --group ipausers
7e1b55
+Processing user 'testsubordinated1' (1/15)
7e1b55
+Processing user 'testsubordinated2' (2/15)
7e1b55
+Processing user 'testsubordinated3' (3/15)
7e1b55
+Processing user 'testsubordinated4' (4/15)
7e1b55
+Processing user 'testsubordinated5' (5/15)
7e1b55
+Processing user 'testsubordinated6' (6/15)
7e1b55
+Processing user 'testsubordinated7' (7/15)
7e1b55
+Processing user 'testsubordinated8' (8/15)
7e1b55
+Processing user 'testsubordinated9' (9/15)
7e1b55
+Processing user 'testsubordinated10' (10/15)
7e1b55
+Processing user 'testsubordinated11' (11/15)
7e1b55
+Processing user 'testsubordinated12' (12/15)
7e1b55
+Processing user 'testsubordinated13' (13/15)
7e1b55
+Processing user 'testsubordinated14' (14/15)
7e1b55
+Processing user 'testsubordinated15' (15/15)
7e1b55
+Processed 15 user(s)
7e1b55
+The ipa-subids command was successful
7e1b55
+```
7e1b55
+
7e1b55
+### Find and match users by any subordinate id
7e1b55
+
7e1b55
+The ``user-find`` command search by start value of subordinate uid and
7e1b55
+gid range. The new command ``user-match-subid`` can be used to find a
7e1b55
+user by any subordinate id in their range.
7e1b55
+
7e1b55
+```raw
7e1b55
+$ ipa user-match-subid --subuid=2153185287
7e1b55
+  User login: asmith
7e1b55
+  First name: Alice
7e1b55
+  Last name: Smith
7e1b55
+  ...
7e1b55
+  SubUID range start: 2153185280
7e1b55
+  SubUID range size: 65536
7e1b55
+  SubGID range start: 2153185280
7e1b55
+  SubGID range size: 65536
7e1b55
+----------------------------
7e1b55
+Number of entries returned 1
7e1b55
+----------------------------
7e1b55
+$ ipa user-match-subid --subuid=2153185279
7e1b55
+  User login: bjones
7e1b55
+  First name: Bob
7e1b55
+  Last name: Jones
7e1b55
+  ...
7e1b55
+  SubUID range start: 2153119744
7e1b55
+  SubUID range size: 65536
7e1b55
+  SubGID range start: 2153119744
7e1b55
+  SubGID range size: 65536
7e1b55
+----------------------------
7e1b55
+Number of entries returned 1
7e1b55
+----------------------------
7e1b55
+```
7e1b55
+
7e1b55
+## SSSD integration
7e1b55
+
7e1b55
+* base: ``cn=accounts,$SUFFIX`` / ``cn=users,cn=accounts,$SUFFIX``
7e1b55
+* scope: ``SCOPE_SUBTREE`` (2) / ``SCOPE_ONELEVEL`` (1)
7e1b55
+* user filter: should include ``(objectClass=posixAccount)``
7e1b55
+* attributes: ``uidNumber ipaSubUidNumber ipaSubUidCount ipaSubGidNumber ipaSubGidCount``
7e1b55
+
7e1b55
+SSSD can safely assume that only *user accounts* of type ``posixAccount``
7e1b55
+have subordinate ids. In the first revision there are no other entries
7e1b55
+with subordinate ids. The ``posixAccount`` object class has ``uid``
7e1b55
+(user login name) and ``uidNumber`` (numeric user id) as mandatory
7e1b55
+attributes. The ``uid`` attribute is guaranteed to be unique across
7e1b55
+all user accounts in an IPA domain.
7e1b55
+
7e1b55
+The ``uidNumber`` attribute is commonly unique, too. However it's
7e1b55
+technically possible that an administrator has assigned the same
7e1b55
+numeric user id to multiple users. Automatically assigned uid numbers
7e1b55
+don't conflict. SSSD should treat multiple users with same numeric
7e1b55
+user id as an error.
7e1b55
+
7e1b55
+The attribute ``ipaSubUidNumber`` is always accompanied by
7e1b55
+``ipaSubUidCount`` and ``ipaSubGidNumber`` is always accompanied
7e1b55
+by ``ipaSubGidCount``. In revision 1 the presence of
7e1b55
+``ipaSubUidNumber`` implies presence of the other three attributes.
7e1b55
+All four subordinate id attributes and ``uidNumber`` are single-value
7e1b55
+``INTEGER`` types. Any value outside of range of ``uint32_t`` must
7e1b55
+treated as invalid. SSSD will never see the DNA magic value ``-1``
7e1b55
+in ``cn=accounts,$SUFFIX`` subtree.
7e1b55
+
7e1b55
+IPA recommends that SSSD simply extends its existing query for user
7e1b55
+accounts and requests the four subordinate attributes additionally to
7e1b55
+RFC 2307 attributes ``rfc2307_user_map``. SSSD can directly take the
7e1b55
+values and return them without further processing, e.g.
7e1b55
+``uidNumber:ipaSubUidNumber:ipaSubUidCount`` for ``/etc/subuid``.
7e1b55
+
7e1b55
+Filters for additional cases:
7e1b55
+
7e1b55
+* subuid filter (find user with subuid by numeric uid):
7e1b55
+  ``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=$UID))``,
7e1b55
+  ``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar
7e1b55
+* subuid enumeration filter:
7e1b55
+  ``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=*))``,
7e1b55
+  ``(objectClass=ipaSubordinateId)``, or similar
7e1b55
+* subgid filter (find user with subgid by numeric uid):
7e1b55
+  ``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=$UID))``,
7e1b55
+  ``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar
7e1b55
+* subgid enumeration filter:
7e1b55
+  ``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=*))``,
7e1b55
+  ``(objectClass=ipaSubordinateId)``, or similar
7e1b55
+
7e1b55
+## Implementation details
7e1b55
+
7e1b55
+* The four subid attributes are not included in
7e1b55
+  ``baseuser.default_attributes`` on purpose. The ``config-mod``
7e1b55
+  command does not permit removal of a user default objectclasses
7e1b55
+  when the class is the last provider of an attribute in
7e1b55
+  ``default_attributes``.
7e1b55
+* ``ipaSubordinateId`` object class does not subclass the other two
7e1b55
+  object classes. LDAP supports
7e1b55
+  ``SUP ( ipaSubordinateGid $ ipaSubordinateUid )`` but 389-DS only
7e1b55
+  auto-inherits from first object class.
7e1b55
+* The idrange entry ``$REALM_subid_range`` has preconfigured base RIDs
7e1b55
+  and SID so idrange plug-in and sidgen task ignore the entry. It's the
7e1b55
+  simplest approach to ensure backwards compatibility with older IPA
7e1b55
+  server versions that don't know how to handle the new range.
7e1b55
+  The SID is ``S-1-5-21-738065-838566-$DOMAIN_HASH``. ``S-1-5-21``
7e1b55
+  is the well-known SID prefix for domain SIDs.  ``738065-838566`` is
7e1b55
+  the decimal representation of the string ``IPA-SUB``. ``DOMAIN_HASH``
7e1b55
+  is the MURMUR-3 hash of the domain name for key ``0xdeadbeef``. SSSD
7e1b55
+  rejects SIDs unless they are prefixed with ``S-1-5-21`` (see
7e1b55
+  ``sss_idmap.c:is_domain_sid()``).
7e1b55
+* The new ``$REALM_subid_range`` entry uses range type ``ipa-ad-trust``
7e1b55
+  instead of range type ``ipa-local-subid`` for backwards compatibility
7e1b55
+  with older SSSD clients, see
7e1b55
+  [SSSD #5571](https://github.com/SSSD/sssd/issues/5571).
7e1b55
+* Shared DNA configuration entries in ``cn=dna,cn=ipa,cn=etc,$SUFFIX``
7e1b55
+  are automatically removed by existing code. Server and replication
7e1b55
+  plug-ins search and delete entries by ``dnaHostname`` attribute.
7e1b55
+
7e1b55
+### TODO
7e1b55
+
7e1b55
+* enable configuration for ``dnaStepAttr``
7e1b55
+* remove ``fake_dna_plugin`` hack from ``baseuser`` plug-in.
7e1b55
+* add custom range type for idranges and teach AD trust, sidgen, and
7e1b55
+  range overlap check code to deal with new range type.
7e1b55
diff --git a/freeipa.spec.in b/freeipa.spec.in
7e1b55
index ae4af099f39641a9f5163d61cfb37e1c3afb6f4b..044e3559975c399f6697d4da94b5a059eb5b407c 100755
7e1b55
--- a/freeipa.spec.in
7e1b55
+++ b/freeipa.spec.in
7e1b55
@@ -1361,6 +1361,7 @@ fi
7e1b55
 %{_libexecdir}/ipa/ipa-pki-wait-running
7e1b55
 %{_libexecdir}/ipa/ipa-otpd
7e1b55
 %{_libexecdir}/ipa/ipa-print-pac
7e1b55
+%{_libexecdir}/ipa/ipa-subids
7e1b55
 %dir %{_libexecdir}/ipa/custodia
7e1b55
 %attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-dmldap
7e1b55
 %attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat
7e1b55
diff --git a/install/share/60basev2.ldif b/install/share/60basev2.ldif
7e1b55
index f253f30c91350c1358b24986806efea7768ea9ce..952755309d13d7df1806a52af351df250185b16d 100644
7e1b55
--- a/install/share/60basev2.ldif
7e1b55
+++ b/install/share/60basev2.ldif
7e1b55
@@ -3,6 +3,7 @@
7e1b55
 ## Attributes:		2.16.840.1.113730.3.8.3 - V2 base attributres
7e1b55
 ## ObjectClasses:	2.16.840.1.113730.3.8.4 - V2 base objectclasses
7e1b55
 ## Attributes:		2.16.840.1.113730.3.8.23 - V4 base attributes
7e1b55
+## ObjectClasses:	2.16.840.1.113730.3.8.24 - V4 base objectclasses
7e1b55
 ##
7e1b55
 dn: cn=schema
7e1b55
 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' )
7e1b55
diff --git a/install/share/60basev4.ldif b/install/share/60basev4.ldif
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..7f5173e593ff68a03d4005957b1dc9b9eb489dc5
7e1b55
--- /dev/null
7e1b55
+++ b/install/share/60basev4.ldif
7e1b55
@@ -0,0 +1,19 @@
7e1b55
+## IPA Base OID:	2.16.840.1.113730.3.8
7e1b55
+##
7e1b55
+## Attributes:		2.16.840.1.113730.3.8.23 - V4 base attributes
7e1b55
+## ObjectClasses:	2.16.840.1.113730.3.8.24 - V4 base objectclasses
7e1b55
+##
7e1b55
+dn: cn=schema
7e1b55
+# subordinate ids
7e1b55
+# range ceiling OIDs are reserved for future use (operational attribute?)
7e1b55
+# object class requires uidNumber but does not subclass posixAccount so we
7e1b55
+# can re-use the object class in idview overrides later.
7e1b55
+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')
7e1b55
+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')
7e1b55
+# 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')
7e1b55
+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')
7e1b55
+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')
7e1b55
+# 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')
7e1b55
+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')
7e1b55
+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')
7e1b55
+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')
7e1b55
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
7e1b55
index 0f1a6975fc3394316769295e67ac3c2e05ee9cee..e0fe4b7d1756bd05f060a92ab52f910b4bd3adc8 100644
7e1b55
--- a/install/share/Makefile.am
7e1b55
+++ b/install/share/Makefile.am
7e1b55
@@ -16,6 +16,7 @@ dist_app_DATA =				\
7e1b55
 	60ipaconfig.ldif		\
7e1b55
 	60basev2.ldif			\
7e1b55
 	60basev3.ldif			\
7e1b55
+	60basev4.ldif			\
7e1b55
 	60ipadns.ldif			\
7e1b55
 	60ipapk11.ldif			\
7e1b55
 	60certificate-profiles.ldif	\
7e1b55
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
7e1b55
index 6a689798451e8cc072284065849f9a95635f8069..16f2ef822eaf56dd68d4140b22a607539645b151 100644
7e1b55
--- a/install/share/bootstrap-template.ldif
7e1b55
+++ b/install/share/bootstrap-template.ldif
7e1b55
@@ -167,6 +167,12 @@ objectClass: nsContainer
7e1b55
 objectClass: top
7e1b55
 cn: posix-ids
7e1b55
 
7e1b55
+dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
7e1b55
+changetype: add
7e1b55
+objectClass: nsContainer
7e1b55
+objectClass: top
7e1b55
+cn: subordinate-ids
7e1b55
+
7e1b55
 dn: cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX
7e1b55
 changetype: add
7e1b55
 objectClass: nsContainer
7e1b55
@@ -476,6 +482,22 @@ ipaBaseID: $IDSTART
7e1b55
 ipaIDRangeSize: $IDRANGE_SIZE
7e1b55
 ipaRangeType: ipa-local
7e1b55
 
7e1b55
+dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX
7e1b55
+changetype: add
7e1b55
+objectClass: top
7e1b55
+objectClass: ipaIDrange
7e1b55
+objectClass: ipaTrustedADDomainRange
7e1b55
+cn: ${REALM}_subid_range
7e1b55
+ipaBaseID: eval($SUBID_RANGE_START)
7e1b55
+ipaIDRangeSize: eval($SUBID_RANGE_SIZE)
7e1b55
+# HACK: RIDs to work around adtrust sidgen issue
7e1b55
+ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE)
7e1b55
+# 738065-838566 = IPA-SUB
7e1b55
+ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH
7e1b55
+# HACK: "ipa-local-subid" range type causes issues with older SSSD clients
7e1b55
+# see https://github.com/SSSD/sssd/issues/5571
7e1b55
+ipaRangeType: ipa-ad-trust
7e1b55
+
7e1b55
 dn: cn=ca,$SUFFIX
7e1b55
 changetype: add
7e1b55
 objectClass: nsContainer
7e1b55
diff --git a/install/share/dna.ldif b/install/share/dna.ldif
7e1b55
index f4bff3691570eb1fe028b13b69d2cc175c7df174..649313e72fc58112865e5901125923b3704276b1 100644
7e1b55
--- a/install/share/dna.ldif
7e1b55
+++ b/install/share/dna.ldif
7e1b55
@@ -16,6 +16,26 @@ dnaThreshold: 500
7e1b55
 dnaSharedCfgDN: cn=posix-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
7e1b55
 dnaExcludeScope: cn=provisioning,$SUFFIX
7e1b55
 
7e1b55
+dn: cn=Subordinate IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
7e1b55
+changetype: add
7e1b55
+objectclass: top
7e1b55
+objectclass: extensibleObject
7e1b55
+cn: Subordinate IDs
7e1b55
+dnaType: ipasubuidnumber
7e1b55
+dnaType: ipasubgidnumber
7e1b55
+dnaNextValue: eval($SUBID_RANGE_START)
7e1b55
+dnaMaxValue: eval($SUBID_RANGE_MAX)
7e1b55
+dnaMagicRegen: -1
7e1b55
+dnaFilter: (objectClass=ipaSubordinateId)
7e1b55
+dnaScope: $SUFFIX
7e1b55
+dnaThreshold: eval($SUBID_DNA_THRESHOLD)
7e1b55
+# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr
7e1b55
+# dnaStepAttr: ipaSubUidCount
7e1b55
+# dnaStepAttr: ipaSubGidCount
7e1b55
+# dnaStepAllowedValues: eval($SUBID_COUNT)
7e1b55
+dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
7e1b55
+dnaExcludeScope: cn=provisioning,$SUFFIX
7e1b55
+
7e1b55
 # Enable the DNA plugin
7e1b55
 dn: cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
7e1b55
 changetype: modify
7e1b55
diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am
7e1b55
index d6fbf9e3bc84bc475d7a797ff663df40da0a0efa..5f36742957505f6d695097c8aab6c73f9d59e146 100644
7e1b55
--- a/install/tools/Makefile.am
7e1b55
+++ b/install/tools/Makefile.am
7e1b55
@@ -38,6 +38,7 @@ dist_noinst_DATA =		\
7e1b55
 	ipa-pki-retrieve-key.in	\
7e1b55
 	ipa-pki-wait-running.in	\
7e1b55
 	ipa-acme-manage.in	\
7e1b55
+	ipa-subids.in	\
7e1b55
 	$(NULL)
7e1b55
 
7e1b55
 nodist_sbin_SCRIPTS =		\
7e1b55
@@ -78,6 +79,7 @@ nodist_app_SCRIPTS =		\
7e1b55
 	ipa-httpd-pwdreader	\
7e1b55
 	ipa-pki-retrieve-key	\
7e1b55
 	ipa-pki-wait-running	\
7e1b55
+	ipa-subids	\
7e1b55
 	$(NULL)
7e1b55
 
7e1b55
 PYTHON_SHEBANG = 					\
7e1b55
diff --git a/install/tools/ipa-subids.in b/install/tools/ipa-subids.in
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..5c7b9f8f788e3c230253e86151cff8234161909b
7e1b55
--- /dev/null
7e1b55
+++ b/install/tools/ipa-subids.in
7e1b55
@@ -0,0 +1,8 @@
7e1b55
+#!/usr/bin/python3
7e1b55
+#
7e1b55
+# Copyright (C) 2021  FreeIPA Contributors see COPYING for license
7e1b55
+#
7e1b55
+
7e1b55
+from ipaserver.install.ipa_subids import IPASubids
7e1b55
+
7e1b55
+IPASubids.run_cli()
7e1b55
diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
7e1b55
index a4eb390b7d9ca0fb8f50245cfedec27ca2607cdd..5b49b0f6edbbbb6c802afb803a6406a0ab796c44 100644
7e1b55
--- a/install/ui/src/freeipa/user.js
7e1b55
+++ b/install/ui/src/freeipa/user.js
7e1b55
@@ -259,6 +259,33 @@ return {
7e1b55
                         }
7e1b55
                     ]
7e1b55
                 },
7e1b55
+                {
7e1b55
+                    name: 'subordinate',
7e1b55
+                    label: '@i18n:objects.subordinate.identity',
7e1b55
+                    fields: [
7e1b55
+                        {
7e1b55
+                            name: 'ipasubuidnumber',
7e1b55
+                            label: '@i18n:objects.subordinate.subuidnumber',
7e1b55
+                            read_only: true
7e1b55
+                        },
7e1b55
+                        {
7e1b55
+                            name: 'ipasubuidcount',
7e1b55
+                            label: '@i18n:objects.subordinate.subuidcount',
7e1b55
+                            read_only: true
7e1b55
+
7e1b55
+                        },
7e1b55
+                        {
7e1b55
+                            name: 'ipasubgidnumber',
7e1b55
+                            label: '@i18n:objects.subordinate.subgidnumber',
7e1b55
+                            read_only: true
7e1b55
+                        },
7e1b55
+                        {
7e1b55
+                            name: 'ipasubgidcount',
7e1b55
+                            label: '@i18n:objects.subordinate.subgidcount',
7e1b55
+                            read_only: true
7e1b55
+                        }
7e1b55
+                    ]
7e1b55
+                },
7e1b55
                 {
7e1b55
                     name: 'pwpolicy',
7e1b55
                     label: '@i18n:objects.pwpolicy.identity',
7e1b55
@@ -451,6 +478,16 @@ return {
7e1b55
                     enable_cond: ['is-locked'],
7e1b55
                     confirm_msg: '@i18n:objects.user.unlock_confirm'
7e1b55
                 },
7e1b55
+                {
7e1b55
+                    $factory: IPA.object_action,
7e1b55
+                    name: 'auto_subid',
7e1b55
+                    method: 'auto_subid',
7e1b55
+                    label: '@i18n:objects.user.auto_subid',
7e1b55
+                    needs_confirm: true,
7e1b55
+                    hide_cond: ['preserved-user'],
7e1b55
+                    enable_cond: ['no-subid'],
7e1b55
+                    confirm_msg: '@i18n:objects.user.auto_subid_confirm'
7e1b55
+                },
7e1b55
                 {
7e1b55
                     $type: 'automember_rebuild',
7e1b55
                     name: 'automember_rebuild',
7e1b55
@@ -461,12 +498,22 @@ return {
7e1b55
                     $type: 'cert_request',
7e1b55
                     hide_cond: ['preserved-user'],
7e1b55
                     title: '@i18n:objects.cert.issue_for_user'
7e1b55
+                },
7e1b55
+                {
7e1b55
+                    $factory: IPA.object_action,
7e1b55
+                    name: 'auto_subid',
7e1b55
+                    method: 'auto_subid',
7e1b55
+                    label: '@i18n:objects.user.auto_subid',
7e1b55
+                    needs_confirm: true,
7e1b55
+                    hide_cond: ['preserved-user'],
7e1b55
+                    enable_cond: ['no-subid'],
7e1b55
+                    confirm_msg: '@i18n:objects.user.auto_subid_confirm'
7e1b55
                 }
7e1b55
             ],
7e1b55
             header_actions: [
7e1b55
                 'reset_password', 'enable', 'disable', 'stage', 'undel',
7e1b55
                 'delete_active_user', 'delete', 'unlock', 'add_otptoken',
7e1b55
-                'automember_rebuild', 'request_cert'
7e1b55
+                'automember_rebuild', 'request_cert', 'auto_subid'
7e1b55
             ],
7e1b55
             state: {
7e1b55
                 evaluators: [
7e1b55
@@ -1159,6 +1206,10 @@ IPA.user.is_locked_evaluator = function(spec) {
7e1b55
             }
7e1b55
         }
7e1b55
 
7e1b55
+        if (!user.ipasubuidnumber) {
7e1b55
+            that.state.push('no-subid');
7e1b55
+        }
7e1b55
+
7e1b55
         that.notify_on_change(old_state);
7e1b55
     };
7e1b55
 
7e1b55
diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update
7e1b55
index 6632f105a98276d0d7e63ce249ade15501c3b673..7f83ab9f04c565a59efdd2f41c3e7ee30f5da9c7 100644
7e1b55
--- a/install/updates/20-indices.update
7e1b55
+++ b/install/updates/20-indices.update
7e1b55
@@ -272,6 +272,24 @@ add:nsIndexType: eq
7e1b55
 add:nsIndexType: pres
7e1b55
 add:nsIndexType: sub
7e1b55
 
7e1b55
+dn: cn=ipaSubGidNumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
7e1b55
+only:cn: ipaSubGidNumber
7e1b55
+default:objectClass: nsIndex
7e1b55
+default:objectClass: top
7e1b55
+default:nsSystemIndex: false
7e1b55
+add:nsIndexType: eq
7e1b55
+add:nsIndexType: pres
7e1b55
+add:nsMatchingRule: integerOrderingMatch
7e1b55
+
7e1b55
+dn: cn=ipaSubUidNumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
7e1b55
+only:cn: ipaSubUidNumber
7e1b55
+default:objectClass: nsIndex
7e1b55
+default:objectClass: top
7e1b55
+default:nsSystemIndex: false
7e1b55
+add:nsIndexType: eq
7e1b55
+add:nsIndexType: pres
7e1b55
+add:nsMatchingRule: integerOrderingMatch
7e1b55
+
7e1b55
 dn: cn=ipasudorunasgroup,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
7e1b55
 only:cn: ipasudorunasgroup
7e1b55
 default:objectClass: nsIndex
7e1b55
diff --git a/install/updates/73-subid.update b/install/updates/73-subid.update
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..2aab3d445a33ae1663f81ca2d61b62ebc94aa37d
7e1b55
--- /dev/null
7e1b55
+++ b/install/updates/73-subid.update
7e1b55
@@ -0,0 +1,102 @@
7e1b55
+# subordinate ids
7e1b55
+
7e1b55
+# self-service RBAC
7e1b55
+dn: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX
7e1b55
+default:objectClass: groupofnames
7e1b55
+default:objectClass: nestedgroup
7e1b55
+default:objectClass: top
7e1b55
+default:cn: Subordinate ID Selfservice User
7e1b55
+default:description: User that can self-request subordiante ids
7e1b55
+# default: member: cn=ipausers,cn=groups,cn=accounts,$SUFFIX
7e1b55
+
7e1b55
+dn: cn=Subordinate ID Selfservice Users,cn=privileges,cn=pbac,$SUFFIX
7e1b55
+default:objectClass: top
7e1b55
+default:objectClass: groupofnames
7e1b55
+default:objectClass: nestedgroup
7e1b55
+default:cn: Subordinate ID Selfservice Users
7e1b55
+default:description: Subordinate ID Selfservice User
7e1b55
+default:member: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX
7e1b55
+
7e1b55
+dn: cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX
7e1b55
+default:objectClass: top
7e1b55
+default:objectClass: groupofnames
7e1b55
+default:objectClass: ipapermission
7e1b55
+default:cn: Self-service subordinate ID
7e1b55
+default:ipapermissiontype: SYSTEM
7e1b55
+default:member: cn=Subordinate ID Selfservice Users,cn=privileges,cn=pbac,$SUFFIX
7e1b55
+
7e1b55
+# Administrator RBAC
7e1b55
+dn: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX
7e1b55
+default:objectClass: top
7e1b55
+default:objectClass: groupofnames
7e1b55
+default:objectClass: nestedgroup
7e1b55
+default:cn: Subordinate ID Administrators
7e1b55
+default:description: Subordinate ID Administrators
7e1b55
+default:member: cn=User Administrator,cn=roles,cn=accounts,$SUFFIX
7e1b55
+
7e1b55
+dn: cn=Manage subordinate ID,cn=permissions,cn=pbac,$SUFFIX
7e1b55
+default:objectClass: top
7e1b55
+default:objectClass: groupofnames
7e1b55
+default:objectClass: ipapermission
7e1b55
+default:cn: Manage subordinate ID
7e1b55
+default:ipapermissiontype: SYSTEM
7e1b55
+default:member: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX
7e1b55
+
7e1b55
+# ACIs (in domain database root so they also apply to staging area)
7e1b55
+#
7e1b55
+# - allow users to request new subid with DNA_MAGIC value, subid count=65536,
7e1b55
+#   and subgid == subuid.
7e1b55
+# - allow user admins to set subids. count=65536 and subgid == subuid
7e1b55
+#   properties are enforced as wel.
7e1b55
+#
7e1b55
+# The delete-when-empty check is required because IPA uses MOD_REPLACE to
7e1b55
+# set attributes, see https://github.com/389ds/389-ds-base/issues/4597.
7e1b55
+#
7e1b55
+# TODO: remove (ipasubuidnumber>=eval($SUBID_RANGE_START) from
7e1b55
+# self-service permission when 389-DS' DNA plugin supports dnaStepAttr and
7e1b55
+# fake_dna_plugin hack has been removed.
7e1b55
+#
7e1b55
+dn: $SUFFIX
7e1b55
+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";)
7e1b55
+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";)
7e1b55
+
7e1b55
+# DNA plugin and idrange configuration
7e1b55
+dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
7e1b55
+default: objectClass: nsContainer
7e1b55
+default: objectClass: top
7e1b55
+default: cn: subordinate-ids
7e1b55
+
7e1b55
+dn: cn=Subordinate IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
7e1b55
+default: objectclass: top
7e1b55
+default: objectclass: extensibleObject
7e1b55
+default: cn: Subordinate IDs
7e1b55
+default: dnaType: ipasubuidnumber
7e1b55
+default: dnaType: ipasubgidnumber
7e1b55
+default: dnaNextValue: eval($SUBID_RANGE_START)
7e1b55
+default: dnaMaxValue: eval($SUBID_RANGE_MAX)
7e1b55
+default: dnaMagicRegen: -1
7e1b55
+default: dnaFilter: (objectClass=ipaSubordinateId)
7e1b55
+default: dnaScope: $SUFFIX
7e1b55
+default: dnaThreshold: eval($SUBID_DNA_THRESHOLD)
7e1b55
+# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr
7e1b55
+# default: dnaStepAttr: ipaSubUidCount
7e1b55
+# default: dnaStepAttr: ipaSubGidCount
7e1b55
+# default: dnaStepAllowedValues: eval($SUBID_COUNT)
7e1b55
+default: dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
7e1b55
+default: dnaExcludeScope: cn=provisioning,$SUFFIX
7e1b55
+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";)
7e1b55
+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";)
7e1b55
+
7e1b55
+dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX
7e1b55
+default: objectClass: top
7e1b55
+default: objectClass: ipaIDrange
7e1b55
+default: objectClass: ipaTrustedADDomainRange
7e1b55
+default: cn: ${REALM}_subid_range
7e1b55
+default: ipaBaseID: $SUBID_RANGE_START
7e1b55
+default: ipaIDRangeSize: $SUBID_RANGE_SIZE
7e1b55
+# HACK: RIDs to work around adtrust sidgen issue
7e1b55
+default: ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE)
7e1b55
+default: ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH
7e1b55
+# HACK: "ipa-local-subid" range type causes issues with older SSSD clients
7e1b55
+# see https://github.com/SSSD/sssd/issues/5571
7e1b55
+default: ipaRangeType: ipa-ad-trust
7e1b55
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
7e1b55
index 5741805a65a09c4c00ea47bf437c8821373d1e80..d4f6acba0dc83e4692edd10b8a7617915bd49e84 100644
7e1b55
--- a/install/updates/Makefile.am
7e1b55
+++ b/install/updates/Makefile.am
7e1b55
@@ -61,6 +61,7 @@ app_DATA =				\
7e1b55
 	71-idviews-sasl-mapping.update  \
7e1b55
 	72-domainlevels.update		\
7e1b55
 	73-custodia.update		\
7e1b55
+	73-subid.update		\
7e1b55
 	73-winsync.update		\
7e1b55
 	73-certmap.update		\
7e1b55
 	75-user-trust-attributes.update	\
7e1b55
diff --git a/ipalib/constants.py b/ipalib/constants.py
7e1b55
index 79ea36f08cb0108a7434bc58cf0a764e9e15a7af..bee4c92fb39769d427e315116575f217924915be 100644
7e1b55
--- a/ipalib/constants.py
7e1b55
+++ b/ipalib/constants.py
7e1b55
@@ -343,3 +343,16 @@ SOFTHSM_DNSSEC_TOKEN_LABEL = u'ipaDNSSEC'
7e1b55
 # Apache's mod_ssl SSLVerifyDepth value (Maximum depth of CA
7e1b55
 # Certificates in Client Certificate verification)
7e1b55
 MOD_SSL_VERIFY_DEPTH = '5'
7e1b55
+
7e1b55
+# subuid / subgid counts are hard-coded
7e1b55
+# An interval of 65536 uids/gids is required to map nobody (65534).
7e1b55
+SUBID_COUNT = 65536
7e1b55
+
7e1b55
+# upper half of uid_t (uint32_t)
7e1b55
+SUBID_RANGE_START = 2 ** 31
7e1b55
+# theoretical max limit is UINT32_MAX-1 ((2 ** 32) - 2)
7e1b55
+# We use a smaller value to keep the topmost subid interval unused.
7e1b55
+SUBID_RANGE_MAX = (2 ** 32) - (2 * SUBID_COUNT)
7e1b55
+SUBID_RANGE_SIZE = SUBID_RANGE_MAX - SUBID_RANGE_START
7e1b55
+# threshold before DNA plugin requests a new range
7e1b55
+SUBID_DNA_THRESHOLD = 500 * SUBID_COUNT
7e1b55
diff --git a/ipaserver/install/adtrustinstance.py b/ipaserver/install/adtrustinstance.py
7e1b55
index a7a403f37db13b7cccf74dff1b92b22529170b8a..24e90f3ecf5b4669f162e1bc68a33ef9d6094514 100644
7e1b55
--- a/ipaserver/install/adtrustinstance.py
7e1b55
+++ b/ipaserver/install/adtrustinstance.py
7e1b55
@@ -36,6 +36,7 @@ from ipaserver.install import service
7e1b55
 from ipaserver.install import installutils
7e1b55
 from ipaserver.install.replication import wait_for_task
7e1b55
 from ipalib import errors, api
7e1b55
+from ipalib.constants import SUBID_RANGE_START
7e1b55
 from ipalib.util import normalize_zone
7e1b55
 from ipapython.dn import DN
7e1b55
 from ipapython import ipachangeconf
7e1b55
@@ -352,12 +353,19 @@ class ADTRUSTInstance(service.Service):
7e1b55
                 DN(api.env.container_ranges, self.suffix),
7e1b55
                 ldap.SCOPE_ONELEVEL, "(objectclass=ipaDomainIDRange)")
7e1b55
 
7e1b55
-            # Filter out ranges where RID base is already set
7e1b55
-            no_rid_base_set = lambda r: not any((
7e1b55
-                                  r.single_value.get('ipaBaseRID'),
7e1b55
-                                  r.single_value.get('ipaSecondaryBaseRID')))
7e1b55
+            ranges_with_no_rid_base = []
7e1b55
+            for entry in ranges:
7e1b55
+                sv = entry.single_value
7e1b55
+                if sv.get('ipaBaseRID') or sv.get('ipaSecondaryBaseRID'):
7e1b55
+                    # skip range where RID base is already set
7e1b55
+                    continue
7e1b55
+                if sv.get('ipaRangeType') == 'ipa-local-subid':
7e1b55
+                    # ignore subid ranges
7e1b55
+                    continue
7e1b55
+                ranges_with_no_rid_base.append(entry)
7e1b55
 
7e1b55
-            ranges_with_no_rid_base = [r for r in ranges if no_rid_base_set(r)]
7e1b55
+            logger.debug(repr(ranges))
7e1b55
+            logger.debug(repr(ranges_with_no_rid_base))
7e1b55
 
7e1b55
             # Return if no range is without RID base
7e1b55
             if len(ranges_with_no_rid_base) == 0:
7e1b55
@@ -384,6 +392,17 @@ class ADTRUSTInstance(service.Service):
7e1b55
                                "They have to differ at least by %d." % size)
7e1b55
                 raise RuntimeError("RID bases too close.\n")
7e1b55
 
7e1b55
+            # values above
7e1b55
+            if any(
7e1b55
+                v + size >= SUBID_RANGE_START
7e1b55
+                for v in (self.rid_base, self.secondary_rid_base)
7e1b55
+            ):
7e1b55
+                self.print_msg(
7e1b55
+                    "Ceiling of primary or secondary base is larger than "
7e1b55
+                    f"start of subordinate id range {SUBID_RANGE_START}."
7e1b55
+                )
7e1b55
+                raise RuntimeError("RID bases overlap with SUBID range.\n")
7e1b55
+
7e1b55
             # Modify the range
7e1b55
             # If the RID bases would cause overlap with some other range,
7e1b55
             # this will be detected by ipa-range-check DS plugin
7e1b55
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
7e1b55
index 6033c04109f6278cb7b6015becd507b2b4699e02..ac9e131bb1b8c6ff8aff911cb257fbb03406d603 100644
7e1b55
--- a/ipaserver/install/dsinstance.py
7e1b55
+++ b/ipaserver/install/dsinstance.py
7e1b55
@@ -23,7 +23,6 @@ from __future__ import print_function, absolute_import
7e1b55
 import logging
7e1b55
 import shutil
7e1b55
 import os
7e1b55
-import time
7e1b55
 import tempfile
7e1b55
 import fnmatch
7e1b55
 
7e1b55
@@ -46,6 +45,7 @@ from ipaserver.install import certs
7e1b55
 from ipaserver.install import replication
7e1b55
 from ipaserver.install import sysupgrade
7e1b55
 from ipaserver.install import upgradeinstance
7e1b55
+from ipaserver.install import ldapupdate
7e1b55
 from ipalib import api
7e1b55
 from ipalib import errors
7e1b55
 from ipalib import constants
7e1b55
@@ -66,6 +66,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
7e1b55
                     "60ipaconfig.ldif",
7e1b55
                     "60basev2.ldif",
7e1b55
                     "60basev3.ldif",
7e1b55
+                    "60basev4.ldif",
7e1b55
                     "60ipapk11.ldif",
7e1b55
                     "60ipadns.ldif",
7e1b55
                     "60certificate-profiles.ldif",
7e1b55
@@ -214,6 +215,8 @@ class DsInstance(service.Service):
7e1b55
         if realm_name:
7e1b55
             self.suffix = ipautil.realm_to_suffix(self.realm)
7e1b55
             self.serverid = ipaldap.realm_to_serverid(self.realm)
7e1b55
+            if self.domain is None:
7e1b55
+                self.domain = self.realm.lower()
7e1b55
             self.__setup_sub_dict()
7e1b55
         else:
7e1b55
             self.suffix = DN()
7e1b55
@@ -497,34 +500,22 @@ class DsInstance(service.Service):
7e1b55
 
7e1b55
     def __setup_sub_dict(self):
7e1b55
         server_root = find_server_root()
7e1b55
-        try:
7e1b55
-            idrange_size = self.idmax - self.idstart + 1
7e1b55
-        except TypeError:
7e1b55
-            idrange_size = None
7e1b55
-        self.sub_dict = dict(
7e1b55
-            FQDN=self.fqdn, SERVERID=self.serverid,
7e1b55
+        self.sub_dict = ldapupdate.get_sub_dict(
7e1b55
+            realm=self.realm,
7e1b55
+            domain=self.domain,
7e1b55
+            suffix=self.suffix,
7e1b55
+            fqdn=self.fqdn,
7e1b55
+            idstart=self.idstart,
7e1b55
+            idmax=self.idmax,
7e1b55
+        )
7e1b55
+        self.sub_dict.update(
7e1b55
+            DOMAIN_LEVEL=self.domainlevel,
7e1b55
+            SERVERID=self.serverid,
7e1b55
             PASSWORD=self.dm_password,
7e1b55
             RANDOM_PASSWORD=ipautil.ipa_generate_password(),
7e1b55
-            SUFFIX=self.suffix,
7e1b55
-            REALM=self.realm, USER=DS_USER,
7e1b55
-            SERVER_ROOT=server_root, DOMAIN=self.domain,
7e1b55
-            TIME=int(time.time()), IDSTART=self.idstart,
7e1b55
-            IDMAX=self.idmax, HOST=self.fqdn,
7e1b55
-            ESCAPED_SUFFIX=str(self.suffix),
7e1b55
+            USER=DS_USER,
7e1b55
             GROUP=DS_GROUP,
7e1b55
-            IDRANGE_SIZE=idrange_size,
7e1b55
-            DOMAIN_LEVEL=self.domainlevel,
7e1b55
-            MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL,
7e1b55
-            MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL,
7e1b55
-            STRIP_ATTRS=" ".join(replication.STRIP_ATTRS),
7e1b55
-            EXCLUDES='(objectclass=*) $ EXCLUDE ' +
7e1b55
-            ' '.join(replication.EXCLUDES),
7e1b55
-            TOTAL_EXCLUDES='(objectclass=*) $ EXCLUDE ' +
7e1b55
-            ' '.join(replication.TOTAL_EXCLUDES),
7e1b55
-            DEFAULT_SHELL=platformconstants.DEFAULT_SHELL,
7e1b55
-            DEFAULT_ADMIN_SHELL=platformconstants.DEFAULT_ADMIN_SHELL,
7e1b55
-            SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT,
7e1b55
-            SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER,
7e1b55
+            SERVER_ROOT=server_root,
7e1b55
         )
7e1b55
 
7e1b55
     def __create_instance(self):
7e1b55
diff --git a/ipaserver/install/ipa_subids.py b/ipaserver/install/ipa_subids.py
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..ac77a4008aec58d92c8b24df5e00b83c6998401f
7e1b55
--- /dev/null
7e1b55
+++ b/ipaserver/install/ipa_subids.py
7e1b55
@@ -0,0 +1,154 @@
7e1b55
+#
7e1b55
+# Copyright (C) 2021  FreeIPA Contributors see COPYING for license
7e1b55
+#
7e1b55
+
7e1b55
+import logging
7e1b55
+
7e1b55
+from ipalib import api
7e1b55
+from ipalib import errors
7e1b55
+from ipalib.facts import is_ipa_configured
7e1b55
+from ipaplatform.paths import paths
7e1b55
+from ipapython.admintool import AdminTool, ScriptError
7e1b55
+from ipapython.dn import DN
7e1b55
+from ipaserver.plugins.baseldap import DNA_MAGIC
7e1b55
+
7e1b55
+logger = logging.getLogger(__name__)
7e1b55
+
7e1b55
+
7e1b55
+class IPASubids(AdminTool):
7e1b55
+    command_name = "ipa-subids"
7e1b55
+    usage = "%prog [--group GROUP|--all-users]"
7e1b55
+    description = "Mass-assign subordinate ids to users"
7e1b55
+
7e1b55
+    @classmethod
7e1b55
+    def add_options(cls, parser):
7e1b55
+        super(IPASubids, cls).add_options(parser, debug_option=True)
7e1b55
+        parser.add_option(
7e1b55
+            "--group",
7e1b55
+            dest="group",
7e1b55
+            action="store",
7e1b55
+            default=None,
7e1b55
+            help="Updates members of a user group.",
7e1b55
+        )
7e1b55
+        parser.add_option(
7e1b55
+            "--all-users",
7e1b55
+            dest="all_users",
7e1b55
+            action="store_true",
7e1b55
+            default=False,
7e1b55
+            help="Update all users.",
7e1b55
+        )
7e1b55
+        parser.add_option(
7e1b55
+            "--filter",
7e1b55
+            dest="user_filter",
7e1b55
+            action="store",
7e1b55
+            default="(!(nsaccountlock=TRUE))",
7e1b55
+            help="Additional raw LDAP filter (default: active users).",
7e1b55
+        )
7e1b55
+        parser.add_option(
7e1b55
+            "--dry-run",
7e1b55
+            dest="dry_run",
7e1b55
+            action="store_true",
7e1b55
+            default=False,
7e1b55
+            help="Dry run mode.",
7e1b55
+        )
7e1b55
+
7e1b55
+    def validate_options(self, neends_root=False):
7e1b55
+        super().validate_options(needs_root=True)
7e1b55
+        opt = self.safe_options
7e1b55
+
7e1b55
+        if opt.all_users and opt.group:
7e1b55
+            raise ScriptError("--group and --all-users are mutually exclusive")
7e1b55
+        if not opt.all_users and not opt.group:
7e1b55
+            raise ScriptError("Either --group or --all-users required")
7e1b55
+
7e1b55
+    def get_group_info(self):
7e1b55
+        assert api.isdone("finalize")
7e1b55
+        group = self.safe_options.group
7e1b55
+        if group is None:
7e1b55
+            return None
7e1b55
+        try:
7e1b55
+            result = api.Command.group_show(group, no_members=True)
7e1b55
+            return result["result"]
7e1b55
+        except errors.NotFound:
7e1b55
+            raise ScriptError(f"Unknown users group '{group}'.")
7e1b55
+
7e1b55
+    def make_filter(self, groupinfo, user_filter):
7e1b55
+        filters = [
7e1b55
+            # only users with posixAccount
7e1b55
+            "(objectClass=posixAccount)",
7e1b55
+            # without subordinate ids
7e1b55
+            "(!(objectClass=ipaSubordinateId))",
7e1b55
+        ]
7e1b55
+        if groupinfo is not None:
7e1b55
+            filters.append(
7e1b55
+                self.ldap2.make_filter({"memberof": groupinfo["dn"]})
7e1b55
+            )
7e1b55
+        if user_filter:
7e1b55
+            filters.append(user_filter)
7e1b55
+        return self.ldap2.combine_filters(filters, self.ldap2.MATCH_ALL)
7e1b55
+
7e1b55
+    def search_users(self, filters):
7e1b55
+        users_dn = DN(api.env.container_user, api.env.basedn)
7e1b55
+        attrs = ["objectclass", "uid", "uidnumber"]
7e1b55
+
7e1b55
+        logger.debug("basedn: %s", users_dn)
7e1b55
+        logger.debug("attrs: %s", attrs)
7e1b55
+        logger.debug("filter: %s", filters)
7e1b55
+
7e1b55
+        try:
7e1b55
+            entries = self.ldap2.get_entries(
7e1b55
+                base_dn=users_dn,
7e1b55
+                filter=filters,
7e1b55
+                attrs_list=attrs,
7e1b55
+            )
7e1b55
+        except errors.NotFound:
7e1b55
+            logger.debug("No entries found")
7e1b55
+            return []
7e1b55
+        else:
7e1b55
+            return entries
7e1b55
+
7e1b55
+    def run(self):
7e1b55
+        if not is_ipa_configured():
7e1b55
+            print("IPA is not configured.")
7e1b55
+            return 2
7e1b55
+
7e1b55
+        api.bootstrap(in_server=True, confdir=paths.ETC_IPA)
7e1b55
+        api.finalize()
7e1b55
+        api.Backend.ldap2.connect()
7e1b55
+        self.ldap2 = api.Backend.ldap2
7e1b55
+        user_obj = api.Object["user"]
7e1b55
+
7e1b55
+        dry_run = self.safe_options.dry_run
7e1b55
+        group_info = self.get_group_info()
7e1b55
+        filters = self.make_filter(
7e1b55
+            group_info, self.safe_options.user_filter
7e1b55
+        )
7e1b55
+
7e1b55
+        entries = self.search_users(filters)
7e1b55
+        total = len(entries)
7e1b55
+        logger.info("Found %i user(s) without subordinate ids", total)
7e1b55
+
7e1b55
+        total = len(entries)
7e1b55
+        for i, entry in enumerate(entries, start=1):
7e1b55
+            logger.info(
7e1b55
+                "  Processing user '%s' (%i/%i)",
7e1b55
+                entry.single_value["uid"],
7e1b55
+                i,
7e1b55
+                total
7e1b55
+            )
7e1b55
+            user_obj.set_subordinate_ids(
7e1b55
+                self.ldap2, entry.dn, entry, DNA_MAGIC
7e1b55
+            )
7e1b55
+            if not dry_run:
7e1b55
+                self.ldap2.update_entry(entry)
7e1b55
+
7e1b55
+        if dry_run:
7e1b55
+            logger.info("Dry run mode, no user was modified")
7e1b55
+        else:
7e1b55
+            logger.info("Updated %s user(s)", total)
7e1b55
+
7e1b55
+        return 0
7e1b55
+
7e1b55
+
7e1b55
+if __name__ == "__main__":
7e1b55
+    IPASubids.run_cli()
7e1b55
diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py
7e1b55
index f21e5a5af465be37541b9fbdddaf800b73f80b71..d0516dc3028366df5d03a960866abe72601aa4b6 100644
7e1b55
--- a/ipaserver/install/ldapupdate.py
7e1b55
+++ b/ipaserver/install/ldapupdate.py
7e1b55
@@ -32,9 +32,9 @@ import os
7e1b55
 import fnmatch
7e1b55
 import warnings
7e1b55
 
7e1b55
+from pysss_murmur import murmurhash3  # pylint: disable=no-name-in-module
7e1b55
 import six
7e1b55
 
7e1b55
-from ipaserver.install import installutils
7e1b55
 from ipapython import ipautil, ipaldap
7e1b55
 from ipalib import errors
7e1b55
 from ipalib import api, create_api
7e1b55
@@ -43,6 +43,7 @@ from ipaplatform.constants import constants as platformconstants
7e1b55
 from ipaplatform.paths import paths
7e1b55
 from ipaplatform.tasks import tasks
7e1b55
 from ipapython.dn import DN
7e1b55
+from ipaserver.install import installutils, replication
7e1b55
 
7e1b55
 if six.PY3:
7e1b55
     unicode = str
7e1b55
@@ -53,6 +54,54 @@ UPDATES_DIR=paths.UPDATES_DIR
7e1b55
 UPDATE_SEARCH_TIME_LIMIT = 30  # seconds
7e1b55
 
7e1b55
 
7e1b55
+def get_sub_dict(realm, domain, suffix, fqdn, idstart=None, idmax=None):
7e1b55
+    """LDAP template substitution dict for installer and updater
7e1b55
+    """
7e1b55
+    if idstart is None:
7e1b55
+        idrange_size = None
7e1b55
+    else:
7e1b55
+        idrange_size = idmax - idstart + 1
7e1b55
+
7e1b55
+    return dict(
7e1b55
+        REALM=realm,
7e1b55
+        DOMAIN=domain,
7e1b55
+        SUFFIX=suffix,
7e1b55
+        ESCAPED_SUFFIX=str(suffix),
7e1b55
+        FQDN=fqdn,
7e1b55
+        HOST=fqdn,
7e1b55
+        LIBARCH=paths.LIBARCH,
7e1b55
+        TIME=int(time.time()),
7e1b55
+        FIPS="#" if tasks.is_fips_enabled() else "",
7e1b55
+        # idstart, idmax, and idrange_size may be None
7e1b55
+        IDSTART=idstart,
7e1b55
+        IDMAX=idmax,
7e1b55
+        IDRANGE_SIZE=idrange_size,
7e1b55
+        SUBID_COUNT=constants.SUBID_COUNT,
7e1b55
+        SUBID_RANGE_START=constants.SUBID_RANGE_START,
7e1b55
+        SUBID_RANGE_SIZE=constants.SUBID_RANGE_SIZE,
7e1b55
+        SUBID_RANGE_MAX=constants.SUBID_RANGE_MAX,
7e1b55
+        SUBID_DNA_THRESHOLD=constants.SUBID_DNA_THRESHOLD,
7e1b55
+        DOMAIN_HASH=murmurhash3(domain, len(domain), 0xdeadbeef),
7e1b55
+        MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL,
7e1b55
+        MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL,
7e1b55
+        STRIP_ATTRS=" ".join(replication.STRIP_ATTRS),
7e1b55
+        EXCLUDES=(
7e1b55
+            '(objectclass=*) $ EXCLUDE ' + ' '.join(replication.EXCLUDES)
7e1b55
+        ),
7e1b55
+        TOTAL_EXCLUDES=(
7e1b55
+            '(objectclass=*) $ EXCLUDE '
7e1b55
+            + ' '.join(replication.TOTAL_EXCLUDES)
7e1b55
+        ),
7e1b55
+        DEFAULT_SHELL=platformconstants.DEFAULT_SHELL,
7e1b55
+        DEFAULT_ADMIN_SHELL=platformconstants.DEFAULT_ADMIN_SHELL,
7e1b55
+        SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT,
7e1b55
+        SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER,
7e1b55
+        # uid / gid for autobind
7e1b55
+        NAMED_UID=platformconstants.NAMED_USER.uid,
7e1b55
+        NAMED_GID=platformconstants.NAMED_GROUP.gid,
7e1b55
+    )
7e1b55
+
7e1b55
+
7e1b55
 def connect(ldapi=False, realm=None, fqdn=None):
7e1b55
     """Create a connection for updates"""
7e1b55
     if ldapi:
7e1b55
@@ -284,35 +333,33 @@ class LDAPUpdate:
7e1b55
             ldap_uri=self.ldapuri
7e1b55
         )
7e1b55
         self.api.finalize()
7e1b55
-
7e1b55
         self.create_connection()
7e1b55
 
7e1b55
+        # get ipa-local domain idrange settings
7e1b55
+        domain_range = f"{self.api.env.realm}_id_range"
7e1b55
+        try:
7e1b55
+            result = self.api.Command.idrange_show(domain_range)["result"]
7e1b55
+        except errors.NotFound:
7e1b55
+            idstart = None
7e1b55
+            idmax = None
7e1b55
+        else:
7e1b55
+            idstart = int(result['ipabaseid'][0])
7e1b55
+            idrange_size = int(result['ipaidrangesize'][0])
7e1b55
+            idmax = idstart + idrange_size - 1
7e1b55
+
7e1b55
+        default_sub = get_sub_dict(
7e1b55
+            realm=api.env.realm,
7e1b55
+            domain=api.env.domain,
7e1b55
+            suffix=api.env.basedn,
7e1b55
+            fqdn=api.env.host,
7e1b55
+            idstart=idstart,
7e1b55
+            idmax=idmax,
7e1b55
+        )
7e1b55
         replication_plugin = (
7e1b55
             installutils.get_replication_plugin_name(self.conn.get_entry)
7e1b55
         )
7e1b55
+        default_sub["REPLICATION_PLUGIN"] = replication_plugin
7e1b55
 
7e1b55
-        default_sub = dict(
7e1b55
-            REALM=api.env.realm,
7e1b55
-            DOMAIN=api.env.domain,
7e1b55
-            SUFFIX=api.env.basedn,
7e1b55
-            ESCAPED_SUFFIX=str(api.env.basedn),
7e1b55
-            FQDN=api.env.host,
7e1b55
-            LIBARCH=paths.LIBARCH,
7e1b55
-            TIME=int(time.time()),
7e1b55
-            MIN_DOMAIN_LEVEL=str(constants.MIN_DOMAIN_LEVEL),
7e1b55
-            MAX_DOMAIN_LEVEL=str(constants.MAX_DOMAIN_LEVEL),
7e1b55
-            STRIP_ATTRS=" ".join(constants.REPL_AGMT_STRIP_ATTRS),
7e1b55
-            EXCLUDES="(objectclass=*) $ EXCLUDE %s" % (
7e1b55
-                " ".join(constants.REPL_AGMT_EXCLUDES)
7e1b55
-            ),
7e1b55
-            TOTAL_EXCLUDES="(objectclass=*) $ EXCLUDE %s" % (
7e1b55
-                " ".join(constants.REPL_AGMT_TOTAL_EXCLUDES)
7e1b55
-            ),
7e1b55
-            SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT,
7e1b55
-            SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER,
7e1b55
-            FIPS="#" if tasks.is_fips_enabled() else "",
7e1b55
-            REPLICATION_PLUGIN=replication_plugin,
7e1b55
-        )
7e1b55
         for k, v in default_sub.items():
7e1b55
             self.sub_dict.setdefault(k, v)
7e1b55
 
7e1b55
diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py
7e1b55
index 6035228f19ef8acaf4992490d5512c126881816d..12ff03c2302ff08aabb9369306965e0c125724f8 100644
7e1b55
--- a/ipaserver/plugins/baseuser.py
7e1b55
+++ b/ipaserver/plugins/baseuser.py
7e1b55
@@ -17,9 +17,10 @@
7e1b55
 # You should have received a copy of the GNU General Public License
7e1b55
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
7e1b55
 
7e1b55
+import random
7e1b55
 import six
7e1b55
 
7e1b55
-from ipalib import api, errors
7e1b55
+from ipalib import api, errors, output, constants
7e1b55
 from ipalib import (
7e1b55
     Flag, Int, Password, Str, Bool, StrEnum, DateTime, DNParam)
7e1b55
 from ipalib.parameters import Principal, Certificate
7e1b55
@@ -27,13 +28,13 @@ from ipalib.plugable import Registry
7e1b55
 from .baseldap import (
7e1b55
     DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete,
7e1b55
     LDAPRetrieve, LDAPAddAttribute, LDAPModAttribute, LDAPRemoveAttribute,
7e1b55
-    LDAPAddMember, LDAPRemoveMember,
7e1b55
+    LDAPQuery, LDAPAddMember, LDAPRemoveMember,
7e1b55
     LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption,
7e1b55
-    add_missing_object_class)
7e1b55
+    add_missing_object_class, DNA_MAGIC, pkey_to_value, entry_to_dict
7e1b55
+)
7e1b55
 from ipaserver.plugins.service import (validate_realm, normalize_principal)
7e1b55
 from ipalib.request import context
7e1b55
 from ipalib import _
7e1b55
-from ipalib.constants import PATTERN_GROUPUSER_NAME
7e1b55
 from ipapython import kerberos
7e1b55
 from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS
7e1b55
 from ipapython.ipavalidate import Email
7e1b55
@@ -161,7 +162,7 @@ class baseuser(LDAPObject):
7e1b55
     possible_objectclasses = [
7e1b55
         'meporiginentry', 'ipauserauthtypeclass', 'ipauser',
7e1b55
         'ipatokenradiusproxyuser', 'ipacertmapobject',
7e1b55
-        'ipantuserattrs'
7e1b55
+        'ipantuserattrs', 'ipasubordinateid',
7e1b55
     ]
7e1b55
     disallow_object_classes = ['krbticketpolicyaux']
7e1b55
     permission_filter_objectclasses = ['posixaccount']
7e1b55
@@ -175,13 +176,15 @@ class baseuser(LDAPObject):
7e1b55
         'krbprincipalexpiration', 'usercertificate;binary',
7e1b55
         'krbprincipalname', 'krbcanonicalname',
7e1b55
         'ipacertmapdata', 'ipantlogonscript', 'ipantprofilepath',
7e1b55
-        'ipanthomedirectory', 'ipanthomedirectorydrive'
7e1b55
+        'ipanthomedirectory', 'ipanthomedirectorydrive',
7e1b55
     ]
7e1b55
     search_display_attributes = [
7e1b55
         'uid', 'givenname', 'sn', 'homedirectory', 'krbcanonicalname',
7e1b55
         'krbprincipalname', 'loginshell',
7e1b55
         'mail', 'telephonenumber', 'title', 'nsaccountlock',
7e1b55
         'uidnumber', 'gidnumber', 'sshpubkeyfp',
7e1b55
+        'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber',
7e1b55
+        'ipasubgidcount',
7e1b55
     ]
7e1b55
     uuid_attribute = 'ipauniqueid'
7e1b55
     attribute_members = {
7e1b55
@@ -198,7 +201,7 @@ class baseuser(LDAPObject):
7e1b55
 
7e1b55
     takes_params = (
7e1b55
         Str('uid',
7e1b55
-            pattern=PATTERN_GROUPUSER_NAME,
7e1b55
+            pattern=constants.PATTERN_GROUPUSER_NAME,
7e1b55
             pattern_errmsg='may only include letters, numbers, _, -, . and $',
7e1b55
             maxlength=255,
7e1b55
             cli_name='login',
7e1b55
@@ -429,6 +432,41 @@ class baseuser(LDAPObject):
7e1b55
                     'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:',
7e1b55
                     'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:'),
7e1b55
                 ),
7e1b55
+        Int(
7e1b55
+            'ipasubuidnumber?',
7e1b55
+            label=_('SubUID range start'),
7e1b55
+            cli_name='subuid',
7e1b55
+            doc=_('Start value for subordinate user ID (subuid) range'),
7e1b55
+            minvalue=constants.SUBID_RANGE_START,
7e1b55
+            maxvalue=constants.SUBID_RANGE_MAX,
7e1b55
+        ),
7e1b55
+        Int(
7e1b55
+            'ipasubuidcount?',
7e1b55
+            label=_('SubUID range size'),
7e1b55
+            cli_name='subuidcount',
7e1b55
+            doc=_('Subordinate user ID count'),
7e1b55
+            flags={'no_create', 'no_update', 'no_search'},
7e1b55
+            minvalue=constants.SUBID_COUNT,
7e1b55
+            maxvalue=constants.SUBID_COUNT,
7e1b55
+        ),
7e1b55
+        Int(
7e1b55
+            'ipasubgidnumber?',
7e1b55
+            label=_('SubGID range start'),
7e1b55
+            cli_name='subgid',
7e1b55
+            doc=_('Start value for subordinate group ID (subgid) range'),
7e1b55
+            flags={'no_create', 'no_update'},
7e1b55
+            minvalue=constants.SUBID_RANGE_START,
7e1b55
+            maxvalue=constants.SUBID_RANGE_MAX,
7e1b55
+        ),
7e1b55
+        Int(
7e1b55
+            'ipasubgidcount?',
7e1b55
+            label=_('SubGID range size'),
7e1b55
+            cli_name='subgidcount',
7e1b55
+            doc=_('Subordinate group ID count'),
7e1b55
+            flags={'no_create', 'no_update', 'no_search'},
7e1b55
+            minvalue=constants.SUBID_COUNT,
7e1b55
+            maxvalue=constants.SUBID_COUNT,
7e1b55
+        ),
7e1b55
     )
7e1b55
 
7e1b55
     def normalize_and_validate_email(self, email, config=None):
7e1b55
@@ -526,6 +564,131 @@ class baseuser(LDAPObject):
7e1b55
         except KeyError:
7e1b55
             pass
7e1b55
 
7e1b55
+    def handle_subordinate_ids(self, ldap, dn, entry_attrs):
7e1b55
+        """Handle ipaSubordinateId object class
7e1b55
+        """
7e1b55
+        obj_classes = entry_attrs.get("objectclass")
7e1b55
+        new_subuid = entry_attrs.single_value.get("ipasubuidnumber")
7e1b55
+        new_subgid = entry_attrs.single_value.get("ipasubgidnumber")
7e1b55
+
7e1b55
+        # entry has object class ipaSubordinateId
7e1b55
+        # default to auto-assigment of subuids
7e1b55
+        if (
7e1b55
+            new_subuid is None
7e1b55
+            and obj_classes is not None
7e1b55
+            and self.has_objectclass(obj_classes, "ipasubordinateid")
7e1b55
+        ):
7e1b55
+            new_subuid = DNA_MAGIC
7e1b55
+
7e1b55
+        # neither auto-assignment nor explicit assignment
7e1b55
+        if new_subuid is None:
7e1b55
+            # nothing to do
7e1b55
+            return False
7e1b55
+
7e1b55
+        # enforce subuid == subgid
7e1b55
+        if new_subgid is not None and new_subgid != new_subuid:
7e1b55
+            raise errors.ValidationError(
7e1b55
+                name="ipasubgidnumber",
7e1b55
+                error=_("subgidnumber must be equal to subuidnumber")
7e1b55
+            )
7e1b55
+
7e1b55
+        self.set_subordinate_ids(ldap, dn, entry_attrs, new_subuid)
7e1b55
+        return True
7e1b55
+
7e1b55
+    def set_subordinate_ids(self, ldap, dn, entry_attrs, subuid):
7e1b55
+        """Set subuid value of an entry
7e1b55
+
7e1b55
+        Takes care of objectclass and sibbling attributes
7e1b55
+        """
7e1b55
+        if "objectclass" in entry_attrs:
7e1b55
+            obj_classes = entry_attrs["objectclass"]
7e1b55
+        else:
7e1b55
+            _entry_attrs = ldap.get_entry(dn, ["objectclass"])
7e1b55
+            entry_attrs["objectclass"] = _entry_attrs["objectclass"]
7e1b55
+            obj_classes = entry_attrs["objectclass"]
7e1b55
+
7e1b55
+        if not self.has_objectclass(obj_classes, "ipasubordinateid"):
7e1b55
+            # could append ipasubordinategid and ipasubordinateuid, too
7e1b55
+            obj_classes.append("ipasubordinateid")
7e1b55
+
7e1b55
+        # XXX HACK, remove later
7e1b55
+        if subuid == DNA_MAGIC:
7e1b55
+            subuid = self._fake_dna_plugin(ldap, dn, entry_attrs)
7e1b55
+
7e1b55
+        entry_attrs["ipasubuidnumber"] = subuid
7e1b55
+        # enforice subuid == subgid for now
7e1b55
+        entry_attrs["ipasubgidnumber"] = subuid
7e1b55
+        # hard-coded constants
7e1b55
+        entry_attrs["ipasubuidcount"] = constants.SUBID_COUNT
7e1b55
+        entry_attrs["ipasubgidcount"] = constants.SUBID_COUNT
7e1b55
+
7e1b55
+    def get_subid_match_candidate_filter(
7e1b55
+        self, ldap, *, subuid, subgid, extra_filters=(), offset=None,
7e1b55
+    ):
7e1b55
+        """Create LDAP filter to locate matching/overlapping subids
7e1b55
+        """
7e1b55
+        if subuid is None and subgid is None:
7e1b55
+            raise ValueError("subuid and subgid are both None")
7e1b55
+        if offset is None:
7e1b55
+            # assumes that no subordinate count is larger than SUBID_COUNT
7e1b55
+            offset = constants.SUBID_COUNT - 1
7e1b55
+
7e1b55
+        class_filters = "(objectclass=ipasubordinateid)"
7e1b55
+        subid_filters = []
7e1b55
+        if subuid is not None:
7e1b55
+            subid_filters.append(
7e1b55
+                ldap.combine_filters(
7e1b55
+                    [
7e1b55
+                        f"(ipasubuidnumber>={subuid - offset})",
7e1b55
+                        f"(ipasubuidnumber<={subuid + offset})",
7e1b55
+                    ],
7e1b55
+                    rules=ldap.MATCH_ALL
7e1b55
+                )
7e1b55
+            )
7e1b55
+        if subgid is not None:
7e1b55
+            subid_filters.append(
7e1b55
+                ldap.combine_filters(
7e1b55
+                    [
7e1b55
+                        f"(ipasubgidnumber>={subgid - offset})",
7e1b55
+                        f"(ipasubgidnumber<={subgid + offset})",
7e1b55
+                    ],
7e1b55
+                    rules=ldap.MATCH_ALL
7e1b55
+                )
7e1b55
+            )
7e1b55
+
7e1b55
+        subid_filters = ldap.combine_filters(
7e1b55
+            subid_filters, rules=ldap.MATCH_ANY
7e1b55
+        )
7e1b55
+        filters = [class_filters, subid_filters]
7e1b55
+        filters.extend(extra_filters)
7e1b55
+        return ldap.combine_filters(filters, rules=ldap.MATCH_ALL)
7e1b55
+
7e1b55
+    def _fake_dna_plugin(self, ldap, dn, entry_attrs):
7e1b55
+        """XXX HACK, remove when 389-DS DNA plugin supports steps"""
7e1b55
+        uidnumber = entry_attrs.single_value.get("uidnumber")
7e1b55
+        if uidnumber is None:
7e1b55
+            entry = ldap.get_entry(dn, ["uidnumber"])
7e1b55
+            uidnumber = entry.single_value["uidnumber"]
7e1b55
+        uidnumber = int(uidnumber)
7e1b55
+
7e1b55
+        if uidnumber == DNA_MAGIC:
7e1b55
+            return (
7e1b55
+                3221225472
7e1b55
+                + random.randint(1, 16382) * constants.SUBID_COUNT
7e1b55
+            )
7e1b55
+
7e1b55
+        if not hasattr(context, "idrange_ipabaseid"):
7e1b55
+            range_name = f"{self.api.env.realm}_id_range"
7e1b55
+            range = self.api.Command.idrange_show(range_name)["result"]
7e1b55
+            context.idrange_ipabaseid = int(range["ipabaseid"][0])
7e1b55
+
7e1b55
+        range_start = context.idrange_ipabaseid
7e1b55
+
7e1b55
+        assert uidnumber >= range_start
7e1b55
+        assert uidnumber < range_start + 2**14
7e1b55
+
7e1b55
+        return (uidnumber - range_start) * constants.SUBID_COUNT + 2**31
7e1b55
+
7e1b55
 
7e1b55
 class baseuser_add(LDAPCreate):
7e1b55
     """
7e1b55
@@ -536,6 +699,7 @@ class baseuser_add(LDAPCreate):
7e1b55
         assert isinstance(dn, DN)
7e1b55
         set_krbcanonicalname(entry_attrs)
7e1b55
         self.obj.convert_usercertificate_pre(entry_attrs)
7e1b55
+        self.obj.handle_subordinate_ids(ldap, dn, entry_attrs)
7e1b55
         if entry_attrs.get('ipatokenradiususername', None):
7e1b55
             add_missing_object_class(ldap, u'ipatokenradiusproxyuser', dn,
7e1b55
                                      entry_attrs, update=False)
7e1b55
@@ -688,6 +852,7 @@ class baseuser_mod(LDAPUpdate):
7e1b55
 
7e1b55
         self.check_objectclass(ldap, dn, entry_attrs)
7e1b55
         self.obj.convert_usercertificate_pre(entry_attrs)
7e1b55
+        self.obj.handle_subordinate_ids(ldap, dn, entry_attrs)
7e1b55
         self.preserve_krbprincipalname_pre(ldap, entry_attrs, *keys, **options)
7e1b55
         update_samba_attrs(ldap, dn, entry_attrs, **options)
7e1b55
 
7e1b55
@@ -968,3 +1133,98 @@ class baseuser_remove_certmapdata(ModCertMapData,
7e1b55
                                   LDAPRemoveAttribute):
7e1b55
     __doc__ = _("Remove one or more certificate mappings from the user entry.")
7e1b55
     msg_summary = _('Removed certificate mappings from user "%(value)s"')
7e1b55
+
7e1b55
+
7e1b55
+class baseuser_auto_subid(LDAPQuery):
7e1b55
+    __doc__ = _("Auto-assign subuid and subgid range to user entry")
7e1b55
+
7e1b55
+    has_output = output.standard_entry
7e1b55
+
7e1b55
+    def execute(self, cn, **options):
7e1b55
+        ldap = self.obj.backend
7e1b55
+        dn = self.obj.get_dn(cn)
7e1b55
+
7e1b55
+        try:
7e1b55
+            entry_attrs = ldap.get_entry(
7e1b55
+                dn, ["objectclass", "ipasubuidnumber"]
7e1b55
+            )
7e1b55
+        except errors.NotFound:
7e1b55
+            raise self.obj.handle_not_found(cn)
7e1b55
+
7e1b55
+        if "ipasubuidnumber" in entry_attrs:
7e1b55
+            raise errors.AlreadyContainsValueError(attr="ipasubuidnumber")
7e1b55
+
7e1b55
+        self.obj.set_subordinate_ids(ldap, dn, entry_attrs, subuid=DNA_MAGIC)
7e1b55
+        ldap.update_entry(entry_attrs)
7e1b55
+
7e1b55
+        # fetch updated entry (use search display attribute to show subids)
7e1b55
+        if options.get('all', False):
7e1b55
+            attrs_list = ['*'] + self.obj.search_display_attributes
7e1b55
+        else:
7e1b55
+            attrs_list = set(self.obj.search_display_attributes)
7e1b55
+            attrs_list.update(entry_attrs.keys())
7e1b55
+            if options.get('no_members', False):
7e1b55
+                attrs_list.difference_update(self.obj.attribute_members)
7e1b55
+            attrs_list = list(attrs_list)
7e1b55
+
7e1b55
+        entry = self._exc_wrapper((cn,), options, ldap.get_entry)(
7e1b55
+            dn, attrs_list
7e1b55
+        )
7e1b55
+        entry_attrs = entry_to_dict(entry, **options)
7e1b55
+        entry_attrs['dn'] = dn
7e1b55
+
7e1b55
+        return dict(result=entry_attrs, value=pkey_to_value(cn, options))
7e1b55
+
7e1b55
+
7e1b55
+class baseuser_match_subid(baseuser_find):
7e1b55
+    __doc__ = _("Match users by any subordinate uid in their range")
7e1b55
+
7e1b55
+    _subid_attrs = {
7e1b55
+        "ipasubuidnumber",
7e1b55
+        "ipasubuidcount",
7e1b55
+        "ipasubgidnumber",
7e1b55
+        "ipasubgidcount"
7e1b55
+    }
7e1b55
+
7e1b55
+    def get_options(self):
7e1b55
+        base_options = {p.name for p in self.obj.takes_params}
7e1b55
+        for option in super().get_options():
7e1b55
+            if option.name == "ipasubuidnumber":
7e1b55
+                yield option.clone(
7e1b55
+                    label=_('SubUID match'),
7e1b55
+                    doc=_('Match value for subordinate user ID'),
7e1b55
+                    required=True,
7e1b55
+                )
7e1b55
+            elif option.name not in base_options:
7e1b55
+                # raw, version
7e1b55
+                yield option.clone()
7e1b55
+
7e1b55
+    def pre_callback(
7e1b55
+        self, ldap, filters, attrs_list, base_dn, scope, *args, **options
7e1b55
+    ):
7e1b55
+        # search for candidates in range
7e1b55
+        # Code assumes that no subordinate count is larger than SUBID_COUNT
7e1b55
+        filters = self.obj.get_subid_match_candidate_filter(
7e1b55
+            ldap, subuid=options["ipasubuidnumber"], subgid=None,
7e1b55
+        )
7e1b55
+        # always include subid attributes
7e1b55
+        for missing in self._subid_attrs.difference(attrs_list):
7e1b55
+            attrs_list.append(missing)
7e1b55
+
7e1b55
+        return filters, base_dn, scope
7e1b55
+
7e1b55
+    def post_callback(self, ldap, entries, truncated, *args, **options):
7e1b55
+        # filter out mismatches manually
7e1b55
+        osubuid = options["ipasubuidnumber"]
7e1b55
+        new_entries = []
7e1b55
+        for entry in entries:
7e1b55
+            esubuid = int(entry.single_value["ipasubuidnumber"])
7e1b55
+            esubcount = int(entry.single_value["ipasubuidcount"])
7e1b55
+            minsubuid = esubuid
7e1b55
+            maxsubuid = esubuid + esubcount - 1
7e1b55
+            if minsubuid <= osubuid <= maxsubuid:
7e1b55
+                new_entries.append(entry)
7e1b55
+
7e1b55
+        entries[:] = new_entries
7e1b55
+
7e1b55
+        return truncated
7e1b55
diff --git a/ipaserver/plugins/idrange.py b/ipaserver/plugins/idrange.py
7e1b55
index 32b9c0c2d01b616d76505fc06fa9b6e5e209b234..3e486b8e27cfb12f2e4732fc1ee113f25dfbac5b 100644
7e1b55
--- a/ipaserver/plugins/idrange.py
7e1b55
+++ b/ipaserver/plugins/idrange.py
7e1b55
@@ -205,6 +205,7 @@ class idrange(LDAPObject):
7e1b55
     # The commented range types are planned but not yet supported
7e1b55
     range_types = {
7e1b55
         u'ipa-local': unicode(_('local domain range')),
7e1b55
+        # u'ipa-local-subid': unicode(_('local domain subid range')),
7e1b55
         # u'ipa-ad-winsync': unicode(_('Active Directory winsync range')),
7e1b55
         u'ipa-ad-trust': unicode(_('Active Directory domain range')),
7e1b55
         u'ipa-ad-trust-posix': unicode(_('Active Directory trust range with '
7e1b55
@@ -221,10 +222,14 @@ class idrange(LDAPObject):
7e1b55
         Int('ipabaseid',
7e1b55
             cli_name='base_id',
7e1b55
             label=_("First Posix ID of the range"),
7e1b55
+            minvalue=1,
7e1b55
+            maxvalue=Int.MAX_UINT32
7e1b55
         ),
7e1b55
         Int('ipaidrangesize',
7e1b55
             cli_name='range_size',
7e1b55
             label=_("Number of IDs in the range"),
7e1b55
+            minvalue=1,
7e1b55
+            maxvalue=Int.MAX_UINT32
7e1b55
         ),
7e1b55
         Int('ipabaserid?',
7e1b55
             cli_name='rid_base',
7e1b55
@@ -669,7 +674,10 @@ class idrange_mod(LDAPUpdate):
7e1b55
         except errors.NotFound:
7e1b55
             raise self.obj.handle_not_found(*keys)
7e1b55
 
7e1b55
-        if old_attrs['iparangetype'][0] == 'ipa-local':
7e1b55
+        if (
7e1b55
+            old_attrs['iparangetype'][0] in {'ipa-local', 'ipa-local-subid'}
7e1b55
+            or old_attrs['cn'][0] == f'{self.api.env.realm}_subid_range'
7e1b55
+        ):
7e1b55
             raise errors.ExecutionError(
7e1b55
                 message=_('This command can not be used to change ID '
7e1b55
                           'allocation for local IPA domain. Run '
7e1b55
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
7e1b55
index 70164eb8654d211523c98722a02b77ee13eb0009..199838b199eb4cdabf597bd34d571d05547fd32e 100644
7e1b55
--- a/ipaserver/plugins/internal.py
7e1b55
+++ b/ipaserver/plugins/internal.py
7e1b55
@@ -1547,6 +1547,13 @@ class i18n_messages(Command):
7e1b55
                     "Drive to mount a home directory"
7e1b55
                 ),
7e1b55
             },
7e1b55
+            "subordinate": {
7e1b55
+                "identity": _("Subordinate user and group id"),
7e1b55
+                "subuidnumber": _("Subordinate user id"),
7e1b55
+                "subuidcount": _("Subordinate user id count"),
7e1b55
+                "subgidnumber": _("Subordinate group id"),
7e1b55
+                "subgidcount": _("Subordinate group id count"),
7e1b55
+            },
7e1b55
             "trustconfig": {
7e1b55
                 "options": _("Options"),
7e1b55
             },
7e1b55
@@ -1570,6 +1577,11 @@ class i18n_messages(Command):
7e1b55
                 "add_into_sudo": _(
7e1b55
                     "Add user '${primary_key}' into sudo rules"
7e1b55
                 ),
7e1b55
+                "auto_subid": _("Auto assign subordinate ids"),
7e1b55
+                "auto_subid_confirm": _(
7e1b55
+                    "Are you sure you want to auto-assign a subordinate id "
7e1b55
+                    "to user ${object}?"
7e1b55
+                ),
7e1b55
                 "contact": _("Contact Settings"),
7e1b55
                 "delete_mode": _("Delete mode"),
7e1b55
                 "employee": _("Employee Information"),
7e1b55
diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py
7e1b55
index e4ee572b236c288fd7dcf1d44c5adf1f836f63aa..f89b3ad5d9c994fe1ceb3da560fde7cc5bf5155a 100644
7e1b55
--- a/ipaserver/plugins/user.py
7e1b55
+++ b/ipaserver/plugins/user.py
7e1b55
@@ -50,7 +50,10 @@ from .baseuser import (
7e1b55
     baseuser_add_principal,
7e1b55
     baseuser_remove_principal,
7e1b55
     baseuser_add_certmapdata,
7e1b55
-    baseuser_remove_certmapdata)
7e1b55
+    baseuser_remove_certmapdata,
7e1b55
+    baseuser_auto_subid,
7e1b55
+    baseuser_match_subid,
7e1b55
+)
7e1b55
 from .idviews import remove_ipaobject_overrides
7e1b55
 from ipalib.plugable import Registry
7e1b55
 from .baseldap import (
7e1b55
@@ -202,6 +205,8 @@ class user(baseuser):
7e1b55
             'ipapermright': {'read', 'search', 'compare'},
7e1b55
             'ipapermdefaultattr': {
7e1b55
                 'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass',
7e1b55
+                'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber',
7e1b55
+                'ipasubgidcount',
7e1b55
             },
7e1b55
             'fixup_function': fix_addressbook_permission_bindrule,
7e1b55
         },
7e1b55
@@ -1306,3 +1311,13 @@ class user_add_principal(baseuser_add_principal):
7e1b55
 class user_remove_principal(baseuser_remove_principal):
7e1b55
     __doc__ = _('Remove principal alias from the user entry')
7e1b55
     msg_summary = _('Removed aliases from user "%(value)s"')
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class user_auto_subid(baseuser_auto_subid):
7e1b55
+    __doc__ = baseuser_auto_subid.__doc__
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class user_match_subid(baseuser_match_subid):
7e1b55
+    __doc__ = baseuser_match_subid.__doc__
7e1b55
diff --git a/ipatests/prci_definitions/gating.yaml b/ipatests/prci_definitions/gating.yaml
7e1b55
index a66b56ad8f62a458e9cc240440e7d222c32c599f..6ddd155c9967fa248581a59c68dfe547a34be623 100644
7e1b55
--- a/ipatests/prci_definitions/gating.yaml
7e1b55
+++ b/ipatests/prci_definitions/gating.yaml
7e1b55
@@ -298,3 +298,15 @@ jobs:
7e1b55
         template: *ci-ipa-4-9-latest
7e1b55
         timeout: 3600
7e1b55
         topology: *master_1repl
7e1b55
+
7e1b55
+  fedora-latest-ipa-4-9/test_subids:
7e1b55
+    requires: [fedora-latest-ipa-4-9/build]
7e1b55
+    priority: 100
7e1b55
+    job:
7e1b55
+      class: RunPytest
7e1b55
+      args:
7e1b55
+        build_url: '{fedora-latest-ipa-4-9/build_url}'
7e1b55
+        test_suite: test_integration/test_subids.py
7e1b55
+        template: *ci-ipa-4-9-latest
7e1b55
+        timeout: 3600
7e1b55
+        topology: *master_1repl
7e1b55
diff --git a/ipatests/test_integration/test_subids.py b/ipatests/test_integration/test_subids.py
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..b462f22ac067f3e1e97ef3f6d63d4e14e4ae79af
7e1b55
--- /dev/null
7e1b55
+++ b/ipatests/test_integration/test_subids.py
7e1b55
@@ -0,0 +1,201 @@
7e1b55
+#
7e1b55
+# Copyright (C) 2021  FreeIPA Contributors see COPYING for license
7e1b55
+#
7e1b55
+
7e1b55
+"""Tests for subordinate ids
7e1b55
+"""
7e1b55
+import os
7e1b55
+
7e1b55
+from ipalib.constants import SUBID_COUNT, SUBID_RANGE_START, SUBID_RANGE_MAX
7e1b55
+from ipaplatform.paths import paths
7e1b55
+from ipatests.pytest_ipa.integration import tasks
7e1b55
+from ipatests.test_integration.base import IntegrationTest
7e1b55
+
7e1b55
+
7e1b55
+class TestSubordinateId(IntegrationTest):
7e1b55
+    num_replicas = 0
7e1b55
+    topology = "star"
7e1b55
+
7e1b55
+    def _parse_result(self, result):
7e1b55
+        info = {}
7e1b55
+        for line in result.stdout_text.split("\n"):
7e1b55
+            line = line.strip()
7e1b55
+            if line:
7e1b55
+                if ":" not in line:
7e1b55
+                    continue
7e1b55
+                k, v = line.split(":", 1)
7e1b55
+                k = k.strip()
7e1b55
+                v = v.strip()
7e1b55
+                try:
7e1b55
+                    v = int(v, 10)
7e1b55
+                except ValueError:
7e1b55
+                    if v == "FALSE":
7e1b55
+                        v = False
7e1b55
+                    elif v == "TRUE":
7e1b55
+                        v = True
7e1b55
+                info.setdefault(k.lower(), []).append(v)
7e1b55
+
7e1b55
+        for k, v in info.items():
7e1b55
+            if len(v) == 1:
7e1b55
+                info[k] = v[0]
7e1b55
+            else:
7e1b55
+                info[k] = set(v)
7e1b55
+        return info
7e1b55
+
7e1b55
+    def get_user(self, uid):
7e1b55
+        cmd = ["ipa", "user-show", "--all", "--raw", uid]
7e1b55
+        result = self.master.run_command(cmd)
7e1b55
+        return self._parse_result(result)
7e1b55
+
7e1b55
+    def user_auto_subid(self, uid, **kwargs):
7e1b55
+        cmd = ["ipa", "user-auto-subid", uid]
7e1b55
+        return self.master.run_command(cmd, **kwargs)
7e1b55
+
7e1b55
+    def test_auto_subid(self):
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+        uid = "testuser_auto1"
7e1b55
+        tasks.user_add(self.master, uid)
7e1b55
+        info = self.get_user(uid)
7e1b55
+        assert "ipasubuidcount" not in info
7e1b55
+
7e1b55
+        self.user_auto_subid(uid)
7e1b55
+        info = self.get_user(uid)
7e1b55
+        assert "ipasubuidcount" in info
7e1b55
+
7e1b55
+        subuid = info["ipasubuidnumber"]
7e1b55
+        result = self.master.run_command(
7e1b55
+            ["ipa", "user-match-subid", f"--subuid={subuid}", "--raw"]
7e1b55
+        )
7e1b55
+        match = self._parse_result(result)
7e1b55
+        assert match["uid"] == uid
7e1b55
+        assert match["ipasubuidnumber"] == info["ipasubuidnumber"]
7e1b55
+        assert match["ipasubuidnumber"] >= SUBID_RANGE_START
7e1b55
+        assert match["ipasubuidnumber"] <= SUBID_RANGE_MAX
7e1b55
+        assert match["ipasubuidcount"] == SUBID_COUNT
7e1b55
+        assert match["ipasubgidnumber"] == info["ipasubgidnumber"]
7e1b55
+        assert match["ipasubgidnumber"] == match["ipasubuidnumber"]
7e1b55
+        assert match["ipasubgidcount"] == SUBID_COUNT
7e1b55
+
7e1b55
+    def test_ipa_subid_script(self):
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+
7e1b55
+        tool = os.path.join(paths.LIBEXEC_IPA_DIR, "ipa-subids")
7e1b55
+        users = []
7e1b55
+        for i in range(1, 11):
7e1b55
+            uid = f"testuser_script{i}"
7e1b55
+            users.append(uid)
7e1b55
+            tasks.user_add(self.master, uid)
7e1b55
+            info = self.get_user(uid)
7e1b55
+            assert "ipasubuidcount" not in info
7e1b55
+
7e1b55
+        cmd = [tool, "--verbose", "--group", "ipausers"]
7e1b55
+        self.master.run_command(cmd)
7e1b55
+
7e1b55
+        for uid in users:
7e1b55
+            info = self.get_user(uid)
7e1b55
+            assert info["ipasubuidnumber"] >= SUBID_RANGE_START
7e1b55
+            assert info["ipasubuidnumber"] <= SUBID_RANGE_MAX
7e1b55
+            assert info["ipasubuidnumber"] == info["ipasubgidnumber"]
7e1b55
+            assert info["ipasubuidcount"] == SUBID_COUNT
7e1b55
+            assert info["ipasubuidcount"] == info["ipasubgidcount"]
7e1b55
+
7e1b55
+    def test_subid_selfservice(self):
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+
7e1b55
+        uid = "testuser_selfservice1"
7e1b55
+        password = "Secret123"
7e1b55
+        role = "Subordinate ID Selfservice User"
7e1b55
+
7e1b55
+        tasks.user_add(self.master, uid, password=password)
7e1b55
+        tasks.kinit_user(
7e1b55
+            self.master, uid, f"{password}\n{password}\n{password}\n"
7e1b55
+        )
7e1b55
+        info = self.get_user(uid)
7e1b55
+        assert "ipasubuidcount" not in info
7e1b55
+        result = self.user_auto_subid(uid, raiseonerr=False)
7e1b55
+        assert result.returncode > 0
7e1b55
+
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+        self.master.run_command(
7e1b55
+            ["ipa", "role-add-member", role, "--groups=ipausers"]
7e1b55
+        )
7e1b55
+
7e1b55
+        try:
7e1b55
+            tasks.kinit_user(self.master, uid, password)
7e1b55
+            self.user_auto_subid(uid)
7e1b55
+            info = self.get_user(uid)
7e1b55
+            assert "ipasubuidcount" in info
7e1b55
+        finally:
7e1b55
+            tasks.kinit_admin(self.master)
7e1b55
+            self.master.run_command(
7e1b55
+                ["ipa", "role-remove-member", role, "--groups=ipausers"]
7e1b55
+            )
7e1b55
+
7e1b55
+    def test_subid_useradmin(self):
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+
7e1b55
+        uid_useradmin = "testuser_usermgr_mgr1"
7e1b55
+        role = "User Administrator"
7e1b55
+        uid = "testuser_usermgr_user1"
7e1b55
+        password = "Secret123"
7e1b55
+
7e1b55
+        # create user administrator
7e1b55
+        tasks.user_add(self.master, uid_useradmin, password=password)
7e1b55
+        # add user to user admin group
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+        self.master.run_command(
7e1b55
+            ["ipa", "role-add-member", role, f"--users={uid_useradmin}"],
7e1b55
+        )
7e1b55
+        # kinit as user admin
7e1b55
+        tasks.kinit_user(
7e1b55
+            self.master,
7e1b55
+            uid_useradmin,
7e1b55
+            f"{password}\n{password}\n{password}\n",
7e1b55
+        )
7e1b55
+        # create new user as user admin
7e1b55
+        tasks.user_add(self.master, uid)
7e1b55
+        # assign new subid to user (with useradmin credentials)
7e1b55
+        self.user_auto_subid(uid)
7e1b55
+
7e1b55
+    def test_subordinate_default_objclass(self):
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+
7e1b55
+        result = self.master.run_command(
7e1b55
+            ["ipa", "config-show", "--raw", "--all"]
7e1b55
+        )
7e1b55
+        info = self._parse_result(result)
7e1b55
+        usercls = info["ipauserobjectclasses"]
7e1b55
+        assert "ipasubordinateid" not in usercls
7e1b55
+
7e1b55
+        cmd = [
7e1b55
+            "ipa",
7e1b55
+            "config-mod",
7e1b55
+            "--addattr",
7e1b55
+            "ipaUserObjectClasses=ipasubordinateid",
7e1b55
+        ]
7e1b55
+        self.master.run_command(cmd)
7e1b55
+
7e1b55
+        uid = "testuser_usercls1"
7e1b55
+        tasks.user_add(self.master, uid)
7e1b55
+        info = self.get_user(uid)
7e1b55
+        assert "ipasubuidcount" in info
7e1b55
+
7e1b55
+    def test_idrange_subid(self):
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+
7e1b55
+        range_name = f"{self.master.domain.realm}_subid_range"
7e1b55
+
7e1b55
+        result = self.master.run_command(
7e1b55
+            ["ipa", "idrange-show", range_name, "--raw"]
7e1b55
+        )
7e1b55
+        info = self._parse_result(result)
7e1b55
+
7e1b55
+        # see https://github.com/SSSD/sssd/issues/5571
7e1b55
+        assert info["iparangetype"] == "ipa-ad-trust"
7e1b55
+        assert info["ipabaseid"] == SUBID_RANGE_START
7e1b55
+        assert info["ipaidrangesize"] == SUBID_RANGE_MAX - SUBID_RANGE_START
7e1b55
+        assert info["ipabaserid"] < SUBID_RANGE_START
7e1b55
+        assert "ipasecondarybaserid" not in info
7e1b55
+        assert info["ipanttrusteddomainsid"].startswith(
7e1b55
+            "S-1-5-21-738065-838566-"
7e1b55
+        )
7e1b55
diff --git a/ipatests/test_xmlrpc/test_range_plugin.py b/ipatests/test_xmlrpc/test_range_plugin.py
7e1b55
index c756bb7941d6c2acae89d44d6c89abc6b80ef5f7..ef683f84e97cbba61972f580e84e3587fda8c63a 100644
7e1b55
--- a/ipatests/test_xmlrpc/test_range_plugin.py
7e1b55
+++ b/ipatests/test_xmlrpc/test_range_plugin.py
7e1b55
@@ -24,6 +24,7 @@ Test the `ipaserver/plugins/idrange.py` module, and XML-RPC in general.
7e1b55
 import six
7e1b55
 
7e1b55
 from ipalib import api, errors, messages
7e1b55
+from ipalib import constants
7e1b55
 from ipaplatform import services
7e1b55
 from ipatests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid
7e1b55
 from ipatests.test_xmlrpc import objectclasses
7e1b55
@@ -46,6 +47,12 @@ rid_shift = 0
7e1b55
 for idrange in api.Command['idrange_find']()['result']:
7e1b55
     size = int(idrange['ipaidrangesize'][0])
7e1b55
     base_id = int(idrange['ipabaseid'][0])
7e1b55
+    rtype = idrange['iparangetype'][0]
7e1b55
+
7e1b55
+    if rtype == 'ipa-local-subid' or base_id == constants.SUBID_RANGE_START:
7e1b55
+        # ignore subordinate id range. It would push values beyond uint32_t.
7e1b55
+        # There is plenty of space below SUBUID_RANGE_START.
7e1b55
+        continue
7e1b55
 
7e1b55
     id_end = base_id + size
7e1b55
     rid_end = 0
7e1b55
-- 
7e1b55
2.26.3
7e1b55