Add basic support for subordinate user/group ids

New LDAP object class "ipaUserSubordinate" with four new fields:
- ipasubuidnumber / ipasubuidcount
- ipasubgidnumber / ipasgbuidcount

New self-service permission to add subids.

New command user-auto-subid to auto-assign subid

The code hard-codes counts to 65536, sets subgid equal to subuid, and
does not allow removal of subids. There is also a hack that emulates a
DNA plugin with step interval 65536 for testing.

Work around problem with older SSSD clients that fail with unknown
idrange type "ipa-local-subid", see: https://github.com/SSSD/sssd/issues/5571

Related: https://pagure.io/freeipa/issue/8361
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Francois Cami <fcami@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
This commit is contained in:
Christian Heimes 2021-03-19 11:48:38 +01:00 committed by Rob Crittenden
parent fbee1549d3
commit 1c4ae37293
31 changed files with 1564 additions and 77 deletions

1
.gitignore vendored
View File

@ -199,6 +199,7 @@ install/tools/ipa-restore
install/tools/ipa-server-certinstall
install/tools/ipa-server-install
install/tools/ipa-server-upgrade
install/tools/ipa-subids
install/tools/ipa-winsync-migrate
ipatests/i18n.py
ipatests/ipa-run-tests

View File

@ -375,7 +375,7 @@ aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber
dn: dc=ipa,dc=example
aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=compat,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read User Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";)
dn: cn=users,cn=accounts,dc=ipa,dc=example
aci: (targetattr = "ipasshpubkey || ipauniqueid || ipauserauthtype || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User IPA Attributes";allow (compare,read,search) userdn = "ldap:///all";)
aci: (targetattr = "ipasshpubkey || ipasubgidcount || ipasubgidnumber || ipasubuidcount || ipasubuidnumber || ipauniqueid || ipauserauthtype || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User IPA Attributes";allow (compare,read,search) userdn = "ldap:///all";)
dn: cn=users,cn=accounts,dc=ipa,dc=example
aci: (targetattr = "krbcanonicalname || krblastpwdchange || krbpasswordexpiration || krbprincipalaliases || krbprincipalexpiration || krbprincipalname || krbprincipaltype || nsaccountlock")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Kerberos Attributes";allow (compare,read,search) userdn = "ldap:///all";)
dn: cn=users,cn=accounts,dc=ipa,dc=example

47
API.txt
View File

@ -4974,7 +4974,7 @@ output: Entry('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: stageuser_add/1
args: 1,45,3
args: 1,46,3
arg: Str('uid', cli_name='login')
option: Str('addattr*', cli_name='addattr')
option: Flag('all', autofill=True, cli_name='all', default=False)
@ -4992,6 +4992,7 @@ option: Str('givenname', cli_name='first')
option: Str('homedirectory?', cli_name='homedir')
option: Str('initials?', autofill=True)
option: Str('ipasshpubkey*', cli_name='sshpubkey')
option: Int('ipasubuidnumber?', cli_name='subuid')
option: Str('ipatokenradiusconfiglink?', cli_name='radius')
option: Str('ipatokenradiususername?', cli_name='radius_username')
option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
@ -5080,7 +5081,7 @@ output: Output('result', type=[<type 'dict'>])
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: ListOfPrimaryKeys('value')
command: stageuser_find/1
args: 1,58,4
args: 1,60,4
arg: Str('criteria?')
option: Flag('all', autofill=True, cli_name='all', default=False)
option: Str('carlicense*', autofill=False)
@ -5104,6 +5105,8 @@ option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir')
option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:'])
option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid')
option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
@ -5145,7 +5148,7 @@ output: ListOfEntries('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: Output('truncated', type=[<type 'bool'>])
command: stageuser_mod/1
args: 1,51,3
args: 1,52,3
arg: Str('uid', cli_name='login')
option: Str('addattr*', cli_name='addattr')
option: Flag('all', autofill=True, cli_name='all', default=False)
@ -5167,6 +5170,7 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d
option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey')
option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
@ -6058,7 +6062,7 @@ output: Entry('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: user_add/1
args: 1,46,3
args: 1,47,3
arg: Str('uid', cli_name='login')
option: Str('addattr*', cli_name='addattr')
option: Flag('all', autofill=True, cli_name='all', default=False)
@ -6075,6 +6079,7 @@ option: Str('givenname', cli_name='first')
option: Str('homedirectory?', cli_name='homedir')
option: Str('initials?', autofill=True)
option: Str('ipasshpubkey*', cli_name='sshpubkey')
option: Int('ipasubuidnumber?', cli_name='subuid')
option: Str('ipatokenradiusconfiglink?', cli_name='radius')
option: Str('ipatokenradiususername?', cli_name='radius_username')
option: StrEnum('ipauserauthtype*', cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
@ -6156,6 +6161,16 @@ option: Str('version?')
output: Entry('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
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=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: user_del/1
args: 1,3,3
arg: Str('uid+', cli_name='login')
@ -6180,7 +6195,7 @@ output: Output('result', type=[<type 'bool'>])
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: user_find/1
args: 1,61,4
args: 1,63,4
arg: Str('criteria?')
option: Flag('all', autofill=True, cli_name='all', default=False)
option: Str('carlicense*', autofill=False)
@ -6204,6 +6219,8 @@ option: Str('ipanthomedirectory?', autofill=False, cli_name='smb_home_dir')
option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_drive', values=[u'A:', u'B:', u'C:', u'D:', u'E:', u'F:', u'G:', u'H:', u'I:', u'J:', u'K:', u'L:', u'M:', u'N:', u'O:', u'P:', u'Q:', u'R:', u'S:', u'T:', u'U:', u'V:', u'W:', u'X:', u'Y:', u'Z:'])
option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
option: Int('ipasubgidnumber?', autofill=False, cli_name='subgid')
option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
@ -6247,8 +6264,23 @@ output: Output('count', type=[<type 'int'>])
output: ListOfEntries('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: Output('truncated', type=[<type 'bool'>])
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=[<type 'int'>])
output: ListOfEntries('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: Output('truncated', type=[<type 'bool'>])
command: user_mod/1
args: 1,52,3
args: 1,53,3
arg: Str('uid', cli_name='login')
option: Str('addattr*', cli_name='addattr')
option: Flag('all', autofill=True, cli_name='all', default=False)
@ -6270,6 +6302,7 @@ option: StrEnum('ipanthomedirectorydrive?', autofill=False, cli_name='smb_home_d
option: Str('ipantlogonscript?', autofill=False, cli_name='smb_logon_script')
option: Str('ipantprofilepath?', autofill=False, cli_name='smb_profile_path')
option: Str('ipasshpubkey*', autofill=False, cli_name='sshpubkey')
option: Int('ipasubuidnumber?', autofill=False, cli_name='subuid')
option: Str('ipatokenradiusconfiglink?', autofill=False, cli_name='radius')
option: Str('ipatokenradiususername?', autofill=False, cli_name='radius_username')
option: StrEnum('ipauserauthtype*', autofill=False, cli_name='user_auth_type', values=[u'password', u'radius', u'otp', u'pkinit', u'hardened'])
@ -7183,10 +7216,12 @@ default: user_add_cert/1
default: user_add_certmapdata/1
default: user_add_manager/1
default: user_add_principal/1
default: user_auto_subid/1
default: user_del/1
default: user_disable/1
default: user_enable/1
default: user_find/1
default: user_match_subid/1
default: user_mod/1
default: user_remove_cert/1
default: user_remove_certmapdata/1

View File

@ -229,7 +229,7 @@ fasttest: $(GENERATED_PYTHON_FILES) ipasetup.py
--ignore $(abspath $(top_srcdir))/ipatests/test_integration \
--ignore $(abspath $(top_srcdir))/ipatests/test_xmlrpc
fastlint: $(GENERATED_PYTHON_FILES) ipasetup.py
fastlint: $(GENERATED_PYTHON_FILES) ipasetup.py acilint apilint
if ! WITH_PYLINT
@echo "ERROR: pylint not available"; exit 1
endif

View File

@ -86,8 +86,8 @@ define(IPA_DATA_VERSION, 20100614120000)
# #
########################################################
define(IPA_API_VERSION_MAJOR, 2)
define(IPA_API_VERSION_MINOR, 242)
# Last change: add status options for cert-find
# Last change: add subordinate id feature
define(IPA_API_VERSION_MINOR, 243)
########################################################

View File

@ -19,3 +19,4 @@ FreeIPA design documentation
hidden-replicas.md
disable-stale-users.md
ldapi-autobind-services.md
subordinate-ids.md

View File

@ -0,0 +1,468 @@
# Central management of subordinate user and group ids
Subordinate ids are a Linux Kernel feature to grant a user additional
user and group id ranges. Amongst others the feature can be used
by container runtime engies to implement rootless containers.
Traditionally subordinate id ranges are configured in ``/etc/subuid``
and ``/etc/subgid``.
To make rootless containers in a large environment as easy as pie, IPA
gains the ability to centrally manage and assign subordinate id ranges.
SSSD and shadow-util are extended to read subordinate ids from IPA and
provide them to userspace tools.
## Overview
Feature requests
* [FreeIPA feature request #8361](https://pagure.io/freeipa/issue/8361)
* [SSSD feature request #5197](https://github.com/SSSD/sssd/issues/5197)
* [shadow-util feature request #154](https://github.com/shadow-maint/shadow/issues/154)
* [389-DS RFE for DNA plugin rhbz#1938239](https://bugzilla.redhat.com/show_bug.cgi?id=1938239)
Man pages
* [man subuid(5)](https://man7.org/linux/man-pages/man5/subuid.5.html)
* [man subgid(5)](https://man7.org/linux/man-pages/man5/subgid.5.html)
* [man user_namespaces(7)](https://man7.org/linux/man-pages/man7/user_namespaces.7.html)
* [man newuidmap(1)](https://man7.org/linux/man-pages/man1/newuidmap.1.html)
Articles / blog posts
* [Basic Setup and Use of Podman in a Rootless environment](https://github.com/containers/podman/blob/master/docs/tutorials/rootless_tutorial.md)
* [How does rootless Podman work](https://opensource.com/article/19/2/how-does-rootless-podman-work)
## Design choices
Some design choices are owed to the circumstance that uids and gids
are limited datatypes. The Linux Kernel and userland defines
``uid_t`` and ``gid_t`` as unsigned 32bit integers (``uint32_t``), which
limits possible values for numeric user and group ids to
``0 .. 2^32-2``. ``(uid_t)-1`` is reserved for error reporting. On the
other hand the user ``nobody`` typically has uid 65534 / gid 65534. This
means we need to assign 65,536 subordinate ids to every user. The
theoretical maximum amount of subordinate ranges is less than 65,536
(``65536 * 65536 == 2^32``). [``logins.def``](https://man7.org/linux/man-pages/man5/login.defs.5.html)
also uses 65536 as default setting for ``SUB_UID_COUNT``.
The practical limit is far smaller. Subordinate ids should not overlap
with system accounts, local user accounts, IPA user accounts, and
mapped accounts from Active Directory. Therefore IPA uses the upper
half of the uid_t range (>= 2^31 == 2,147,483,648) for subordinate ids.
The high bit is rarely used. IPA limits general numeric ids
(``uidNumber``, ``gidNumber``, ID ranges) to maximum values of signed
32bit integer (2^31-1) for backwards compatibility with XML-RPC.
``logins.def`` defaults to ``SUB_UID_MAX`` 600,100,000.
A default subordinate id count of 65,536 and a total range of approx.
2.1 billion limits IPA to slightly more than 32,000 possible ranges. It
may sound like a lot of users, but there are much bigger installations
of IPA. For comparison Fedora Accounts has over 120,000 users stored in
IPA.
For that reason we treat subordinate id space as premium real estate
and don't auto-map or auto-assign subordinate ids by default. Instead
we give the admin several options to assign them manually, semi-manual,
or automatically.
### Revision 1 limitation
The first revision of the feature is deliberately limited and
restricted. We are aiming for a simple implementation that covers
basic use cases. Some restrictions may be lifted in the future.
* subuid and subgids cannot be set independently. They are always set
to the same value.
* counts are hard-coded to value 65536
* once assigned subids cannot be removed
* IPA does not support multiple subordinate id ranges. Contrary to
``/etc/subuid``, users are limited to one set of subordinate ids.
* subids are auto-assigned. Auto-assignment is currently emulated
until 389-DS has been extended to support DNA with step interval.
* subids are allocated from hard-coded range
``[2147483648..4294901767]`` (``2^31`` to ``2^32-1-65536``), which
is the upper 2.1 billion uids of ``uid_t`` (``uint32_t``). The range
can hold little 32,767 subordinate id ranges.
* Active Directory support is out of scope and may be provided in the
future.
### Subid assignment example
```
>>> import itertools
>>> def subids():
... for n in itertools.count(start=0):
... start = SUBID_RANGE_START + (n * SUBID_COUNT)
... last = start + SUBID_COUNT - 1
... yield (start, last)
...
>>> gen = subids()
>>> next(gen)
(2147483648, 2147549183)
>>> next(gen)
(2147549184, 2147614719)
>>> next(gen)
(2147614720, 2147680255)
```
The first user has 65565 subordinate ids from uid/gid ``2147483648``
to ``2147549183``, the next user has ``2147549184`` to ``2147614719``,
and so on. The range count includes the start value.
An installation with multiple servers, 389-DS'
[DNA](https://directory.fedoraproject.org/docs/389ds/design/dna-plugin.html)
plug-in takes care of delegating and assigning chunks of subid ranges
to servers. The DNA plug-in guarantees uniqueness across servers.
## LDAP
### LDAP schema extension
The subordinate id feature introduces a new auxiliar object class
``ipaSubordinateId`` with four required attributes ``ipaSubUidNumber``,
``ipaSubUidCount``, ``ipaSubGidNumber``, and ``ipaSubGidCount``. The
attributes with ``number`` suffix store the start value of the interval.
The ``count`` attributes contain the size of the interval including the
start value. The maximum subid is
``ipaSubUidNumber + ipaSubUidCount - 1``.
All four attributes are single-value ``INTEGER`` type with standard
integer matching rules. OIDs ``2.16.840.1.113730.3.8.23.8`` and
``2.16.840.1.113730.3.8.23.11`` are reserved for future use.
```raw
attributeTypes: (
2.16.840.1.113730.3.8.23.6
NAME 'ipaSubUidNumber'
DESC 'Numerical subordinate user ID (range start value)'
EQUALITY integerMatch ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
X-ORIGIN 'IPA v4.9'
)
attributeTypes: (
2.16.840.1.113730.3.8.23.7
NAME 'ipaSubUidCount'
DESC 'Subordinate user ID count (range size)'
EQUALITY integerMatch ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
X-ORIGIN 'IPA v4.9'
)
attributeTypes: (
2.16.840.1.113730.3.8.23.9
NAME 'ipaSubGidNumber'
DESC 'Numerical subordinate group ID (range start value)'
EQUALITY integerMatch ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
X-ORIGIN 'IPA v4.9'
)
attributeTypes: (
2.16.840.1.113730.3.8.23.10
NAME 'ipaSubGidCount'
DESC 'Subordinate group ID count (range size)'
EQUALITY integerMatch ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
X-ORIGIN 'IPA v4.9'
)
```
The ``ipaSubordinateId`` object class is an auxiliar subclass of
``top`` and requires all four subordinate id attributes as well as
``uidNumber``. It does not subclass ``posixAccount`` to make
the class reusable in idview overrides later.
```raw
objectClasses: (
2.16.840.1.113730.3.8.24.4
NAME 'ipaSubordinateId'
DESC 'Subordinate uid and gid for users'
SUP top AUXILIARY
MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount )
X-ORIGIN 'IPA v4.9'
)
```
The ``ipaSubordinateGid`` and ``ipaSubordinateUid`` are defined for
future use. IPA always assumes the presence of ``ipaSubordinateId`` and
does not use these object classes.
```raw
objectClasses: (
2.16.840.1.113730.3.8.24.2
NAME 'ipaSubordinateUid'
DESC 'Subordinate uids for users, see subuid(5)'
SUP top AUXILIARY
MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount )
X-ORIGIN 'IPA v4.9'
)
objectClasses: (
2.16.840.1.113730.3.8.24.3
NAME 'ipaSubordinateGid'
DESC 'Subordinate gids for users, see subgid(5)'
SUP top AUXILIARY
MUST ( uidNumber $ ipaSubGidNumber $ ipaSubGidCount )
X-ORIGIN 'IPA v4.9'
)
```
### Index
The attributes ``ipaSubUidNumber`` and ``ipaSubGidNumber`` are index
for ``pres`` and ``eq`` with ``nsMatchingRule: integerOrderingMatch``
to enable efficient ``=``, ``>=``, and ``<=`` searches.
### Distributed numeric assignment (DNA) plug-in extension
Subordinate id auto-assignment requires an extension of 389-DS'
[DNA](https://directory.fedoraproject.org/docs/389ds/design/dna-plugin.html)
plug-in. The DNA plug-in is responsible for safely assigning unique
numeric ids across all replicas.
Currently the DNA plug-in only supports a step size of ``1``. A new
option ``dnaStepAttr`` (name is tentative) will tell the DNA plug-in
to use the value of entry attributes as step size.
## Permissions, Privileges, Roles
### Self-servive RBAC
The self-service permission enables users to request auto-assignment
of subordinate uid and gid ranges for themselves. Subordinate ids cannot
be modified or deleted.
* ACI: *selfservice: Add subordinate id*
* Permission: *Self-service subordinate ID*
* Privilege: *Subordinate ID Selfservice User*
* Role: *Subordinate ID Selfservice Users*
* role default member: n/a
### Administrator RBAC
The administrator permission allows privileged users to auto-assign
subordinate ids to users. Once assigned subordinate ids cannot
be modified or deleted.
* ACI: *Add subordinate ids to any user*
* Permission: *Manage subordinate ID*
* Privilege: *Subordinate ID Administrators*
* default privilege role: *User Administrator*
## Workflows
In the default configuration of IPA, neither existing users nor new
users will have subordinate ids assigned. There are a couple of ways
to assign subordinate ids to users.
### User administrator
Users with *User Administrator* role and members of the *admins* group
have permission to auto-assign new subordinate ids to any user. Auto
assignment can be performed with new ``user-auto-subid`` command on the
command line or with the *Auto assign subordinate ids* action in the
*Actions* drop-down menu in the web UI.
```shell
$ ipa user-auto-subid someusername
```
### Self-service for group members
Ordinary users cannot self-service subordinate ids by default. Admins
can assign the new *Subordinate ID Selfservice User* to users group to
enable self-service for members of the group.
For example to enable self-service for all members of the default user
group ``ipausers``, do:
```shell
$ ipa role-add-member "Subordinate ID Selfservice User" --groups=ipausers
```
This allows members of ``ipausers`` to request subordinate ids with
the ``user-auto-subid`` command or the *Auto assign subordinate ids*
action in the web UI.
```shell
$ ipa user-auto-subid myusername
```
### Auto assignment with user default object class
Admins can also enable auto-assignment of subordinate ids for all new
users by adding ``ipasubordinateid`` as a default user objectclass.
This can be accomplished in the web UI under "IPA Server" /
"Configuration" / "Default user objectclasses" or on the command line
with:
```shell
$ ipa config-mod --addattr="ipaUserObjectClasses=ipasubordinateid"
```
**NOTE:** The objectclass must be written all lower case.
### ipa-subid tool
Finally IPA includes a new tool for mass-assignment of subordinate ids.
The command uses automatic LDAPI EXTERNAL bind when it's executed as
root user. Other it requires valid Kerberos TGT of an admin or user
administrator.
```raw
# /usr/libexec/ipa/ipa-subids --help
Usage: ipa-subids
Mass-assign subordinate ids
Options:
--version show program's version number and exit
-h, --help show this help message and exit
--group=GROUP Filter by group membership
--filter=USER_FILTER Raw LDAP filter
--dry-run Dry run mode.
--all-users All users
Logging and output options:
-v, --verbose print debugging information
-d, --debug alias for --verbose (deprecated)
-q, --quiet output only errors
--log-file=FILE log to the given file
# # /usr/libexec/ipa/ipa-subids --group ipausers
Processing user 'testsubordinated1' (1/15)
Processing user 'testsubordinated2' (2/15)
Processing user 'testsubordinated3' (3/15)
Processing user 'testsubordinated4' (4/15)
Processing user 'testsubordinated5' (5/15)
Processing user 'testsubordinated6' (6/15)
Processing user 'testsubordinated7' (7/15)
Processing user 'testsubordinated8' (8/15)
Processing user 'testsubordinated9' (9/15)
Processing user 'testsubordinated10' (10/15)
Processing user 'testsubordinated11' (11/15)
Processing user 'testsubordinated12' (12/15)
Processing user 'testsubordinated13' (13/15)
Processing user 'testsubordinated14' (14/15)
Processing user 'testsubordinated15' (15/15)
Processed 15 user(s)
The ipa-subids command was successful
```
### Find and match users by any subordinate id
The ``user-find`` command search by start value of subordinate uid and
gid range. The new command ``user-match-subid`` can be used to find a
user by any subordinate id in their range.
```raw
$ ipa user-match-subid --subuid=2153185287
User login: asmith
First name: Alice
Last name: Smith
...
SubUID range start: 2153185280
SubUID range size: 65536
SubGID range start: 2153185280
SubGID range size: 65536
----------------------------
Number of entries returned 1
----------------------------
$ ipa user-match-subid --subuid=2153185279
User login: bjones
First name: Bob
Last name: Jones
...
SubUID range start: 2153119744
SubUID range size: 65536
SubGID range start: 2153119744
SubGID range size: 65536
----------------------------
Number of entries returned 1
----------------------------
```
## SSSD integration
* base: ``cn=accounts,$SUFFIX`` / ``cn=users,cn=accounts,$SUFFIX``
* scope: ``SCOPE_SUBTREE`` (2) / ``SCOPE_ONELEVEL`` (1)
* user filter: should include ``(objectClass=posixAccount)``
* attributes: ``uidNumber ipaSubUidNumber ipaSubUidCount ipaSubGidNumber ipaSubGidCount``
SSSD can safely assume that only *user accounts* of type ``posixAccount``
have subordinate ids. In the first revision there are no other entries
with subordinate ids. The ``posixAccount`` object class has ``uid``
(user login name) and ``uidNumber`` (numeric user id) as mandatory
attributes. The ``uid`` attribute is guaranteed to be unique across
all user accounts in an IPA domain.
The ``uidNumber`` attribute is commonly unique, too. However it's
technically possible that an administrator has assigned the same
numeric user id to multiple users. Automatically assigned uid numbers
don't conflict. SSSD should treat multiple users with same numeric
user id as an error.
The attribute ``ipaSubUidNumber`` is always accompanied by
``ipaSubUidCount`` and ``ipaSubGidNumber`` is always accompanied
by ``ipaSubGidCount``. In revision 1 the presence of
``ipaSubUidNumber`` implies presence of the other three attributes.
All four subordinate id attributes and ``uidNumber`` are single-value
``INTEGER`` types. Any value outside of range of ``uint32_t`` must
treated as invalid. SSSD will never see the DNA magic value ``-1``
in ``cn=accounts,$SUFFIX`` subtree.
IPA recommends that SSSD simply extends its existing query for user
accounts and requests the four subordinate attributes additionally to
RFC 2307 attributes ``rfc2307_user_map``. SSSD can directly take the
values and return them without further processing, e.g.
``uidNumber:ipaSubUidNumber:ipaSubUidCount`` for ``/etc/subuid``.
Filters for additional cases:
* subuid filter (find user with subuid by numeric uid):
``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=$UID))``,
``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar
* subuid enumeration filter:
``&((objectClass=posixAccount)(ipaSubUidNumber=*)(uidNumber=*))``,
``(objectClass=ipaSubordinateId)``, or similar
* subgid filter (find user with subgid by numeric uid):
``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=$UID))``,
``(&(objectClass=ipaSubordinateId)(uidNumber=$UID))``, or similar
* subgid enumeration filter:
``&((objectClass=posixAccount)(ipaSubGidNumber=*)(uidNumber=*))``,
``(objectClass=ipaSubordinateId)``, or similar
## Implementation details
* The four subid attributes are not included in
``baseuser.default_attributes`` on purpose. The ``config-mod``
command does not permit removal of a user default objectclasses
when the class is the last provider of an attribute in
``default_attributes``.
* ``ipaSubordinateId`` object class does not subclass the other two
object classes. LDAP supports
``SUP ( ipaSubordinateGid $ ipaSubordinateUid )`` but 389-DS only
auto-inherits from first object class.
* The idrange entry ``$REALM_subid_range`` has preconfigured base RIDs
and SID so idrange plug-in and sidgen task ignore the entry. It's the
simplest approach to ensure backwards compatibility with older IPA
server versions that don't know how to handle the new range.
The SID is ``S-1-5-21-738065-838566-$DOMAIN_HASH``. ``S-1-5-21``
is the well-known SID prefix for domain SIDs. ``738065-838566`` is
the decimal representation of the string ``IPA-SUB``. ``DOMAIN_HASH``
is the MURMUR-3 hash of the domain name for key ``0xdeadbeef``. SSSD
rejects SIDs unless they are prefixed with ``S-1-5-21`` (see
``sss_idmap.c:is_domain_sid()``).
* The new ``$REALM_subid_range`` entry uses range type ``ipa-ad-trust``
instead of range type ``ipa-local-subid`` for backwards compatibility
with older SSSD clients, see
[SSSD #5571](https://github.com/SSSD/sssd/issues/5571).
* Shared DNA configuration entries in ``cn=dna,cn=ipa,cn=etc,$SUFFIX``
are automatically removed by existing code. Server and replication
plug-ins search and delete entries by ``dnaHostname`` attribute.
### TODO
* enable configuration for ``dnaStepAttr``
* remove ``fake_dna_plugin`` hack from ``baseuser`` plug-in.
* add custom range type for idranges and teach AD trust, sidgen, and
range overlap check code to deal with new range type.

View File

@ -1361,6 +1361,7 @@ fi
%{_libexecdir}/ipa/ipa-pki-wait-running
%{_libexecdir}/ipa/ipa-otpd
%{_libexecdir}/ipa/ipa-print-pac
%{_libexecdir}/ipa/ipa-subids
%dir %{_libexecdir}/ipa/custodia
%attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-dmldap
%attr(755,root,root) %{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat

View File

@ -3,6 +3,7 @@
## Attributes: 2.16.840.1.113730.3.8.3 - V2 base attributres
## ObjectClasses: 2.16.840.1.113730.3.8.4 - V2 base objectclasses
## Attributes: 2.16.840.1.113730.3.8.23 - V4 base attributes
## ObjectClasses: 2.16.840.1.113730.3.8.24 - V4 base objectclasses
##
dn: cn=schema
attributeTypes: (2.16.840.1.113730.3.8.3.1 NAME 'ipaUniqueID' DESC 'Unique identifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )

View File

@ -0,0 +1,19 @@
## IPA Base OID: 2.16.840.1.113730.3.8
##
## Attributes: 2.16.840.1.113730.3.8.23 - V4 base attributes
## ObjectClasses: 2.16.840.1.113730.3.8.24 - V4 base objectclasses
##
dn: cn=schema
# subordinate ids
# range ceiling OIDs are reserved for future use (operational attribute?)
# object class requires uidNumber but does not subclass posixAccount so we
# can re-use the object class in idview overrides later.
attributeTypes: ( 2.16.840.1.113730.3.8.23.6 NAME 'ipaSubUidNumber' DESC 'Numerical subordinate user ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
attributeTypes: ( 2.16.840.1.113730.3.8.23.7 NAME 'ipaSubUidCount' DESC 'Subordinate user ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
# attributeTypes: ( 2.16.840.1.113730.3.8.23.8 NAME 'ipaSubUidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
attributeTypes: ( 2.16.840.1.113730.3.8.23.9 NAME 'ipaSubGidNumber' DESC 'Numerical subordinate group ID (range start value)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
attributeTypes: ( 2.16.840.1.113730.3.8.23.10 NAME 'ipaSubGidCount' DESC 'Subordinate group ID count (range size)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
# attributeTypes: ( 2.16.840.1.113730.3.8.23.11 NAME 'ipaSubGidCeiling' DESC 'Numerical subordinate user ID ceiling (largest value in range)' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.9')
objectClasses: (2.16.840.1.113730.3.8.24.2 NAME 'ipaSubordinateUid' DESC 'Subordinate uids for users, see subuid(5)' SUP top AUXILIARY MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount ) X-ORIGIN 'IPA v4.9')
objectClasses: (2.16.840.1.113730.3.8.24.3 NAME 'ipaSubordinateGid' DESC 'Subordinate gids for users, see subgid(5)' SUP top AUXILIARY MUST ( uidNumber $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9')
objectClasses: (2.16.840.1.113730.3.8.24.4 NAME 'ipaSubordinateId' DESC 'Subordinate uid and gid for users' SUP top AUXILIARY MUST ( uidNumber $ ipaSubUidNumber $ ipaSubUidCount $ ipaSubGidNumber $ ipaSubGidCount ) X-ORIGIN 'IPA v4.9')

View File

@ -16,6 +16,7 @@ dist_app_DATA = \
60ipaconfig.ldif \
60basev2.ldif \
60basev3.ldif \
60basev4.ldif \
60ipadns.ldif \
60ipapk11.ldif \
60certificate-profiles.ldif \

View File

@ -167,6 +167,12 @@ objectClass: nsContainer
objectClass: top
cn: posix-ids
dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
changetype: add
objectClass: nsContainer
objectClass: top
cn: subordinate-ids
dn: cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX
changetype: add
objectClass: nsContainer
@ -476,6 +482,22 @@ ipaBaseID: $IDSTART
ipaIDRangeSize: $IDRANGE_SIZE
ipaRangeType: ipa-local
dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX
changetype: add
objectClass: top
objectClass: ipaIDrange
objectClass: ipaTrustedADDomainRange
cn: ${REALM}_subid_range
ipaBaseID: eval($SUBID_RANGE_START)
ipaIDRangeSize: eval($SUBID_RANGE_SIZE)
# HACK: RIDs to work around adtrust sidgen issue
ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE)
# 738065-838566 = IPA-SUB
ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH
# HACK: "ipa-local-subid" range type causes issues with older SSSD clients
# see https://github.com/SSSD/sssd/issues/5571
ipaRangeType: ipa-ad-trust
dn: cn=ca,$SUFFIX
changetype: add
objectClass: nsContainer

View File

@ -16,6 +16,26 @@ dnaThreshold: 500
dnaSharedCfgDN: cn=posix-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
dnaExcludeScope: cn=provisioning,$SUFFIX
dn: cn=Subordinate IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
changetype: add
objectclass: top
objectclass: extensibleObject
cn: Subordinate IDs
dnaType: ipasubuidnumber
dnaType: ipasubgidnumber
dnaNextValue: eval($SUBID_RANGE_START)
dnaMaxValue: eval($SUBID_RANGE_MAX)
dnaMagicRegen: -1
dnaFilter: (objectClass=ipaSubordinateId)
dnaScope: $SUFFIX
dnaThreshold: eval($SUBID_DNA_THRESHOLD)
# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr
# dnaStepAttr: ipaSubUidCount
# dnaStepAttr: ipaSubGidCount
# dnaStepAllowedValues: eval($SUBID_COUNT)
dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
dnaExcludeScope: cn=provisioning,$SUFFIX
# Enable the DNA plugin
dn: cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
changetype: modify

View File

@ -38,6 +38,7 @@ dist_noinst_DATA = \
ipa-pki-retrieve-key.in \
ipa-pki-wait-running.in \
ipa-acme-manage.in \
ipa-subids.in \
$(NULL)
nodist_sbin_SCRIPTS = \
@ -78,6 +79,7 @@ nodist_app_SCRIPTS = \
ipa-httpd-pwdreader \
ipa-pki-retrieve-key \
ipa-pki-wait-running \
ipa-subids \
$(NULL)
PYTHON_SHEBANG = \

View File

@ -0,0 +1,8 @@
#!/usr/bin/python3
#
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
from ipaserver.install.ipa_subids import IPASubids
IPASubids.run_cli()

View File

@ -259,6 +259,33 @@ return {
}
]
},
{
name: 'subordinate',
label: '@i18n:objects.subordinate.identity',
fields: [
{
name: 'ipasubuidnumber',
label: '@i18n:objects.subordinate.subuidnumber',
read_only: true
},
{
name: 'ipasubuidcount',
label: '@i18n:objects.subordinate.subuidcount',
read_only: true
},
{
name: 'ipasubgidnumber',
label: '@i18n:objects.subordinate.subgidnumber',
read_only: true
},
{
name: 'ipasubgidcount',
label: '@i18n:objects.subordinate.subgidcount',
read_only: true
}
]
},
{
name: 'pwpolicy',
label: '@i18n:objects.pwpolicy.identity',
@ -451,6 +478,16 @@ return {
enable_cond: ['is-locked'],
confirm_msg: '@i18n:objects.user.unlock_confirm'
},
{
$factory: IPA.object_action,
name: 'auto_subid',
method: 'auto_subid',
label: '@i18n:objects.user.auto_subid',
needs_confirm: true,
hide_cond: ['preserved-user'],
enable_cond: ['no-subid'],
confirm_msg: '@i18n:objects.user.auto_subid_confirm'
},
{
$type: 'automember_rebuild',
name: 'automember_rebuild',
@ -461,12 +498,22 @@ return {
$type: 'cert_request',
hide_cond: ['preserved-user'],
title: '@i18n:objects.cert.issue_for_user'
},
{
$factory: IPA.object_action,
name: 'auto_subid',
method: 'auto_subid',
label: '@i18n:objects.user.auto_subid',
needs_confirm: true,
hide_cond: ['preserved-user'],
enable_cond: ['no-subid'],
confirm_msg: '@i18n:objects.user.auto_subid_confirm'
}
],
header_actions: [
'reset_password', 'enable', 'disable', 'stage', 'undel',
'delete_active_user', 'delete', 'unlock', 'add_otptoken',
'automember_rebuild', 'request_cert'
'automember_rebuild', 'request_cert', 'auto_subid'
],
state: {
evaluators: [
@ -1159,6 +1206,10 @@ IPA.user.is_locked_evaluator = function(spec) {
}
}
if (!user.ipasubuidnumber) {
that.state.push('no-subid');
}
that.notify_on_change(old_state);
};

View File

@ -272,6 +272,24 @@ add:nsIndexType: eq
add:nsIndexType: pres
add:nsIndexType: sub
dn: cn=ipaSubGidNumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
only:cn: ipaSubGidNumber
default:objectClass: nsIndex
default:objectClass: top
default:nsSystemIndex: false
add:nsIndexType: eq
add:nsIndexType: pres
add:nsMatchingRule: integerOrderingMatch
dn: cn=ipaSubUidNumber,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
only:cn: ipaSubUidNumber
default:objectClass: nsIndex
default:objectClass: top
default:nsSystemIndex: false
add:nsIndexType: eq
add:nsIndexType: pres
add:nsMatchingRule: integerOrderingMatch
dn: cn=ipasudorunasgroup,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
only:cn: ipasudorunasgroup
default:objectClass: nsIndex

View File

@ -0,0 +1,102 @@
# subordinate ids
# self-service RBAC
dn: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX
default:objectClass: groupofnames
default:objectClass: nestedgroup
default:objectClass: top
default:cn: Subordinate ID Selfservice User
default:description: User that can self-request subordiante ids
# default: member: cn=ipausers,cn=groups,cn=accounts,$SUFFIX
dn: cn=Subordinate ID Selfservice Users,cn=privileges,cn=pbac,$SUFFIX
default:objectClass: top
default:objectClass: groupofnames
default:objectClass: nestedgroup
default:cn: Subordinate ID Selfservice Users
default:description: Subordinate ID Selfservice User
default:member: cn=Subordinate ID Selfservice User,cn=roles,cn=accounts,$SUFFIX
dn: cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX
default:objectClass: top
default:objectClass: groupofnames
default:objectClass: ipapermission
default:cn: Self-service subordinate ID
default:ipapermissiontype: SYSTEM
default:member: cn=Subordinate ID Selfservice Users,cn=privileges,cn=pbac,$SUFFIX
# Administrator RBAC
dn: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX
default:objectClass: top
default:objectClass: groupofnames
default:objectClass: nestedgroup
default:cn: Subordinate ID Administrators
default:description: Subordinate ID Administrators
default:member: cn=User Administrator,cn=roles,cn=accounts,$SUFFIX
dn: cn=Manage subordinate ID,cn=permissions,cn=pbac,$SUFFIX
default:objectClass: top
default:objectClass: groupofnames
default:objectClass: ipapermission
default:cn: Manage subordinate ID
default:ipapermissiontype: SYSTEM
default:member: cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX
# ACIs (in domain database root so they also apply to staging area)
#
# - allow users to request new subid with DNA_MAGIC value, subid count=65536,
# and subgid == subuid.
# - allow user admins to set subids. count=65536 and subgid == subuid
# properties are enforced as wel.
#
# The delete-when-empty check is required because IPA uses MOD_REPLACE to
# set attributes, see https://github.com/389ds/389-ds-base/issues/4597.
#
# TODO: remove (ipasubuidnumber>=eval($SUBID_RANGE_START) from
# self-service permission when 389-DS' DNA plugin supports dnaStepAttr and
# fake_dna_plugin hack has been removed.
#
dn: $SUFFIX
add: aci: (targetfilter = "(objectclass=posixaccount)")(targattrfilters = "add=objectClass:(|(objectClass=ipasubordinateid)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=eval($SUBID_RANGE_START))(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=eval($SUBID_RANGE_START))(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "selfservice: Add subordinate id";allow (write) userdn = "ldap:///self" and groupdn="ldap:///cn=Self-service subordinate ID,cn=permissions,cn=pbac,$SUFFIX";)
add: aci: (targetfilter = "(objectclass=posixaccount)")(targattrfilters = "add=objectClass:(|(objectClass=ipasubordinateid)(objectClass=ipasubordinategid)(objectClass=ipasubordinateuid)) && ipasubuidnumber:(|(ipasubuidnumber>=1)(ipasubuidnumber=-1)) && ipasubuidcount:(ipasubuidcount=eval($SUBID_COUNT)) && ipasubgidnumber:(|(ipasubgidnumber>=1)(ipasubgidnumber=-1)) && ipasubgidcount:(ipasubgidcount=eval($SUBID_COUNT)), del=ipasubuidnumber:(!(ipasubuidnumber=*)) && ipasubuidcount:(!(ipasubuidcount=*)) && ipasubgidnumber:(!(ipasubgidnumber=*)) && ipasubgidcount:(!(ipasubgidcount=*))")(version 3.0;acl "Add subordinate ids to any user";allow (write) groupdn="ldap:///cn=Subordinate ID Administrators,cn=privileges,cn=pbac,$SUFFIX";)
# DNA plugin and idrange configuration
dn: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
default: objectClass: nsContainer
default: objectClass: top
default: cn: subordinate-ids
dn: cn=Subordinate IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
default: objectclass: top
default: objectclass: extensibleObject
default: cn: Subordinate IDs
default: dnaType: ipasubuidnumber
default: dnaType: ipasubgidnumber
default: dnaNextValue: eval($SUBID_RANGE_START)
default: dnaMaxValue: eval($SUBID_RANGE_MAX)
default: dnaMagicRegen: -1
default: dnaFilter: (objectClass=ipaSubordinateId)
default: dnaScope: $SUFFIX
default: dnaThreshold: eval($SUBID_DNA_THRESHOLD)
# TODO: enable when 389-DS' DNA plugin supports dnaStepAttr
# default: dnaStepAttr: ipaSubUidCount
# default: dnaStepAttr: ipaSubGidCount
# default: dnaStepAllowedValues: eval($SUBID_COUNT)
default: dnaSharedCfgDN: cn=subordinate-ids,cn=dna,cn=ipa,cn=etc,$SUFFIX
default: dnaExcludeScope: cn=provisioning,$SUFFIX
default: aci: (targetattr = "dnaNextRange || dnaNextValue || dnaMaxValue")(version 3.0;acl "permission:Modify DNA Range";allow (write) groupdn = "ldap:///cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX";)
default: aci: (targetattr = "cn || dnaMaxValue || dnaNextRange || dnaNextValue || dnaThreshold || dnaType || objectclass")(version 3.0;acl "permission:Read DNA Range";allow (read, search, compare) groupdn = "ldap:///cn=Read DNA Range,cn=permissions,cn=pbac,$SUFFIX";)
dn: cn=${REALM}_subid_range,cn=ranges,cn=etc,$SUFFIX
default: objectClass: top
default: objectClass: ipaIDrange
default: objectClass: ipaTrustedADDomainRange
default: cn: ${REALM}_subid_range
default: ipaBaseID: $SUBID_RANGE_START
default: ipaIDRangeSize: $SUBID_RANGE_SIZE
# HACK: RIDs to work around adtrust sidgen issue
default: ipaBaseRID: eval($SUBID_RANGE_START - $IDRANGE_SIZE)
default: ipaNTTrustedDomainSID: S-1-5-21-738065-838566-$DOMAIN_HASH
# HACK: "ipa-local-subid" range type causes issues with older SSSD clients
# see https://github.com/SSSD/sssd/issues/5571
default: ipaRangeType: ipa-ad-trust

View File

@ -62,6 +62,7 @@ app_DATA = \
71-idviews-sasl-mapping.update \
72-domainlevels.update \
73-custodia.update \
73-subid.update \
73-winsync.update \
73-certmap.update \
75-user-trust-attributes.update \

View File

@ -343,3 +343,16 @@ SOFTHSM_DNSSEC_TOKEN_LABEL = u'ipaDNSSEC'
# Apache's mod_ssl SSLVerifyDepth value (Maximum depth of CA
# Certificates in Client Certificate verification)
MOD_SSL_VERIFY_DEPTH = '5'
# subuid / subgid counts are hard-coded
# An interval of 65536 uids/gids is required to map nobody (65534).
SUBID_COUNT = 65536
# upper half of uid_t (uint32_t)
SUBID_RANGE_START = 2 ** 31
# theoretical max limit is UINT32_MAX-1 ((2 ** 32) - 2)
# We use a smaller value to keep the topmost subid interval unused.
SUBID_RANGE_MAX = (2 ** 32) - (2 * SUBID_COUNT)
SUBID_RANGE_SIZE = SUBID_RANGE_MAX - SUBID_RANGE_START
# threshold before DNA plugin requests a new range
SUBID_DNA_THRESHOLD = 500 * SUBID_COUNT

View File

@ -36,6 +36,7 @@ from ipaserver.install import service
from ipaserver.install import installutils
from ipaserver.install.replication import wait_for_task
from ipalib import errors, api
from ipalib.constants import SUBID_RANGE_START
from ipalib.util import normalize_zone
from ipapython.dn import DN
from ipapython import ipachangeconf
@ -352,12 +353,19 @@ class ADTRUSTInstance(service.Service):
DN(api.env.container_ranges, self.suffix),
ldap.SCOPE_ONELEVEL, "(objectclass=ipaDomainIDRange)")
# Filter out ranges where RID base is already set
no_rid_base_set = lambda r: not any((
r.single_value.get('ipaBaseRID'),
r.single_value.get('ipaSecondaryBaseRID')))
ranges_with_no_rid_base = []
for entry in ranges:
sv = entry.single_value
if sv.get('ipaBaseRID') or sv.get('ipaSecondaryBaseRID'):
# skip range where RID base is already set
continue
if sv.get('ipaRangeType') == 'ipa-local-subid':
# ignore subid ranges
continue
ranges_with_no_rid_base.append(entry)
ranges_with_no_rid_base = [r for r in ranges if no_rid_base_set(r)]
logger.debug(repr(ranges))
logger.debug(repr(ranges_with_no_rid_base))
# Return if no range is without RID base
if len(ranges_with_no_rid_base) == 0:
@ -384,6 +392,17 @@ class ADTRUSTInstance(service.Service):
"They have to differ at least by %d." % size)
raise RuntimeError("RID bases too close.\n")
# values above
if any(
v + size >= SUBID_RANGE_START
for v in (self.rid_base, self.secondary_rid_base)
):
self.print_msg(
"Ceiling of primary or secondary base is larger than "
f"start of subordinate id range {SUBID_RANGE_START}."
)
raise RuntimeError("RID bases overlap with SUBID range.\n")
# Modify the range
# If the RID bases would cause overlap with some other range,
# this will be detected by ipa-range-check DS plugin

View File

@ -23,7 +23,6 @@ from __future__ import print_function, absolute_import
import logging
import shutil
import os
import time
import tempfile
import fnmatch
@ -46,6 +45,7 @@ from ipaserver.install import certs
from ipaserver.install import replication
from ipaserver.install import sysupgrade
from ipaserver.install import upgradeinstance
from ipaserver.install import ldapupdate
from ipalib import api
from ipalib import errors
from ipalib import constants
@ -66,6 +66,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
"60ipaconfig.ldif",
"60basev2.ldif",
"60basev3.ldif",
"60basev4.ldif",
"60ipapk11.ldif",
"60ipadns.ldif",
"60certificate-profiles.ldif",
@ -214,6 +215,8 @@ class DsInstance(service.Service):
if realm_name:
self.suffix = ipautil.realm_to_suffix(self.realm)
self.serverid = ipaldap.realm_to_serverid(self.realm)
if self.domain is None:
self.domain = self.realm.lower()
self.__setup_sub_dict()
else:
self.suffix = DN()
@ -497,34 +500,22 @@ class DsInstance(service.Service):
def __setup_sub_dict(self):
server_root = find_server_root()
try:
idrange_size = self.idmax - self.idstart + 1
except TypeError:
idrange_size = None
self.sub_dict = dict(
FQDN=self.fqdn, SERVERID=self.serverid,
self.sub_dict = ldapupdate.get_sub_dict(
realm=self.realm,
domain=self.domain,
suffix=self.suffix,
fqdn=self.fqdn,
idstart=self.idstart,
idmax=self.idmax,
)
self.sub_dict.update(
DOMAIN_LEVEL=self.domainlevel,
SERVERID=self.serverid,
PASSWORD=self.dm_password,
RANDOM_PASSWORD=ipautil.ipa_generate_password(),
SUFFIX=self.suffix,
REALM=self.realm, USER=DS_USER,
SERVER_ROOT=server_root, DOMAIN=self.domain,
TIME=int(time.time()), IDSTART=self.idstart,
IDMAX=self.idmax, HOST=self.fqdn,
ESCAPED_SUFFIX=str(self.suffix),
USER=DS_USER,
GROUP=DS_GROUP,
IDRANGE_SIZE=idrange_size,
DOMAIN_LEVEL=self.domainlevel,
MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL,
MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL,
STRIP_ATTRS=" ".join(replication.STRIP_ATTRS),
EXCLUDES='(objectclass=*) $ EXCLUDE ' +
' '.join(replication.EXCLUDES),
TOTAL_EXCLUDES='(objectclass=*) $ EXCLUDE ' +
' '.join(replication.TOTAL_EXCLUDES),
DEFAULT_SHELL=platformconstants.DEFAULT_SHELL,
DEFAULT_ADMIN_SHELL=platformconstants.DEFAULT_ADMIN_SHELL,
SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT,
SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER,
SERVER_ROOT=server_root,
)
def __create_instance(self):

View File

@ -0,0 +1,154 @@
#
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
import logging
from ipalib import api
from ipalib import errors
from ipalib.facts import is_ipa_configured
from ipaplatform.paths import paths
from ipapython.admintool import AdminTool, ScriptError
from ipapython.dn import DN
from ipaserver.plugins.baseldap import DNA_MAGIC
logger = logging.getLogger(__name__)
class IPASubids(AdminTool):
command_name = "ipa-subids"
usage = "%prog [--group GROUP|--all-users]"
description = "Mass-assign subordinate ids to users"
@classmethod
def add_options(cls, parser):
super(IPASubids, cls).add_options(parser, debug_option=True)
parser.add_option(
"--group",
dest="group",
action="store",
default=None,
help="Updates members of a user group.",
)
parser.add_option(
"--all-users",
dest="all_users",
action="store_true",
default=False,
help="Update all users.",
)
parser.add_option(
"--filter",
dest="user_filter",
action="store",
default="(!(nsaccountlock=TRUE))",
help="Additional raw LDAP filter (default: active users).",
)
parser.add_option(
"--dry-run",
dest="dry_run",
action="store_true",
default=False,
help="Dry run mode.",
)
def validate_options(self, neends_root=False):
super().validate_options(needs_root=True)
opt = self.safe_options
if opt.all_users and opt.group:
raise ScriptError("--group and --all-users are mutually exclusive")
if not opt.all_users and not opt.group:
raise ScriptError("Either --group or --all-users required")
def get_group_info(self):
assert api.isdone("finalize")
group = self.safe_options.group
if group is None:
return None
try:
result = api.Command.group_show(group, no_members=True)
return result["result"]
except errors.NotFound:
raise ScriptError(f"Unknown users group '{group}'.")
def make_filter(self, groupinfo, user_filter):
filters = [
# only users with posixAccount
"(objectClass=posixAccount)",
# without subordinate ids
"(!(objectClass=ipaSubordinateId))",
]
if groupinfo is not None:
filters.append(
self.ldap2.make_filter({"memberof": groupinfo["dn"]})
)
if user_filter:
filters.append(user_filter)
return self.ldap2.combine_filters(filters, self.ldap2.MATCH_ALL)
def search_users(self, filters):
users_dn = DN(api.env.container_user, api.env.basedn)
attrs = ["objectclass", "uid", "uidnumber"]
logger.debug("basedn: %s", users_dn)
logger.debug("attrs: %s", attrs)
logger.debug("filter: %s", filters)
try:
entries = self.ldap2.get_entries(
base_dn=users_dn,
filter=filters,
attrs_list=attrs,
)
except errors.NotFound:
logger.debug("No entries found")
return []
else:
return entries
def run(self):
if not is_ipa_configured():
print("IPA is not configured.")
return 2
api.bootstrap(in_server=True, confdir=paths.ETC_IPA)
api.finalize()
api.Backend.ldap2.connect()
self.ldap2 = api.Backend.ldap2
user_obj = api.Object["user"]
dry_run = self.safe_options.dry_run
group_info = self.get_group_info()
filters = self.make_filter(
group_info, self.safe_options.user_filter
)
entries = self.search_users(filters)
total = len(entries)
logger.info("Found %i user(s) without subordinate ids", total)
total = len(entries)
for i, entry in enumerate(entries, start=1):
logger.info(
" Processing user '%s' (%i/%i)",
entry.single_value["uid"],
i,
total
)
user_obj.set_subordinate_ids(
self.ldap2, entry.dn, entry, DNA_MAGIC
)
if not dry_run:
self.ldap2.update_entry(entry)
if dry_run:
logger.info("Dry run mode, no user was modified")
else:
logger.info("Updated %s user(s)", total)
return 0
if __name__ == "__main__":
IPASubids.run_cli()

View File

@ -32,6 +32,7 @@ import os
import fnmatch
import warnings
from pysss_murmur import murmurhash3 # pylint: disable=no-name-in-module
import six
from ipapython import ipautil, ipaldap
@ -53,6 +54,54 @@ UPDATES_DIR=paths.UPDATES_DIR
UPDATE_SEARCH_TIME_LIMIT = 30 # seconds
def get_sub_dict(realm, domain, suffix, fqdn, idstart=None, idmax=None):
"""LDAP template substitution dict for installer and updater
"""
if idstart is None:
idrange_size = None
else:
idrange_size = idmax - idstart + 1
return dict(
REALM=realm,
DOMAIN=domain,
SUFFIX=suffix,
ESCAPED_SUFFIX=str(suffix),
FQDN=fqdn,
HOST=fqdn,
LIBARCH=paths.LIBARCH,
TIME=int(time.time()),
FIPS="#" if tasks.is_fips_enabled() else "",
# idstart, idmax, and idrange_size may be None
IDSTART=idstart,
IDMAX=idmax,
IDRANGE_SIZE=idrange_size,
SUBID_COUNT=constants.SUBID_COUNT,
SUBID_RANGE_START=constants.SUBID_RANGE_START,
SUBID_RANGE_SIZE=constants.SUBID_RANGE_SIZE,
SUBID_RANGE_MAX=constants.SUBID_RANGE_MAX,
SUBID_DNA_THRESHOLD=constants.SUBID_DNA_THRESHOLD,
DOMAIN_HASH=murmurhash3(domain, len(domain), 0xdeadbeef),
MAX_DOMAIN_LEVEL=constants.MAX_DOMAIN_LEVEL,
MIN_DOMAIN_LEVEL=constants.MIN_DOMAIN_LEVEL,
STRIP_ATTRS=" ".join(replication.STRIP_ATTRS),
EXCLUDES=(
'(objectclass=*) $ EXCLUDE ' + ' '.join(replication.EXCLUDES)
),
TOTAL_EXCLUDES=(
'(objectclass=*) $ EXCLUDE '
+ ' '.join(replication.TOTAL_EXCLUDES)
),
DEFAULT_SHELL=platformconstants.DEFAULT_SHELL,
DEFAULT_ADMIN_SHELL=platformconstants.DEFAULT_ADMIN_SHELL,
SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT,
SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER,
# uid / gid for autobind
NAMED_UID=platformconstants.NAMED_USER.uid,
NAMED_GID=platformconstants.NAMED_GROUP.gid,
)
def connect(ldapi=False, realm=None, fqdn=None):
"""Create a connection for updates"""
if ldapi:
@ -314,38 +363,33 @@ class LDAPUpdate:
ldap_uri=self.ldapuri
)
self.api.finalize()
self.create_connection()
# get ipa-local domain idrange settings
domain_range = f"{self.api.env.realm}_id_range"
try:
result = self.api.Command.idrange_show(domain_range)["result"]
except errors.NotFound:
idstart = None
idmax = None
else:
idstart = int(result['ipabaseid'][0])
idrange_size = int(result['ipaidrangesize'][0])
idmax = idstart + idrange_size - 1
default_sub = get_sub_dict(
realm=api.env.realm,
domain=api.env.domain,
suffix=api.env.basedn,
fqdn=api.env.host,
idstart=idstart,
idmax=idmax,
)
replication_plugin = (
installutils.get_replication_plugin_name(self.conn.get_entry)
)
default_sub["REPLICATION_PLUGIN"] = replication_plugin
default_sub = dict(
REALM=api.env.realm,
DOMAIN=api.env.domain,
SUFFIX=api.env.basedn,
ESCAPED_SUFFIX=str(api.env.basedn),
FQDN=api.env.host,
LIBARCH=paths.LIBARCH,
TIME=int(time.time()),
MIN_DOMAIN_LEVEL=str(constants.MIN_DOMAIN_LEVEL),
MAX_DOMAIN_LEVEL=str(constants.MAX_DOMAIN_LEVEL),
STRIP_ATTRS=" ".join(constants.REPL_AGMT_STRIP_ATTRS),
EXCLUDES="(objectclass=*) $ EXCLUDE %s" % (
" ".join(constants.REPL_AGMT_EXCLUDES)
),
TOTAL_EXCLUDES="(objectclass=*) $ EXCLUDE %s" % (
" ".join(constants.REPL_AGMT_TOTAL_EXCLUDES)
),
SELINUX_USERMAP_DEFAULT=platformconstants.SELINUX_USERMAP_DEFAULT,
SELINUX_USERMAP_ORDER=platformconstants.SELINUX_USERMAP_ORDER,
FIPS="#" if tasks.is_fips_enabled() else "",
# uid / gid for autobind
NAMED_UID=platformconstants.NAMED_USER.uid,
NAMED_GID=platformconstants.NAMED_GROUP.gid,
REPLICATION_PLUGIN=replication_plugin,
)
for k, v in default_sub.items():
self.sub_dict.setdefault(k, v)

View File

@ -17,9 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import random
import six
from ipalib import api, errors
from ipalib import api, errors, output, constants
from ipalib import (
Flag, Int, Password, Str, Bool, StrEnum, DateTime, DNParam)
from ipalib.parameters import Principal, Certificate
@ -27,13 +28,13 @@ from ipalib.plugable import Registry
from .baseldap import (
DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete,
LDAPRetrieve, LDAPAddAttribute, LDAPModAttribute, LDAPRemoveAttribute,
LDAPAddMember, LDAPRemoveMember,
LDAPQuery, LDAPAddMember, LDAPRemoveMember,
LDAPAddAttributeViaOption, LDAPRemoveAttributeViaOption,
add_missing_object_class)
add_missing_object_class, DNA_MAGIC, pkey_to_value, entry_to_dict
)
from ipaserver.plugins.service import (validate_realm, normalize_principal)
from ipalib.request import context
from ipalib import _
from ipalib.constants import PATTERN_GROUPUSER_NAME
from ipapython import kerberos
from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS
from ipapython.ipavalidate import Email
@ -161,7 +162,7 @@ class baseuser(LDAPObject):
possible_objectclasses = [
'meporiginentry', 'ipauserauthtypeclass', 'ipauser',
'ipatokenradiusproxyuser', 'ipacertmapobject',
'ipantuserattrs'
'ipantuserattrs', 'ipasubordinateid',
]
disallow_object_classes = ['krbticketpolicyaux']
permission_filter_objectclasses = ['posixaccount']
@ -175,13 +176,15 @@ class baseuser(LDAPObject):
'krbprincipalexpiration', 'usercertificate;binary',
'krbprincipalname', 'krbcanonicalname',
'ipacertmapdata', 'ipantlogonscript', 'ipantprofilepath',
'ipanthomedirectory', 'ipanthomedirectorydrive'
'ipanthomedirectory', 'ipanthomedirectorydrive',
]
search_display_attributes = [
'uid', 'givenname', 'sn', 'homedirectory', 'krbcanonicalname',
'krbprincipalname', 'loginshell',
'mail', 'telephonenumber', 'title', 'nsaccountlock',
'uidnumber', 'gidnumber', 'sshpubkeyfp',
'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber',
'ipasubgidcount',
]
uuid_attribute = 'ipauniqueid'
attribute_members = {
@ -198,7 +201,7 @@ class baseuser(LDAPObject):
takes_params = (
Str('uid',
pattern=PATTERN_GROUPUSER_NAME,
pattern=constants.PATTERN_GROUPUSER_NAME,
pattern_errmsg='may only include letters, numbers, _, -, . and $',
maxlength=255,
cli_name='login',
@ -429,6 +432,41 @@ class baseuser(LDAPObject):
'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:',
'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:'),
),
Int(
'ipasubuidnumber?',
label=_('SubUID range start'),
cli_name='subuid',
doc=_('Start value for subordinate user ID (subuid) range'),
minvalue=constants.SUBID_RANGE_START,
maxvalue=constants.SUBID_RANGE_MAX,
),
Int(
'ipasubuidcount?',
label=_('SubUID range size'),
cli_name='subuidcount',
doc=_('Subordinate user ID count'),
flags={'no_create', 'no_update', 'no_search'},
minvalue=constants.SUBID_COUNT,
maxvalue=constants.SUBID_COUNT,
),
Int(
'ipasubgidnumber?',
label=_('SubGID range start'),
cli_name='subgid',
doc=_('Start value for subordinate group ID (subgid) range'),
flags={'no_create', 'no_update'},
minvalue=constants.SUBID_RANGE_START,
maxvalue=constants.SUBID_RANGE_MAX,
),
Int(
'ipasubgidcount?',
label=_('SubGID range size'),
cli_name='subgidcount',
doc=_('Subordinate group ID count'),
flags={'no_create', 'no_update', 'no_search'},
minvalue=constants.SUBID_COUNT,
maxvalue=constants.SUBID_COUNT,
),
)
def normalize_and_validate_email(self, email, config=None):
@ -526,6 +564,131 @@ class baseuser(LDAPObject):
except KeyError:
pass
def handle_subordinate_ids(self, ldap, dn, entry_attrs):
"""Handle ipaSubordinateId object class
"""
obj_classes = entry_attrs.get("objectclass")
new_subuid = entry_attrs.single_value.get("ipasubuidnumber")
new_subgid = entry_attrs.single_value.get("ipasubgidnumber")
# entry has object class ipaSubordinateId
# default to auto-assigment of subuids
if (
new_subuid is None
and obj_classes is not None
and self.has_objectclass(obj_classes, "ipasubordinateid")
):
new_subuid = DNA_MAGIC
# neither auto-assignment nor explicit assignment
if new_subuid is None:
# nothing to do
return False
# enforce subuid == subgid
if new_subgid is not None and new_subgid != new_subuid:
raise errors.ValidationError(
name="ipasubgidnumber",
error=_("subgidnumber must be equal to subuidnumber")
)
self.set_subordinate_ids(ldap, dn, entry_attrs, new_subuid)
return True
def set_subordinate_ids(self, ldap, dn, entry_attrs, subuid):
"""Set subuid value of an entry
Takes care of objectclass and sibbling attributes
"""
if "objectclass" in entry_attrs:
obj_classes = entry_attrs["objectclass"]
else:
_entry_attrs = ldap.get_entry(dn, ["objectclass"])
entry_attrs["objectclass"] = _entry_attrs["objectclass"]
obj_classes = entry_attrs["objectclass"]
if not self.has_objectclass(obj_classes, "ipasubordinateid"):
# could append ipasubordinategid and ipasubordinateuid, too
obj_classes.append("ipasubordinateid")
# XXX HACK, remove later
if subuid == DNA_MAGIC:
subuid = self._fake_dna_plugin(ldap, dn, entry_attrs)
entry_attrs["ipasubuidnumber"] = subuid
# enforice subuid == subgid for now
entry_attrs["ipasubgidnumber"] = subuid
# hard-coded constants
entry_attrs["ipasubuidcount"] = constants.SUBID_COUNT
entry_attrs["ipasubgidcount"] = constants.SUBID_COUNT
def get_subid_match_candidate_filter(
self, ldap, *, subuid, subgid, extra_filters=(), offset=None,
):
"""Create LDAP filter to locate matching/overlapping subids
"""
if subuid is None and subgid is None:
raise ValueError("subuid and subgid are both None")
if offset is None:
# assumes that no subordinate count is larger than SUBID_COUNT
offset = constants.SUBID_COUNT - 1
class_filters = "(objectclass=ipasubordinateid)"
subid_filters = []
if subuid is not None:
subid_filters.append(
ldap.combine_filters(
[
f"(ipasubuidnumber>={subuid - offset})",
f"(ipasubuidnumber<={subuid + offset})",
],
rules=ldap.MATCH_ALL
)
)
if subgid is not None:
subid_filters.append(
ldap.combine_filters(
[
f"(ipasubgidnumber>={subgid - offset})",
f"(ipasubgidnumber<={subgid + offset})",
],
rules=ldap.MATCH_ALL
)
)
subid_filters = ldap.combine_filters(
subid_filters, rules=ldap.MATCH_ANY
)
filters = [class_filters, subid_filters]
filters.extend(extra_filters)
return ldap.combine_filters(filters, rules=ldap.MATCH_ALL)
def _fake_dna_plugin(self, ldap, dn, entry_attrs):
"""XXX HACK, remove when 389-DS DNA plugin supports steps"""
uidnumber = entry_attrs.single_value.get("uidnumber")
if uidnumber is None:
entry = ldap.get_entry(dn, ["uidnumber"])
uidnumber = entry.single_value["uidnumber"]
uidnumber = int(uidnumber)
if uidnumber == DNA_MAGIC:
return (
3221225472
+ random.randint(1, 16382) * constants.SUBID_COUNT
)
if not hasattr(context, "idrange_ipabaseid"):
range_name = f"{self.api.env.realm}_id_range"
range = self.api.Command.idrange_show(range_name)["result"]
context.idrange_ipabaseid = int(range["ipabaseid"][0])
range_start = context.idrange_ipabaseid
assert uidnumber >= range_start
assert uidnumber < range_start + 2**14
return (uidnumber - range_start) * constants.SUBID_COUNT + 2**31
class baseuser_add(LDAPCreate):
"""
@ -536,6 +699,7 @@ class baseuser_add(LDAPCreate):
assert isinstance(dn, DN)
set_krbcanonicalname(entry_attrs)
self.obj.convert_usercertificate_pre(entry_attrs)
self.obj.handle_subordinate_ids(ldap, dn, entry_attrs)
if entry_attrs.get('ipatokenradiususername', None):
add_missing_object_class(ldap, u'ipatokenradiusproxyuser', dn,
entry_attrs, update=False)
@ -688,6 +852,7 @@ class baseuser_mod(LDAPUpdate):
self.check_objectclass(ldap, dn, entry_attrs)
self.obj.convert_usercertificate_pre(entry_attrs)
self.obj.handle_subordinate_ids(ldap, dn, entry_attrs)
self.preserve_krbprincipalname_pre(ldap, entry_attrs, *keys, **options)
update_samba_attrs(ldap, dn, entry_attrs, **options)
@ -968,3 +1133,98 @@ class baseuser_remove_certmapdata(ModCertMapData,
LDAPRemoveAttribute):
__doc__ = _("Remove one or more certificate mappings from the user entry.")
msg_summary = _('Removed certificate mappings from user "%(value)s"')
class baseuser_auto_subid(LDAPQuery):
__doc__ = _("Auto-assign subuid and subgid range to user entry")
has_output = output.standard_entry
def execute(self, cn, **options):
ldap = self.obj.backend
dn = self.obj.get_dn(cn)
try:
entry_attrs = ldap.get_entry(
dn, ["objectclass", "ipasubuidnumber"]
)
except errors.NotFound:
raise self.obj.handle_not_found(cn)
if "ipasubuidnumber" in entry_attrs:
raise errors.AlreadyContainsValueError(attr="ipasubuidnumber")
self.obj.set_subordinate_ids(ldap, dn, entry_attrs, subuid=DNA_MAGIC)
ldap.update_entry(entry_attrs)
# fetch updated entry (use search display attribute to show subids)
if options.get('all', False):
attrs_list = ['*'] + self.obj.search_display_attributes
else:
attrs_list = set(self.obj.search_display_attributes)
attrs_list.update(entry_attrs.keys())
if options.get('no_members', False):
attrs_list.difference_update(self.obj.attribute_members)
attrs_list = list(attrs_list)
entry = self._exc_wrapper((cn,), options, ldap.get_entry)(
dn, attrs_list
)
entry_attrs = entry_to_dict(entry, **options)
entry_attrs['dn'] = dn
return dict(result=entry_attrs, value=pkey_to_value(cn, options))
class baseuser_match_subid(baseuser_find):
__doc__ = _("Match users by any subordinate uid in their range")
_subid_attrs = {
"ipasubuidnumber",
"ipasubuidcount",
"ipasubgidnumber",
"ipasubgidcount"
}
def get_options(self):
base_options = {p.name for p in self.obj.takes_params}
for option in super().get_options():
if option.name == "ipasubuidnumber":
yield option.clone(
label=_('SubUID match'),
doc=_('Match value for subordinate user ID'),
required=True,
)
elif option.name not in base_options:
# raw, version
yield option.clone()
def pre_callback(
self, ldap, filters, attrs_list, base_dn, scope, *args, **options
):
# search for candidates in range
# Code assumes that no subordinate count is larger than SUBID_COUNT
filters = self.obj.get_subid_match_candidate_filter(
ldap, subuid=options["ipasubuidnumber"], subgid=None,
)
# always include subid attributes
for missing in self._subid_attrs.difference(attrs_list):
attrs_list.append(missing)
return filters, base_dn, scope
def post_callback(self, ldap, entries, truncated, *args, **options):
# filter out mismatches manually
osubuid = options["ipasubuidnumber"]
new_entries = []
for entry in entries:
esubuid = int(entry.single_value["ipasubuidnumber"])
esubcount = int(entry.single_value["ipasubuidcount"])
minsubuid = esubuid
maxsubuid = esubuid + esubcount - 1
if minsubuid <= osubuid <= maxsubuid:
new_entries.append(entry)
entries[:] = new_entries
return truncated

View File

@ -205,6 +205,7 @@ class idrange(LDAPObject):
# The commented range types are planned but not yet supported
range_types = {
u'ipa-local': unicode(_('local domain range')),
# u'ipa-local-subid': unicode(_('local domain subid range')),
# u'ipa-ad-winsync': unicode(_('Active Directory winsync range')),
u'ipa-ad-trust': unicode(_('Active Directory domain range')),
u'ipa-ad-trust-posix': unicode(_('Active Directory trust range with '
@ -221,10 +222,14 @@ class idrange(LDAPObject):
Int('ipabaseid',
cli_name='base_id',
label=_("First Posix ID of the range"),
minvalue=1,
maxvalue=Int.MAX_UINT32
),
Int('ipaidrangesize',
cli_name='range_size',
label=_("Number of IDs in the range"),
minvalue=1,
maxvalue=Int.MAX_UINT32
),
Int('ipabaserid?',
cli_name='rid_base',
@ -669,7 +674,10 @@ class idrange_mod(LDAPUpdate):
except errors.NotFound:
raise self.obj.handle_not_found(*keys)
if old_attrs['iparangetype'][0] == 'ipa-local':
if (
old_attrs['iparangetype'][0] in {'ipa-local', 'ipa-local-subid'}
or old_attrs['cn'][0] == f'{self.api.env.realm}_subid_range'
):
raise errors.ExecutionError(
message=_('This command can not be used to change ID '
'allocation for local IPA domain. Run '

View File

@ -1547,6 +1547,13 @@ class i18n_messages(Command):
"Drive to mount a home directory"
),
},
"subordinate": {
"identity": _("Subordinate user and group id"),
"subuidnumber": _("Subordinate user id"),
"subuidcount": _("Subordinate user id count"),
"subgidnumber": _("Subordinate group id"),
"subgidcount": _("Subordinate group id count"),
},
"trustconfig": {
"options": _("Options"),
},
@ -1570,6 +1577,11 @@ class i18n_messages(Command):
"add_into_sudo": _(
"Add user '${primary_key}' into sudo rules"
),
"auto_subid": _("Auto assign subordinate ids"),
"auto_subid_confirm": _(
"Are you sure you want to auto-assign a subordinate id "
"to user ${object}?"
),
"contact": _("Contact Settings"),
"delete_mode": _("Delete mode"),
"employee": _("Employee Information"),

View File

@ -50,7 +50,10 @@ from .baseuser import (
baseuser_add_principal,
baseuser_remove_principal,
baseuser_add_certmapdata,
baseuser_remove_certmapdata)
baseuser_remove_certmapdata,
baseuser_auto_subid,
baseuser_match_subid,
)
from .idviews import remove_ipaobject_overrides
from ipalib.plugable import Registry
from .baseldap import (
@ -202,6 +205,8 @@ class user(baseuser):
'ipapermright': {'read', 'search', 'compare'},
'ipapermdefaultattr': {
'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass',
'ipasubuidnumber', 'ipasubuidcount', 'ipasubgidnumber',
'ipasubgidcount',
},
'fixup_function': fix_addressbook_permission_bindrule,
},
@ -1306,3 +1311,13 @@ class user_add_principal(baseuser_add_principal):
class user_remove_principal(baseuser_remove_principal):
__doc__ = _('Remove principal alias from the user entry')
msg_summary = _('Removed aliases from user "%(value)s"')
@register()
class user_auto_subid(baseuser_auto_subid):
__doc__ = baseuser_auto_subid.__doc__
@register()
class user_match_subid(baseuser_match_subid):
__doc__ = baseuser_match_subid.__doc__

View File

@ -298,3 +298,15 @@ jobs:
template: *ci-master-latest
timeout: 3600
topology: *master_1repl
fedora-latest/test_subids:
requires: [fedora-latest/build]
priority: 100
job:
class: RunPytest
args:
build_url: '{fedora-latest/build_url}'
test_suite: test_integration/test_subids.py
template: *ci-master-latest
timeout: 3600
topology: *master_1repl

View File

@ -0,0 +1,201 @@
#
# Copyright (C) 2021 FreeIPA Contributors see COPYING for license
#
"""Tests for subordinate ids
"""
import os
from ipalib.constants import SUBID_COUNT, SUBID_RANGE_START, SUBID_RANGE_MAX
from ipaplatform.paths import paths
from ipatests.pytest_ipa.integration import tasks
from ipatests.test_integration.base import IntegrationTest
class TestSubordinateId(IntegrationTest):
num_replicas = 0
topology = "star"
def _parse_result(self, result):
info = {}
for line in result.stdout_text.split("\n"):
line = line.strip()
if line:
if ":" not in line:
continue
k, v = line.split(":", 1)
k = k.strip()
v = v.strip()
try:
v = int(v, 10)
except ValueError:
if v == "FALSE":
v = False
elif v == "TRUE":
v = True
info.setdefault(k.lower(), []).append(v)
for k, v in info.items():
if len(v) == 1:
info[k] = v[0]
else:
info[k] = set(v)
return info
def get_user(self, uid):
cmd = ["ipa", "user-show", "--all", "--raw", uid]
result = self.master.run_command(cmd)
return self._parse_result(result)
def user_auto_subid(self, uid, **kwargs):
cmd = ["ipa", "user-auto-subid", uid]
return self.master.run_command(cmd, **kwargs)
def test_auto_subid(self):
tasks.kinit_admin(self.master)
uid = "testuser_auto1"
tasks.user_add(self.master, uid)
info = self.get_user(uid)
assert "ipasubuidcount" not in info
self.user_auto_subid(uid)
info = self.get_user(uid)
assert "ipasubuidcount" in info
subuid = info["ipasubuidnumber"]
result = self.master.run_command(
["ipa", "user-match-subid", f"--subuid={subuid}", "--raw"]
)
match = self._parse_result(result)
assert match["uid"] == uid
assert match["ipasubuidnumber"] == info["ipasubuidnumber"]
assert match["ipasubuidnumber"] >= SUBID_RANGE_START
assert match["ipasubuidnumber"] <= SUBID_RANGE_MAX
assert match["ipasubuidcount"] == SUBID_COUNT
assert match["ipasubgidnumber"] == info["ipasubgidnumber"]
assert match["ipasubgidnumber"] == match["ipasubuidnumber"]
assert match["ipasubgidcount"] == SUBID_COUNT
def test_ipa_subid_script(self):
tasks.kinit_admin(self.master)
tool = os.path.join(paths.LIBEXEC_IPA_DIR, "ipa-subids")
users = []
for i in range(1, 11):
uid = f"testuser_script{i}"
users.append(uid)
tasks.user_add(self.master, uid)
info = self.get_user(uid)
assert "ipasubuidcount" not in info
cmd = [tool, "--verbose", "--group", "ipausers"]
self.master.run_command(cmd)
for uid in users:
info = self.get_user(uid)
assert info["ipasubuidnumber"] >= SUBID_RANGE_START
assert info["ipasubuidnumber"] <= SUBID_RANGE_MAX
assert info["ipasubuidnumber"] == info["ipasubgidnumber"]
assert info["ipasubuidcount"] == SUBID_COUNT
assert info["ipasubuidcount"] == info["ipasubgidcount"]
def test_subid_selfservice(self):
tasks.kinit_admin(self.master)
uid = "testuser_selfservice1"
password = "Secret123"
role = "Subordinate ID Selfservice User"
tasks.user_add(self.master, uid, password=password)
tasks.kinit_user(
self.master, uid, f"{password}\n{password}\n{password}\n"
)
info = self.get_user(uid)
assert "ipasubuidcount" not in info
result = self.user_auto_subid(uid, raiseonerr=False)
assert result.returncode > 0
tasks.kinit_admin(self.master)
self.master.run_command(
["ipa", "role-add-member", role, "--groups=ipausers"]
)
try:
tasks.kinit_user(self.master, uid, password)
self.user_auto_subid(uid)
info = self.get_user(uid)
assert "ipasubuidcount" in info
finally:
tasks.kinit_admin(self.master)
self.master.run_command(
["ipa", "role-remove-member", role, "--groups=ipausers"]
)
def test_subid_useradmin(self):
tasks.kinit_admin(self.master)
uid_useradmin = "testuser_usermgr_mgr1"
role = "User Administrator"
uid = "testuser_usermgr_user1"
password = "Secret123"
# create user administrator
tasks.user_add(self.master, uid_useradmin, password=password)
# add user to user admin group
tasks.kinit_admin(self.master)
self.master.run_command(
["ipa", "role-add-member", role, f"--users={uid_useradmin}"],
)
# kinit as user admin
tasks.kinit_user(
self.master,
uid_useradmin,
f"{password}\n{password}\n{password}\n",
)
# create new user as user admin
tasks.user_add(self.master, uid)
# assign new subid to user (with useradmin credentials)
self.user_auto_subid(uid)
def test_subordinate_default_objclass(self):
tasks.kinit_admin(self.master)
result = self.master.run_command(
["ipa", "config-show", "--raw", "--all"]
)
info = self._parse_result(result)
usercls = info["ipauserobjectclasses"]
assert "ipasubordinateid" not in usercls
cmd = [
"ipa",
"config-mod",
"--addattr",
"ipaUserObjectClasses=ipasubordinateid",
]
self.master.run_command(cmd)
uid = "testuser_usercls1"
tasks.user_add(self.master, uid)
info = self.get_user(uid)
assert "ipasubuidcount" in info
def test_idrange_subid(self):
tasks.kinit_admin(self.master)
range_name = f"{self.master.domain.realm}_subid_range"
result = self.master.run_command(
["ipa", "idrange-show", range_name, "--raw"]
)
info = self._parse_result(result)
# see https://github.com/SSSD/sssd/issues/5571
assert info["iparangetype"] == "ipa-ad-trust"
assert info["ipabaseid"] == SUBID_RANGE_START
assert info["ipaidrangesize"] == SUBID_RANGE_MAX - SUBID_RANGE_START
assert info["ipabaserid"] < SUBID_RANGE_START
assert "ipasecondarybaserid" not in info
assert info["ipanttrusteddomainsid"].startswith(
"S-1-5-21-738065-838566-"
)

View File

@ -24,6 +24,7 @@ Test the `ipaserver/plugins/idrange.py` module, and XML-RPC in general.
import six
from ipalib import api, errors, messages
from ipalib import constants
from ipaplatform import services
from ipatests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid
from ipatests.test_xmlrpc import objectclasses
@ -46,6 +47,12 @@ rid_shift = 0
for idrange in api.Command['idrange_find']()['result']:
size = int(idrange['ipaidrangesize'][0])
base_id = int(idrange['ipabaseid'][0])
rtype = idrange['iparangetype'][0]
if rtype == 'ipa-local-subid' or base_id == constants.SUBID_RANGE_START:
# ignore subordinate id range. It would push values beyond uint32_t.
# There is plenty of space below SUBUID_RANGE_START.
continue
id_end = base_id + size
rid_end = 0