smolnar82 commented on code in PR #1454:
URL: https://github.com/apache/knox/pull/1454#discussion_r4323143878
##########
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java:
##########
@@ -261,9 +269,13 @@ public boolean isSupportedSearchBase(String searchBase) {
if (searchBase == null) {
return false;
}
- String searchBaseLowerCase = searchBase.toLowerCase(ROOT);
- return searchBaseLowerCase.endsWith(proxyBaseDn.toLowerCase(ROOT)) ||
- searchBaseLowerCase.endsWith(remoteBaseDn.toLowerCase(ROOT));
+ try {
+ Dn searchBaseDn = new Dn(searchBase);
+ return searchBaseDn.isDescendantOf(proxyBaseDn) ||
+ searchBaseDn.isDescendantOf(remoteBaseDn);
Review Comment:
`new Dn(searchBase)` has no SchemaManager, so RDN values are compared
case-sensitively (api-ldap-model 2.1.8: `Ava.equals` -> `Value.equals` ->
`upValue.equals(...)` when there is no AttributeType). The caller
(`UserSearchInterceptor`) passes `ctx.getDn().toString()`, which is the
user-provided form. So `-b 'ou=people,DC=Proxy,DC=com'`, or a remote base
configured as `DC=Example,DC=com` and queried as `dc=example,dc=com`, is
silently skipped and returns no backend results, even though these DNs are
equal under LDAP matching rules (`dc`/`ou`/`cn` use caseIgnoreMatch).
Suggest a schema-aware Dn (pass the SchemaManager or the normalized
`ctx.getDn()` from `UserSearchInterceptor`), or at least lowercase both sides:
Dn searchBaseDn = new Dn(searchBase.toLowerCase(Locale.ROOT));
return searchBaseDn.isDescendantOf(proxyBaseDn.toLowerCase(Locale.ROOT))
||
searchBaseDn.isDescendantOf(remoteBaseDn.toLowerCase(Locale.ROOT));
A mixed-case base in `testIsSupportedSearchBase` would cover it.
##########
knox-site/docs/service_ldap_server.md:
##########
@@ -232,12 +276,165 @@ knoxcli.sh create-alias my_backend_bind_password --value
<password>
If the alias may be created shortly after the configuration is loaded (for
example when a Kubernetes Secret and the configuration referencing it are
created together), set `gateway.ldap.backend.password.alias.retry.attempts` to
have Knox look up a missing alias again every
`gateway.ldap.backend.password.alias.retry.interval.ms` milliseconds before
giving up ([KNOX-3513](https://issues.apache.org/jira/browse/KNOX-3513)). Each
retry is logged at WARN; the ERROR is only logged once the retries are
exhausted. All missing aliases are retried together, so the LDAP service waits
at most attempts x interval while starting or reloading. The Knox CLI
(`knoxcli.sh`) never retries, so commands such as `create-alias` are not held
up by the missing alias.
+### Role Strategy
+
+The role strategy, selected with `gateway.ldap.roles.lookup.strategy`, is
where the [Roles Lookup Interceptor](#roles-lookup-interceptor-roleslookup)
gets its role data. The same strategy answers three questions:
+
+| Question | Used for |
+| :--- | :--- |
+| Which roles does a user have? (`user` and the user's `groups` -> roles) |
Role mapping of user and group entries |
+| Which roles exist? | Role listing |
+| Which users and groups is a role assigned to? (role -> users and groups) |
Reverse role lookup |
+
+A role is identified by a scope and a name. It is written as `<scope>:<name>`
(for example `platform:admin`) wherever Knox shows or accepts a role, such as
in `memberOf` values, role entry DNs (`cn=platform:admin,ou=groups,<base dn>`)
and role listings. A role without a scope is written as just `<name>`.
+
+#### File-based Roles Strategy (`file`)
+
+The file strategy reads role assignments from a JSON file. It is suited to
testing and small, static environments. Set
`gateway.ldap.roles.lookup.strategy` to `file` and
`gateway.ldap.roles.lookup.file.path` to the path of the file.
+
+The file contains a JSON array. Each element assigns roles to one user or one
group:
+
+| Field | Description |
+| :--- | :--- |
+| `id` | The user name or group name the roles are assigned to. |
+| `type` | `user` or `group`. |
+| `roles` | The list of roles assigned. Each role has a `name` and an optional
`scope`. |
+
+```json
+[
+ {
+ "id": "alice",
+ "type": "user",
+ "roles": [
+ { "scope": "platform", "name": "awc-admin" }
+ ]
+ },
+ {
+ "id": "engineering",
+ "type": "group",
+ "roles": [
+ { "scope": "ml-workspace-abc", "name": "viewer" },
+ { "scope": "platform", "name": "analyst" }
+ ]
+ }
+]
+```
+
+The entries are used as follows:
+
+- **User to roles.** The roles of a user are the roles of every `user` entry
whose `id` equals the user name, plus the roles of every `group` entry whose
`id` is one of the user's groups. Duplicate roles are removed.
+- **Role to assignees.** The assignees of a role are the `id` of every entry
that has the role, split into users (`type` is `user`) and groups (`type` is
`group`). Roles are compared as `<scope>:<name>`, ignoring case.
+- **Listing roles.** The distinct `<scope>:<name>` values of all the entries.
The file format does not support roles that have no assignments.
+
+The same file is used for all three use-cases to ensure consist results. The
file is read on every request, so changes take effect without restarting the
gateway. If the file does not exist or cannot be parsed, the request that needs
it fails with the error `Role mapping file ... does not exist` or `Error
reading roles mapping file`.
+
+#### Roles REST API contract (`rest`)
+
+The REST strategy calls an external service. Set
`gateway.ldap.roles.lookup.strategy` to `rest` and configure all three
endpoints:
+
+| Property | HTTP request | Purpose |
+| :--- | :--- |:---------------------------------------|
+| `gateway.ldap.roles.lookup.rest.api.endpoint` | `POST <endpoint>` | Get the
roles of a user and/or groups. |
+| `gateway.ldap.roles.list.rest.api.endpoint` | `GET
<endpoint>?name=&scope=&offset=` | List roles, one page at a time. |
+| `gateway.ldap.roles.get.rest.api.endpoint` | `GET <endpoint>/<role id>` |
Get one role and its members. |
+
+All responses must have status `200` and a JSON body. Any other status, or an
empty body, makes the request fail and the failure is logged. Unknown JSON
properties in responses are ignored.
+
+##### Look up the roles of a user
+
+Knox sends a `POST` with a JSON body (`Content-Type: application/json`)
containing the username and the names of the user's groups:
+
+```json
+{
+ "user_id": "alice",
+ "groups": ["engineering", "analysts"]
+}
+```
+
+The service responds with the roles that apply to the user and groups. `scope`
may be omitted or empty for a role that has no scope.
+
+```json
+{
+ "user_id": "alice",
+ "roles": [
+ { "scope": "platform", "name": "awc-admin" },
+ { "scope": "ml-workspace-abc", "name": "viewer" }
+ ]
+}
+```
+
+##### List roles
+
+Knox sends a `GET` with the header `Accept: application/json`. The query
parameters are:
+
+| Parameter | Description |
+| :--- | :--- |
+| `offset` | Always sent. The number of roles Knox has already retrieved,
starting at `0`. |
+| `name` | Only sent when Knox looks for one role (reverse role lookup). Only
roles with this name should be returned. |
+| `scope` | Only sent when Knox looks for one role (reverse role lookup) and
the role has a scope. Only roles with this scope should be returned. |
+
+The service responds with a page of roles and the paging information:
+
+```json
+{
+ "data": [
+ { "id": "role-1", "name": "admin", "scope": "platform" },
+ { "id": "role-2", "name": "viewer", "scope": "workspace-abc" }
+ ],
+ "pagination": {
+ "limit": 2,
+ "offset": 0,
+ "total": 5
+ }
+}
+```
+
+| Field | Description |
+| :--- | :--- |
+| `data[].id` | The unique id of the role. It is used to get the role's
members. |
+| `data[].name` | The role name. |
+| `data[].scope` | The role scope. |
+| `pagination.total` | The total number of roles that match the request. |
+
+Knox keeps requesting pages, advancing `offset` by the number of roles it has
received, until it has received `pagination.total` roles. The page size is
chosen by the service. A page with no roles before `total` is reached fails the
listing with `No more roles to list`.
+
+When a single role is looked up, Knox searches the returned roles for the one
whose `name` and `scope` equal the requested role, ignoring case. The service
may therefore return more than the exact match, but it must include the
matching role.
+
+##### Get a role and its members
+
+Knox sends a `GET` with the header `Accept: application/json` to the get
endpoint with the role `id` appended as a path segment. The id is URL-encoded,
with spaces encoded as `%20`. For example, with the endpoint
`http://roles.example.com/roles/get` and the role id `role 1`, Knox requests
`http://roles.example.com/roles/get/role%201`.
+
+The service responds with the role and its members:
+
+```json
+{
+ "id": "role-1",
+ "name": "admin",
+ "scope": "platform",
+ "members": [
+ { "id": "m-1", "member_type": "user", "member_id": "alice" },
+ { "id": "m-2", "member_type": "group", "member_id": "engineering" }
+ ]
+}
+```
+
+| Field | Description |
+| :--- | :--- |
+| `members[].member_type` | `user` or `group` (case-insensitive). Members of
any other type are ignored and logged. |
Review Comment:
"Members of any other type are ignored and logged." no longer matches the
code: `RestApiLdapRolesReverseLookup` now throws a `RoleException` for an
unknown `member_type`, which fails the LDAP search (see
`testLookupRoleAssigneesThrowsOnUnknownMemberType`). Please update the docs to
say an unknown `member_type` fails the lookup and the LDAP search, or switch
the code to log-and-skip if that's the intended contract.
##########
knox-site/docs/service_ldap_server.md:
##########
@@ -140,25 +149,59 @@ The disabled user filter interceptor will recognize and
filter disabled users. I
#### Roles Lookup Interceptor (`rolesLookup`)
-The rolesLookup interceptor is created if the `interceptorType` configuration
is set to `rolesLookup`. This interceptor transforms the response entities
based on the mappings provided by the Role Lookup Service. For each entity, a
request will be made to lookup roles based on the user's name and group
membership. These roles will replace the values in the `memberOf` attribute.
+The rolesLookup interceptor is created if the `interceptorType` configuration
is set to `rolesLookup`. It uses the Role Lookup Service, configured with
`gateway.ldap.roles.lookup.strategy`, to apply roles to search requests in
three ways.
+
+**Role mapping.** For each user entry returned, a request is made to lookup
roles based on the user's name and group membership. These roles replace the
values in the `memberOf` attribute. Group entries are replaced with role
entries named after the roles mapped to the group.
+
+E.g.,
+```shell
+ldapsearch -H ldap://<ldaphost>:<ldapport> -D <bind_dn> -w <bind_password> -b
'ou=people,dc=proxy,dc=com', -s sub '(objectClass=inetOrgPerson)' '*'
+```
+
+**Role listing.** A search for all groups is answered with the roles known to
the Role Lookup Service instead of the backend groups. Each role is returned as
a `groupOfNames` entry with the DN `cn=<scope>:<name>,ou=groups,<base dn>`.
Listed Role entries do not contain the `member` attribute because the role data
is returned through the role strategy without further search requests to LDAP.
A search is an all-groups search when its scope is not `base` (object) and its
filter is an all-groups filter, and it is issued either from `ou=groups,<base
dn>` or from the root of the directory (a base DN of at most two RDNs). A
filter is an all-groups filter if it is one of:
+
+- `(objectClass=groupOfNames)` or `(objectClass=groupOfUniqueNames)`
+- `(cn=*)` or `(objectClass=*)`
+- an AND filter where every child is an all-groups filter, for example
`(&(objectClass=groupOfNames)(cn=*))`
+- an OR filter where at least one child is an all-groups filter
Review Comment:
The all-groups rules in the code changed with the last fixes, but this
paragraph still describes the old ones:
- Line 161, "from the root of the directory (a base DN of at most two
RDNs)": the code now accepts any ancestor of `ou=groups,<base dn>` (compared as
a `Dn`). It no longer counts RDNs.
- Line 164: `(cn=*)` / `(objectClass=*)` are only all-groups filters when
the base is `ou=groups,<base dn>`. From an ancestor they now go to the backend.
- Line 166, "an OR filter where at least one child is an all-groups filter":
this only applies at `ou=groups,<base dn>`. From an ancestor, every OR child
must be `(objectClass=groupOfNames)` or `(objectClass=groupOfUniqueNames)`. An
AND needs at least one such objectClass child, and every other child must be an
all-groups filter.
Suggest splitting the list by search base, for example:
From `ou=groups,<base dn>`: `(objectClass=groupOfNames)`,
`(objectClass=groupOfUniqueNames)`, `(cn=*)`, `(objectClass=*)`, an AND where
every child is one of these, or an OR where at least one child is.
From an ancestor of `ou=groups,<base dn>` (e.g. `<base dn>`):
`(objectClass=groupOfNames)`, `(objectClass=groupOfUniqueNames)`, an OR of only
those, or an AND that contains at least one of them and whose other children
are all-groups filters, e.g. `(&(objectClass=groupOfNames)(cn=*))`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]