Bug 58432 - implement ABAC (attribute-based access control) authorization in UDM core
Summary: implement ABAC (attribute-based access control) authorization in UDM core
Status: CLOSED FIXED
Alias: None
Product: UCS
Classification: Unclassified
Component: UDM (Generic)
Version: UCS 5.2
Hardware: Other Linux
: P5 normal
Target Milestone: UCS 5.2-2-errata
Assignee: Florian Best
QA Contact: Felix Botner
URL:
Keywords:
Depends on: 58113
Blocks:
  Show dependency treegraph
 
Reported: 2025-06-30 23:24 CEST by Florian Best
Modified: 2025-08-25 11:18 CEST (History)
5 users (show)

See Also:
What kind of report is it?: Feature Request
What type of bug is this?: ---
Who will be affected by this bug?: ---
How will those affected feel about the bug?: ---
User Pain:
Enterprise Customer affected?:
School Customer affected?:
ISV affected?:
Waiting Support:
Flags outvoted (downgraded) after PO Review:
Ticket number:
Bug group (optional): API change, Role and Access Model, Security, UCS Performance
Customer ID:
Max CVSS v3 score:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Florian Best univentionstaff 2025-06-30 23:24:55 CEST
The UDM library should support attribute-based access control (ABAC) for all operations (e.g.: `create`, `modify`, `move`, `rename`, `remove` of objects, creation of reports, license import) performed on UDM objects and its properties.
The authorization model must integrate with the Guardian concept, primarily using the `guardianRoles` attribute to determine permissions of the actor.

Some requirements:
* UDM must enforce ABAC rules on all object-level operations.
* All write and read operations must be executed using a privileged LDAP account (e.g. cn=admin).
* A bypass mechanism is required for system accounts and components that lack `guardianRoles` (and to avoid cyclic dependencies e.g. Guardian fetching actor information from UDM REST API).
* ABAC checks must be enableable per LDAP connection to support system-level operations without restrictions.

* The old (JSON based) prototype implementation in the UDM UMC module (See Bug #58113) has to be removed and replaces with this solution.

* A domain-specific language (DSL) should be introduced to describe ABAC policies declaratively.
As the Guardian concept is abstract, the configuration of policies/rules for all UDM modules would be very large and not maintainable.

* The DSL must abstract Guardian internals and offer a simple, maintainable way to define access rules.

* UDM should ship with (besides a "Domain Admin" role) three predefined role policies:
1. A Organizational Unit Administrator, which is able to administrate a certain Organizational Unit (without being able to privilege escalation e.g. restrict the access to `guardianRoles`).
2. A Helpdesk Operator, which is able to modify passwords of users in a certain OU.
3. A Linux Client Join operator, which is able to create computers/linux accounts in a certain OU.

* The API of UDM must mimic LDAP e.g. to prevent information disclosure
Example: a `permissionDenied` exception would reveal the existence of a object, therefor if no read permission exists, a `noObject` exception must be raised.

* The implementation should be in a object oriented way and allow re-use.
* The implementation must cover UMC-UDM and UDM REST API.

* As we don't have third party components under our control, no API changes are possible.
However, to keep this in a secure way, the third party extensions must align with the necessary adjustments.
They have to be adjusted by App ISVs, PS, etc accordingly. This was a decision from product management.

The implementation is the next increment (spike) for the delegative administration feature.
Further changes are required to bring it into product maturity.

1) A sketch for the API and the initialization of authorization in a service:

```python
# API Sketch

class Authorization:

    def enable(self, get_privileged_connection_callback):
        …

    def inject_ldap_connection(self, ldap_connection):
        …  # transforms user LDAP connection into connection with admin powers

    def is_create_allowed(self, obj):
        …

    def is_modify_allowed(self, obj):
        …

    def is_rename_allowed(self, obj):
        …

    def is_move_allowed(self, obj, target):
        …

    def is_remove_allowed(self, obj):
        …

    def is_receive_allowed(self, obj):
        …

    def filter_search_results(self, results):
        …

    def filter_object_properties(self, obj):
        …

# Example usage

def init_the_service()
    lo_admin = univention.admin.uldap.getAdminConnection()  # e.g. cache somehwere
    univention.admin.authorization.Authorization.enable(lambda: lo_admin)

    # get user connection
    lo = univention.admin.uldap.access(
        binddn='uid=ou1admin,cn=users,dc=ucs,dc=test',
        bindpw='univention',
        base=ucr['ldap/base']
    )
    po = position(lo.base)
    lo = univention.admin.authorization.Authorization.inject_ldap_connection(lo)  # extend user connection to have admin powers


def main()
    init_the_service()
    users = univention.admin.modules.get('users/user')
    univention.admin.modules.init(lo, po, users)

    user = users.object(None, lo, po, )
    user.create()

# Enforcement in Object logic

class object(simpleLDAP):

    def create(self):
        …
        if not self.lo.authz.is_create_allowed(self):
            raise permissionDenied()
        self.lo.authz_connection.add(self.dn, self.addlist)
        …
```

+++ This bug was initially created as a clone of Bug #58113 +++
Comment 1 Florian Best univentionstaff 2025-07-08 17:47:16 CEST
The ABAC authorization concept has been integrated into UDM.

The authorization checks are enabled per LDAP connection (and additionally globally per service):

It can be enabled in UMC and UDM REST API (not yet in UDM-CLI - we just didn't integrate a UCR variable for it, can possibly just be enabled there as well) via the following UCR variables.

`directory/manager/web/delegative-administration/enabled`=true
`directory/manager/rest/delegative-administration/enabled`=true
Via `directory/manager/rest/delegative-administration/excluded-users/.*` one can specify users which are excluded from these authorization checks, e.g. necessary so that `cn=admin` stil works.
TODO: implement the same for 'directory/manager/rest/delegative-administration/excluded-groups/.*'

Configuration is still locally and not distributed somewhere. Can be configured via:

/usr/share/univention-directory-manager-tools/univention-configure-udm-authorization --store-local prune
/usr/share/univention-directory-manager-tools/univention-configure-udm-authorization --store-local create-permissions
/usr/share/univention-directory-manager-tools/univention-configure-udm-authorization --store-local create-default-roles

A language to describe the policy rules has been implemented: a UDM domain specific language (DSL) following an extended BNF grammar.
This is parsed by a LALR (Look-Ahead Left <- Right) parser.

The default rules are in: `/usr/share/univention-directory-manager-modules/udm-default-authorization-roles.policy`
Currently rules for the same role must be defined in the same `.policy` file.
Syntax highlighting for this file (e.g. via vim) can be enabled, see toolshed:`vim/syntax/udm.vim` and `vim/ftdetect/udm.vim`

Example:
```
# Domain Administrators
access by role="udm:default-roles:domain-administrator"
  description="Domain Admins are allowed to do anything in the whole domain"
  to objecttype="*"
    grant actions="*"
    grant properties="*" permission="write"
```

Further policies can be activated via:
`/usr/share/univention-directory-manager-tools/univention-configure-udm-authorization --store-local create-roles --config /etc/….policy`

UDM delivers the following default roles:
* Domain Administator (can do anything): udm:default-roles:domain-administrator 
* Domain User (can read himself): udm:default-roles:domain-user
* Self Service Profile view (can write self service properties): udm:default-roles:self-service-profile
* Organizational Unit Administrtor (can administate users and groups in a OU): udm:default-roles:organizational-unit-admin (only in combination with the udm:contexts:position context)
* Helpdesk Operator (can reset passwords): udm:default-roles:helpdesk-operator
* Linux Client Mananger (can create computers/linux accounts): udm:default-roles:linux-ou-client-manager

For those users with these roles, LDAP ACL's can be added which grant nothing anymore. By default every of these users are able to read the whole contents of the LDAP directory, without sensitive data such as password hashes:
```
access to *
     by dn.base="uid=ou1-admin,cn=users,dc=ucs,dc=test" none stop
     by * +0 break
```

# Implications on UDM usage:
UDM is not prepared to handle such a API change, which we did in a backwards compatible manner.
Error handling now must be extended everywhere to catch e.g. `permissionDenied` and `noObject`.
Authorization checks and object or property filtering needs to be done manually in various places, to not introduce priviledge escalation or information disclosure vulnerabilities.
External hooks/modules/syntax classes must implement this manually.
The ldap connection instance `lo` provides therefore the new members `authz` (providing authorization methods), `authz_connection` (being a privileged `cn=admin` connection) and other utility functions (which might change in the future).

The use of `authz_connection` can be compared with encoding/decoding of data.
The `lo.authz_connection` must be used for all operations like `get_schema()`, `add()`, `modify()`, `rename()`, `delete()`, `getPolicies()`, `get()`, `getAttr()`, `search()` and `searchDn()`.
The `lo` connection must be used for all other operations, e.g. passing to UDM objects `univention.admin.modules.lookup(lo, …)`, `univention.admin.modules.get("users/user").object(…, lo, …)` or `univention.admin.modules.init(module, lo, …)`

Additionally the methods `get()`, `getAttr()`, `search()` and `searchDn()` must be guarded, to filter the results for information the user is not allowed to read!
E.g. alternatives are `lo.search_filtered()` and `lo.search_dn_filtered()`.

In general it's very important that things raise the same exception signature like LDAP would do, if no permissions exists, otherwise with that information leak it would be possible to find out which objects exsits, especially if the user has control over the used LDAP filter, it can be used to obtain arbitrary domain data.

The implementation still has several vulnerabilities:
* probably not all endpoints of UMC and UDM REST API covered
* various endpoints which offer the possibility to provide an LDAP filter or LDAP search base + scope, which doesn't respond equally to if the object doesn't exists in LDAP
* information disclosure in various methods where the behavior differs from LDAP. most known places we e.g. raise `noObject` instead of `permissionDenied`. leftover: whole tree hierarchy must be checked.
* information disclosure via specially crafted LDAP DNs mighgt allow this (e.g. `uid = Administrator` is equal to `uid=Administrator`). This is currently safely hanlded. If we switch to real Guardian and evaluate this with rego, it will probably vulnerable.
* time based side channel attacks might be possible now.
* UDM modules which implement `lookup()` on their own, instead of using the generic variant e.g. computers/ipmanagedclient.
* ~~DNS and DHCP objects if access to computers is allowed~~ I don't consider those information secret!? They could also be fetched via a DNS request.
* UDM modules with a defect `hasChange()` method
* pseudo-properties without LDAP attribute, might be not correctly detected by `hasChange()`
* UDM objects which aren't `open()`ed, when the open logic adds state
* It's possible to "touch" any object, by sending a modify request without changed properties.
* All marked places in the UDM source code matching: `git grep 'information disclosure'`
* probably many other things

From product management we were not allowed to implement this feature by creating mappings to LDAP ACLs, which would make us safe against all this (and would have been a lot easier to implement).
Therefor we have to address them somewhen.

Other vulnerabilities are possible in the rules itself.
Creating rules requires knowledge about the object structure.
To protect against privilege escalation we must prevent write access to (certain values of) properties like users/user or groups/group:password,serviceSpecificPassword,guardianRoles,guardianMemberRoles.
Also certain pathes in homeSharePath and other attacks are imaginable.

Other wrong role definitions can lead to information loss, e.g.:
If the actor user has access to read group XYZ and another actor modificator of groups has not, the groups will be removed.

The place of implementation is also kind of wrong:
Authorization checks are done very early in modify()/create()/etc to prevent that the _ldap_pre_create/modify() logic, which already does things like moving, creation, can be executed by any arbitraty actor, not having the permissions.
But exactly that logic often sets default values, modifies states, executes hooks, etc which is then not covered by the authorization checks.
Maybe it would be best to just check the permissions twice.
Comment 2 Florian Best univentionstaff 2025-07-08 17:47:45 CEST
Overview of commits:

univention-s4-connector.yaml
3775a280133a | chore(s4-connector): update advisory

univention-s4-connector (16.2.1)
d8f18a6b16cd | feat(s4-connector): always set LDAP base when initializing uldap.access()

univention-python.yaml
d11530bad88e | chore(udm): add advisory

univention-python (15.2.1)
cbf79206dbb8 | feat(uldap): add optional strict type checking

univention-python (15.2.0)
dc5a075e1e25 | feat(udm): implement ABAC (attribute-based access control) authorization in UDM
22d22946f2fc | feat(python): add univention.dn module
e7243b16dc85 | fix!(univention-python): don't allow access() without a valid base
d3be950d34c5 | perf(uldap): compare LDAP base DN correctly but make a lazy comparision first

univention-management-console-module-udm.yaml
d11530bad88e | chore(udm): add advisory

univention-management-console-module-udm (12.2.6)
9f87a2b49e49 | fix(udm-umc): fix error handling in case the LDAP base is not readable

univention-management-console-module-udm (12.2.5)
06e2c0866a2f | feat(umc-udm): check if license-import is allowed
5d03d310bfcf | feat(udm-umc): check if permissions for report creation exists
a78614db7618 | fix(udm): use priviledged LDAP connection
c82b56c9437b | feat(udm-rest): enable ABAC (attribute-based access control) authorization in UDM REST API
dc5a075e1e25 | feat(udm): implement ABAC (attribute-based access control) authorization in UDM

univention-lib.yaml
bb124112316e | feat(lib): set base in uldap.access()

univention-lib (11.2.0)
bb124112316e | feat(lib): set base in uldap.access()

univention-directory-manager-rest.yaml
d11530bad88e | chore(udm): add advisory

univention-directory-manager-rest (12.2.1)
5d03d310bfcf | feat(udm-umc): check if permissions for report creation exists
90db702381f0 | fix(udm-rest): fix error handling
c82b56c9437b | feat(udm-rest): enable ABAC (attribute-based access control) authorization in UDM REST API

univention-directory-manager-rest (12.2.0)
53ee0cabf764 | perf(udm-rest): enhance resolving of relations

univention-directory-manager-modules.yaml
d11530bad88e | chore(udm): add advisory

univention-directory-manager-modules (17.2.9)
e32875852783 | feat(udm): add further warning about wrong use of univention.uldap.access() within UDM code
f9205d390111 | feat(udm): Configure ABAC in UDM using Guardian-like engine and UDM Domain Specific Language
0e3509b603c0 | fix(udm): don't call modify if nothing has been changed
a78614db7618 | fix(udm): use priviledged LDAP connection
dc5a075e1e25 | feat(udm): implement ABAC (attribute-based access control) authorization in UDM

univention-directory-manager-modules (17.2.8)
a2406e87ecf7 | feat(udm): add Helpdesk Operator (helpdesk-operator) role
e439d797073f | feat(udm): add linux-client-manager role
4bba3e1654ea | fix(udm): users/ldap doesn't have guardianInheritedRoles
8a76fc4c1592 | fix(udm): do nagios cleanup only if nagios option is enabled
af6969c0cc36 | fix(udm): don't change the object's groups property when loading guardian roles
419e93e14d95 | feat(udm): adjust size of guardian roles in web UI
a21441affa2d | test(udm): implement authorization unittests
f9d3121af3f7 | test(udm): implement authorization tests

univention-directory-manager-modules (17.2.11)
0d8376907865 | feat(authorization): fetch setting objects with privileged connection

univention-directory-manager-modules (17.2.10)
9b94d7b81ae7 | style(udm): transform type comment into type annotations
62998f8405f1 | feat(udm): prevent traceback during upgrade
e2ce6daec9dd | style(udm): fix some ruff issues
442217ae7669 | perf(udm): fix RUF015 unnecessary-iterable-allocation-for-first-element
2a72f3074c16 | refactor(udm): remove circular import
056a28fbd921 | style(udm): Drop Python 2 specific code
a1ad885b2e4e | docs(uldap): document public API
4ba8d433696f | feat(udm): introduce optional by default deactivated strict type checking

univention-authorization.yaml
d11530bad88e | chore(udm): add advisory

univention-authorization (1.0.0)
b6991bdc50c8 | feat(authorization): Implement guardian-like authorization engine

univention-ad-connector.yaml
3775a280133a | chore(s4-connector): update advisory

univention-ad-connector (16.2.1)
5c34ccb789d8 | feat(ad-connector): always set LDAP base when initializing uldap.access()

ucs-test (12.2.22)
4b2eb4080832 | test(ad-connector): fix usage of uldap.access in AD connector tests

ucs-test (12.2.21)
ce76b57d4bbf | test(ucs-test): fix typo in 32_udm_nonposix_user_actions

ucs-test (12.2.19)
dfa393b611c3 | test(ucs-test): fix hardcoded use of "Domain Admins"

ucs-test (12.2.18)
aa51bfd55384 | test(udm-authorization): expect users/ldap with no priviledges to not be able to search for anything
061737636002 | test(udm-authorization): fix creation of initial OU structure
936d6daa9d43 | test(udm-authorization): fix role names

ucs-test (12.2.17)
a61fe7326956 | test(ucs-test): fix 45_radius/11_service_specific_password.py

ucs-test (12.2.16)
01c44e99abdb | test(ucs-test): fix usage of univention.admin.uldap.access

ucs-test (12.2.15)
af411806ad6d | refactor(udm-authorization): rewrite create-delegated-administration-setup

ucs-test (12.2.14)
482085c80156 | test(udm-authorization): hierarchical ou structure
3f7f9b3c37f0 | test(base): rewrite test 45udm-cli.py in pytest
7d014ead27b6 | test(radius): align to error handling
18064223b558 | test(udm-authorization): consider primary group roles
f9205d390111 | feat(udm): Configure ABAC in UDM using Guardian-like engine and UDM Domain Specific Language
dc5a075e1e25 | feat(udm): implement ABAC (attribute-based access control) authorization in UDM
7681e747fdf3 | test(udm): add authorization tests for helpdesk-operator role
e439d797073f | feat(udm): add linux-client-manager role
076c444a8fba | test(udm): add authorization tests
f9d3121af3f7 | test(udm): implement authorization tests
d23c0d89202f | ci(jenkins): add guardian setup for delegated administration
bf06fbd1fba1 | test(ucs-test-authorization): move existing authorization tests into new test section 42_authorization

test_simple.yaml
a21441affa2d | test(udm): implement authorization unittests

test_multiple_positions2.yaml
a21441affa2d | test(udm): implement authorization unittests

test_multiple_positions.yaml
a21441affa2d | test(udm): implement authorization unittests

NONE
88a6f2a2e022 | test(upgrade): prune old kernels
f2a01ec7cd9d | ci(utils): set directory/mananger/type-checking/strict=true in all instances
f83c31c70b3e | test(udm-authorization): new test job for main branch
Comment 3 Felix Botner univentionstaff 2025-07-09 14:04:53 CEST
OK - yaml
OK - packages
OK - tests