diff --git a/example_configs/bootstrap/bootstrap.md b/example_configs/bootstrap/bootstrap.md index 22b2694..65807dd 100644 --- a/example_configs/bootstrap/bootstrap.md +++ b/example_configs/bootstrap/bootstrap.md @@ -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. @@ -12,7 +12,7 @@ The script can: * create groups * delete redundant users and groups (when `DO_CLEANUP` env var is true) * maintain the desired state described in JSON config files - +* create user/group user-defined attributes ![](bootstrap-example-log-1.jpeg) @@ -27,11 +27,13 @@ The script can: ## Environment variables -- `LLDAP_URL` or `LLDAP_URL_FILE` - URL to your lldap instance or path to file that contains URL (**MANDATORY**) -- `LLDAP_ADMIN_USERNAME` or `LLDAP_ADMIN_USERNAME_FILE` - admin username or path to file that contains username (**MANDATORY**) -- `LLDAP_ADMIN_PASSWORD` or `LLDAP_ADMIN_PASSWORD_FILE` - admin password or path to file that contains password (**MANDATORY**) -- `USER_CONFIGS_DIR` (default value: `/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 +- `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` (default value: `admin`) - admin username or path to file that contains username +- `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: `/bootstrap/user-configs`) - directory where the user 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`) - `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 ### Manually @@ -110,11 +150,21 @@ export LLDAP_ADMIN_USERNAME=admin export LLDAP_ADMIN_PASSWORD=changeme export USER_CONFIGS_DIR="$(realpath ./configs/user)" 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 DO_CLEANUP=false ./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 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 └─ 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: @@ -160,6 +217,8 @@ services: - LLDAP_ADMIN_PASSWORD=changeme # same as LLDAP_LDAP_USER_PASS - USER_CONFIGS_DIR=/bootstrap/user-configs - GROUP_CONFIGS_DIR=/bootstrap/group-configs + - USER_SCHEMAS_DIR=/bootstrap/user-schemas + - GROUP_SCHEMAS_DIR=/bootstrap/group-schemas - DO_CLEANUP=false ``` @@ -205,14 +264,15 @@ spec: volumeMounts: - name: bootstrap mountPath: /bootstrap/bootstrap.sh + readOnly: true subPath: bootstrap.sh - name: user-configs - mountPath: /user-configs + mountPath: /bootstrap/user-configs readOnly: true - name: group-configs - mountPath: /group-configs + mountPath: /bootstrap/group-configs readOnly: true volumes: diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 20933ae..6001eb3 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -3,14 +3,24 @@ set -e set -o pipefail -LLDAP_URL="${LLDAP_URL}" -LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME}" -LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD}" -USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/user-configs}" -GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/group-configs}" +LLDAP_URL="${LLDAP_URL:-http://localhost:17170}" +LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME:-admin}" +LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD:-password}" +USER_SCHEMAS_DIR="${USER_SCHEMAS_DIR:-/bootstrap/user-schemas}" +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}" 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() { local commands=('curl' 'jq' 'jo') local commands_not_found='false' @@ -280,6 +290,80 @@ delete_user() { 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() { local \ query="$1" \ @@ -387,8 +471,18 @@ main() { local user_config_files=("${USER_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 fi @@ -399,6 +493,28 @@ main() { 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='' redundant_groups="$(get_group_list | jq '[ .data.groups[].displayName ]' | jq --compact-output '. - ["lldap_admin","lldap_password_manager","lldap_strict_readonly"]')"