mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2026-09-20 22:54:26 +02:00
The Kratos admin API is unauthenticated by design and relies on network isolation, but so-kratos published 0.0.0.0:4434:4434 and daemon.json leaves userland-proxy at its default of true. Docker therefore ran a proxy listener on the host, and any container reached the admin API through the manager IP or the bridge gateway regardless of its docker network. so-kratos now sits alone on a new soauth network and no longer publishes 4434. 4433 is still published, so nginx is unchanged. so-soc is dual homed and reaches the admin API over soauth, with publicHostUrl set explicitly. so-user reaches it through docker exec, and wait_for_kratos polls the container address instead of the host port. firewall/iptables.jinja hardcoded sobridge in every generated DNAT, ACCEPT, masquerade and isolation rule, so it is now driven by a docker:networks map and a per-container networks list. Containers that do not declare one default to sobridge, and .ip still resolves to the primary network's address. soauth is only created on the roles that run so-kratos. Setup already allows a custom docker range, so it now also prompts for the auth network range and writes it to the docker pillar. Grids on the default range need no decision during soup, since they get the same 172.17.2.0/24 a fresh install would. Grids that set a custom range are prompted, defaulting to the adjacent /24, and unattended upgrades take that default with a notice rather than blocking. so-kratos and so-soc are removed in up_to_3.4.0 so the highstate recreates them with the correct network membership.
720 lines
23 KiB
Bash
Executable File
720 lines
23 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
|
# Elastic License 2.0.
|
|
|
|
if [[ -f /usr/sbin/so-common ]]; then
|
|
source /usr/sbin/so-common
|
|
else
|
|
source $(dirname $0)/../../../common/tools/sbin/so-common
|
|
fi
|
|
|
|
|
|
DEFAULT_ROLE=analyst
|
|
|
|
function usage() {
|
|
cat <<USAGE_EOF
|
|
Usage: $0 <operation> [supporting parameters]
|
|
|
|
where <operation> is one of the following:
|
|
|
|
list: Lists all user email addresses currently defined in the identity system
|
|
|
|
add: Adds a new user to the identity system
|
|
Required parameters:
|
|
--email <email>
|
|
Optional parameters:
|
|
--role <role> (defaults to $DEFAULT_ROLE)
|
|
--firstName <firstName> (defaults to blank)
|
|
--lastName <lastName> (defaults to blank)
|
|
--note <note> (defaults to blank)
|
|
--skip-sync (defers the Elastic sync until the next scheduled time)
|
|
|
|
addrole: Grants a role to an existing user
|
|
Required parameters:
|
|
--email <email>
|
|
--role <role>
|
|
Optional parameters:
|
|
--skip-sync (defers the Elastic sync until the next scheduled time)
|
|
|
|
delrole: Removes a role from an existing user
|
|
Required parameters:
|
|
--email <email>
|
|
--role <role>
|
|
Optional parameters:
|
|
--skip-sync (defers the Elastic sync until the next scheduled time)
|
|
|
|
password: Updates a user's password and disables MFA
|
|
Required parameters:
|
|
--email <email>
|
|
Optional parameters:
|
|
--skip-sync (defers the Elastic sync until the next scheduled time)
|
|
|
|
profile: Updates a user's profile information
|
|
Required parameters:
|
|
--email <email>
|
|
Optional parameters:
|
|
--role <role> (defaults to $DEFAULT_ROLE)
|
|
--firstName <firstName> (defaults to blank)
|
|
--lastName <lastName> (defaults to blank)
|
|
--note <note> (defaults to blank)
|
|
|
|
enable: Enables a user
|
|
Required parameters:
|
|
--email <email>
|
|
Optional parameters:
|
|
--skip-sync (defers the Elastic sync until the next scheduled time)
|
|
|
|
disable: Disables a user
|
|
Required parameters:
|
|
--email <email>
|
|
Optional parameters:
|
|
--skip-sync (defers the Elastic sync until the next scheduled time)
|
|
|
|
validate: Validates that the given email address and password are acceptable
|
|
Required parameters:
|
|
--email <email>
|
|
|
|
valemail: Validates that the given email address is acceptable; requires 'email' parameter
|
|
Required parameters:
|
|
--email <email>
|
|
|
|
valpass: Validates that a password is acceptable
|
|
|
|
Note that the password can be piped into STDIN to avoid prompting for it
|
|
USAGE_EOF
|
|
exit 1
|
|
}
|
|
|
|
if [[ $# -lt 1 || $1 == --help || $1 == -h || $1 == -? || $1 == --h ]]; then
|
|
usage
|
|
fi
|
|
|
|
operation=$1
|
|
shift
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
param=$1
|
|
shift
|
|
case "$param" in
|
|
--email)
|
|
email=$(echo $1 | sed 's/"/\\"/g')
|
|
shift
|
|
;;
|
|
--role)
|
|
role=$(echo $1 | sed 's/"/\\"/g')
|
|
shift
|
|
;;
|
|
--firstName)
|
|
firstName=$(echo $1 | sed 's/"/\\"/g')
|
|
shift
|
|
;;
|
|
--lastName)
|
|
lastName=$(echo $1 | sed 's/"/\\"/g')
|
|
shift
|
|
;;
|
|
--note)
|
|
note=$(echo $1 | sed 's/"/\\"/g')
|
|
shift
|
|
;;
|
|
--skip-sync)
|
|
SKIP_SYNC=1
|
|
;;
|
|
*)
|
|
echo "Encountered unexpected parameter: $param"
|
|
usage
|
|
;;
|
|
esac
|
|
done
|
|
|
|
kratosContainer=${KRATOS_CONTAINER:-so-kratos}
|
|
kratosUrl=${KRATOS_URL:-http://localhost:4434/admin}
|
|
databasePath=${KRATOS_DB_PATH:-/nsm/kratos/db/db.sqlite}
|
|
databaseTimeout=${KRATOS_DB_TIMEOUT:-5000}
|
|
bcryptRounds=${BCRYPT_ROUNDS:-12}
|
|
elasticUsersFile=${ELASTIC_USERS_FILE:-/opt/so/saltstack/local/salt/elasticsearch/files/users}
|
|
elasticRolesFile=${ELASTIC_ROLES_FILE:-/opt/so/saltstack/local/salt/elasticsearch/files/users_roles}
|
|
socRolesFile=${SOC_ROLES_FILE:-/opt/so/conf/soc/soc_users_roles}
|
|
esUID=${ELASTIC_UID:-930}
|
|
esGID=${ELASTIC_GID:-930}
|
|
soUID=${SOCORE_UID:-939}
|
|
soGID=${SOCORE_GID:-939}
|
|
|
|
function lock() {
|
|
# Obtain file descriptor lock
|
|
exec 99>/var/tmp/so-user.lock || fail "Unable to create lock descriptor; if the system was not shutdown gracefully you may need to remove /var/tmp/so-user.lock manually."
|
|
flock -w 10 99 || fail "Another process is using so-user; if the system was not shutdown gracefully you may need to remove /var/tmp/so-user.lock manually."
|
|
trap 'rm -f /var/tmp/so-user.lock' EXIT
|
|
}
|
|
|
|
function fail() {
|
|
msg=$1
|
|
echo "$1"
|
|
exit 1
|
|
}
|
|
|
|
function kratosCurl() {
|
|
docker exec -i "$kratosContainer" curl "$@"
|
|
}
|
|
|
|
function require() {
|
|
cmd=$1
|
|
which "$1" 2>&1 > /dev/null
|
|
[[ $? != 0 ]] && fail "This script requires the following command be installed: ${cmd}"
|
|
}
|
|
|
|
# Verify this environment is capable of running this script
|
|
function verifyEnvironment() {
|
|
require "htpasswd"
|
|
require "jq"
|
|
require "docker"
|
|
require "openssl"
|
|
require "sqlite3"
|
|
[[ ! -f $databasePath ]] && fail "Unable to find database file; specify path via KRATOS_DB_PATH environment variable"
|
|
response=$(kratosCurl -Ss -L ${kratosUrl}/)
|
|
[[ "$response" != "404 page not found" ]] && fail "Unable to communicate with Kratos; specify URL via KRATOS_URL environment variable"
|
|
}
|
|
|
|
function findIdByEmail() {
|
|
email=${1,,}
|
|
|
|
response=$(kratosCurl -Ss -L ${kratosUrl}/identities)
|
|
identityId=$(echo "${response}" | jq -r ".[] | select(.verifiable_addresses[0].value == \"$email\") | .id")
|
|
echo $identityId
|
|
}
|
|
|
|
function validatePassword() {
|
|
password=$1
|
|
|
|
len=$(expr length "$password")
|
|
if [[ $len -lt 8 ]]; then
|
|
fail "Password does not meet the minimum requirements"
|
|
fi
|
|
if [[ $len -gt 72 ]]; then
|
|
fail "Password is too long (max: 72)"
|
|
fi
|
|
check_password_and_exit "$password"
|
|
}
|
|
|
|
function validateEmail() {
|
|
email=$1
|
|
requireLower=$2
|
|
# (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
|
|
if [[ ! "$email" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}$ ]]; then
|
|
fail "Email address is invalid"
|
|
fi
|
|
|
|
if [[ "$requireLower" == "true" && "$email" =~ [A-Z] ]]; then
|
|
fail "Email addresses cannot contain uppercase letters"
|
|
fi
|
|
}
|
|
|
|
function hashPassword() {
|
|
password=$1
|
|
|
|
passwordHash=$(echo "${password}" | htpasswd -niBC $bcryptRounds SOUSER)
|
|
passwordHash=$(echo "$passwordHash" | cut -c 11-)
|
|
passwordHash="\$2a${passwordHash}" # still waiting for https://github.com/elastic/elasticsearch/issues/51132
|
|
echo "$passwordHash"
|
|
}
|
|
|
|
|
|
function updatePassword() {
|
|
identityId=$1
|
|
|
|
if [ -z "$password" ]; then
|
|
# Read password from stdin (show prompt only if no stdin was piped in)
|
|
test -t 0
|
|
if [[ $? == 0 ]]; then
|
|
echo "Enter new password:"
|
|
fi
|
|
read -rs password
|
|
|
|
validatePassword "$password"
|
|
fi
|
|
|
|
if [[ -n "$identityId" ]]; then
|
|
# Generate password hash
|
|
passwordHash=$(hashPassword "$password")
|
|
# Update DB with new hash
|
|
echo "update identity_credentials set config=CAST('{\"hashed_password\":\"$passwordHash\"}' as BLOB), created_at=datetime('now'), updated_at=datetime('now') where identity_id='${identityId}' and identity_credential_type_id=(select id from identity_credential_types where name='password');" | sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath"
|
|
[[ $? != 0 ]] && fail "Unable to update password"
|
|
# Deactivate MFA
|
|
echo "delete from identity_credential_identifiers where identity_credential_id in (select id from identity_credentials where identity_id='${identityId}' and identity_credential_type_id in (select id from identity_credential_types where name in ('totp', 'webauthn', 'oidc')));" | sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath"
|
|
[[ $? != 0 ]] && fail "Unable to clear aal2 identity IDs"
|
|
echo "delete from identity_credentials where identity_id='${identityId}' and identity_credential_type_id in (select id from identity_credential_types where name in ('totp', 'webauthn', 'oidc'));" | sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath"
|
|
[[ $? != 0 ]] && fail "Unable to clear aal2 identity credentials"
|
|
echo "delete from session_devices where session_id in (select id from sessions where identity_id='${identityId}');" | sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath"
|
|
[[ $? != 0 ]] && fail "Unable to clear session devices"
|
|
echo "delete from sessions where identity_id='${identityId}';" | sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath"
|
|
[[ $? != 0 ]] && fail "Unable to clear sessions"
|
|
echo "update identities set available_aal='aal1' where id='${identityId}';" | sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath"
|
|
[[ $? != 0 ]] && fail "Unable to reset aal"
|
|
fi
|
|
}
|
|
|
|
function createFile() {
|
|
filename=$1
|
|
uid=$2
|
|
gid=$3
|
|
|
|
mkdir -p $(dirname "$filename")
|
|
truncate -s 0 "$filename"
|
|
chmod 600 "$filename"
|
|
chown "${uid}:${gid}" "$filename"
|
|
}
|
|
|
|
function ensureRoleFileExists() {
|
|
if [[ ! -f "$socRolesFile" || ! -s "$socRolesFile" ]]; then
|
|
# Generate the new users file
|
|
rolesTmpFile="${socRolesFile}.tmp"
|
|
createFile "$rolesTmpFile" "$soUID" "$soGID"
|
|
|
|
if [[ -f "$databasePath" ]]; then
|
|
echo "Migrating roles to new file: $socRolesFile"
|
|
|
|
echo "select 'superuser:' || id from identities;" | sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath" \
|
|
>> "$rolesTmpFile"
|
|
[[ $? != 0 ]] && fail "Unable to read identities from database"
|
|
|
|
echo "The following users have all been migrated with the super user role:"
|
|
cat "${rolesTmpFile}"
|
|
else
|
|
echo "Database file does not exist yet, installation is likely not yet complete."
|
|
fi
|
|
|
|
if [[ -d "$socRolesFile" ]]; then
|
|
echo "Removing invalid roles directory created by Docker"
|
|
rm -fr "$socRolesFile"
|
|
fi
|
|
mv "${rolesTmpFile}" "${socRolesFile}"
|
|
fi
|
|
}
|
|
|
|
function syncElasticSystemUser() {
|
|
json=$1
|
|
userid=$2
|
|
usersFile=$3
|
|
|
|
user=$(echo "$json" | jq -r ".local.users.$userid.user")
|
|
pass=$(echo "$json" | jq -r ".local.users.$userid.pass")
|
|
|
|
[[ -z "$user" || -z "$pass" ]] && fail "Elastic auth credentials for system user '$userid' are missing"
|
|
hash=$(hashPassword "$pass")
|
|
|
|
echo "${user}:${hash}" >> "$usersFile"
|
|
}
|
|
|
|
function syncElasticSystemRole() {
|
|
json=$1
|
|
userid=$2
|
|
role=$3
|
|
rolesFile=$4
|
|
|
|
user=$(echo "$json" | jq -r ".local.users.$userid.user")
|
|
|
|
[[ -z "$user" ]] && fail "Elastic auth credentials for system user '$userid' are missing"
|
|
|
|
echo "${role}:${user}" >> "$rolesFile"
|
|
}
|
|
|
|
function syncElastic() {
|
|
[[ -n $SKIP_SYNC ]] && return
|
|
|
|
echo "Syncing users and roles between SOC and Elastic..."
|
|
|
|
usersTmpFile="${elasticUsersFile}.tmp"
|
|
createFile "${usersTmpFile}" "$esUID" "$esGID"
|
|
rolesTmpFile="${elasticRolesFile}.tmp"
|
|
createFile "${rolesTmpFile}" "$esUID" "$esGID"
|
|
|
|
authPillarJson=$(lookup_salt_value "auth" "elasticsearch" "pillar" "json")
|
|
|
|
syncElasticSystemUser "$authPillarJson" "so_elastic_user" "$usersTmpFile"
|
|
syncElasticSystemUser "$authPillarJson" "so_kibana_user" "$usersTmpFile"
|
|
syncElasticSystemUser "$authPillarJson" "so_logstash_user" "$usersTmpFile"
|
|
syncElasticSystemUser "$authPillarJson" "so_beats_user" "$usersTmpFile"
|
|
syncElasticSystemUser "$authPillarJson" "so_monitor_user" "$usersTmpFile"
|
|
|
|
syncElasticSystemRole "$authPillarJson" "so_elastic_user" "superuser" "$rolesTmpFile"
|
|
syncElasticSystemRole "$authPillarJson" "so_kibana_user" "kibana_system" "$rolesTmpFile"
|
|
syncElasticSystemRole "$authPillarJson" "so_logstash_user" "superuser" "$rolesTmpFile"
|
|
syncElasticSystemRole "$authPillarJson" "so_beats_user" "superuser" "$rolesTmpFile"
|
|
syncElasticSystemRole "$authPillarJson" "so_monitor_user" "remote_monitoring_collector" "$rolesTmpFile"
|
|
syncElasticSystemRole "$authPillarJson" "so_monitor_user" "remote_monitoring_agent" "$rolesTmpFile"
|
|
syncElasticSystemRole "$authPillarJson" "so_monitor_user" "monitoring_user" "$rolesTmpFile"
|
|
|
|
if [[ -f "$databasePath" && -f "$socRolesFile" ]]; then
|
|
# Append the SOC users
|
|
userData=$(echo "select '{\"user\":\"' || ici.identifier || '\", \"data\":' || ic.config || '}'" \
|
|
"from identity_credential_identifiers ici, identity_credentials ic, identities i, identity_credential_types ict " \
|
|
"where " \
|
|
" ici.identity_credential_id=ic.id " \
|
|
" and ic.identity_id=i.id " \
|
|
" and ict.id=ic.identity_credential_type_id " \
|
|
" and ict.name='password' " \
|
|
" and i.state == 'active' " \
|
|
"order by ici.identifier;" | \
|
|
sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath")
|
|
[[ $? != 0 ]] && fail "Unable to read credential hashes from database"
|
|
|
|
user_data_formatted=$(echo "${userData}" | jq -r '.user + ":" + .data.hashed_password')
|
|
if lookup_salt_value "features" "" "pillar" | grep -qx odc; then
|
|
# generate random placeholder salt/hash for users without passwords
|
|
random_crypt=$(get_random_value 53)
|
|
user_data_formatted=$(echo "${user_data_formatted}" | sed -r "s/^(.+:)\$/\\1\$2a\$12${random_crypt}/")
|
|
fi
|
|
echo "${user_data_formatted}" >> "$usersTmpFile"
|
|
|
|
# Append the user roles
|
|
while IFS="" read -r rolePair || [ -n "$rolePair" ]; do
|
|
userId=$(echo "$rolePair" | cut -d: -f2)
|
|
role=$(echo "$rolePair" | cut -d: -f1)
|
|
echo "select '$role:' || ici.identifier " \
|
|
"from identity_credential_identifiers ici, identity_credentials ic, identity_credential_types ict " \
|
|
"where ici.identity_credential_id=ic.id " \
|
|
" and ict.id=ic.identity_credential_type_id " \
|
|
" and ict.name='password' " \
|
|
" and ic.identity_id = '$userId';" | \
|
|
sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath" >> "$rolesTmpFile"
|
|
[[ $? != 0 ]] && fail "Unable to read role identities from database"
|
|
done < "$socRolesFile"
|
|
else
|
|
echo "Database file or soc roles file does not exist yet, skipping users export"
|
|
fi
|
|
|
|
if [[ -s "${usersTmpFile}" ]]; then
|
|
mv "${usersTmpFile}" "${elasticUsersFile}"
|
|
mv "${rolesTmpFile}" "${elasticRolesFile}"
|
|
|
|
if [[ -z "$SKIP_STATE_APPLY" ]]; then
|
|
echo "Elastic state will be re-applied to affected minions. This will run in the background and may take several minutes to complete."
|
|
echo "Applying elastic state to elastic minions at $(date)" >> /opt/so/log/soc/sync.log 2>&1
|
|
salt --async -C 'I@elasticsearch:enabled:true' state.apply elasticsearch queue=True >> /opt/so/log/soc/sync.log 2>&1
|
|
fi
|
|
else
|
|
echo "Newly generated users/roles files are incomplete; aborting."
|
|
fi
|
|
}
|
|
|
|
function syncAll() {
|
|
ensureRoleFileExists
|
|
|
|
# Check if a sync is needed. Sync is not needed if the following are true:
|
|
# - user database entries are all older than the elastic users file
|
|
# - soc roles file last modify date is older than the elastic roles file
|
|
if [[ -z "$FORCE_SYNC" && -f "$databasePath" && -f "$elasticUsersFile" ]]; then
|
|
usersFileAgeSecs=$(echo $(($(date +%s) - $(date +%s -r "$elasticUsersFile"))))
|
|
staleCount=$(echo "select count(*) from identity_credentials where updated_at >= Datetime('now', '-${usersFileAgeSecs} seconds');" \
|
|
| sqlite3 -cmd ".timeout ${databaseTimeout}" "$databasePath")
|
|
[[ $? != 0 ]] && fail "Unable to read user count from database"
|
|
if [[ "$staleCount" == "0" && "$elasticRolesFile" -nt "$socRolesFile" ]]; then
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
syncElastic
|
|
|
|
return 0
|
|
}
|
|
|
|
function listUsers() {
|
|
response=$(kratosCurl -Ss -L ${kratosUrl}/identities)
|
|
[[ $? != 0 ]] && fail "Unable to communicate with Kratos"
|
|
|
|
users=$(echo "${response}" | jq -r ".[] | .verifiable_addresses[0].value" | sort)
|
|
for user in $users; do
|
|
roles=$(grep ":$user\$" "$elasticRolesFile" | cut -d: -f1 | tr '\n' ' ')
|
|
echo "$user: $roles"
|
|
done
|
|
}
|
|
|
|
function addUserRole() {
|
|
email=$1
|
|
role=$2
|
|
|
|
adjustUserRole "$email" "$role" "add"
|
|
}
|
|
|
|
function deleteUserRole() {
|
|
email=$1
|
|
role=$2
|
|
|
|
adjustUserRole "$email" "$role" "del"
|
|
}
|
|
|
|
function adjustUserRole() {
|
|
email=$1
|
|
role=$2
|
|
op=$3
|
|
|
|
identityId=$(findIdByEmail "$email")
|
|
[[ ${identityId} == "" ]] && fail "User not found"
|
|
|
|
ensureRoleFileExists
|
|
|
|
filename="$socRolesFile"
|
|
hasRole=0
|
|
grep "^$role:" "$socRolesFile" | grep -q "$identityId" && hasRole=1
|
|
if [[ "$op" == "add" ]]; then
|
|
if [[ "$hasRole" == "1" ]]; then
|
|
echo "User '$email' already has the role: $role"
|
|
return 1
|
|
else
|
|
echo "$role:$identityId" >> "$filename"
|
|
fi
|
|
elif [[ "$op" == "del" ]]; then
|
|
if [[ "$hasRole" -ne 1 ]]; then
|
|
fail "User '$email' does not have the role: $role"
|
|
else
|
|
sed "/^$role:$identityId\$/d" "$filename" > "$filename.tmp"
|
|
cat "$filename".tmp > "$filename"
|
|
rm -f "$filename".tmp
|
|
fi
|
|
else
|
|
fail "Unsupported role adjustment operation: $op"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
function createUser() {
|
|
email=$1
|
|
role=$2
|
|
firstName=$3
|
|
lastName=$4
|
|
note=$5
|
|
|
|
now=$(date -u +%FT%TZ)
|
|
addUserJson=$(cat <<EOF
|
|
{
|
|
"traits": {
|
|
"email": "${email}",
|
|
"firstName": "${firstName}",
|
|
"lastName": "${lastName}",
|
|
"note": "${note}"
|
|
},
|
|
"schema_id": "default"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
response=$(kratosCurl -Ss -L ${kratosUrl}/identities -d "$addUserJson")
|
|
[[ $? != 0 ]] && fail "Unable to communicate with Kratos"
|
|
|
|
identityId=$(echo "${response}" | jq -r ".id")
|
|
if [[ "${identityId}" == "null" ]]; then
|
|
code=$(echo "${response}" | jq ".error.code")
|
|
[[ "${code}" == "409" ]] && fail "User already exists"
|
|
|
|
reason=$(echo "${response}" | jq ".error.message")
|
|
[[ $? == 0 ]] && fail "Unable to add user: ${reason}"
|
|
else
|
|
updatePassword "$identityId"
|
|
addUserRole "$email" "$role"
|
|
fi
|
|
}
|
|
|
|
function updateStatus() {
|
|
email=$1
|
|
status=$2
|
|
|
|
identityId=$(findIdByEmail "$email")
|
|
[[ ${identityId} == "" ]] && fail "User not found"
|
|
|
|
response=$(kratosCurl -Ss -L "${kratosUrl}/identities/$identityId")
|
|
[[ $? != 0 ]] && fail "Unable to communicate with Kratos"
|
|
|
|
schemaId=$(echo "$response" | jq -r .schema_id)
|
|
|
|
# Capture traits and remove obsolete 'status' trait if exists
|
|
traitBlock=$(echo "$response" | jq -c .traits | sed -re 's/,?"status":".*?"//')
|
|
|
|
state="active"
|
|
if [[ "$status" == "locked" ]]; then
|
|
state="inactive"
|
|
fi
|
|
body="{ \"schema_id\": \"$schemaId\", \"state\": \"$state\", \"traits\": $traitBlock }"
|
|
response=$(kratosCurl -fSsL -XPUT -H "Content-Type: application/json" "${kratosUrl}/identities/$identityId" -d "$body")
|
|
[[ $? != 0 ]] && fail "Unable to update user"
|
|
}
|
|
|
|
function updateUserPassword() {
|
|
email=$1
|
|
|
|
identityId=$(findIdByEmail "$email")
|
|
[[ ${identityId} == "" ]] && fail "User not found"
|
|
|
|
updatePassword "$identityId"
|
|
}
|
|
|
|
function updateUserProfile() {
|
|
email=$1
|
|
|
|
identityId=$(findIdByEmail "$email")
|
|
[[ ${identityId} == "" ]] && fail "User not found"
|
|
|
|
response=$(kratosCurl -Ss -L "${kratosUrl}/identities/$identityId")
|
|
[[ $? != 0 ]] && fail "Unable to communicate with Kratos"
|
|
|
|
schemaId=$(echo "$response" | jq -r .schema_id)
|
|
state=$(echo "$response" | jq -r .state)
|
|
|
|
traitBlock="{\"email\":\"$email\",\"firstName\":\"$firstName\",\"lastName\":\"$lastName\",\"note\":\"$note\"}"
|
|
|
|
body="{ \"schema_id\": \"$schemaId\", \"state\": \"$state\", \"traits\": $traitBlock }"
|
|
response=$(kratosCurl -fSsL -XPUT -H "Content-Type: application/json" "${kratosUrl}/identities/$identityId" -d "$body")
|
|
[[ $? != 0 ]] && fail "Unable to update user"
|
|
}
|
|
|
|
function deleteUser() {
|
|
email=$1
|
|
|
|
identityId=$(findIdByEmail "$email")
|
|
[[ ${identityId} == "" ]] && fail "User not found"
|
|
|
|
response=$(kratosCurl -Ss -XDELETE -L "${kratosUrl}/identities/$identityId")
|
|
[[ $? != 0 ]] && fail "Unable to communicate with Kratos"
|
|
|
|
rolesTmpFile="${socRolesFile}.tmp"
|
|
createFile "$rolesTmpFile" "$soUID" "$soGID"
|
|
grep -v "$identityId" "$socRolesFile" > "$rolesTmpFile"
|
|
cat "$rolesTmpFile" > "$socRolesFile"
|
|
}
|
|
|
|
case "${operation}" in
|
|
"add")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
|
|
lock
|
|
validateEmail "$email" true
|
|
updatePassword
|
|
createUser "$email" "${role:-$DEFAULT_ROLE}" "${firstName}" "${lastName}" "${note}"
|
|
syncAll
|
|
echo "Successfully added new user to SOC"
|
|
echo "$password" | so-influxdb-manage useradd "$email"
|
|
if [[ "$role" == "superuser" ]]; then
|
|
echo "$password" | so-influxdb-manage userpromote "$email"
|
|
fi
|
|
;;
|
|
|
|
"list")
|
|
verifyEnvironment
|
|
listUsers
|
|
;;
|
|
|
|
"addrole")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
[[ "$role" == "" ]] && fail "Role must be provided"
|
|
|
|
lock
|
|
validateEmail "$email"
|
|
if addUserRole "$email" "$role"; then
|
|
syncElastic
|
|
echo "Successfully added role to user"
|
|
if [[ "$role" == "superuser" ]]; then
|
|
echo "$password" | so-influxdb-manage userpromote "$email"
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
"delrole")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
[[ "$role" == "" ]] && fail "Role must be provided"
|
|
|
|
lock
|
|
validateEmail "$email"
|
|
deleteUserRole "$email" "$role"
|
|
syncElastic
|
|
echo "Successfully removed role from user"
|
|
if [[ "$role" == "superuser" ]]; then
|
|
echo "$password" | so-influxdb-manage userdemote "$email"
|
|
fi
|
|
;;
|
|
|
|
"password")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
|
|
lock
|
|
updateUserPassword "$email"
|
|
syncAll
|
|
echo "Successfully updated user password"
|
|
echo "$password" | so-influxdb-manage userpass "$email"
|
|
;;
|
|
|
|
"profile")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
|
|
lock
|
|
updateUserProfile "$email"
|
|
echo "Successfully updated user profile"
|
|
;;
|
|
|
|
"enable")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
|
|
lock
|
|
updateStatus "$email" 'active'
|
|
syncAll
|
|
echo "Successfully enabled user"
|
|
so-influxdb-manage userenable "$email"
|
|
;;
|
|
|
|
"disable")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
|
|
lock
|
|
updateStatus "$email" 'locked'
|
|
syncAll
|
|
echo "Successfully disabled user"
|
|
so-influxdb-manage userdisable "$email"
|
|
;;
|
|
|
|
"delete")
|
|
verifyEnvironment
|
|
[[ "$email" == "" ]] && fail "Email address must be provided"
|
|
|
|
lock
|
|
deleteUser "$email"
|
|
syncAll
|
|
echo "Successfully deleted user"
|
|
so-influxdb-manage userdel "$email"
|
|
;;
|
|
|
|
"sync")
|
|
lock
|
|
syncAll
|
|
;;
|
|
|
|
"validate")
|
|
validateEmail "$email" true
|
|
updatePassword
|
|
echo "Email and password are acceptable"
|
|
;;
|
|
|
|
"valemail")
|
|
validateEmail "$email" true
|
|
echo "Email is acceptable"
|
|
;;
|
|
|
|
"valpass")
|
|
updatePassword
|
|
echo "Password is acceptable"
|
|
;;
|
|
|
|
*)
|
|
fail "Unsupported operation: $operation"
|
|
usage
|
|
;;
|
|
esac
|
|
|
|
exit 0
|