add ldapgroup contract

This commit is contained in:
ibizaman 2025-01-21 20:49:53 +01:00
parent b34a1e57a7
commit 3213f119d2
10 changed files with 469 additions and 56 deletions

View file

@ -1,6 +1,9 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
inherit (lib) mkOption;
inherit (lib.types) attrsOf str submodule;
cfg = config.shb.ldap; cfg = config.shb.ldap;
contracts = pkgs.callPackage ../contracts {}; contracts = pkgs.callPackage ../contracts {};
@ -149,16 +152,42 @@ in
}; };
groups = lib.mkOption { groups = lib.mkOption {
description = "LDAP Groups to manage declaratively."; description = "LDAP Groups to manage declaratively following the [ldap group contract](./contracts-ldap-group.html).";
default = {}; default = {};
example = lib.literalExpression '' example = lib.literalExpression ''
{ {
family = {}; family = {};
} }
''; '';
type = lib.types.attrsOf (lib.types.submodule { type = attrsOf (submodule ({ name, config, ... }: {
options = {}; options = contracts.ldapgroup.mkProvider {
}); settings = mkOption {
description = ''
Settings specific to the LLDAP provider.
By default it is the same as the field name.
'';
default = {
inherit name;
};
type = submodule {
options = {
name = mkOption {
description = "Name of the LDAP group";
type = str;
default = name;
};
};
};
};
resultCfg = {
name = config.settings.name;
nameText = name;
};
};
}));
}; };
deleteUnmanagedUsers = lib.mkOption { deleteUnmanagedUsers = lib.mkOption {
@ -372,8 +401,6 @@ in
systemd.services.lldap.postStart = systemd.services.lldap.postStart =
let let
configFile = (pkgs.formats.toml {}).generate "lldap_config.toml" config.services.lldap.settings;
login = ['' login = [''
set -euo pipefail set -euo pipefail
@ -388,7 +415,7 @@ in
deleteGroups = ['' deleteGroups = [''
allUids=(${lib.concatStringsSep " " ( allUids=(${lib.concatStringsSep " " (
(lib.mapAttrsToList (id: g: id) cfg.groups) (lib.mapAttrsToList (id: g: g.settings.name) cfg.groups)
++ [ "lldap_admin" "lldap_password_manager" "lldap_strict_readonly" ]) ++ [ "lldap_admin" "lldap_password_manager" "lldap_strict_readonly" ])
}) })
echo All managed groups are: ''${allUids[*]} echo All managed groups are: ''${allUids[*]}
@ -405,7 +432,7 @@ in
createGroups = ['' createGroups = [''
existingUids=$(${lldap-cli}/bin/lldap-cli group list | ${pkgs.jq}/bin/jq -r 'map(.displayName)[]') existingUids=$(${lldap-cli}/bin/lldap-cli group list | ${pkgs.jq}/bin/jq -r 'map(.displayName)[]')
managedUids=(${lib.concatStringsSep " " ( managedUids=(${lib.concatStringsSep " " (
(lib.mapAttrsToList (id: g: id) cfg.groups) (lib.mapAttrsToList (id: g: g.settings.name) cfg.groups)
++ [ "lldap_admin" "lldap_password_manager" "lldap_strict_readonly" ]) ++ [ "lldap_admin" "lldap_password_manager" "lldap_strict_readonly" ])
}) })
echo All managed groups are: ''${managedUids[*]} echo All managed groups are: ''${managedUids[*]}

View file

@ -0,0 +1,133 @@
# LLDAP Block {#blocks-lldap}
Defined in [`/modules/blocks/lldap.nix`](@REPO@/modules/blocks/lldap.nix).
This block sets up a LDAP server using [LLDAP][].
[LLDAP]: https://github.com/lldap/lldap
## Tests {#blocks-lldap-tests}
Specific integration tests are defined in [`/test/blocks/lldap.nix`](@REPO@/test/blocks/lldap.nix).
The tests use a neat trick using specialization and switching configuration
to make sure changing the declarative configuration has the expected result.
## Provider Contracts {#blocks-lldap-contract-provider}
This block provides the following contract:
- [groups ldap contract](contracts-groups-ldap.html) under the [`shb.ldap.groups`][groups] option.
It is tested with [contract tests][groups ldap contract tests].
[groups]: #blocks-lldap-options-shb.ldap.groups
[groups ldap contract tests]: @REPO@/test/contracts/groups-ldap.nix
## Usage {#blocks-lldap-usage}
### Manage groups {#blocks-lldap-usage-groups}
The following snippet will create group named "family" if it does not exist yet.
Also, all other groups will be deleted and only the "family" group will remain.
Note that the "admin" group, which is internal to LLDAP, will never be deleted.
```nix
{
shb.ldap.groups = {
family = {};
};
}
```
Changing the configuration to the following will add a new group "friends":
```nix
{
shb.ldap.groups = {
family = {};
friends = {};
};
}
```
Switching back the configuration to the previous one will delete the group "friends":
```nix
{
shb.ldap.groups = {
family = {};
};
}
```
Currently, only the empty attrset is supported as the value for a group.
This will change in the future when LLDAP supports custom group attributes.
### Manage users {#blocks-lldap-usage-users}
The following snippet creates a user and makes it a member of the "family" group.
```nix
{
shb.ldap.users = {
dad = {
email = "dad@example.com";
displayName = "Dad";
firstName = "First Name";
lastName = "Last Name";
groups = [ "family" ];
password.result = config.shb.sops.secret."dad".result;
};
};
shb.sops.secret."dad".request =
shb.ldap.users.dad.password.request;
}
```
The password field assumes usage of the [sops block][] to provide secrets
although any blocks providing the [secrets contract][] works too.
[sops block]: ./blocks-sops.html
[secrets contract]: ./contracts-secrets.html
The user is still editable through the UI.
That being said, any change will be overwritten next time the configuration is applied.
If instead you just want to set initial values,
there are fields for that:
```nix
{
shb.ldap.users = {
dad = {
initialEmail = "dad@example.com";
initialDisplayName = "Dad";
initialFirstName = "First Name";
initialLastName = "Last Name";
initialGroups = [ "family" ];
initialPassword.result = config.shb.sops.secret."dad".result;
};
};
shb.sops.secret."dad".request =
shb.ldap.users.dad.password.request;
}
```
Also, all fields apart from the email are optional, even the password.
Adding or removing groups to the `shb.ldap.users.<name>.groups` will make the user member of the groups listed in the option.
## Troubleshooting {#blocks-lldap-troubleshooting}
To see the logs, run `journalctl -u lldap.service`.
To see the trace of the GraphQL queries, set `shb.ldap.debug = true;`.
## Options Reference {#blocks-lldap-options}
```{=include=} options
id-prefix: blocks-lldap-options-
list-id: selfhostblocks-block-lldap-options
source: @OPTIONS_JSON@
```

View file

@ -19,29 +19,39 @@ source: @OPTIONS_JSON@
## Usage {#contract-databasebackup-usage} ## Usage {#contract-databasebackup-usage}
A database that can be backed up will provide a `databasebackup` option. What this contract defines is, from the user perspective - that is _you_ - an implementation detail
Such a service is a `requester` providing a `request` for a module `provider` of this contract.
What this option defines is, from the user perspective - that is _you_ - an implementation detail
but it will at least define how to create a database dump, but it will at least define how to create a database dump,
the user to backup with the user to backup with
and how to restore from a database dump. and how to restore from a database dump.
A NixOS module that can be backed up using this contract will provide a `databasebackup` option.
Such a service is a `requester` which has a `request`.
Here is an example module defining such a `databasebackup` option: Here is an example module defining such a `databasebackup` option:
```nix ```nix
{ {
options = { options = {
myservice.databasebackup = mkOption { databasebackupservices.instances = mkOption {
type = contracts.databasebackup.request; description = ''
default = { Backup configuration.
user = "myservice"; '';
backupCmd = ''
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable default = {};
''; type = submodule {
restoreCmd = '' options = contracts.databasebackup.mkRequester {
${pkgs.gzip}/bin/gunzip | ${pkgs.postgresql}/bin/psql postgres user = "postgres";
'';
backupName = "postgres.sql";
backupCmd = ''
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
'';
restoreCmd = ''
${pkgs.gzip}/bin/gunzip | ${pkgs.postgresql}/bin/psql postgres
'';
};
}; };
}; };
}; };
@ -51,28 +61,60 @@ Here is an example module defining such a `databasebackup` option:
Now, on the other side we have a service that uses this `backup` option and actually backs up files. Now, on the other side we have a service that uses this `backup` option and actually backs up files.
This service is a `provider` of this contract and will provide a `result` option. This service is a `provider` of this contract and will provide a `result` option.
Let's assume such a module is available under the `databasebackupservice` option ```nix
and that one can create multiple backup instances under `databasebackupservice.instances`. {
Then, to actually backup the `myservice` service, one would write: options = {
instances = mkOption {
description = "Files to backup.";
default = {};
type = attrsOf (submodule ({ name, config, ... }: {
options = contracts.backup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the this provider.
'';
type = submodule {
options = {
enable = mkEnableOption "this backup intance.";
# ... Other options specific to this provider.
};
};
};
resultCfg = let
fullName = name: repository: "backups-${name}_${repository.path}";
in {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}));
};
};
}
```
Then, to actually backup the `myservice` service,
one would need to link the requester to the provider with:
```nix ```nix
databasebackupservice.instances.myservice = { databasebackupservice.instances.myservice = {
request = myservice.databasebackup; request = config.myservice.databasebackup.request;
settings = { settings = {
enable = true; enable = true;
repository = { # ... Other options specific to this provider.
path = "/srv/backup/myservice";
};
# ... Other options specific to backupservice like scheduling.
}; };
}; };
``` ```
It is advised to backup files to different location, to improve redundancy. It is advised to backup files to different location, to improve redundancy.
Thanks to using contracts, this can be made easily either with the same `databasebackupservice`: Thanks to using contracts, this can be done easily either with the same `databasebackupservice`:
```nix ```nix
databasebackupservice.instances.myservice_2 = { databasebackupservice.instances.myservice_2 = {

View file

@ -47,6 +47,7 @@ in
{ {
databasebackup = importContract ./databasebackup.nix; databasebackup = importContract ./databasebackup.nix;
backup = importContract ./backup.nix; backup = importContract ./backup.nix;
ldapgroup = importContract ./ldapgroup.nix;
mount = import ./mount.nix { inherit lib; }; mount = import ./mount.nix { inherit lib; };
secret = importContract ./secret.nix; secret = importContract ./secret.nix;
ssl = import ./ssl.nix { inherit lib; }; ssl = import ./ssl.nix { inherit lib; };

View file

@ -0,0 +1,83 @@
{ pkgs, lib, ... }:
let
inherit (lib) literalMD mkOption optionalAttrs optionalString;
inherit (lib.types) str submodule;
shblib = pkgs.callPackage ../../lib {};
inherit (shblib) anyNotNull;
in
{
mkRequest = {
name ? "",
nameText ? null,
}: mkOption {
description = ''
Request part of the ldap group contract.
Options set by the requester module
enforcing what properties the group should have.
This is intentionally empty as the only required property
is the existence of this option.
'';
default = {
inherit name;
};
defaultText = optionalString (anyNotNull [
nameText
]) (literalMD ''
{
name = ${if nameText != null then nameText else name};
}
'');
type = submodule {
options = {
name = mkOption {
description = ''
Name of the LDAP group.
'';
type = str;
default = name;
} // optionalAttrs (nameText != null) {
defaultText = literalMD nameText;
};
};
};
};
mkResult = {
name ? "",
nameText ? null,
}: mkOption {
description = ''
Result part of the ldap group contract.
Options set by the provider module that indicates the name of the group and other properties.
'';
default = {
inherit name;
};
defaultText = optionalString (anyNotNull [
nameText
]) (literalMD ''
{
name = ${if nameText != null then nameText else name};
}
'');
type = submodule {
options = {
name = mkOption {
description = ''
Name of the LDAP group.
'';
type = str;
default = name;
} // optionalAttrs (nameText != null) {
defaultText = literalMD nameText;
};
};
};
};
}

View file

@ -0,0 +1,124 @@
# LDAP Group Contract {#contract-ldapgroup}
This NixOS contract represents an LDAP group
that must be created.
It is a contract between a service that needs an LDAP group
and a service that can provide such a group.
## Contract Reference {#contract-ldapgroup-options}
These are all the options that are expected to exist for this contract to be respected.
```{=include=} options
id-prefix: contracts-ldapgroup-options-
list-id: selfhostblocks-options
source: @OPTIONS_JSON@
```
## Usage {#contract-ldapgroup-usage}
What this contract defines is, from the user perspective - that is _you_ - an implementation detail
but you can know it simply defines the LDAP group name.
A NixOS module that needs a LDAP group using this contract will provide a `ldapgroup` option or similarly named.
Such a service is a `requester` providing a `request` for a module `provider` of this contract.
Here is an example module defining such a `ldapgroup` option:
```nix
{
options = {
myservice.ldapgroup = mkOption {
type = contracts.ldapgroup.request;
};
};
};
```
Now, on the other side we have a service that uses this `ldapgroup` option and actually creates the LDAP group.
This service is a `provider` of this contract and will provide a `result` option containing the name of the group.
```nix
{
options = {
ldap.groups = lib.mkOption {
description = "LDAP Groups to manage declaratively.";
default = {};
example = lib.literalExpression ''
{
family = {};
}
'';
type = attrsOf (submodule ({ name, config, ... }: {
options = contracts.ldapgroup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the LLDAP provider.
By default it is the same as the field name.
'';
default = {
inherit name;
};
type = submodule {
options = {
name = mkOption {
description = "Name of the LDAP group";
type = str;
default = name;
};
};
};
};
resultCfg = {
name = config.settings.name;
nameText = name;
};
};
}));
};
};
};
```
Then, to actually backup the `myservice` service,
one would need to link the requester to the provider with:
```nix
# requester -> provider
ldapgroupservice.groups.myservice = {
request = config.myservice.ldapgroup.request;
};
# provider -> requester
myservice.ldapgroup.result = config.ldapgroupservice.groups.myservice.result;
```
By default, the name of the LDAP group will be the same as the field name under the `groups` option. Here, `"myservice"`.
Usually, a service will require two LDAP groups to work properly,
one for users and another one for admin users.
In this case, the linking them together will look like so:
```nix
# requester -> provider
ldapgroupservice.groups = {
myservice_user.request = config.myservice.ldap.userGroup.request;
myservice_admin.request = config.myservice.ldap.adminGroup.request;
};
# provider -> requester
myservice.ldap = {
userGroup.result = config.ldapgroupservice.groups.myservice_user.result;
adminGroup.result = config.ldapgroupservice.groups.myservice_admin.result;
};
```
## Providers of the Database Backup Contract {#contract-ldapgroup-providers}
- [LDAP block](blocks-ldap.html).
## Requester Blocks and Services {#contract-ldapgroup-requesters}
- [Nextcloud service](services-nextcloud.html#services-nextcloud-contract-ldapgroup).

View file

@ -98,15 +98,13 @@ in
}; };
userGroup = mkOption { userGroup = mkOption {
type = str; type = contracts.ldapgroup.request;
description = "Group users must belong to be able to login."; description = "Group users must belong to be able to login.";
default = "forgejo_user";
}; };
adminGroup = mkOption { adminGroup = mkOption {
type = str; type = contracts.ldapgroup.request;
description = "Group users must belong to be admins."; description = "Group users must belong to be admins.";
default = "forgejo_admin";
}; };
}; };
}); });
@ -394,8 +392,8 @@ in
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \ --bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
--security-protocol Unencrypted \ --security-protocol Unencrypted \
--user-search-base ou=people,${cfg.ldap.dcdomain} \ --user-search-base ou=people,${cfg.ldap.dcdomain} \
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ --user-filter '(&(memberof=cn=${cfg.ldap.userGroup.result.name},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ --admin-filter '(memberof=cn=${cfg.ldap.adminGroup.result.name},ou=groups,${cfg.ldap.dcdomain})' \
--username-attribute uid \ --username-attribute uid \
--firstname-attribute givenName \ --firstname-attribute givenName \
--surname-attribute sn \ --surname-attribute sn \
@ -413,8 +411,8 @@ in
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \ --bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
--security-protocol Unencrypted \ --security-protocol Unencrypted \
--user-search-base ou=people,${cfg.ldap.dcdomain} \ --user-search-base ou=people,${cfg.ldap.dcdomain} \
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ --user-filter '(&(memberof=cn=${cfg.ldap.userGroup.result.name},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ --admin-filter '(memberof=cn=${cfg.ldap.adminGroup.result.name},ou=groups,${cfg.ldap.dcdomain})' \
--username-attribute uid \ --username-attribute uid \
--firstname-attribute givenName \ --firstname-attribute givenName \
--surname-attribute sn \ --surname-attribute sn \

View file

@ -122,9 +122,8 @@ in
}; };
userGroup = lib.mkOption { userGroup = lib.mkOption {
type = lib.types.str; type = contracts.ldapgroup.request;
description = "Group users must belong to to be able to login to Nextcloud."; description = "Group users must belong to to be able to login to Nextcloud.";
default = "homeassistant_user";
}; };
keepDefaultAuth = lib.mkOption { keepDefaultAuth = lib.mkOption {
@ -241,7 +240,7 @@ in
{ {
type = "command_line"; type = "command_line";
command = ldap_auth_script + "/bin/ldap_auth.sh"; command = ldap_auth_script + "/bin/ldap_auth.sh";
args = [ "http://${cfg.ldap.host}:${toString cfg.ldap.port}" cfg.ldap.userGroup ]; args = [ "http://${cfg.ldap.host}:${toString cfg.ldap.port}" cfg.ldap.userGroup.result.name ];
meta = true; meta = true;
} }
]); ]);

View file

@ -56,15 +56,23 @@ in
}; };
userGroup = lib.mkOption { userGroup = lib.mkOption {
type = lib.types.str;
description = "LDAP user group"; description = "LDAP user group";
default = "jellyfin_user"; default = {};
type = lib.types.submodule {
options = contracts.ldapgroup.mkRequester {
name = "jellyfin_user";
};
};
}; };
adminGroup = lib.mkOption { adminGroup = lib.mkOption {
type = lib.types.str;
description = "LDAP admin group"; description = "LDAP admin group";
default = "jellyfin_admin"; default = {};
type = lib.types.submodule {
options = contracts.ldapgroup.mkRequester {
name = "jellyfin_admin";
};
};
}; };
adminPassword = lib.mkOption { adminPassword = lib.mkOption {
@ -313,9 +321,9 @@ in
<LdapBindUser>uid=admin,ou=people,${cfg.ldap.dcdomain}</LdapBindUser> <LdapBindUser>uid=admin,ou=people,${cfg.ldap.dcdomain}</LdapBindUser>
<LdapBindPassword>%LDAP_PASSWORD%</LdapBindPassword> <LdapBindPassword>%LDAP_PASSWORD%</LdapBindPassword>
<LdapBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapBaseDn> <LdapBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapBaseDn>
<LdapSearchFilter>(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})</LdapSearchFilter> <LdapSearchFilter>(memberof=cn=${cfg.ldap.userGroup.result.name},ou=groups,${cfg.ldap.dcdomain})</LdapSearchFilter>
<LdapAdminBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapAdminBaseDn> <LdapAdminBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapAdminBaseDn>
<LdapAdminFilter>(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})</LdapAdminFilter> <LdapAdminFilter>(memberof=cn=${cfg.ldap.adminGroup.result.name},ou=groups,${cfg.ldap.dcdomain})</LdapAdminFilter>
<EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid> <EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid>
<LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes> <LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes>
<LdapClientCertPath /> <LdapClientCertPath />

View file

@ -414,11 +414,9 @@ in
}; };
}; };
userGroup = lib.mkOption { userGroup = lib.mkOption {
type = lib.types.str; type = contracts.ldapgroup.request;
description = "Group users must belong to to be able to login to Nextcloud."; description = "Group users must belong to to be able to login to Nextcloud.";
default = "nextcloud_user";
}; };
configID = lib.mkOption { configID = lib.mkOption {
@ -1010,21 +1008,21 @@ in
${occ} ldap:set-config "${cID}" 'ldapEmailAttribute' \ ${occ} ldap:set-config "${cID}" 'ldapEmailAttribute' \
'mail' 'mail'
${occ} ldap:set-config "${cID}" 'ldapGroupFilter' \ ${occ} ldap:set-config "${cID}" 'ldapGroupFilter' \
'(&(|(objectclass=groupOfUniqueNames))(|(cn=${cfg'.userGroup})))' '(&(|(objectclass=groupOfUniqueNames))(|(cn=${cfg'.userGroup.result.name})))'
${occ} ldap:set-config "${cID}" 'ldapGroupFilterGroups' \ ${occ} ldap:set-config "${cID}" 'ldapGroupFilterGroups' \
'${cfg'.userGroup}' '${cfg'.userGroup.result.name}'
${occ} ldap:set-config "${cID}" 'ldapGroupFilterObjectclass' \ ${occ} ldap:set-config "${cID}" 'ldapGroupFilterObjectclass' \
'groupOfUniqueNames' 'groupOfUniqueNames'
${occ} ldap:set-config "${cID}" 'ldapGroupMemberAssocAttr' \ ${occ} ldap:set-config "${cID}" 'ldapGroupMemberAssocAttr' \
'uniqueMember' 'uniqueMember'
${occ} ldap:set-config "${cID}" 'ldapLoginFilter' \ ${occ} ldap:set-config "${cID}" 'ldapLoginFilter' \
'(&(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))(|(uid=%uid)(|(mail=%uid)(objectclass=%uid))))' '(&(&(objectclass=person)(memberOf=cn=${cfg'.userGroup.result.name},ou=groups,${cfg'.dcdomain}))(|(uid=%uid)(|(mail=%uid)(objectclass=%uid))))'
${occ} ldap:set-config "${cID}" 'ldapLoginFilterAttributes' \ ${occ} ldap:set-config "${cID}" 'ldapLoginFilterAttributes' \
'mail;objectclass' 'mail;objectclass'
${occ} ldap:set-config "${cID}" 'ldapUserDisplayName' \ ${occ} ldap:set-config "${cID}" 'ldapUserDisplayName' \
'displayname' 'displayname'
${occ} ldap:set-config "${cID}" 'ldapUserFilter' \ ${occ} ldap:set-config "${cID}" 'ldapUserFilter' \
'(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))' '(&(objectclass=person)(memberOf=cn=${cfg'.userGroup.result.name},ou=groups,${cfg'.dcdomain}))'
${occ} ldap:set-config "${cID}" 'ldapUserFilterMode' \ ${occ} ldap:set-config "${cID}" 'ldapUserFilterMode' \
'1' '1'
${occ} ldap:set-config "${cID}" 'ldapUserFilterObjectclass' \ ${occ} ldap:set-config "${cID}" 'ldapUserFilterObjectclass' \