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