Add support for bootstrapping schemas (#991)

* Moved default bootstrap dirs into single /bootstrap parent dir in order to have single docker volume bind (with fallback to previous folder hierarchy)
* Added default values for LDAP user and credentials
* Added support for bootstrapping schema

Place schema files under /bootstrap/(user|group)-schemas/*.json

Sample content:
[
  {
    "name" : "test_attrib",
    "attributeType" : "STRING",
    "isEditable" : true,
    "isList" : false,
    "isVisible" : true
  }
]
This commit is contained in:
Grzegorz Godlewski 2024-10-10 21:05:01 +02:00 committed by GitHub
parent a6eac55fc7
commit dcb45d4f6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 194 additions and 18 deletions

View file

@ -1,4 +1,4 @@
# Bootstrapping lldap using [bootstrap.sh](bootstrap.sh) script # Bootstrapping lldap using [bootstrap.sh](/scripts/bootstrap.sh) script
bootstrap.sh allows managing your lldap in a git-ops, declarative way using JSON config files. bootstrap.sh allows managing your lldap in a git-ops, declarative way using JSON config files.
@ -12,7 +12,7 @@ The script can:
* create groups * create groups
* delete redundant users and groups (when `DO_CLEANUP` env var is true) * delete redundant users and groups (when `DO_CLEANUP` env var is true)
* maintain the desired state described in JSON config files * maintain the desired state described in JSON config files
* create user/group user-defined attributes
![](bootstrap-example-log-1.jpeg) ![](bootstrap-example-log-1.jpeg)
@ -27,11 +27,13 @@ The script can:
## Environment variables ## Environment variables
- `LLDAP_URL` or `LLDAP_URL_FILE` - URL to your lldap instance or path to file that contains URL (**MANDATORY**) - `LLDAP_URL` or `LLDAP_URL_FILE` (default value: `http://localhost:17170`) - URL to your lldap instance or path to file that contains URL
- `LLDAP_ADMIN_USERNAME` or `LLDAP_ADMIN_USERNAME_FILE` - admin username or path to file that contains username (**MANDATORY**) - `LLDAP_ADMIN_USERNAME` or `LLDAP_ADMIN_USERNAME_FILE` (default value: `admin`) - admin username or path to file that contains username
- `LLDAP_ADMIN_PASSWORD` or `LLDAP_ADMIN_PASSWORD_FILE` - admin password or path to file that contains password (**MANDATORY**) - `LLDAP_ADMIN_PASSWORD` or `LLDAP_ADMIN_PASSWORD_FILE` (default value: `password`) - admin password or path to file that contains password
- `USER_CONFIGS_DIR` (default value: `/user-configs`) - directory where the user JSON configs could be found - `USER_CONFIGS_DIR` (default value: `/bootstrap/user-configs`) - directory where the user JSON configs could be found
- `GROUP_CONFIGS_DIR` (default value: `/group-configs`) - directory where the group JSON configs could be found - `GROUP_CONFIGS_DIR` (default value: `/bootstrap/group-configs`) - directory where the group JSON configs could be found
- `USER_SCHEMAS_DIR` (default value: `/bootstrap/user-schemas`) - directory where the user schema JSON configs could be found
- `GROUP_SCHEMAS_DIR` (default value: `/bootstrap/group-schemas`) - directory where the group schema JSON configs could be found
- `LLDAP_SET_PASSWORD_PATH` - path to the `lldap_set_password` utility (default value: `/app/lldap_set_password`) - `LLDAP_SET_PASSWORD_PATH` - path to the `lldap_set_password` utility (default value: `/app/lldap_set_password`)
- `DO_CLEANUP` (default value: `false`) - delete groups and users not specified in config files, also remove users from groups that they do not belong to - `DO_CLEANUP` (default value: `false`) - delete groups and users not specified in config files, also remove users from groups that they do not belong to
@ -96,6 +98,44 @@ Fields description:
``` ```
### User and group schema config file example
User and group schema have the same structure.
Fields description:
* `name`: name of field, case insensitve - you should use lowercase
* `attributeType`: `STRING` / `INTEGER` / `JPEG` / `DATE_TIME`
* `isList`: single on multiple value field
* `isEditable`: self-explanatory
* `isVisible`: self-explanatory
```json
[
{
"name": "uid",
"attributeType": "INTEGER",
"isEditable": false,
"isList": false,
"isVisible": true
},
{
"name": "mailbox",
"attributeType": "STRING",
"isEditable": false,
"isList": false,
"isVisible": true
},
{
"name": "mail_alias",
"attributeType": "STRING",
"isEditable": false,
"isList": true,
"isVisible": true
}
]
```
## Usage example ## Usage example
### Manually ### Manually
@ -110,11 +150,21 @@ export LLDAP_ADMIN_USERNAME=admin
export LLDAP_ADMIN_PASSWORD=changeme export LLDAP_ADMIN_PASSWORD=changeme
export USER_CONFIGS_DIR="$(realpath ./configs/user)" export USER_CONFIGS_DIR="$(realpath ./configs/user)"
export GROUP_CONFIGS_DIR="$(realpath ./configs/group)" export GROUP_CONFIGS_DIR="$(realpath ./configs/group)"
export USER_SCHEMAS_DIR="$(realpath ./configs/user-schema)"
export GROUP_SCHEMAS_DIR="$(realpath ./configs/group-schema)"
export LLDAP_SET_PASSWORD_PATH="$(realpath ./lldap_set_password)" export LLDAP_SET_PASSWORD_PATH="$(realpath ./lldap_set_password)"
export DO_CLEANUP=false export DO_CLEANUP=false
./bootstrap.sh ./bootstrap.sh
``` ```
### Manually from running docker container or service
After setting a docker container you can bootstrap users using:
```
docker exec -e LLDAP_ADMIN_PASSWORD_FILE=password -v ./bootstrap:/bootstrap -it $(docker ps --filter name=lldap -q) /app/bootstrap.sh
```
### Docker compose ### Docker compose
Let's suppose you have the next file structure: Let's suppose you have the next file structure:
@ -129,10 +179,17 @@ Let's suppose you have the next file structure:
│ ├─ ... │ ├─ ...
│ └─ user-n.json │ └─ user-n.json
└─ group-configs └─ group-configs
├─ group-1.json | ├─ group-1.json
| ├─ ...
| └─ group-n.json
└─ user-schemas
| ├─ user-attrs-1.json
| ├─ ...
| └─ user-attrs-n.json
└─ group-schemas
├─ group-attrs-1.json
├─ ... ├─ ...
└─ group-n.json └─ group-attrs-n.json
``` ```
You should mount `bootstrap` dir to lldap container and set the corresponding `env` variables: You should mount `bootstrap` dir to lldap container and set the corresponding `env` variables:
@ -160,6 +217,8 @@ services:
- LLDAP_ADMIN_PASSWORD=changeme # same as LLDAP_LDAP_USER_PASS - LLDAP_ADMIN_PASSWORD=changeme # same as LLDAP_LDAP_USER_PASS
- USER_CONFIGS_DIR=/bootstrap/user-configs - USER_CONFIGS_DIR=/bootstrap/user-configs
- GROUP_CONFIGS_DIR=/bootstrap/group-configs - GROUP_CONFIGS_DIR=/bootstrap/group-configs
- USER_SCHEMAS_DIR=/bootstrap/user-schemas
- GROUP_SCHEMAS_DIR=/bootstrap/group-schemas
- DO_CLEANUP=false - DO_CLEANUP=false
``` ```
@ -205,14 +264,15 @@ spec:
volumeMounts: volumeMounts:
- name: bootstrap - name: bootstrap
mountPath: /bootstrap/bootstrap.sh mountPath: /bootstrap/bootstrap.sh
readOnly: true
subPath: bootstrap.sh subPath: bootstrap.sh
- name: user-configs - name: user-configs
mountPath: /user-configs mountPath: /bootstrap/user-configs
readOnly: true readOnly: true
- name: group-configs - name: group-configs
mountPath: /group-configs mountPath: /bootstrap/group-configs
readOnly: true readOnly: true
volumes: volumes:

View file

@ -3,14 +3,24 @@
set -e set -e
set -o pipefail set -o pipefail
LLDAP_URL="${LLDAP_URL}" LLDAP_URL="${LLDAP_URL:-http://localhost:17170}"
LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME}" LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME:-admin}"
LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD}" LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD:-password}"
USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/user-configs}" USER_SCHEMAS_DIR="${USER_SCHEMAS_DIR:-/bootstrap/user-schemas}"
GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/group-configs}" GROUP_SCHEMAS_DIR="${GROUP_SCHEMAS_DIR:-/bootstrap/group-schemas}"
USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/bootstrap/user-configs}"
GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/bootstrap/group-configs}"
LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}" LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}"
DO_CLEANUP="${DO_CLEANUP:-false}" DO_CLEANUP="${DO_CLEANUP:-false}"
# Fallback to support legacy defaults
if [[ ! -d $USER_CONFIGS_DIR ]] && [[ -d "/user-configs" ]]; then
USER_CONFIGS_DIR="/user-configs"
fi
if [[ ! -d $GROUP_CONFIGS_DIR ]] && [[ -d "/group-configs" ]]; then
GROUP_CONFIGS_DIR="/group-configs"
fi
check_install_dependencies() { check_install_dependencies() {
local commands=('curl' 'jq' 'jo') local commands=('curl' 'jq' 'jo')
local commands_not_found='false' local commands_not_found='false'
@ -280,6 +290,80 @@ delete_user() {
fi fi
} }
get_group_property_list() {
local query='{"query":"query GetGroupAttributesSchema { schema { groupSchema { attributes { name }}}}","operationName":"GetGroupAttributesSchema"}'
make_query <(printf '%s' "$query") <(printf '{}')
}
group_property_exists() {
if [[ "$(get_group_property_list | jq --raw-output --arg name "$1" '.data.schema.groupSchema.attributes | any(.[]; select(.name == $name))')" == 'true' ]]; then
return 0
else
return 1
fi
}
create_group_schema_property() {
local name="$1"
local attributeType="$2"
local isEditable="$3"
local isList="$4"
local isVisible="$5"
if group_property_exists "$name"; then
printf 'Group property "%s" already exists\n' "$name"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateGroupAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {addGroupAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {ok}}","operationName":"CreateGroupAttribute"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- name="$name" attributeType="$attributeType" isEditable="$isEditable" isList="$isList" isVisible="$isVisible"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group attribute "%s" successfully created\n' "$name"
fi
}
get_user_property_list() {
local query='{"query":"query GetUserAttributesSchema { schema { userSchema { attributes { name }}}}","operationName":"GetUserAttributesSchema"}'
make_query <(printf '%s' "$query") <(printf '{}')
}
user_property_exists() {
if [[ "$(get_user_property_list | jq --raw-output --arg name "$1" '.data.schema.userSchema.attributes | any(.[]; select(.name == $name))')" == 'true' ]]; then
return 0
else
return 1
fi
}
create_user_schema_property() {
local name="$1"
local attributeType="$2"
local isEditable="$3"
local isList="$4"
local isVisible="$5"
if user_property_exists "$name"; then
printf 'User property "%s" already exists\n' "$name"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateUserAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {addUserAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {ok}}","operationName":"CreateUserAttribute"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- name="$name" attributeType="$attributeType" isEditable="$isEditable" isList="$isList" isVisible="$isVisible"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User attribute "%s" successfully created\n' "$name"
fi
}
__common_user_mutation_query() { __common_user_mutation_query() {
local \ local \
query="$1" \ query="$1" \
@ -387,8 +471,18 @@ main() {
local user_config_files=("${USER_CONFIGS_DIR}"/*.json) local user_config_files=("${USER_CONFIGS_DIR}"/*.json)
local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json) local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json)
local user_schema_files=()
local group_schema_files=()
if ! check_configs_validity "${group_config_files[@]}" "${user_config_files[@]}"; then local file=''
[[ -d "$USER_SCHEMAS_DIR" ]] && for file in "${USER_SCHEMAS_DIR}"/*.json; do
user_schema_files+=("$file")
done
[[ -d "$GROUP_SCHEMAS_DIR" ]] && for file in "${GROUP_SCHEMAS_DIR}"/*.json; do
group_schema_files+=("$file")
done
if ! check_configs_validity "${group_config_files[@]}" "${user_config_files[@]}" "${group_schema_files[@]}" "${user_schema_files[@]}"; then
exit 1 exit 1
fi fi
@ -399,6 +493,28 @@ main() {
auth "$LLDAP_URL" "$LLDAP_ADMIN_USERNAME" "$LLDAP_ADMIN_PASSWORD" auth "$LLDAP_URL" "$LLDAP_ADMIN_USERNAME" "$LLDAP_ADMIN_PASSWORD"
printf -- '\n--- group schemas ---\n'
local group_schema_config_row=''
[[ ${#group_schema_files[@]} -gt 0 ]] && while read -r group_schema_config_row; do
local field='' name='' attributeType='' isEditable='' isList='' isVisible=''
for field in 'name' 'attributeType' 'isEditable' 'isList' 'isVisible'; do
declare "$field"="$(printf '%s' "$group_schema_config_row" | jq --raw-output --arg field "$field" '.[$field]')"
done
create_group_schema_property "$name" "$attributeType" "$isEditable" "$isList" "$isVisible"
done < <(jq --compact-output '.[]' -- "${group_schema_files[@]}")
printf -- '--- group schemas ---\n'
printf -- '\n--- user schemas ---\n'
local user_schema_config_row=''
[[ ${#user_schema_files[@]} -gt 0 ]] && while read -r user_schema_config_row; do
local field='' name='' attributeType='' isEditable='' isList='' isVisible=''
for field in 'name' 'attributeType' 'isEditable' 'isList' 'isVisible'; do
declare "$field"="$(printf '%s' "$user_schema_config_row" | jq --raw-output --arg field "$field" '.[$field]')"
done
create_user_schema_property "$name" "$attributeType" "$isEditable" "$isList" "$isVisible"
done < <(jq --compact-output '.[]' -- "${user_schema_files[@]}")
printf -- '--- user schemas ---\n'
local redundant_groups='' local redundant_groups=''
redundant_groups="$(get_group_list | jq '[ .data.groups[].displayName ]' | jq --compact-output '. - ["lldap_admin","lldap_password_manager","lldap_strict_readonly"]')" redundant_groups="$(get_group_list | jq '[ .data.groups[].displayName ]' | jq --compact-output '. - ["lldap_admin","lldap_password_manager","lldap_strict_readonly"]')"