7e1b55
From f6fd0abeaa64d927f2d993235e97ac3009e64f2c Mon Sep 17 00:00:00 2001
7e1b55
From: Christian Heimes <cheimes@redhat.com>
7e1b55
Date: Wed, 14 Apr 2021 15:21:18 +0200
7e1b55
Subject: [PATCH] Redesign subid feature
7e1b55
7e1b55
Subordinate ids are now handled by a new plugin class and stored in
7e1b55
separate entries in the cn=subids,cn=accounts subtree.
7e1b55
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                                       |  12 +-
7e1b55
 API.txt                                       | 153 +++--
7e1b55
 doc/designs/subordinate-ids.md                | 358 ++++++++---
7e1b55
 install/share/60basev4.ldif                   |  25 +-
7e1b55
 install/share/60ipaconfig.ldif                |   5 +-
7e1b55
 install/share/dna.ldif                        |   8 +-
7e1b55
 install/share/memberof-conf.ldif              |   4 +-
7e1b55
 install/ui/src/freeipa/app.js                 |   1 +
7e1b55
 .../ui/src/freeipa/navigation/menu_spec.js    |   3 +-
7e1b55
 install/ui/src/freeipa/serverconfig.js        |   4 +
7e1b55
 install/ui/src/freeipa/subid.js               |  92 +++
7e1b55
 install/ui/src/freeipa/user.js                | 108 ++--
7e1b55
 install/updates/10-uniqueness.update          |  19 +
7e1b55
 install/updates/20-indices.update             |  12 +
7e1b55
 install/updates/25-referint.update            |   1 +
7e1b55
 install/updates/73-subid.update               |  28 +-
7e1b55
 ipalib/constants.py                           |   6 +-
7e1b55
 ipaserver/install/ipa_subids.py               |  18 +-
7e1b55
 .../plugins/update_dna_shared_config.py       |   2 +-
7e1b55
 ipaserver/plugins/baseldap.py                 |  10 +-
7e1b55
 ipaserver/plugins/baseuser.py                 | 272 +-------
7e1b55
 ipaserver/plugins/config.py                   |   8 +-
7e1b55
 ipaserver/plugins/internal.py                 |   2 +-
7e1b55
 ipaserver/plugins/subid.py                    | 608 ++++++++++++++++++
7e1b55
 ipaserver/plugins/user.py                     |  39 +-
7e1b55
 ipatests/test_integration/test_subids.py      | 182 +++---
7e1b55
 26 files changed, 1389 insertions(+), 591 deletions(-)
7e1b55
 create mode 100644 install/ui/src/freeipa/subid.js
7e1b55
 create mode 100644 ipaserver/plugins/subid.py
7e1b55
7e1b55
diff --git a/ACI.txt b/ACI.txt
7e1b55
index fce02a333b212de9b61f920515eed3e356b1391b..e985461cd1c10cc98d1080daa81cfd90e2433dbb 100644
7e1b55
--- a/ACI.txt
7e1b55
+++ b/ACI.txt
7e1b55
@@ -61,7 +61,7 @@ aci: (targetattr = "cn || description || ipacertprofilestoreissued")(targetfilte
7e1b55
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
7e1b55
 aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacertprofilestoreissued || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Read Certificate Profiles";allow (compare,read,search) userdn = "ldap:///all";)
7e1b55
 dn: cn=ipaconfig,cn=etc,dc=ipa,dc=example
7e1b55
-aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipadomainresolutionorder || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxhostnamelength || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";)
7e1b55
+aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipadomainresolutionorder || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxhostnamelength || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserdefaultsubordinateid || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";)
7e1b55
 dn: cn=costemplates,cn=accounts,dc=ipa,dc=example
7e1b55
 aci: (targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Add Group Password Policy costemplate";allow (add) groupdn = "ldap:///cn=System: Add Group Password Policy costemplate,cn=permissions,cn=pbac,dc=ipa,dc=example";)
7e1b55
 dn: cn=costemplates,cn=accounts,dc=ipa,dc=example
7e1b55
@@ -318,6 +318,14 @@ dn: cn=deleted users,cn=accounts,cn=provisioning,dc=ipa,dc=example
7e1b55
 aci: (targetattr = "krblastpwdchange || krbpasswordexpiration || krbprincipalkey || userpassword")(target = "ldap:///uid=*,cn=deleted users,cn=accounts,cn=provisioning,dc=ipa,dc=example")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Reset Preserved User password";allow (read,search,write) groupdn = "ldap:///cn=System: Reset Preserved User password,cn=permissions,cn=pbac,dc=ipa,dc=example";)
7e1b55
 dn: dc=ipa,dc=example
7e1b55
 aci: (target_to = "ldap:///cn=users,cn=accounts,dc=ipa,dc=example")(target_from = "ldap:///cn=deleted users,cn=accounts,cn=provisioning,dc=ipa,dc=example")(targetfilter = "(objectclass=nsContainer)")(version 3.0;acl "permission:System: Undelete User";allow (moddn) groupdn = "ldap:///cn=System: Undelete User,cn=permissions,cn=pbac,dc=ipa,dc=example";)
7e1b55
+dn: cn=subids,cn=accounts,dc=ipa,dc=example
7e1b55
+aci: (targetattr = "description || ipaowner")(targetfilter = "(objectclass=ipasubordinateidentry)")(version 3.0;acl "permission:System: Manage Subordinate Ids";allow (write) groupdn = "ldap:///cn=System: Manage Subordinate Ids,cn=permissions,cn=pbac,dc=ipa,dc=example";)
7e1b55
+dn: cn=subids,cn=accounts,dc=ipa,dc=example
7e1b55
+aci: (targetattr = "createtimestamp || description || entryusn || ipaowner || ipasubgidcount || ipasubgidnumber || ipasubuidcount || ipasubuidnumber || ipauniqueid || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipasubordinateidentry)")(version 3.0;acl "permission:System: Read Subordinate Id Attributes";allow (compare,read,search) userdn = "ldap:///all";)
7e1b55
+dn: cn=subids,cn=accounts,dc=ipa,dc=example
7e1b55
+aci: (targetattr = "numsubordinates")(target = "ldap:///cn=subids,cn=accounts,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read Subordinate Id Count";allow (compare,read,search) userdn = "ldap:///all";)
7e1b55
+dn: cn=subids,cn=accounts,dc=ipa,dc=example
7e1b55
+aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(version 3.0;acl "permission:System: Remove Subordinate Ids";allow (delete) groupdn = "ldap:///cn=System: Remove Subordinate Ids,cn=permissions,cn=pbac,dc=ipa,dc=example";)
7e1b55
 dn: cn=sudocmds,cn=sudo,dc=ipa,dc=example
7e1b55
 aci: (targetfilter = "(objectclass=ipasudocmd)")(version 3.0;acl "permission:System: Add Sudo Command";allow (add) groupdn = "ldap:///cn=System: Add Sudo Command,cn=permissions,cn=pbac,dc=ipa,dc=example";)
7e1b55
 dn: cn=sudocmds,cn=sudo,dc=ipa,dc=example
7e1b55
@@ -375,7 +383,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 || 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
+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
 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 262b4d6a72c7d7032a7027116f7a4f65aa620615..6c80028bfe8e9b739637fa11e015441efbf984b5 100644
7e1b55
--- a/API.txt
7e1b55
+++ b/API.txt
7e1b55
@@ -1076,7 +1076,7 @@ args: 0,1,1
7e1b55
 option: Str('version?')
7e1b55
 output: Output('result')
7e1b55
 command: config_mod/1
7e1b55
-args: 0,28,3
7e1b55
+args: 0,29,3
7e1b55
 option: Str('addattr*', cli_name='addattr')
7e1b55
 option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
 option: Str('ca_renewal_master_server?', autofill=False)
7e1b55
@@ -1099,6 +1099,7 @@ option: Int('ipasearchtimelimit?', autofill=False, cli_name='searchtimelimit')
7e1b55
 option: Str('ipaselinuxusermapdefault?', autofill=False)
7e1b55
 option: Str('ipaselinuxusermaporder?', autofill=False)
7e1b55
 option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened', u'disabled'])
7e1b55
+option: Bool('ipauserdefaultsubordinateid?', autofill=False, cli_name='user_default_subid')
7e1b55
 option: Str('ipauserobjectclasses*', autofill=False, cli_name='userobjectclasses')
7e1b55
 option: IA5Str('ipausersearchfields?', autofill=False, cli_name='usersearch')
7e1b55
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
7e1b55
@@ -4974,7 +4975,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,46,3
7e1b55
+args: 1,45,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,7 +4993,6 @@ 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
@@ -5099,14 +5099,13 @@ option: Str('in_group*', cli_name='in_groups')
7e1b55
 option: Str('in_hbacrule*', cli_name='in_hbacrules')
7e1b55
 option: Str('in_netgroup*', cli_name='in_netgroups')
7e1b55
 option: Str('in_role*', cli_name='in_roles')
7e1b55
+option: Str('in_subid*', cli_name='in_subids')
7e1b55
 option: Str('in_sudorule*', cli_name='in_sudorules')
7e1b55
 option: Str('initials?', autofill=False)
7e1b55
 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
@@ -5123,6 +5122,7 @@ option: Str('not_in_group*', cli_name='not_in_groups')
7e1b55
 option: Str('not_in_hbacrule*', cli_name='not_in_hbacrules')
7e1b55
 option: Str('not_in_netgroup*', cli_name='not_in_netgroups')
7e1b55
 option: Str('not_in_role*', cli_name='not_in_roles')
7e1b55
+option: Str('not_in_subid*', cli_name='not_in_subids')
7e1b55
 option: Str('not_in_sudorule*', cli_name='not_in_sudorules')
7e1b55
 option: Str('ou?', autofill=False, cli_name='orgunit')
7e1b55
 option: Str('pager*', autofill=False)
7e1b55
@@ -5148,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,52,3
7e1b55
+args: 1,51,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
@@ -5170,7 +5170,6 @@ 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
@@ -5263,6 +5262,100 @@ option: Str('version?')
7e1b55
 output: Entry('result')
7e1b55
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
 output: PrimaryKey('value')
7e1b55
+command: subid_add/1
7e1b55
+args: 1,8,3
7e1b55
+arg: Str('ipauniqueid?', cli_name='id')
7e1b55
+option: Str('addattr*', cli_name='addattr')
7e1b55
+option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
+option: Str('description?', cli_name='desc')
7e1b55
+option: Str('ipaowner', cli_name='owner')
7e1b55
+option: Int('ipasubuidnumber?', cli_name='subuid')
7e1b55
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
7e1b55
+option: Str('setattr*', cli_name='setattr')
7e1b55
+option: Str('version?')
7e1b55
+output: Entry('result')
7e1b55
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
+output: PrimaryKey('value')
7e1b55
+command: subid_del/1
7e1b55
+args: 1,2,3
7e1b55
+arg: Str('ipauniqueid+', cli_name='id')
7e1b55
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
7e1b55
+option: Str('version?')
7e1b55
+output: Output('result', type=[<type 'dict'>])
7e1b55
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
+output: ListOfPrimaryKeys('value')
7e1b55
+command: subid_find/1
7e1b55
+args: 1,11,4
7e1b55
+arg: Str('criteria?')
7e1b55
+option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
+option: Str('description?', autofill=False, cli_name='desc')
7e1b55
+option: Str('ipaowner?', autofill=False, cli_name='owner')
7e1b55
+option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid')
7e1b55
+option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
7e1b55
+option: Str('ipauniqueid?', autofill=False, cli_name='id')
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: subid_generate/1
7e1b55
+args: 0,4,3
7e1b55
+option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
+option: Str('ipaowner?', cli_name='owner')
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: subid_match/1
7e1b55
+args: 1,7,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('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: subid_mod/1
7e1b55
+args: 1,8,3
7e1b55
+arg: Str('ipauniqueid', cli_name='id')
7e1b55
+option: Str('addattr*', cli_name='addattr')
7e1b55
+option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
+option: Str('delattr*', cli_name='delattr')
7e1b55
+option: Str('description?', autofill=False, cli_name='desc')
7e1b55
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
7e1b55
+option: Flag('rights', autofill=True, default=False)
7e1b55
+option: Str('setattr*', cli_name='setattr')
7e1b55
+option: Str('version?')
7e1b55
+output: Entry('result')
7e1b55
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
7e1b55
+output: PrimaryKey('value')
7e1b55
+command: subid_show/1
7e1b55
+args: 1,4,3
7e1b55
+arg: Str('ipauniqueid', cli_name='id')
7e1b55
+option: Flag('all', autofill=True, cli_name='all', default=False)
7e1b55
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
7e1b55
+option: Flag('rights', autofill=True, 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: subid_stats/1
7e1b55
+args: 0,3,2
7e1b55
+option: Flag('all', autofill=True, cli_name='all', 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
 command: sudocmd_add/1
7e1b55
 args: 1,7,3
7e1b55
 arg: Str('sudocmd', cli_name='command')
7e1b55
@@ -6062,7 +6155,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,47,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
@@ -6079,7 +6172,6 @@ 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
@@ -6161,16 +6253,6 @@ 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
@@ -6213,14 +6295,13 @@ option: Str('in_group*', cli_name='in_groups')
7e1b55
 option: Str('in_hbacrule*', cli_name='in_hbacrules')
7e1b55
 option: Str('in_netgroup*', cli_name='in_netgroups')
7e1b55
 option: Str('in_role*', cli_name='in_roles')
7e1b55
+option: Str('in_subid*', cli_name='in_subids')
7e1b55
 option: Str('in_sudorule*', cli_name='in_sudorules')
7e1b55
 option: Str('initials?', autofill=False)
7e1b55
 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
@@ -6237,6 +6318,7 @@ option: Str('not_in_group*', cli_name='not_in_groups')
7e1b55
 option: Str('not_in_hbacrule*', cli_name='not_in_hbacrules')
7e1b55
 option: Str('not_in_netgroup*', cli_name='not_in_netgroups')
7e1b55
 option: Str('not_in_role*', cli_name='not_in_roles')
7e1b55
+option: Str('not_in_subid*', cli_name='not_in_subids')
7e1b55
 option: Str('not_in_sudorule*', cli_name='not_in_sudorules')
7e1b55
 option: Bool('nsaccountlock?', autofill=False, cli_name='disabled', default=False)
7e1b55
 option: Str('ou?', autofill=False, cli_name='orgunit')
7e1b55
@@ -6264,23 +6346,8 @@ 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,53,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
@@ -6302,7 +6369,6 @@ 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
@@ -7138,6 +7204,15 @@ default: stageuser_remove_certmapdata/1
7e1b55
 default: stageuser_remove_manager/1
7e1b55
 default: stageuser_remove_principal/1
7e1b55
 default: stageuser_show/1
7e1b55
+default: subid/1
7e1b55
+default: subid_add/1
7e1b55
+default: subid_del/1
7e1b55
+default: subid_find/1
7e1b55
+default: subid_generate/1
7e1b55
+default: subid_match/1
7e1b55
+default: subid_mod/1
7e1b55
+default: subid_show/1
7e1b55
+default: subid_stats/1
7e1b55
 default: sudocmd/1
7e1b55
 default: sudocmd_add/1
7e1b55
 default: sudocmd_del/1
7e1b55
@@ -7216,12 +7291,10 @@ 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/doc/designs/subordinate-ids.md b/doc/designs/subordinate-ids.md
7e1b55
index 1b578667a8cfdda223af38a14d142c72a5d5c073..b3be3bcfa275e836e777f807d5210a4db6be0f79 100644
7e1b55
--- a/doc/designs/subordinate-ids.md
7e1b55
+++ b/doc/designs/subordinate-ids.md
7e1b55
@@ -1,5 +1,9 @@
7e1b55
 # Central management of subordinate user and group ids
7e1b55
 
7e1b55
+## OUTDATED
7e1b55
+
7e1b55
+**The design document does not reflect new implementation yet!**
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
@@ -74,8 +78,10 @@ basic use cases. Some restrictions may be lifted in the future.
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
+* IPA does not support multiple subordinate id ranges, yet. Contrary to
7e1b55
+  ``/etc/subuid``, users are limited to one set of subordinate ids. The
7e1b55
+  limitation is implemented with a unique index on owner reference and
7e1b55
+  can be lifted in the future.
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
@@ -118,20 +124,21 @@ to servers. The DNA plug-in guarantees uniqueness across servers.
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
+``ipaSubordinateId`` with five required attributes ``ipaOwner``,
7e1b55
+``ipaSubUidNumber``, ``ipaSubUidCount``, ``ipaSubGidNumber``, and
7e1b55
+``ipaSubGidCount``. The attributes with ``number`` suffix store the
7e1b55
+start value of the interval. The ``count`` attributes contain the
7e1b55
+size of the interval including the start value. The maximum subid is
7e1b55
+``ipaSubUidNumber + ipaSubUidCount - 1``. The ``ipaOwner`` attribute
7e1b55
+is a reference to the owning user.
7e1b55
+
7e1b55
+All count and number attributes are single-value ``INTEGER`` type with
7e1b55
+standard 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
+  2.16.840.1.113730.3.8.23.7
7e1b55
   NAME 'ipaSubUidNumber'
7e1b55
   DESC 'Numerical subordinate user ID (range start value)'
7e1b55
   EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
@@ -139,7 +146,7 @@ attributeTypes: (
7e1b55
   X-ORIGIN 'IPA v4.9'
7e1b55
 )
7e1b55
 attributeTypes: (
7e1b55
-  2.16.840.1.113730.3.8.23.7
7e1b55
+  2.16.840.1.113730.3.8.23.8
7e1b55
   NAME 'ipaSubUidCount'
7e1b55
   DESC 'Subordinate user ID count (range size)'
7e1b55
   EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
@@ -147,7 +154,7 @@ attributeTypes: (
7e1b55
   X-ORIGIN 'IPA v4.9'
7e1b55
 )
7e1b55
 attributeTypes: (
7e1b55
-  2.16.840.1.113730.3.8.23.9
7e1b55
+  2.16.840.1.113730.3.8.23.10
7e1b55
   NAME 'ipaSubGidNumber'
7e1b55
   DESC 'Numerical subordinate group ID (range start value)'
7e1b55
   EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
@@ -155,7 +162,7 @@ attributeTypes: (
7e1b55
   X-ORIGIN 'IPA v4.9'
7e1b55
 )
7e1b55
 attributeTypes: (
7e1b55
-  2.16.840.1.113730.3.8.23.10
7e1b55
+  2.16.840.1.113730.3.8.23.11
7e1b55
   NAME 'ipaSubGidCount'
7e1b55
   DESC 'Subordinate group ID count (range size)'
7e1b55
   EQUALITY integerMatch ORDERING integerOrderingMatch
7e1b55
@@ -164,51 +171,96 @@ attributeTypes: (
7e1b55
 )
7e1b55
 ```
7e1b55
 
7e1b55
-The ``ipaSubordinateId`` object class is an auxiliar subclass of
7e1b55
+The ``ipaOwner`` attribute is a single-value DN attribute that refers
7e1b55
+to user entry that owns the subordinate ID entry. The proposal does not
7e1b55
+reuse any of the existing attributes like ``owner`` or ``member``,
7e1b55
+because they are all multi-valued.
7e1b55
+
7e1b55
+```
7e1b55
+attributeTypes: (
7e1b55
+  2.16.840.1.113730.3.8.23.13
7e1b55
+  NAME 'ipaOwner'
7e1b55
+  DESC 'Owner of an entry'
7e1b55
+  SUP distinguishedName
7e1b55
+  EQUALITY distinguishedNameMatch
7e1b55
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
7e1b55
+  SINGLE-VALUE
7e1b55
+  X-ORIGIN 'IPA v4.9')
7e1b55
+```
7e1b55
+
7e1b55
+The ``ipaSubordinateId`` object class is an auxiliary 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
+``ipaOwner`` attribute``
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
+  SUP top
7e1b55
+  AUXILIARY
7e1b55
+  MUST ( ipaOwner $ 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
+future use. IPA always assumes the presence of ``ipaSubordinateId``.
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
+  SUP top
7e1b55
+  AUXILIARY
7e1b55
+  MUST ( ipaOwner $ 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
+  SUP top
7e1b55
+  AUXILIARY
7e1b55
+  MUST ( ipaOwner $ ipaSubGidNumber $ ipaSubGidCount )
7e1b55
   X-ORIGIN 'IPA v4.9'
7e1b55
 )
7e1b55
 ```
7e1b55
 
7e1b55
-### Index
7e1b55
+Subordinate id entries have the structural object class
7e1b55
+``ipaSubordinateIdEntry`` and one or more of the auxiliary object
7e1b55
+classes ``ipaSubordinateId``, ``ipaSubordinateGid``, or
7e1b55
+``ipaSubordinateUid``. ``ipaUniqueId`` is used as a primary key (RDN).
7e1b55
+
7e1b55
+```raw
7e1b55
+objectClasses: (
7e1b55
+  2.16.840.1.113730.3.8.24.5
7e1b55
+  NAME 'ipaSubordinateIdEntry'
7e1b55
+  DESC 'Subordinate uid and gid entry'
7e1b55
+  SUP top
7e1b55
+  STRUCTURAL
7e1b55
+  MUST ( ipaUniqueId ) MAY ( description ) X-ORIGIN 'IPA v4.9'
7e1b55
+)
7e1b55
+```
7e1b55
+
7e1b55
+### cn=subids,cn=accounts,$SUFFIX
7e1b55
+
7e1b55
+Subordiante ids and ACIs are stored in the new subtree
7e1b55
+``cn=subids,cn=accounts,$SUFFIX``.
7e1b55
+
7e1b55
+### Index, integrity, memberOf
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
+The attribute ``ipaOwner`` is indexed for ``pres`` and ``eq``. This DN
7e1b55
+attribute is also checked for referential integrity and uniqueness
7e1b55
+within the ``cn=subids,cn=accounts,$SUFFIX`` subtree. The memberOf
7e1b55
+plugin creates back-references for ``ipaOwner`` references.
7e1b55
+
7e1b55
+
7e1b55
 ### Distributed numeric assignment (DNA) plug-in extension
7e1b55
 
7e1b55
 Subordinate id auto-assignment requires an extension of 389-DS'
7e1b55
@@ -221,6 +273,85 @@ 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
+## IPA plugins and commands
7e1b55
+
7e1b55
+The config plugin has a new option to enable or disable generation of
7e1b55
+subordinate id entries for new users:
7e1b55
+
7e1b55
+```raw
7e1b55
+$ ipa config-mod --user-default-subid=true
7e1b55
+```
7e1b55
+
7e1b55
+Subordinate ids are managed by a new plugin class. The ``subid-add``
7e1b55
+and ``subid-del`` commands are hidden from command line. New subordinate
7e1b55
+ids are generated and auto-assigned with ``subid-generate``.
7e1b55
+
7e1b55
+```raw
7e1b55
+$ ipa help subid
7e1b55
+Topic commands:
7e1b55
+  subid-find      Search for subordinate id.
7e1b55
+  subid-generate  Generate and auto-assign subuid and subgid range to user entry
7e1b55
+  subid-match     Match users by any subordinate uid in their range
7e1b55
+  subid-mod       Modify a subordinate id.
7e1b55
+  subid-show      Display information about a subordinate id.
7e1b55
+  subid-stats     Subordinate id statistics
7e1b55
+```
7e1b55
+
7e1b55
+```raw
7e1b55
+$ ipa subid-generate --owner testuser9
7e1b55
+-----------------------------------------------------------
7e1b55
+Added subordinate id "aa28f132-457c-488b-82e1-d123727e4f81"
7e1b55
+-----------------------------------------------------------
7e1b55
+  Unique ID: aa28f132-457c-488b-82e1-d123727e4f81
7e1b55
+  Description: auto-assigned subid
7e1b55
+  Owner: testuser9
7e1b55
+  SubUID range start: 3922132992
7e1b55
+  SubUID range size: 65536
7e1b55
+  SubGID range start: 3922132992
7e1b55
+  SubGID range size: 65536
7e1b55
+```
7e1b55
+
7e1b55
+
7e1b55
+```raw
7e1b55
+$ ipa subid-find --owner testuser9
7e1b55
+------------------------
7e1b55
+1 subordinate id matched
7e1b55
+------------------------
7e1b55
+  Unique ID: aa28f132-457c-488b-82e1-d123727e4f81
7e1b55
+  Owner: testuser9
7e1b55
+  SubUID range start: 3922132992
7e1b55
+  SubUID range size: 65536
7e1b55
+  SubGID range start: 3922132992
7e1b55
+  SubGID range size: 65536
7e1b55
+----------------------------
7e1b55
+Number of entries returned 1
7e1b55
+----------------------------
7e1b55
+```
7e1b55
+
7e1b55
+```raw
7e1b55
+$ ipa -vv subid-stats
7e1b55
+...
7e1b55
+ipa: INFO: Response: {
7e1b55
+    "error": null,
7e1b55
+    "id": 0,
7e1b55
+    "principal": "admin@IPASUBID.TEST",
7e1b55
+    "result": {
7e1b55
+        "result": {
7e1b55
+            "assigned_subids": 20,
7e1b55
+            "baseid": 2147483648,
7e1b55
+            "dna_remaining": 4293394434,
7e1b55
+            "rangesize": 2147352576,
7e1b55
+            "remaining_subids": 65512
7e1b55
+        },
7e1b55
+        "summary": "65532 remaining subordinate id ranges"
7e1b55
+    },
7e1b55
+    "version": "4.10.0.dev"
7e1b55
+}
7e1b55
+-------------------------------------
7e1b55
+65532 remaining subordinate id ranges
7e1b55
+-------------------------------------
7e1b55
+```
7e1b55
+
7e1b55
 ## Permissions, Privileges, Roles
7e1b55
 
7e1b55
 ### Self-servive RBAC
7e1b55
@@ -246,6 +377,13 @@ be modified or deleted.
7e1b55
 * Privilege: *Subordinate ID Administrators*
7e1b55
 * default privilege role: *User Administrator*
7e1b55
 
7e1b55
+### Managed permissions
7e1b55
+
7e1b55
+* *System: Read Subordinate Id Attributes* (all authenticated users)
7e1b55
+* *System: Read Subordinate Id Count* (all authenticated usrs)
7e1b55
+* *System: Manage Subordinate Ids* (User Administrators)
7e1b55
+* *System: Remove Subordinate Ids* (User Administrators)
7e1b55
+
7e1b55
 
7e1b55
 ## Workflows
7e1b55
 
7e1b55
@@ -257,12 +395,12 @@ to assign subordinate ids to users.
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
+assignment can be performed with new ``subid-generate`` 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
+$ ipa subid-generate --owner myusername
7e1b55
 ```
7e1b55
 
7e1b55
 ### Self-service for group members
7e1b55
@@ -279,27 +417,14 @@ $ 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
+the ``subid-generate`` command or the *Auto assign subordinate ids*
7e1b55
+action in the web UI (**TODO** not implemented yet). The command picks
7e1b55
+the name of the current user principal automatically.
7e1b55
 
7e1b55
 ```shell
7e1b55
-$ ipa config-mod --addattr="ipaUserObjectClasses=ipasubordinateid"
7e1b55
+$ ipa subid-generate
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
@@ -355,26 +480,25 @@ 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
+$ ipa subid-match --subuid=2147549183
7e1b55
+------------------------
7e1b55
+1 subordinate id matched
7e1b55
+------------------------
7e1b55
+  Name: asmith-auto
7e1b55
+  Owner: asmith
7e1b55
+  SubUID range start: 2147483648
7e1b55
   SubUID range size: 65536
7e1b55
-  SubGID range start: 2153185280
7e1b55
+  SubGID range start: 2147483648
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
+$ ipa user-match-subid --subuid=2147549184
7e1b55
+  Name: bjones-auto
7e1b55
+  Owner: bjones
7e1b55
+  SubUID range start: 2147549184
7e1b55
   SubUID range size: 65536
7e1b55
-  SubGID range start: 2153119744
7e1b55
+  SubGID range start: 2147549184
7e1b55
   SubGID range size: 65536
7e1b55
 ----------------------------
7e1b55
 Number of entries returned 1
7e1b55
@@ -383,61 +507,54 @@ Number of entries returned 1
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
+* search base: ``cn=subids,cn=accounts,$SUFFIX``
7e1b55
+* scope: ``SCOPE_ONELEVEL`` (1)
7e1b55
+* filter: ``(objectClass=ipaSubordinateId)``
7e1b55
+* attributes: ``ipaOwner ipaSubUidNumber ipaSubUidCount ipaSubGidNumber ipaSubGidCount``
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
+All four subordinate id attributes are single-value ``INTEGER`` types.
7e1b55
+Any value outside of range of ``uint32_t`` must treated as invalid.
7e1b55
+SSSD will never see the DNA magic value ``-1`` in
7e1b55
+``cn=accounts,$SUFFIX`` subtree. In revision 1 each user subordinate
7e1b55
+id entry is assigned to exactly one user and each user has either 0
7e1b55
+or 1 subid.
7e1b55
+
7e1b55
+IPA recommends that SSSD uses LDAP deref controls for ``ipaOwner``
7e1b55
+attribute to fetch ``uidNumber`` from the user object.
7e1b55
+
7e1b55
+### Deref control example
7e1b55
+
7e1b55
+```raw
7e1b55
+$ ldapsearch -L -E '!deref=ipaOwner:uid,uidNumber' \
7e1b55
+      -b 'cn=subids,cn=accounts,dc=ipasubid,dc=test' \
7e1b55
+      '(ipaOwner=uid=testuser10,cn=users,cn=accounts,dc=ipasubid,dc=test)'
7e1b55
+
7e1b55
+dn: ipauniqueid=35c02c93-3799-4551-a355-ebbf042e431c,cn=subids,cn=accounts,dc=ipasubid,dc=test
7e1b55
+# control: 1.3.6.1.4.1.4203.666.5.16 false MIQAAABxMIQAAABrBAhpcGFPd25lcgQ3dWlk
7e1b55
+ PXRlc3R1c2VyMTAsY249dXNlcnMsY249YWNjb3VudHMsZGM9aXBhc3ViaWQsZGM9dGVzdKCEAAAAIj
7e1b55
+ CEAAAAHAQJdWlkTnVtYmVyMYQAAAALBAk2MjgwMDAwMTE=
7e1b55
+# ipaOwner: <uidNumber=628000011>;uid=testuser10,cn=users,cn=accounts,dc=ipasubid,dc=test
7e1b55
+
7e1b55
+ipaOwner: uid=testuser10,cn=users,cn=accounts,dc=ipasubid,dc=test
7e1b55
+ipaUniqueID: 35c02c93-3799-4551-a355-ebbf042e431c
7e1b55
+description: auto-assigned subid
7e1b55
+objectClass: ipasubordinateidentry
7e1b55
+objectClass: ipasubordinategid
7e1b55
+objectClass: ipasubordinateuid
7e1b55
+objectClass: ipasubordinateid
7e1b55
+objectClass: top
7e1b55
+ipaSubUidNumber: 3434020864
7e1b55
+ipaSubGidNumber: 3434020864
7e1b55
+ipaSubUidCount: 65536
7e1b55
+ipaSubGidCount: 65536
7e1b55
+```
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
@@ -459,6 +576,13 @@ Filters for additional cases:
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
+* ``ipaSubordinateId`` entries no longer contains ``uidNumber``
7e1b55
+  attribute. I considered to use CoS plugin to provide ``uidNumber``
7e1b55
+  as virtual attribute. However it's not possible to
7e1b55
+  ``objectClass: cosIndirectDefinition`` with
7e1b55
+  ``cosIndirectSpecifier: ipaOwner`` and
7e1b55
+  ``cosAttribute: uidNumber override`` for the task. Indexes and
7e1b55
+  searches don't work with virtual attributes.
7e1b55
 
7e1b55
 ### TODO
7e1b55
 
7e1b55
@@ -466,3 +590,23 @@ Filters for additional cases:
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
+
7e1b55
+#### user-del --preserve
7e1b55
+
7e1b55
+Preserving a user with ``ipa user-del --preserve`` currently fails with
7e1b55
+an ObjectclassViolation error (err=65). The problem is caused by
7e1b55
+configuration of referential integrity postoperation plug-in. The
7e1b55
+plug-in excludes the subtree
7e1b55
+``nsslapd-pluginexcludeentryscope: cn=provisioning,$SUFFX``. Preserved
7e1b55
+users are moved into the staging area of the provisioning subtree.
7e1b55
+Since the ``ipaOwner`` DN target is now out of scope, the plug-in
7e1b55
+attempts to delete references. However ``ipaOwner`` is a required
7e1b55
+attribute, which triggers the objectclass violation.
7e1b55
+
7e1b55
+Possible solutions
7e1b55
+
7e1b55
+* Don't preserve subid entries
7e1b55
+* Implement preserve feature for subid entries and move subids of
7e1b55
+  preserved users into
7e1b55
+  ``cn=deleted subids,cn=accounts,cn=provisioning,$SUFFIX`` subtree.
7e1b55
+* Change ``nsslapd-pluginexcludeentryscope`` setting
7e1b55
diff --git a/install/share/60basev4.ldif b/install/share/60basev4.ldif
7e1b55
index 7f5173e593ff68a03d4005957b1dc9b9eb489dc5..c48b0c36a3012d86a74e12a77695e29cceafb698 100644
7e1b55
--- a/install/share/60basev4.ldif
7e1b55
+++ b/install/share/60basev4.ldif
7e1b55
@@ -5,15 +5,16 @@
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
+# range ceiling OIDs are reserved for future use
7e1b55
+attributeTypes: ( 2.16.840.1.113730.3.8.23.7 NAME 'ipaSubUidNumber' DESC 'Numerical subordinate user ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
+attributeTypes: ( 2.16.840.1.113730.3.8.23.8 NAME 'ipaSubUidCount' DESC 'Subordinate user ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
+# attributeTypes: ( 2.16.840.1.113730.3.8.23.9 NAME 'ipaSubUidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
+attributeTypes: ( 2.16.840.1.113730.3.8.23.10 NAME 'ipaSubGidNumber' DESC 'Numerical subordinate group ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
+attributeTypes: ( 2.16.840.1.113730.3.8.23.11 NAME 'ipaSubGidCount' DESC 'Subordinate group ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
+# attributeTypes: ( 2.16.840.1.113730.3.8.23.12 NAME 'ipaSubGidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
+attributeTypes: ( 2.16.840.1.113730.3.8.23.13 NAME 'ipaOwner' DESC 'Owner of an entry' SUP distinguishedName EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
+# attribute 2.16.840.1.113730.3.8.23.14 'ipaUserDefaultSubordinateId' is defined in 60ipaconfig.ldif
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 ( ipaOwner $ 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 ( ipaOwner $ 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 ( ipaOwner $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9')
7e1b55
+objectClasses: (2.16.840.1.113730.3.8.24.5 NAME 'ipaSubordinateIdEntry' DESC 'Subordinate uid and gid entry' SUP top STRUCTURAL MUST ( ipaUniqueId ) MAY ( description ) X-ORIGIN 'IPA v4.9')
7e1b55
diff --git a/install/share/60ipaconfig.ldif b/install/share/60ipaconfig.ldif
7e1b55
index dbcf9ee603b361f6aac1413d0a53fff5561b6f89..f84b38ead1d70ff408f5669029f1517b0c98ecf1 100644
7e1b55
--- a/install/share/60ipaconfig.ldif
7e1b55
+++ b/install/share/60ipaconfig.ldif
7e1b55
@@ -6,6 +6,7 @@
7e1b55
 ## ObjectClasses:	2.16.840.1.113730.3.8.2 - V1
7e1b55
 ## Attributes:		2.16.840.1.113730.3.8.3 - V2
7e1b55
 ## ObjectClasses:	2.16.840.1.113730.3.8.4 - V2
7e1b55
+## Attributes:		2.16.840.1.113730.3.8.23 - V4 base attributes
7e1b55
 dn: cn=schema
7e1b55
 ###############################################
7e1b55
 ##
7e1b55
@@ -45,11 +46,13 @@ attributeTypes: ( 2.16.840.1.113730.3.8.3.26 NAME 'ipaSELinuxUserMapDefault' DES
7e1b55
 attributeTypes: ( 2.16.840.1.113730.3.8.3.27 NAME 'ipaSELinuxUserMapOrder' DESC 'Available SELinux user context ordering' EQUALITY caseIgnoreMatch ORDERING caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v3')
7e1b55
 ## ipaMaxHostnameLength - maximum hostname length to allow
7e1b55
 attributeTypes: ( 2.16.840.1.113730.3.8.1.28 NAME 'ipaMaxHostnameLength' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE)
7e1b55
+# ipaUserDefaultSubordinateId - if TRUE new user entries gain subordinate id by default
7e1b55
+attributeTypes: ( 2.16.840.1.113730.3.8.3.23.14 NAME 'ipaUserDefaultSubordinateId' DESC 'Enable adding user entries with subordinate id' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
7e1b55
 ###############################################
7e1b55
 ##
7e1b55
 ## ObjectClasses
7e1b55
 ##
7e1b55
 ## ipaGuiConfig - GUI config parameters objectclass
7e1b55
-objectClasses: ( 2.16.840.1.113730.3.8.2.1 NAME 'ipaGuiConfig' AUXILIARY MAY ( ipaUserSearchFields $ ipaGroupSearchFields $ ipaSearchTimeLimit $ ipaSearchRecordsLimit $ ipaCustomFields $ ipaHomesRootDir $ ipaDefaultLoginShell $ ipaDefaultPrimaryGroup $ ipaMaxUsernameLength $ ipaPwdExpAdvNotify $ ipaUserObjectClasses $ ipaGroupObjectClasses $ ipaDefaultEmailDomain $ ipaMigrationEnabled $ ipaCertificateSubjectBase $ ipaSELinuxUserMapDefault $ ipaSELinuxUserMapOrder $ ipaKrbAuthzData $ ipaMaxHostnameLength) )
7e1b55
+objectClasses: ( 2.16.840.1.113730.3.8.2.1 NAME 'ipaGuiConfig' AUXILIARY MAY ( ipaUserSearchFields $ ipaGroupSearchFields $ ipaSearchTimeLimit $ ipaSearchRecordsLimit $ ipaCustomFields $ ipaHomesRootDir $ ipaDefaultLoginShell $ ipaDefaultPrimaryGroup $ ipaMaxUsernameLength $ ipaPwdExpAdvNotify $ ipaUserObjectClasses $ ipaGroupObjectClasses $ ipaDefaultEmailDomain $ ipaMigrationEnabled $ ipaCertificateSubjectBase $ ipaSELinuxUserMapDefault $ ipaSELinuxUserMapOrder $ ipaKrbAuthzData $ ipaMaxHostnameLength $ ipaUserDefaultSubordinateId) )
7e1b55
 ## ipaConfigObject - Generic config strings object holder
7e1b55
 objectClasses: (2.16.840.1.113730.3.8.4.13 NAME 'ipaConfigObject' DESC 'generic config object for IPA' AUXILIARY MAY ( ipaConfigString ) X-ORIGIN 'IPA v2' )
7e1b55
diff --git a/install/share/dna.ldif b/install/share/dna.ldif
7e1b55
index 649313e72fc58112865e5901125923b3704276b1..735faab8261feef59486f7c933b01c57ad511166 100644
7e1b55
--- a/install/share/dna.ldif
7e1b55
+++ b/install/share/dna.ldif
7e1b55
@@ -29,12 +29,12 @@ 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
+# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr
7e1b55
+# dnaIntervalAttr: ipasubuidcount
7e1b55
+# dnaIntervalAttr: ipasubgidcount
7e1b55
+# dnaMaxInterval: eval($SUBID_COUNT)
7e1b55
 
7e1b55
 # Enable the DNA plugin
7e1b55
 dn: cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
7e1b55
diff --git a/install/share/memberof-conf.ldif b/install/share/memberof-conf.ldif
7e1b55
index 79ad647e76feb6647524553a634c91c66ebd178e..3c22dfa9e52b8005bac88cd0962571c6fea18e7b 100644
7e1b55
--- a/install/share/memberof-conf.ldif
7e1b55
+++ b/install/share/memberof-conf.ldif
7e1b55
@@ -8,4 +8,6 @@ memberofgroupattr: memberUser
7e1b55
 -
7e1b55
 add: memberofgroupattr
7e1b55
 memberofgroupattr: memberHost
7e1b55
-
7e1b55
+-
7e1b55
+add: memberofgroupattr
7e1b55
+memberofgroupattr: ipaOwner
7e1b55
diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js
7e1b55
index 093737b8f923b41e8a1eabc90f66c6709991e239..9e0007528febb3d6641d152c72c51328d2d72cf6 100644
7e1b55
--- a/install/ui/src/freeipa/app.js
7e1b55
+++ b/install/ui/src/freeipa/app.js
7e1b55
@@ -52,6 +52,7 @@ define([
7e1b55
     './serverconfig',
7e1b55
     './service',
7e1b55
     './stageuser',
7e1b55
+    './subid',
7e1b55
     './sudo',
7e1b55
     './trust',
7e1b55
     './topology',
7e1b55
diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js
7e1b55
index 0c30459691d8f652dc35ccf74ed27fae7654020d..6ccd06919fbe04c7e8d2034ff7a1f644f373c607 100644
7e1b55
--- a/install/ui/src/freeipa/navigation/menu_spec.js
7e1b55
+++ b/install/ui/src/freeipa/navigation/menu_spec.js
7e1b55
@@ -103,7 +103,8 @@ var nav = {};
7e1b55
                             ]
7e1b55
                         }
7e1b55
                     ]
7e1b55
-                }
7e1b55
+                },
7e1b55
+                { entity: 'subid' }
7e1b55
             ]
7e1b55
         },
7e1b55
         {
7e1b55
diff --git a/install/ui/src/freeipa/serverconfig.js b/install/ui/src/freeipa/serverconfig.js
7e1b55
index bb26b107317ae12ee032fb767fcfc9060690c66e..9de0bab4b64abf1094e52dffda8add5349dc0e3c 100644
7e1b55
--- a/install/ui/src/freeipa/serverconfig.js
7e1b55
+++ b/install/ui/src/freeipa/serverconfig.js
7e1b55
@@ -123,6 +123,10 @@ return {
7e1b55
                             $type: 'checkbox',
7e1b55
                             name: 'ipamigrationenabled'
7e1b55
                         },
7e1b55
+                        {
7e1b55
+                            $type: 'checkbox',
7e1b55
+                            name: 'ipauserdefaultsubordinateid'
7e1b55
+                        },
7e1b55
                         {
7e1b55
                             $type: 'multivalued',
7e1b55
                             name: 'ipauserobjectclasses'
7e1b55
diff --git a/install/ui/src/freeipa/subid.js b/install/ui/src/freeipa/subid.js
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..f286165070b08badf77cac6c30e93cab916c2acc
7e1b55
--- /dev/null
7e1b55
+++ b/install/ui/src/freeipa/subid.js
7e1b55
@@ -0,0 +1,92 @@
7e1b55
+/*
7e1b55
+ * Copyright (C) 2021  FreeIPA Contributors see COPYING for license
7e1b55
+ */
7e1b55
+
7e1b55
+define([
7e1b55
+        'dojo/on',
7e1b55
+        './ipa',
7e1b55
+        './jquery',
7e1b55
+        './phases',
7e1b55
+        './reg',
7e1b55
+        './details',
7e1b55
+        './search',
7e1b55
+        './association',
7e1b55
+        './entity'],
7e1b55
+            function(on, IPA, $, phases, reg) {
7e1b55
+
7e1b55
+var exp = IPA.subid = {};
7e1b55
+
7e1b55
+var make_spec = function() {
7e1b55
+return {
7e1b55
+    name: 'subid',
7e1b55
+    facets: [
7e1b55
+        {
7e1b55
+            $type: 'search',
7e1b55
+            columns: [
7e1b55
+                'ipauniqueid',
7e1b55
+                'ipaowner',
7e1b55
+                'ipasubgidnumber',
7e1b55
+                'ipasubuidnumber'
7e1b55
+            ]
7e1b55
+        },
7e1b55
+        {
7e1b55
+            $type: 'details',
7e1b55
+            sections: [
7e1b55
+                {
7e1b55
+                    name: 'details',
7e1b55
+                    fields: [
7e1b55
+                        'ipauniqueid',
7e1b55
+                        'description',
7e1b55
+                        {
7e1b55
+                            name: 'ipaowner',
7e1b55
+                            label: '@i18n:objects.subid.ipaowner',
7e1b55
+                            title: '@mo-param:subid:ipaowner:label'
7e1b55
+                        },
7e1b55
+                        {
7e1b55
+                            name: 'ipasubgidnumber',
7e1b55
+                            label: '@i18n:objects.subid.ipasubgidnumber',
7e1b55
+                            title: '@mo-param:subid:ipasubgidnumber:label'
7e1b55
+                        },
7e1b55
+                        {
7e1b55
+                            name: 'ipasubgidcount',
7e1b55
+                            label: '@i18n:objects.subid.ipasubgidcount',
7e1b55
+                            title: '@mo-param:subid:ipasubgidcount:label'
7e1b55
+                        },
7e1b55
+                        {
7e1b55
+                            name: 'ipasubuidnumber',
7e1b55
+                            label: '@i18n:objects.subid.ipasubuidnumber',
7e1b55
+                            title: '@mo-param:subid:ipasubuidnumber:label'
7e1b55
+                        },
7e1b55
+                        {
7e1b55
+                            name: 'ipasubuidcount',
7e1b55
+                            label: '@i18n:objects.subid.ipasubuidcount',
7e1b55
+                            title: '@mo-param:subid:ipasubuidcount:label'
7e1b55
+                        }
7e1b55
+                    ]
7e1b55
+                }
7e1b55
+            ]
7e1b55
+        }
7e1b55
+    ],
7e1b55
+    adder_dialog: {
7e1b55
+        title: '@i18n:objects.subid.add',
7e1b55
+        method: 'generate',
7e1b55
+        fields: [
7e1b55
+            {
7e1b55
+                $type: 'entity_select',
7e1b55
+                name: 'ipaowner',
7e1b55
+                other_entity: 'user',
7e1b55
+                other_field: 'uid'
7e1b55
+            }
7e1b55
+        ]
7e1b55
+    }
7e1b55
+};};
7e1b55
+
7e1b55
+exp.entity_spec = make_spec();
7e1b55
+exp.register = function() {
7e1b55
+    var e = reg.entity;
7e1b55
+    e.register({type: 'subid', spec: exp.entity_spec});
7e1b55
+};
7e1b55
+phases.on('registration', exp.register);
7e1b55
+
7e1b55
+return {};
7e1b55
+});
7e1b55
diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
7e1b55
index 5b49b0f6edbbbb6c802afb803a6406a0ab796c44..56bb6f4feffb637d33a57aecf9a98f08d4639550 100644
7e1b55
--- a/install/ui/src/freeipa/user.js
7e1b55
+++ b/install/ui/src/freeipa/user.js
7e1b55
@@ -259,33 +259,6 @@ 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
@@ -478,16 +451,6 @@ 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
@@ -500,20 +463,15 @@ return {
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
+                    $type: 'subid_generate',
7e1b55
                     hide_cond: ['preserved-user'],
7e1b55
-                    enable_cond: ['no-subid'],
7e1b55
-                    confirm_msg: '@i18n:objects.user.auto_subid_confirm'
7e1b55
+                    enable_cond: ['no-subid']
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', 'auto_subid'
7e1b55
+                'automember_rebuild', 'request_cert', 'subid_generate'
7e1b55
             ],
7e1b55
             state: {
7e1b55
                 evaluators: [
7e1b55
@@ -532,7 +490,8 @@ return {
7e1b55
                     IPA.user.self_service_other_user_evaluator,
7e1b55
                     IPA.user.preserved_user_evaluator,
7e1b55
                     IPA.user.is_locked_evaluator,
7e1b55
-                    IPA.cert.certificate_evaluator
7e1b55
+                    IPA.cert.certificate_evaluator,
7e1b55
+                    IPA.user.has_subid_evaluator
7e1b55
                 ],
7e1b55
                 summary_conditions: [
7e1b55
                     {
7e1b55
@@ -593,6 +552,12 @@ return {
7e1b55
             add_title: '@i18n:objects.user.add_into_sudo',
7e1b55
             remove_method: 'remove_user',
7e1b55
             remove_title: '@i18n:objects.user.remove_from_sudo'
7e1b55
+        },
7e1b55
+        {
7e1b55
+            $type: 'association',
7e1b55
+            name: 'memberof_subid',
7e1b55
+            associator: IPA.serial_associator,
7e1b55
+            read_only: true
7e1b55
         }
7e1b55
     ],
7e1b55
     standard_association_facets: {
7e1b55
@@ -1206,7 +1171,31 @@ IPA.user.is_locked_evaluator = function(spec) {
7e1b55
             }
7e1b55
         }
7e1b55
 
7e1b55
-        if (!user.ipasubuidnumber) {
7e1b55
+        that.notify_on_change(old_state);
7e1b55
+    };
7e1b55
+
7e1b55
+    return that;
7e1b55
+};
7e1b55
+
7e1b55
+IPA.user.has_subid_evaluator = function(spec) {
7e1b55
+
7e1b55
+    spec = spec || {};
7e1b55
+    spec.event = spec.event || 'post_load';
7e1b55
+
7e1b55
+    var that = IPA.state_evaluator(spec);
7e1b55
+    that.name = spec.name || 'has_subid_evaluator';
7e1b55
+    that.param = spec.param || 'memberof_subid';
7e1b55
+
7e1b55
+    /**
7e1b55
+     * Evaluates if user already has a subid
7e1b55
+     */
7e1b55
+    that.on_event = function(data) {
7e1b55
+
7e1b55
+        var old_state = that.state;
7e1b55
+        that.state = [];
7e1b55
+
7e1b55
+        var value = that.adapter.load(data);
7e1b55
+        if (value.length === 0) {
7e1b55
             that.state.push('no-subid');
7e1b55
         }
7e1b55
 
7e1b55
@@ -1216,6 +1205,30 @@ IPA.user.is_locked_evaluator = function(spec) {
7e1b55
     return that;
7e1b55
 };
7e1b55
 
7e1b55
+IPA.user.subid_generate_action = function(spec) {
7e1b55
+
7e1b55
+    spec = spec || {};
7e1b55
+    spec.name = spec.name || 'subid_generate';
7e1b55
+    spec.label = spec.label || '@i18n:objects.user.auto_subid';
7e1b55
+    spec.hide_cond = spec.hide_cond || ['preserved-user'];
7e1b55
+    spec.confirm_msg = spec.confirm_msg || '@i18n:objects.user.auto_subid_confirm';
7e1b55
+
7e1b55
+    var that = IPA.action(spec);
7e1b55
+
7e1b55
+    that.execute_action = function(facet) {
7e1b55
+
7e1b55
+        var subid_e = reg.entity.get('subid');
7e1b55
+        var dialog = subid_e.get_dialog('add');
7e1b55
+        dialog.open();
7e1b55
+        if (!IPA.is_selfservice) {
7e1b55
+            var owner = facet.get_pkey();
7e1b55
+            dialog.get_field('ipaowner').set_value([owner]);
7e1b55
+        }
7e1b55
+    };
7e1b55
+
7e1b55
+    return that;
7e1b55
+};
7e1b55
+
7e1b55
 exp.entity_spec = make_spec();
7e1b55
 exp.register = function() {
7e1b55
     var e = reg.entity;
7e1b55
@@ -1225,6 +1238,7 @@ exp.register = function() {
7e1b55
     a.register('reset_password', IPA.user.reset_password_action);
7e1b55
     a.register('add_otptoken', IPA.user.add_otptoken_action);
7e1b55
     a.register('delete_active_user', IPA.user.delete_active_user_action);
7e1b55
+    a.register('subid_generate', IPA.user.subid_generate_action);
7e1b55
     d.copy('password', 'user_password', {
7e1b55
         factory: IPA.user.password_dialog,
7e1b55
         pre_ops: [IPA.user.password_dialog_pre_op]
7e1b55
diff --git a/install/updates/10-uniqueness.update b/install/updates/10-uniqueness.update
7e1b55
index 77facba195cb5a1564818010f97afdd15d65a274..699de3b4d3305def5d81aeb945106b80eef0ef40 100644
7e1b55
--- a/install/updates/10-uniqueness.update
7e1b55
+++ b/install/updates/10-uniqueness.update
7e1b55
@@ -109,3 +109,22 @@ default:nsslapd-plugin-depends-on-type: database
7e1b55
 default:nsslapd-pluginId: NSUniqueAttr
7e1b55
 default:nsslapd-pluginVersion: 1.1.0
7e1b55
 default:nsslapd-pluginVendor: Fedora Project
7e1b55
+
7e1b55
+dn: cn=ipaSubordinateIdEntry ipaOwner uniqueness,cn=plugins,cn=config
7e1b55
+default:objectClass: top
7e1b55
+default:objectClass: nsSlapdPlugin
7e1b55
+default:objectClass: extensibleObject
7e1b55
+default:cn: ipaSubordinateIdEntry ipaOwner uniqueness
7e1b55
+default:nsslapd-pluginDescription: Enforce unique attribute values of ipaOwner
7e1b55
+default:nsslapd-pluginPath: libattr-unique-plugin
7e1b55
+default:nsslapd-pluginInitfunc: NSUniqueAttr_Init
7e1b55
+default:nsslapd-pluginType: preoperation
7e1b55
+default:nsslapd-pluginEnabled: on
7e1b55
+default:uniqueness-attribute-name: ipaOwner
7e1b55
+default:uniqueness-subtrees: cn=subids,cn=accounts,$SUFFIX
7e1b55
+default:uniqueness-across-all-subtrees: on
7e1b55
+default:uniqueness-subtree-entries-oc: ipaSubordinateIdEntry
7e1b55
+default:nsslapd-plugin-depends-on-type: database
7e1b55
+default:nsslapd-pluginId: NSUniqueAttr
7e1b55
+default:nsslapd-pluginVersion: 1.1.0
7e1b55
+default:nsslapd-pluginVendor: Fedora Project
7e1b55
diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update
7e1b55
index 7f83ab9f04c565a59efdd2f41c3e7ee30f5da9c7..d6df5b37d0a9092e936d2c6002bce71ed876ec27 100644
7e1b55
--- a/install/updates/20-indices.update
7e1b55
+++ b/install/updates/20-indices.update
7e1b55
@@ -263,6 +263,14 @@ default:nsSystemIndex: false
7e1b55
 add:nsIndexType: eq
7e1b55
 add:nsIndexType: pres
7e1b55
 
7e1b55
+dn: cn=ipaOwner,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
7e1b55
+only:cn: ipaOwner
7e1b55
+default:objectClass: nsIndex
7e1b55
+default:objectClass: top
7e1b55
+default:nsSystemIndex: false
7e1b55
+add:nsIndexType: eq
7e1b55
+add:nsIndexType: pres
7e1b55
+
7e1b55
 dn: cn=ipasudorunas,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
7e1b55
 only:cn: ipasudorunas
7e1b55
 default:objectClass: nsIndex
7e1b55
@@ -425,6 +433,10 @@ default:nsSystemIndex: false
7e1b55
 add:nsIndexType: eq
7e1b55
 add:nsIndexType: pres
7e1b55
 
7e1b55
+dn: cn=memberOf,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
7e1b55
+only:cn: member
7e1b55
+add:nsIndexType: sub
7e1b55
+
7e1b55
 dn: cn=memberPrincipal,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
7e1b55
 only:cn: memberPrincipal
7e1b55
 default:objectClass: nsIndex
7e1b55
diff --git a/install/updates/25-referint.update b/install/updates/25-referint.update
7e1b55
index 89bc5ef91b2f5c85ee3a4a2c7d112a0549d4e1da..b29926a4e3bd410115cde4ede3ed7886a412d300 100644
7e1b55
--- a/install/updates/25-referint.update
7e1b55
+++ b/install/updates/25-referint.update
7e1b55
@@ -21,3 +21,4 @@ add: referint-membership-attr: ipamemberca
7e1b55
 add: referint-membership-attr: ipamembercertprofile
7e1b55
 add: referint-membership-attr: ipalocation
7e1b55
 add: referint-membership-attr: membermanager
7e1b55
+add: referint-membership-attr: ipaowner
7e1b55
diff --git a/install/updates/73-subid.update b/install/updates/73-subid.update
7e1b55
index 2aab3d445a33ae1663f81ca2d61b62ebc94aa37d..1aa43822a8b8c220583b81e08d70b648ca594363 100644
7e1b55
--- a/install/updates/73-subid.update
7e1b55
+++ b/install/updates/73-subid.update
7e1b55
@@ -1,5 +1,15 @@
7e1b55
 # subordinate ids
7e1b55
 
7e1b55
+# create memberOf attributes for ipaOwner
7e1b55
+dn: cn=MemberOf Plugin,cn=plugins,cn=config
7e1b55
+add: memberofgroupattr: ipaOwner
7e1b55
+
7e1b55
+# container
7e1b55
+dn: cn=subids,cn=accounts,$SUFFIX
7e1b55
+default: objectClass: top
7e1b55
+default: objectClass: nsContainer
7e1b55
+default: cn: subids
7e1b55
+
7e1b55
 # self-service RBAC
7e1b55
 dn: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX
7e1b55
 default:objectClass: groupofnames
7e1b55
@@ -56,9 +66,9 @@ default:member: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX
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
+dn: cn=subids,cn=accounts,$SUFFIX
7e1b55
+add: aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(targetattr="description || ipaowner || ipauniqueid")(targattrfilters = "add=objectClass:(|(objectClass=top)(objectClass=ipasubordinateid)(objectClass=ipasubordinateidentry)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=eval($SUBID_RANGE_START))(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=eval($SUBID_RANGE_START))(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "selfservice: Add subordinate id";allow (add, write) userattr = "ipaowner#SELFDN" and groupdn="ldap:///cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX";)
7e1b55
+add: aci: (targetfilter = "(objectclass=ipasubordinateidentry)")(targetattr="description || ipaowner || ipauniqueid")(targattrfilters = "add=objectClass:(|(objectClass=top)(objectClass=ipasubordinateid)(objectClass=ipasubordinateidentry)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=1)(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=1)(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "Add subordinate ids to any user";allow (add, write) groupdn="ldap:///cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX";)
7e1b55
 
7e1b55
 # DNA plugin and idrange configuration
7e1b55
 dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
7e1b55
@@ -78,14 +88,14 @@ 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
+# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr
7e1b55
+# add: dnaIntervalAttr: ipasubuidcount
7e1b55
+# add: dnaIntervalAttr: ipasubgidcount
7e1b55
+# addifnew: dnaMaxInterval: eval($SUBID_COUNT)
7e1b55
+add: aci: (targetattr = "dnaNextRange || dnaNextValue || dnaMaxValue")(version 3.0;acl "permission:Modify DNA Range";allow (write) groupdn = "ldap:///cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX";)
7e1b55
+add: aci: (targetattr = "cn || dnaMaxValue || dnaNextRange || dnaNextValue  || dnaThreshold || dnaType || objectclass")(version 3.0;acl "permission:Read DNA Range";allow (read, search, compare) groupdn = "ldap:///cn=Read DNA Range,cn=permissions,cn=pbac,$SUFFIX";)
7e1b55
 
7e1b55
 dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX
7e1b55
 default: objectClass: top
7e1b55
diff --git a/ipalib/constants.py b/ipalib/constants.py
7e1b55
index bee4c92fb39769d427e315116575f217924915be..bff899ba64c75832e2037870ccbca4587458d97b 100644
7e1b55
--- a/ipalib/constants.py
7e1b55
+++ b/ipalib/constants.py
7e1b55
@@ -131,6 +131,9 @@ DEFAULT_CONFIG = (
7e1b55
     ('container_ranges', DN(('cn', 'ranges'), ('cn', 'etc'))),
7e1b55
     ('container_dna', DN(('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))),
7e1b55
     ('container_dna_posix_ids', DN(('cn', 'posix-ids'), ('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))),
7e1b55
+    ('container_dna_subordinate_ids', DN(
7e1b55
+        ('cn', 'subordinate-ids'), ('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc')
7e1b55
+    )),
7e1b55
     ('container_realm_domains', DN(('cn', 'Realm Domains'), ('cn', 'ipa'), ('cn', 'etc'))),
7e1b55
     ('container_otp', DN(('cn', 'otp'))),
7e1b55
     ('container_radiusproxy', DN(('cn', 'radiusproxy'))),
7e1b55
@@ -148,6 +151,7 @@ DEFAULT_CONFIG = (
7e1b55
     ('container_certmaprules', DN(('cn', 'certmaprules'), ('cn', 'certmap'))),
7e1b55
     ('container_ca_renewal',
7e1b55
         DN(('cn', 'ca_renewal'), ('cn', 'ipa'), ('cn', 'etc'))),
7e1b55
+    ('container_subids', DN(('cn', 'subids'), ('cn', 'accounts'))),
7e1b55
 
7e1b55
     # Ports, hosts, and URIs:
7e1b55
     # Following values do not have any reasonable default.
7e1b55
@@ -355,4 +359,4 @@ SUBID_RANGE_START = 2 ** 31
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
+SUBID_DNA_THRESHOLD = 500
7e1b55
diff --git a/ipaserver/install/ipa_subids.py b/ipaserver/install/ipa_subids.py
7e1b55
index ac77a4008aec58d92c8b24df5e00b83c6998401f..2b33b667084f529fa50e2f11eeefda8a8927f68c 100644
7e1b55
--- a/ipaserver/install/ipa_subids.py
7e1b55
+++ b/ipaserver/install/ipa_subids.py
7e1b55
@@ -10,7 +10,7 @@ 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
+from ipapython.version import API_VERSION
7e1b55
 
7e1b55
 logger = logging.getLogger(__name__)
7e1b55
 
7e1b55
@@ -77,7 +77,7 @@ class IPASubids(AdminTool):
7e1b55
             # only users with posixAccount
7e1b55
             "(objectClass=posixAccount)",
7e1b55
             # without subordinate ids
7e1b55
-            "(!(objectClass=ipaSubordinateId))",
7e1b55
+            f"(!(memberOf=*,cn=subids,cn=accounts,{api.env.basedn}))",
7e1b55
         ]
7e1b55
         if groupinfo is not None:
7e1b55
             filters.append(
7e1b55
@@ -89,7 +89,7 @@ class IPASubids(AdminTool):
7e1b55
 
7e1b55
     def search_users(self, filters):
7e1b55
         users_dn = DN(api.env.container_user, api.env.basedn)
7e1b55
-        attrs = ["objectclass", "uid", "uidnumber"]
7e1b55
+        attrs = ["objectclass", "uid"]
7e1b55
 
7e1b55
         logger.debug("basedn: %s", users_dn)
7e1b55
         logger.debug("attrs: %s", attrs)
7e1b55
@@ -116,7 +116,7 @@ class IPASubids(AdminTool):
7e1b55
         api.finalize()
7e1b55
         api.Backend.ldap2.connect()
7e1b55
         self.ldap2 = api.Backend.ldap2
7e1b55
-        user_obj = api.Object["user"]
7e1b55
+        subid_generate = api.Command.subid_generate
7e1b55
 
7e1b55
         dry_run = self.safe_options.dry_run
7e1b55
         group_info = self.get_group_info()
7e1b55
@@ -136,11 +136,13 @@ class IPASubids(AdminTool):
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
+                # TODO: check for duplicate entry (race condition)
7e1b55
+                # TODO: log new subid
7e1b55
+                subid_generate(
7e1b55
+                    ipaowner=entry.single_value["uid"],
7e1b55
+                    version=API_VERSION
7e1b55
+                )
7e1b55
 
7e1b55
         if dry_run:
7e1b55
             logger.info("Dry run mode, no user was modified")
7e1b55
diff --git a/ipaserver/install/plugins/update_dna_shared_config.py b/ipaserver/install/plugins/update_dna_shared_config.py
7e1b55
index 0f704c68a0551a7b7db6d42275498d02885b70fc..955bee5dd830f0dcad3f0810e7e2f1a1c725a0aa 100644
7e1b55
--- a/ipaserver/install/plugins/update_dna_shared_config.py
7e1b55
+++ b/ipaserver/install/plugins/update_dna_shared_config.py
7e1b55
@@ -17,7 +17,7 @@ register = Registry()
7e1b55
 
7e1b55
 @register()
7e1b55
 class update_dna_shared_config(Updater):
7e1b55
-    dna_plugin_names = ('posix IDs',)
7e1b55
+    dna_plugin_names = ('posix IDs', 'Subordinate IDs')
7e1b55
 
7e1b55
     dna_plugin_dn = DN(
7e1b55
         ('cn', 'Distributed Numeric Assignment Plugin'),
7e1b55
diff --git a/ipaserver/plugins/baseldap.py b/ipaserver/plugins/baseldap.py
7e1b55
index 3ccd2e38a254e274ba3685b9233f23b2313f8eec..0b7839536f66740a60377460c7432ade7c0654c2 100644
7e1b55
--- a/ipaserver/plugins/baseldap.py
7e1b55
+++ b/ipaserver/plugins/baseldap.py
7e1b55
@@ -121,6 +121,8 @@ global_output_params = (
7e1b55
     Str('memberof_hbacrule?',
7e1b55
         label='Member of HBAC rule',
7e1b55
     ),
7e1b55
+    Str('memberof_subid?',
7e1b55
+        label='Subordinate ids',),
7e1b55
     Str('member_idoverrideuser?',
7e1b55
         label=_('Member ID user overrides'),),
7e1b55
     Str('memberindirect_idoverrideuser?',
7e1b55
@@ -795,7 +797,13 @@ class LDAPObject(Object):
7e1b55
 
7e1b55
         dn = entry.dn
7e1b55
         filter = self.backend.make_filter(
7e1b55
-            {'member': dn, 'memberuser': dn, 'memberhost': dn})
7e1b55
+            {
7e1b55
+                'member': dn,
7e1b55
+                'memberuser': dn,
7e1b55
+                'memberhost': dn,
7e1b55
+                'ipaowner': dn
7e1b55
+            }
7e1b55
+        )
7e1b55
         try:
7e1b55
             result = self.backend.get_entries(
7e1b55
                 self.api.env.basedn,
7e1b55
diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py
7e1b55
index 12ff03c2302ff08aabb9369306965e0c125724f8..14a71b2217d3370701662b35c43f4207c1ab9b95 100644
7e1b55
--- a/ipaserver/plugins/baseuser.py
7e1b55
+++ b/ipaserver/plugins/baseuser.py
7e1b55
@@ -17,10 +17,9 @@
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, output, constants
7e1b55
+from ipalib import api, errors, constants
7e1b55
 from ipalib import (
7e1b55
     Flag, Int, Password, Str, Bool, StrEnum, DateTime, DNParam)
7e1b55
 from ipalib.parameters import Principal, Certificate
7e1b55
@@ -28,9 +27,9 @@ from ipalib.plugable import Registry
7e1b55
 from .baseldap import (
7e1b55
     DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete,
7e1b55
     LDAPRetrieve, LDAPAddAttribute, LDAPModAttribute, LDAPRemoveAttribute,
7e1b55
-    LDAPQuery, LDAPAddMember, LDAPRemoveMember,
7e1b55
+    LDAPAddMember, LDAPRemoveMember,
7e1b55
     LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption,
7e1b55
-    add_missing_object_class, DNA_MAGIC, pkey_to_value, entry_to_dict
7e1b55
+    add_missing_object_class
7e1b55
 )
7e1b55
 from ipaserver.plugins.service import (validate_realm, normalize_principal)
7e1b55
 from ipalib.request import context
7e1b55
@@ -162,7 +161,7 @@ class baseuser(LDAPObject):
7e1b55
     possible_objectclasses = [
7e1b55
         'meporiginentry', 'ipauserauthtypeclass', 'ipauser',
7e1b55
         'ipatokenradiusproxyuser', 'ipacertmapobject',
7e1b55
-        'ipantuserattrs', 'ipasubordinateid',
7e1b55
+        'ipantuserattrs',
7e1b55
     ]
7e1b55
     disallow_object_classes = ['krbticketpolicyaux']
7e1b55
     permission_filter_objectclasses = ['posixaccount']
7e1b55
@@ -183,13 +182,13 @@ class baseuser(LDAPObject):
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
         'manager': ['user'],
7e1b55
-        'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'],
7e1b55
+        'memberof': [
7e1b55
+            'group', 'netgroup', 'role', 'hbacrule', 'sudorule', 'subid'
7e1b55
+        ],
7e1b55
         'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'],
7e1b55
     }
7e1b55
     allow_rename = True
7e1b55
@@ -432,41 +431,6 @@ 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
@@ -564,131 +528,6 @@ 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
@@ -699,7 +538,6 @@ 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
@@ -852,7 +690,6 @@ 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
@@ -1133,98 +970,3 @@ 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/config.py b/ipaserver/plugins/config.py
7e1b55
index ace66e589e50dac098aefd6b393b5e835cac9d7f..3526153ec117a05846daca7d42447ff50b5b7934 100644
7e1b55
--- a/ipaserver/plugins/config.py
7e1b55
+++ b/ipaserver/plugins/config.py
7e1b55
@@ -121,6 +121,7 @@ class config(LDAPObject):
7e1b55
         'ipapwdexpadvnotify', 'ipaselinuxusermaporder',
7e1b55
         'ipaselinuxusermapdefault', 'ipaconfigstring', 'ipakrbauthzdata',
7e1b55
         'ipauserauthtype', 'ipadomainresolutionorder', 'ipamaxhostnamelength',
7e1b55
+        'ipauserdefaultsubordinateid',
7e1b55
     ]
7e1b55
     container_dn = DN(('cn', 'ipaconfig'), ('cn', 'etc'))
7e1b55
     permission_filter_objectclasses = ['ipaguiconfig']
7e1b55
@@ -142,7 +143,7 @@ class config(LDAPObject):
7e1b55
                 'ipasearchrecordslimit', 'ipasearchtimelimit',
7e1b55
                 'ipauserauthtype', 'ipauserobjectclasses',
7e1b55
                 'ipausersearchfields', 'ipacustomfields',
7e1b55
-                'ipamaxhostnamelength',
7e1b55
+                'ipamaxhostnamelength', 'ipauserdefaultsubordinateid',
7e1b55
             },
7e1b55
         },
7e1b55
     }
7e1b55
@@ -261,6 +262,11 @@ class config(LDAPObject):
7e1b55
             values=(u'password', u'radius', u'otp',
7e1b55
                     u'pkinit', u'hardened', u'disabled'),
7e1b55
         ),
7e1b55
+        Bool('ipauserdefaultsubordinateid?',
7e1b55
+             cli_name='user_default_subid',
7e1b55
+             label=_('Enable adding subids to new users'),
7e1b55
+             doc=_('Enable adding subids to new users'),
7e1b55
+             ),
7e1b55
         Str(
7e1b55
             'ipa_master_server*',
7e1b55
             label=_('IPA masters'),
7e1b55
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
7e1b55
index 199838b199eb4cdabf597bd34d571d05547fd32e..5ef940c2b88cc2b132a15d619772349b30731306 100644
7e1b55
--- a/ipaserver/plugins/internal.py
7e1b55
+++ b/ipaserver/plugins/internal.py
7e1b55
@@ -1547,7 +1547,7 @@ class i18n_messages(Command):
7e1b55
                     "Drive to mount a home directory"
7e1b55
                 ),
7e1b55
             },
7e1b55
-            "subordinate": {
7e1b55
+            "subid": {
7e1b55
                 "identity": _("Subordinate user and group id"),
7e1b55
                 "subuidnumber": _("Subordinate user id"),
7e1b55
                 "subuidcount": _("Subordinate user id count"),
7e1b55
diff --git a/ipaserver/plugins/subid.py b/ipaserver/plugins/subid.py
7e1b55
new file mode 100644
7e1b55
index 0000000000000000000000000000000000000000..7d9a2f33e84bc7cdf17900346343e49d5eda0d8c
7e1b55
--- /dev/null
7e1b55
+++ b/ipaserver/plugins/subid.py
7e1b55
@@ -0,0 +1,608 @@
7e1b55
+#
7e1b55
+# Copyright (C) 2021  FreeIPA Contributors see COPYING for license
7e1b55
+#
7e1b55
+
7e1b55
+import random
7e1b55
+import uuid
7e1b55
+
7e1b55
+from ipalib import api
7e1b55
+from ipalib import constants
7e1b55
+from ipalib import errors
7e1b55
+from ipalib import output
7e1b55
+from ipalib.plugable import Registry
7e1b55
+from ipalib.parameters import Int, Str
7e1b55
+from ipalib.request import context
7e1b55
+from ipalib.text import _, ngettext
7e1b55
+from ipapython.dn import DN
7e1b55
+
7e1b55
+from .baseldap import (
7e1b55
+    LDAPObject,
7e1b55
+    LDAPCreate,
7e1b55
+    LDAPDelete,
7e1b55
+    LDAPUpdate,
7e1b55
+    LDAPSearch,
7e1b55
+    LDAPRetrieve,
7e1b55
+    LDAPQuery,
7e1b55
+    DNA_MAGIC,
7e1b55
+)
7e1b55
+
7e1b55
+__doc__ = _(
7e1b55
+    """
7e1b55
+Subordinate ids
7e1b55
+
7e1b55
+Manage subordinate user and group ids for users
7e1b55
+
7e1b55
+EXAMPLES:
7e1b55
+
7e1b55
+ Auto-assign a subordinate id range to current user
7e1b55
+   ipa subid-generate
7e1b55
+
7e1b55
+ Auto-assign a subordinate id range to user alice:
7e1b55
+   ipa subid-generate --owner=alice
7e1b55
+
7e1b55
+ Find subordinate ids for user alice:
7e1b55
+   ipa subid-find --owner=alice
7e1b55
+
7e1b55
+ Match entry by any subordinate uid in range:
7e1b55
+   ipa subid-match --subuid=2147483649
7e1b55
+"""
7e1b55
+)
7e1b55
+
7e1b55
+register = Registry()
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid(LDAPObject):
7e1b55
+    """Subordinate id object."""
7e1b55
+
7e1b55
+    container_dn = api.env.container_subids
7e1b55
+
7e1b55
+    object_name = _("Subordinate id")
7e1b55
+    object_name_plural = _("Subordinate ids")
7e1b55
+    label = _("Subordinate ids")
7e1b55
+    label_singular = _("Subordinate id")
7e1b55
+
7e1b55
+    object_class = ["ipasubordinateidentry"]
7e1b55
+    possible_objectclasses = [
7e1b55
+        "ipasubordinategid",
7e1b55
+        "ipasubordinateuid",
7e1b55
+        "ipasubordinateid",
7e1b55
+    ]
7e1b55
+    default_attributes = [
7e1b55
+        "ipauniqueid",
7e1b55
+        "ipaowner",
7e1b55
+        "ipasubuidnumber",
7e1b55
+        "ipasubuidcount",
7e1b55
+        "ipasubgidnumber",
7e1b55
+        "ipasubgidcount",
7e1b55
+    ]
7e1b55
+    allow_rename = False
7e1b55
+
7e1b55
+    permission_filter_objectclasses_string = (
7e1b55
+        "(objectclass=ipasubordinateidentry)"
7e1b55
+    )
7e1b55
+    managed_permissions = {
7e1b55
+        # all authenticated principals can read subordinate id information
7e1b55
+        "System: Read Subordinate Id Attributes": {
7e1b55
+            "ipapermbindruletype": "all",
7e1b55
+            "ipapermright": {"read", "search", "compare"},
7e1b55
+            "ipapermtargetfilter": [
7e1b55
+                permission_filter_objectclasses_string,
7e1b55
+            ],
7e1b55
+            "ipapermdefaultattr": {
7e1b55
+                "objectclass",
7e1b55
+                "ipauniqueid",
7e1b55
+                "description",
7e1b55
+                "ipaowner",
7e1b55
+                "ipasubuidnumber",
7e1b55
+                "ipasubuidcount",
7e1b55
+                "ipasubgidnumber",
7e1b55
+                "ipasubgidcount",
7e1b55
+            },
7e1b55
+        },
7e1b55
+        "System: Read Subordinate Id Count": {
7e1b55
+            "ipapermbindruletype": "all",
7e1b55
+            "ipapermright": {"read", "search", "compare"},
7e1b55
+            "ipapermtargetfilter": [],
7e1b55
+            "ipapermtarget": DN(container_dn, api.env.basedn),
7e1b55
+            "ipapermdefaultattr": {"numSubordinates"},
7e1b55
+        },
7e1b55
+        # user administrators can remove subordinate ids or update the
7e1b55
+        # ipaowner attribute. This enables user admins to remove users
7e1b55
+        # with assigned subids or move them to staging area (--preserve).
7e1b55
+        "System: Manage Subordinate Ids": {
7e1b55
+            "ipapermright": {"write"},
7e1b55
+            "ipapermtargetfilter": [
7e1b55
+                permission_filter_objectclasses_string,
7e1b55
+            ],
7e1b55
+            "ipapermdefaultattr": {
7e1b55
+                "description",
7e1b55
+                "ipaowner",  # allow user admins to preserve users
7e1b55
+            },
7e1b55
+            "default_privileges": {"User Administrators"},
7e1b55
+        },
7e1b55
+        "System: Remove Subordinate Ids": {
7e1b55
+            "ipapermright": {"delete"},
7e1b55
+            "ipapermtargetfilter": [
7e1b55
+                permission_filter_objectclasses_string,
7e1b55
+            ],
7e1b55
+            "default_privileges": {"User Administrators"},
7e1b55
+        },
7e1b55
+    }
7e1b55
+
7e1b55
+    takes_params = (
7e1b55
+        Str(
7e1b55
+            "ipauniqueid",
7e1b55
+            cli_name="id",
7e1b55
+            label=_("Unique ID"),
7e1b55
+            primary_key=True,
7e1b55
+            flags={"optional_create"},
7e1b55
+        ),
7e1b55
+        Str(
7e1b55
+            "description?",
7e1b55
+            cli_name="desc",
7e1b55
+            label=_("Description"),
7e1b55
+            doc=_("Subordinate id description"),
7e1b55
+        ),
7e1b55
+        Str(
7e1b55
+            "ipaowner",
7e1b55
+            cli_name="owner",
7e1b55
+            label=_("Owner"),
7e1b55
+            doc=_("Owning user of subordinate id entry"),
7e1b55
+            flags={"no_update"},
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
+            flags={"no_update"},
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"},  # auto-assigned
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"},  # auto-assigned
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"},  # auto-assigned
7e1b55
+            minvalue=constants.SUBID_COUNT,
7e1b55
+            maxvalue=constants.SUBID_COUNT,
7e1b55
+        ),
7e1b55
+    )
7e1b55
+
7e1b55
+    def fixup_objectclass(self, entry_attrs):
7e1b55
+        """Add missing object classes to entry"""
7e1b55
+        has_subuid = "ipasubuidnumber" in entry_attrs
7e1b55
+        has_subgid = "ipasubgidnumber" in entry_attrs
7e1b55
+
7e1b55
+        candicates = set(self.object_class)
7e1b55
+        if has_subgid:
7e1b55
+            candicates.add("ipasubordinategid")
7e1b55
+        if has_subuid:
7e1b55
+            candicates.add("ipasubordinateuid")
7e1b55
+        if has_subgid and has_subuid:
7e1b55
+            candicates.add("ipasubordinateid")
7e1b55
+
7e1b55
+        entry_oc = entry_attrs.setdefault("objectclass", [])
7e1b55
+        current_oc = {x.lower() for x in entry_oc}
7e1b55
+        for oc in candicates.difference(current_oc):
7e1b55
+            entry_oc.append(oc)
7e1b55
+
7e1b55
+    def handle_duplicate_entry(self, *keys):
7e1b55
+        if hasattr(context, "subid_owner_dn"):
7e1b55
+            uid = context.subid_owner_dn[0].value
7e1b55
+            msg = _(
7e1b55
+                '%(oname)s with with name "%(pkey)s" or for user "%(uid)s" '
7e1b55
+                "already exists."
7e1b55
+            ) % {
7e1b55
+                "uid": uid,
7e1b55
+                "pkey": keys[-1] if keys else "",
7e1b55
+                "oname": self.object_name,
7e1b55
+            }
7e1b55
+            raise errors.DuplicateEntry(message=msg) from None
7e1b55
+        else:
7e1b55
+            super().handle_duplicate_entry(*keys)
7e1b55
+
7e1b55
+    def convert_owner(self, entry_attrs, options):
7e1b55
+        """Change owner from DN to uid string"""
7e1b55
+        if not options.get("raw", False) and "ipaowner" in entry_attrs:
7e1b55
+            userobj = self.api.Object.user
7e1b55
+            entry_attrs["ipaowner"] = [
7e1b55
+                userobj.get_primary_key_from_dn(entry_attrs["ipaowner"][0])
7e1b55
+            ]
7e1b55
+
7e1b55
+    def get_owner_dn(self, *keys, **options):
7e1b55
+        """Get owning user entry entry (username or DN)"""
7e1b55
+        owner = keys[-1]
7e1b55
+        userobj = self.api.Object.user
7e1b55
+        if isinstance(owner, DN):
7e1b55
+            # it's already a DN, validate it's either an active or preserved
7e1b55
+            # user. Ref integrity plugin checks that it's not a dangling DN.
7e1b55
+            user_dns = (
7e1b55
+                DN(userobj.active_container_dn, self.api.env.basedn),
7e1b55
+                DN(userobj.delete_container_dn, self.api.env.basedn),
7e1b55
+            )
7e1b55
+            if not owner.endswith(user_dns):
7e1b55
+                raise errors.ValidationError(
7e1b55
+                    name="ipaowner",
7e1b55
+                    error=_("'%(dn)s is not a valid user") % {"dn": owner},
7e1b55
+                )
7e1b55
+            return owner
7e1b55
+
7e1b55
+        # similar to user.get_either_dn() but with error reporting and
7e1b55
+        # returns an entry
7e1b55
+        ldap = self.backend
7e1b55
+        try:
7e1b55
+            active_dn = userobj.get_dn(owner, **options)
7e1b55
+            entry = ldap.get_entry(active_dn, attrs_list=[])
7e1b55
+            return entry.dn
7e1b55
+        except errors.NotFound:
7e1b55
+            # fall back to deleted user
7e1b55
+            try:
7e1b55
+                delete_dn = userobj.get_delete_dn(owner, **options)
7e1b55
+                entry = ldap.get_entry(delete_dn, attrs_list=[])
7e1b55
+                return entry.dn
7e1b55
+            except errors.NotFound:
7e1b55
+                raise userobj.handle_not_found(owner)
7e1b55
+
7e1b55
+    def handle_subordinate_ids(self, ldap, dn, entry_attrs):
7e1b55
+        """Handle ipaSubordinateId object class"""
7e1b55
+        new_subuid = entry_attrs.single_value.get("ipasubuidnumber")
7e1b55
+        new_subgid = entry_attrs.single_value.get("ipasubgidnumber")
7e1b55
+
7e1b55
+        if new_subuid is None:
7e1b55
+            new_subuid = DNA_MAGIC
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" not in entry_attrs:
7e1b55
+            _entry_attrs = ldap.get_entry(dn, ["objectclass"])
7e1b55
+            entry_attrs["objectclass"] = _entry_attrs["objectclass"]
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
+        self.fixup_objectclass(entry_attrs)
7e1b55
+
7e1b55
+    def get_subid_match_candidate_filter(
7e1b55
+        self,
7e1b55
+        ldap,
7e1b55
+        *,
7e1b55
+        subuid,
7e1b55
+        subgid,
7e1b55
+        extra_filters=(),
7e1b55
+        offset=None,
7e1b55
+    ):
7e1b55
+        """Create LDAP filter to locate matching/overlapping subids"""
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
+        return (
7e1b55
+            constants.SUBID_RANGE_START
7e1b55
+            + random.randint(1, 32764 - 2) * constants.SUBID_COUNT
7e1b55
+        )
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_add(LDAPCreate):
7e1b55
+    __doc__ = _("Add a new subordinate id.")
7e1b55
+    msg_summary = _('Added subordinate id "%(value)s"')
7e1b55
+
7e1b55
+    # internal command, use subid-auto to auto-assign subids
7e1b55
+    NO_CLI = True
7e1b55
+
7e1b55
+    def pre_callback(
7e1b55
+        self, ldap, dn, entry_attrs, attrs_list, *keys, **options
7e1b55
+    ):
7e1b55
+        # XXX let ref integrity plugin validate DN?
7e1b55
+        owner_dn = self.obj.get_owner_dn(entry_attrs["ipaowner"], **options)
7e1b55
+        context.subid_owner_dn = owner_dn
7e1b55
+        entry_attrs["ipaowner"] = owner_dn
7e1b55
+
7e1b55
+        self.obj.handle_subordinate_ids(ldap, dn, entry_attrs)
7e1b55
+        attrs_list.append("objectclass")
7e1b55
+
7e1b55
+        return dn
7e1b55
+
7e1b55
+    def execute(self, ipauniqueid=None, **options):
7e1b55
+        if ipauniqueid is None:
7e1b55
+            ipauniqueid = str(uuid.uuid4())
7e1b55
+        return super().execute(ipauniqueid, **options)
7e1b55
+
7e1b55
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
7e1b55
+        self.obj.convert_owner(entry_attrs, options)
7e1b55
+        return super(subid_add, self).post_callback(
7e1b55
+            ldap, dn, entry_attrs, *keys, **options
7e1b55
+        )
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_del(LDAPDelete):
7e1b55
+    __doc__ = _("Delete a subordinate id.")
7e1b55
+    msg_summary = _('Deleted subordinate id "%(value)s"')
7e1b55
+
7e1b55
+    # internal command, subids cannot be removed
7e1b55
+    NO_CLI = True
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_mod(LDAPUpdate):
7e1b55
+    __doc__ = _("Modify a subordinate id.")
7e1b55
+    msg_summary = _('Modified subordinate id "%(value)s"')
7e1b55
+
7e1b55
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
7e1b55
+        self.obj.convert_owner(entry_attrs, options)
7e1b55
+        return super(subid_mod, self).post_callback(
7e1b55
+            ldap, dn, entry_attrs, *keys, **options
7e1b55
+        )
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_find(LDAPSearch):
7e1b55
+    __doc__ = _("Search for subordinate id.")
7e1b55
+    msg_summary = ngettext(
7e1b55
+        "%(count)d subordinate id matched",
7e1b55
+        "%(count)d subordinate ids matched",
7e1b55
+        0,
7e1b55
+    )
7e1b55
+
7e1b55
+    def pre_callback(
7e1b55
+        self, ldap, filters, attrs_list, base_dn, scope, *args, **options
7e1b55
+    ):
7e1b55
+        attrs_list.append("objectclass")
7e1b55
+        return super(subid_find, self).pre_callback(
7e1b55
+            ldap, filters, attrs_list, base_dn, scope, *args, **options
7e1b55
+        )
7e1b55
+
7e1b55
+    def args_options_2_entry(self, *args, **options):
7e1b55
+        entry_attrs = super(subid_find, self).args_options_2_entry(
7e1b55
+            *args, **options
7e1b55
+        )
7e1b55
+        owner = entry_attrs.get("ipaowner")
7e1b55
+        if owner is not None:
7e1b55
+            owner_dn = self.obj.get_owner_dn(owner, **options)
7e1b55
+            entry_attrs["ipaowner"] = owner_dn
7e1b55
+        return entry_attrs
7e1b55
+
7e1b55
+    def post_callback(self, ldap, entries, truncated, *args, **options):
7e1b55
+        for entry in entries:
7e1b55
+            self.obj.convert_owner(entry, options)
7e1b55
+        return super(subid_find, self).post_callback(
7e1b55
+            ldap, entries, truncated, *args, **options
7e1b55
+        )
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_show(LDAPRetrieve):
7e1b55
+    __doc__ = _("Display information about a subordinate id.")
7e1b55
+
7e1b55
+    def pre_callback(self, ldap, dn, attrs_list, *keys, **options):
7e1b55
+        attrs_list.append("objectclass")
7e1b55
+        return super(subid_show, self).pre_callback(
7e1b55
+            ldap, dn, attrs_list, *keys, **options
7e1b55
+        )
7e1b55
+
7e1b55
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
7e1b55
+        self.obj.convert_owner(entry_attrs, options)
7e1b55
+        return super(subid_show, self).post_callback(
7e1b55
+            ldap, dn, entry_attrs, *keys, **options
7e1b55
+        )
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_generate(LDAPQuery):
7e1b55
+    __doc__ = _(
7e1b55
+        "Generate and auto-assign subuid and subgid range to user entry"
7e1b55
+    )
7e1b55
+
7e1b55
+    has_output = output.standard_entry
7e1b55
+
7e1b55
+    takes_options = LDAPQuery.takes_options + (
7e1b55
+        Str(
7e1b55
+            "ipaowner?",
7e1b55
+            cli_name="owner",
7e1b55
+            label=_("Owner"),
7e1b55
+            doc=_("Owning user of subordinate id entry"),
7e1b55
+        ),
7e1b55
+    )
7e1b55
+
7e1b55
+    def get_args(self):
7e1b55
+        return []
7e1b55
+
7e1b55
+    def execute(self, *keys, **options):
7e1b55
+        owner_uid = options.get("ipaowner")
7e1b55
+        # default to current user
7e1b55
+        if owner_uid is None:
7e1b55
+            owner_dn = DN(self.api.Backend.ldap2.conn.whoami_s()[4:])
7e1b55
+            # validate it's a user and not a service or host
7e1b55
+            owner_dn = self.obj.get_owner_dn(owner_dn)
7e1b55
+            owner_uid = owner_dn[0].value
7e1b55
+
7e1b55
+        return self.api.Command.subid_add(
7e1b55
+            description="auto-assigned subid",
7e1b55
+            ipaowner=owner_uid,
7e1b55
+            version=options["version"],
7e1b55
+        )
7e1b55
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_match(subid_find):
7e1b55
+    __doc__ = _("Match users by any subordinate uid in their range")
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,
7e1b55
+            subuid=options["ipasubuidnumber"],
7e1b55
+            subgid=None,
7e1b55
+        )
7e1b55
+        attrs_list.extend(self.obj.default_attributes)
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
+
7e1b55
+
7e1b55
+@register()
7e1b55
+class subid_stats(LDAPQuery):
7e1b55
+    __doc__ = _("Subordinate id statistics")
7e1b55
+
7e1b55
+    takes_options = ()
7e1b55
+    has_output = (
7e1b55
+        output.summary,
7e1b55
+        output.Entry("result"),
7e1b55
+    )
7e1b55
+
7e1b55
+    def get_args(self):
7e1b55
+        return ()
7e1b55
+
7e1b55
+    def get_remaining_dna(self, ldap, **options):
7e1b55
+        base_dn = DN(
7e1b55
+            self.api.env.container_dna_subordinate_ids, self.api.env.basedn
7e1b55
+        )
7e1b55
+        entries, _truncated = ldap.find_entries(
7e1b55
+            "(objectClass=dnaSharedConfig)",
7e1b55
+            attrs_list=["dnaRemainingValues"],
7e1b55
+            base_dn=base_dn,
7e1b55
+            scope=ldap.SCOPE_ONELEVEL,
7e1b55
+        )
7e1b55
+        return sum(
7e1b55
+            int(entry.single_value["dnaRemainingValues"]) for entry in entries
7e1b55
+        )
7e1b55
+
7e1b55
+    def get_idrange(self, ldap, **options):
7e1b55
+        cn = f"{self.api.env.realm}_subid_range"
7e1b55
+        result = self.api.Command.idrange_show(cn, version=options["version"])
7e1b55
+        baseid = int(result["result"]["ipabaseid"][0])
7e1b55
+        rangesize = int(result["result"]["ipaidrangesize"][0])
7e1b55
+        return baseid, rangesize
7e1b55
+
7e1b55
+    def get_subid_assigned(self, ldap, **options):
7e1b55
+        dn = DN(self.api.env.container_subids, self.api.env.basedn)
7e1b55
+        entry = ldap.get_entry(dn=dn, attrs_list=["numSubordinates"])
7e1b55
+        return int(entry.single_value["numSubordinates"])
7e1b55
+
7e1b55
+    def execute(self, *keys, **options):
7e1b55
+        ldap = self.obj.backend
7e1b55
+        dna_remaining = self.get_remaining_dna(ldap, **options)
7e1b55
+        baseid, rangesize = self.get_idrange(ldap, **options)
7e1b55
+        assigned_subids = self.get_subid_assigned(ldap, **options)
7e1b55
+        remaining_subids = dna_remaining // constants.SUBID_COUNT
7e1b55
+        return dict(
7e1b55
+            summary=_("%(remaining)i remaining subordinate id ranges")
7e1b55
+            % {
7e1b55
+                "remaining": remaining_subids,
7e1b55
+            },
7e1b55
+            result=dict(
7e1b55
+                baseid=baseid,
7e1b55
+                rangesize=rangesize,
7e1b55
+                dna_remaining=dna_remaining,
7e1b55
+                assigned_subids=assigned_subids,
7e1b55
+                remaining_subids=remaining_subids,
7e1b55
+            ),
7e1b55
+        )
7e1b55
diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py
7e1b55
index f89b3ad5d9c994fe1ceb3da560fde7cc5bf5155a..19d07e6d61a451a0b1177adf2cf8ae1b7fceeb67 100644
7e1b55
--- a/ipaserver/plugins/user.py
7e1b55
+++ b/ipaserver/plugins/user.py
7e1b55
@@ -51,8 +51,6 @@ from .baseuser import (
7e1b55
     baseuser_remove_principal,
7e1b55
     baseuser_add_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
@@ -205,8 +203,6 @@ 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
@@ -670,6 +666,17 @@ class user_add(baseuser_add):
7e1b55
                 # if both randompassword and userpassword options were used
7e1b55
                 pass
7e1b55
 
7e1b55
+        # generate subid
7e1b55
+        default_subid = config.single_value.get(
7e1b55
+            'ipaUserDefaultSubordinateId', 'FALSE'
7e1b55
+        )
7e1b55
+        if default_subid == 'TRUE':
7e1b55
+            result = self.api.Command.subid_generate(
7e1b55
+                ipaowner=entry_attrs.single_value['uid'],
7e1b55
+                version=options['version']
7e1b55
+            )
7e1b55
+            entry_attrs["memberOf"].append(result['result']['dn'])
7e1b55
+
7e1b55
         self.obj.get_preserved_attribute(entry_attrs, options)
7e1b55
 
7e1b55
         self.post_common_callback(ldap, dn, entry_attrs, *keys, **options)
7e1b55
@@ -757,7 +764,9 @@ class user_del(baseuser_del):
7e1b55
         # of OTP tokens.
7e1b55
         check_protected_member(keys[-1])
7e1b55
 
7e1b55
-        if not options.get('preserve', False):
7e1b55
+        preserve = options.get('preserve', False)
7e1b55
+
7e1b55
+        if not preserve:
7e1b55
             # Remove any ID overrides tied with this user
7e1b55
             try:
7e1b55
                 remove_ipaobject_overrides(self.obj.backend, self.obj.api, dn)
7e1b55
@@ -780,6 +789,15 @@ class user_del(baseuser_del):
7e1b55
             else:
7e1b55
                 self.api.Command.otptoken_del(token)
7e1b55
 
7e1b55
+        # XXX: preserving doesn't work yet, see subordinate-ids.md
7e1b55
+        # Delete all subid entries owned by this user.
7e1b55
+        results = self.api.Command.subid_find(ipaowner=owner)["result"]
7e1b55
+        for subid_entry in results:
7e1b55
+            subid_pkey = self.api.Object.subid.get_primary_key_from_dn(
7e1b55
+                subid_entry["dn"]
7e1b55
+            )
7e1b55
+            self.api.Command.subid_del(subid_pkey)
7e1b55
+
7e1b55
         return dn
7e1b55
 
7e1b55
     def execute(self, *keys, **options):
7e1b55
@@ -829,6 +847,7 @@ class user_mod(baseuser_mod):
7e1b55
         self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys,
7e1b55
                                  **options)
7e1b55
         validate_nsaccountlock(entry_attrs)
7e1b55
+        # TODO: forward uidNumber changes and rename to subids
7e1b55
         return dn
7e1b55
 
7e1b55
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
7e1b55
@@ -1311,13 +1330,3 @@ 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/test_integration/test_subids.py b/ipatests/test_integration/test_subids.py
7e1b55
index b462f22ac067f3e1e97ef3f6d63d4e14e4ae79af..48e58c26464f52605438afe865575e5ca4c8f1f8 100644
7e1b55
--- a/ipatests/test_integration/test_subids.py
7e1b55
+++ b/ipatests/test_integration/test_subids.py
7e1b55
@@ -17,6 +17,7 @@ class TestSubordinateId(IntegrationTest):
7e1b55
     topology = "star"
7e1b55
 
7e1b55
     def _parse_result(self, result):
7e1b55
+        # ipa CLI should get an --outform json option
7e1b55
         info = {}
7e1b55
         for line in result.stdout_text.split("\n"):
7e1b55
             line = line.strip()
7e1b55
@@ -42,39 +43,69 @@ class TestSubordinateId(IntegrationTest):
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
+    def assert_subid_info(self, uid, info):
7e1b55
+        assert info["ipauniqueid"]
7e1b55
+        basedn = self.master.domain.basedn
7e1b55
+        assert info["ipaowner"] == f"uid={uid},cn=users,cn=accounts,{basedn}"
7e1b55
+        assert info["ipasubuidnumber"] == info["ipasubuidnumber"]
7e1b55
+        assert info["ipasubuidnumber"] >= SUBID_RANGE_START
7e1b55
+        assert info["ipasubuidnumber"] <= SUBID_RANGE_MAX
7e1b55
+        assert info["ipasubuidcount"] == SUBID_COUNT
7e1b55
+        assert info["ipasubgidnumber"] == info["ipasubgidnumber"]
7e1b55
+        assert info["ipasubgidnumber"] == info["ipasubuidnumber"]
7e1b55
+        assert info["ipasubgidcount"] == SUBID_COUNT
7e1b55
+
7e1b55
+    def assert_subid(self, uid, *, match):
7e1b55
+        cmd = ["ipa", "subid-find", "--raw", "--owner", uid]
7e1b55
+        result = self.master.run_command(cmd, raiseonerr=False)
7e1b55
+        if not match:
7e1b55
+            assert result.returncode >= 1
7e1b55
+            if result.returncode == 1:
7e1b55
+                assert "0 subordinate ids matched" in result.stdout_text
7e1b55
+            elif result.returncode == 2:
7e1b55
+                assert "user not found" in result.stderr_text
7e1b55
+            return None
7e1b55
+        else:
7e1b55
+            assert result.returncode == 0
7e1b55
+            assert "1 subordinate id matched" in result.stdout_text
7e1b55
+            info = self._parse_result(result)
7e1b55
+            self.assert_subid_info(uid, info)
7e1b55
+            self.master.run_command(
7e1b55
+                ["ipa", "subid-show", info["ipauniqueid"]]
7e1b55
+            )
7e1b55
+            return info
7e1b55
 
7e1b55
-    def user_auto_subid(self, uid, **kwargs):
7e1b55
-        cmd = ["ipa", "user-auto-subid", uid]
7e1b55
+    def subid_generate(self, uid, **kwargs):
7e1b55
+        cmd = ["ipa", "subid-generate"]
7e1b55
+        if uid is not None:
7e1b55
+            cmd.extend(("--owner", uid))
7e1b55
         return self.master.run_command(cmd, **kwargs)
7e1b55
 
7e1b55
-    def test_auto_subid(self):
7e1b55
-        tasks.kinit_admin(self.master)
7e1b55
+    def test_auto_generate_subid(self):
7e1b55
         uid = "testuser_auto1"
7e1b55
-        tasks.user_add(self.master, uid)
7e1b55
-        info = self.get_user(uid)
7e1b55
-        assert "ipasubuidcount" not in info
7e1b55
+        passwd = "Secret123"
7e1b55
+        tasks.create_active_user(self.master, uid, password=passwd)
7e1b55
 
7e1b55
-        self.user_auto_subid(uid)
7e1b55
-        info = self.get_user(uid)
7e1b55
-        assert "ipasubuidcount" in info
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+        self.assert_subid(uid, match=False)
7e1b55
+
7e1b55
+        # add subid by name
7e1b55
+        self.subid_generate(uid)
7e1b55
+        info = self.assert_subid(uid, match=True)
7e1b55
+
7e1b55
+        # second generate fails due to unique index on ipaowner
7e1b55
+        result = self.subid_generate(uid, raiseonerr=False)
7e1b55
+        assert result.returncode > 0
7e1b55
+        assert f'for user "{uid}" already exists' in result.stderr_text
7e1b55
 
7e1b55
+        # check matching
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
+        for offset in (0, 1, 65535):
7e1b55
+            result = self.master.run_command(
7e1b55
+                ["ipa", "subid-match", f"--subuid={subuid + offset}", "--raw"]
7e1b55
+            )
7e1b55
+            match = self._parse_result(result)
7e1b55
+            self.assert_subid_info(uid, match)
7e1b55
 
7e1b55
     def test_ipa_subid_script(self):
7e1b55
         tasks.kinit_admin(self.master)
7e1b55
@@ -85,34 +116,28 @@ class TestSubordinateId(IntegrationTest):
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
+            self.assert_subid(uid, match=False)
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
+            self.assert_subid(uid, match=True)
7e1b55
 
7e1b55
     def test_subid_selfservice(self):
7e1b55
-        tasks.kinit_admin(self.master)
7e1b55
-
7e1b55
-        uid = "testuser_selfservice1"
7e1b55
+        uid1 = "testuser_selfservice1"
7e1b55
+        uid2 = "testuser_selfservice2"
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
+        tasks.create_active_user(self.master, uid1, password=password)
7e1b55
+        tasks.create_active_user(self.master, uid2, password=password)
7e1b55
+
7e1b55
+        tasks.kinit_user(self.master, uid1, password=password)
7e1b55
+        self.assert_subid(uid1, match=False)
7e1b55
+        result = self.subid_generate(uid1, raiseonerr=False)
7e1b55
+        assert result.returncode > 0
7e1b55
+        result = self.subid_generate(None, raiseonerr=False)
7e1b55
         assert result.returncode > 0
7e1b55
 
7e1b55
         tasks.kinit_admin(self.master)
7e1b55
@@ -121,10 +146,14 @@ class TestSubordinateId(IntegrationTest):
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
+            tasks.kinit_user(self.master, uid1, password)
7e1b55
+            self.subid_generate(uid1)
7e1b55
+            self.assert_subid(uid1, match=True)
7e1b55
+
7e1b55
+            # add subid from whoami
7e1b55
+            tasks.kinit_as_user(self.master, uid2, password=password)
7e1b55
+            self.subid_generate(None)
7e1b55
+            self.assert_subid(uid2, match=True)
7e1b55
         finally:
7e1b55
             tasks.kinit_admin(self.master)
7e1b55
             self.master.run_command(
7e1b55
@@ -140,45 +169,46 @@ class TestSubordinateId(IntegrationTest):
7e1b55
         password = "Secret123"
7e1b55
 
7e1b55
         # create user administrator
7e1b55
-        tasks.user_add(self.master, uid_useradmin, password=password)
7e1b55
+        tasks.create_active_user(
7e1b55
+            self.master, uid_useradmin, password=password
7e1b55
+        )
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
+        tasks.kinit_user(self.master, uid_useradmin, password)
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
+        self.subid_generate(uid)
7e1b55
+
7e1b55
+        # test that user admin can preserve and delete users with subids
7e1b55
+        self.master.run_command(["ipa", "user-del", "--preserve", uid])
7e1b55
+        # XXX does not work, see subordinate-ids.md
7e1b55
+        # subid should still exist
7e1b55
+        # self.assert_subid(uid, match=True)
7e1b55
+        # final delete should remove the user and subid
7e1b55
+        self.master.run_command(["ipa", "user-del", uid])
7e1b55
+        self.assert_subid(uid, match=False)
7e1b55
+
7e1b55
+    def tset_subid_auto_assign(self):
7e1b55
         tasks.kinit_admin(self.master)
7e1b55
+        uid = "testuser_autoassign_user1"
7e1b55
 
7e1b55
-        result = self.master.run_command(
7e1b55
-            ["ipa", "config-show", "--raw", "--all"]
7e1b55
+        self.master.run_command(
7e1b55
+            ["ipa", "config-mod", "--user-default-subid=true"]
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
+        try:
7e1b55
+            tasks.user_add(self.master, uid)
7e1b55
+            self.assert_subid(uid, match=True)
7e1b55
+        finally:
7e1b55
+            self.master.run_command(
7e1b55
+                ["ipa", "config-mod", "--user-default-subid=false"]
7e1b55
+            )
7e1b55
 
7e1b55
     def test_idrange_subid(self):
7e1b55
         tasks.kinit_admin(self.master)
7e1b55
@@ -199,3 +229,7 @@ class TestSubordinateId(IntegrationTest):
7e1b55
         assert info["ipanttrusteddomainsid"].startswith(
7e1b55
             "S-1-5-21-738065-838566-"
7e1b55
         )
7e1b55
+
7e1b55
+    def test_subid_stats(self):
7e1b55
+        tasks.kinit_admin(self.master)
7e1b55
+        self.master.run_command(["ipa", "subid-stats"])
7e1b55
-- 
7e1b55
2.26.3
7e1b55