Posted on
Artificial Intelligence

Automating User Management with Artificial Intelligence and Bash

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Automating User Management with Artificial Intelligence and Bash

If your “simple” user onboarding/offboarding process still involves copying columns out of an HR spreadsheet and pasting shell commands into a terminal, you’re one typo away from a bad day. The good news: you can turn that brittle human pipeline into a repeatable, auditable system by combining Bash with a small dose of AI to tame messy inputs.

This article shows you how to:

  • Normalize HR data with an AI assistant or a strict schema

  • Generate an idempotent plan to create/update/remove users in Bash

  • Review and safely apply changes

  • Schedule everything so it runs without you

We’ll keep it distro-friendly and include installation commands for apt, dnf, and zypper wherever tools are introduced.


Why Bash + AI for user management?

  • Real-world data is messy: HR sends CSVs with inconsistent headers, nicknames, or departments instead of group names. AI can help normalize this to a strict schema.

  • Bash is battle-tested: useradd/usermod/chpasswd/groupadd are everywhere, easy to audit, and transparent.

  • Separation of concerns: let AI clean up inputs; let Bash perform exact, controlled changes under least privilege.

The result is a safer process that still fits your Linux workflows.


Prerequisites and installation

We’ll use these tools:

  • jq: JSON processing in shell

  • Miller (mlr): robust CSV→JSON handling

  • curl: HTTP client (for AI APIs or local LLMs)

  • cron/cronie: scheduling

  • Core account-management tools (useradd/usermod/chpasswd/etc.)

Install per distro:

  • Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y jq miller curl cron passwd
sudo systemctl enable --now cron
  • Fedora/RHEL (dnf):
sudo dnf install -y jq miller curl cronie shadow-utils
sudo systemctl enable --now crond
  • openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y jq miller curl cron shadow
sudo systemctl enable --now cron

Notes:

  • The package providing useradd/usermod is named differently by family: passwd (Debian/Ubuntu), shadow-utils (Fedora/RHEL), shadow (openSUSE).

  • cron service name varies: cron (Debian/openSUSE) vs crond (RHEL/Fedora).


Step 1: Model your desired state

Start with a clear schema. A simple CSV works well:

username,full_name,groups,shell,state,temp_password
jdoe,Jane Doe,engineering;docker,/bin/bash,present,
akhan,Ali Khan,contractors;/bin/false,/bin/false,present,temp-P@ss-123
dlee,Dan Lee,engineering,/bin/bash,absent,
  • groups: semicolon- or comma-separated

  • state: present or absent

  • temp_password: leave empty to keep locked or handle via SSO

Prefer JSON for complex cases:

[
  {
    "username": "jdoe",
    "full_name": "Jane Doe",
    "groups": ["engineering", "docker"],
    "shell": "/bin/bash",
    "state": "present",
    "temp_password": null
  }
]

Optional: Use AI to normalize messy HR exports

If the HR export has inconsistent columns or free-text roles, ask an LLM to transform it into your strict JSON schema. Two portable patterns:

  • OpenAI-compatible API (replace endpoint/model/key as appropriate):
export AI_API_ENDPOINT="https://your-llm-endpoint/v1/chat/completions"
export AI_API_KEY="YOUR_API_KEY"

raw_input="$(cat hr_export.txt | jq -Rs .)"

curl -s \
  -H "Authorization: Bearer $AI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "your-model",
    "temperature": 0,
    "messages": [
      {"role":"system","content":"You normalize HR user data. Output JSON only: an array of objects with fields: username (lowercase a-z0-9._-), full_name, groups (array of strings), shell, state (present|absent), temp_password (string|null). If missing values, infer conservatively or set null."},
      {"role":"user","content": '"$raw_input"'}
    ]
  }' \
  "$AI_API_ENDPOINT" \
| jq -r '.choices[0].message.content' > users.json
  • Local LLM via Ollama (example; install Ollama per their docs, then run a model like mistral):
prompt="Normalize to JSON array with fields: username, full_name, groups[], shell, state, temp_password (null if missing). Output JSON only.

$(cat hr_export.txt)"

curl -s http://localhost:11434/api/generate -d "{
  \"model\": \"mistral\",
  \"prompt\": $(jq -Rs '.' <<< \"$prompt\"),
  \"options\": {\"temperature\": 0}
}" | jq -r '.response' > users.json

Always review AI output before applying. Treat AI as a helpful parser, not an oracle.


Step 2: Turn the desired state into an idempotent Bash plan

The following Bash script reads a CSV (via Miller) or JSON and prints a plan. By default it runs in dry-run mode; pass --apply to execute. It also:

  • Creates missing groups

  • Creates or updates users

  • Adds users to groups (non-destructively)

  • Removes users marked absent

Save as ai_user_plan.sh and make executable.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

usage() {
  echo "Usage: $0 (--csv users.csv | --json users.json) [--apply]"
  exit 1
}

[[ $# -lt 2 ]] && usage

INPUT_FMT=""
INPUT_FILE=""
APPLY=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --csv) INPUT_FMT="csv"; INPUT_FILE="$2"; shift 2;;
    --json) INPUT_FMT="json"; INPUT_FILE="$2"; shift 2;;
    --apply) APPLY=1; shift;;
    *) usage;;
  esac
done

need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1" >&2; exit 1; }; }
need jq
need getent
need useradd
need usermod
need userdel
need groupadd
need chpasswd
need logger

to_json() {
  if [[ "$INPUT_FMT" == "csv" ]]; then
    need mlr
    mlr --icsv --ojson cat "$INPUT_FILE"
  else
    cat "$INPUT_FILE" | jq '.'
  fi
}

say() { echo "$@"; logger -t ai-user-mgmt "$*"; }

safe_username() {
  # allow a-z0-9._- and strip others
  tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/[^a-z0-9._-]+//g'
}

json="$(to_json)"

echo "# Planning user changes (dry-run by default)"
echo "# Source: $INPUT_FILE"

apply() {
  if [[ $APPLY -eq 1 ]]; then "$@"; else echo "+ $*"; fi
}

set_password() {
  local user="$1" pass="$2"
  if [[ -z "$pass" || "$pass" == "null" ]]; then return 0; fi
  if [[ $APPLY -eq 1 ]]; then
    printf "%s:%s\n" "$user" "$pass" | chpasswd
    chage -d 0 "$user" || true
  else
    echo "+ chpasswd (redacted) | chage -d 0 $user"
  fi
}

echo "$json" | jq -c '.[]' | while read -r rec; do
  username_raw=$(jq -r '.username // empty' <<<"$rec")
  full_name=$(jq -r '.full_name // ""' <<<"$rec")
  shell=$(jq -r '.shell // "/bin/bash"' <<<"$rec")
  state=$(jq -r '.state // "present"' <<<"$rec")
  temp_password=$(jq -r '.temp_password // empty' <<<"$rec")

  if [[ -z "$username_raw" ]]; then
    echo "Skipping record with empty username: $rec" >&2
    continue
  fi

  username="$(safe_username "$username_raw")"
  groups_csv=$(jq -r '[.groups[]?] | join(",")' <<<"$rec")
  groups_csv="${groups_csv:-}"

  exists=0
  getent passwd "$username" >/dev/null 2>&1 || exists=1

  if [[ "$state" == "absent" ]]; then
    if [[ $exists -eq 0 ]]; then
      apply userdel -r "$username" || true
      say "User removed: $username"
    else
      echo "# already absent: $username"
    fi
    continue
  fi

  # Ensure groups exist
  if [[ -n "$groups_csv" ]]; then
    IFS=',' read -r -a glist <<<"$groups_csv"
    for g in "${glist[@]}"; do
      [[ -z "$g" ]] && continue
      if ! getent group "$g" >/dev/null 2>&1; then
        apply groupadd -f "$g"
        say "Group ensured: $g"
      fi
    done
  fi

  if [[ $exists -ne 0 ]]; then
    # Create user
    if [[ -n "$groups_csv" ]]; then
      apply useradd -m -c "$full_name" -s "$shell" -G "$groups_csv" "$username"
    else
      apply useradd -m -c "$full_name" -s "$shell" "$username"
    fi
    set_password "$username" "$temp_password"
    say "User created: $username"
  else
    # Update shell/comment; add to groups (non-destructive)
    apply usermod -c "$full_name" -s "$shell" "$username"
    if [[ -n "$groups_csv" ]]; then
      apply usermod -a -G "$groups_csv" "$username"
    fi
    # Optional: only set password if provided
    set_password "$username" "$temp_password"
    say "User updated: $username"
  fi
done

Try a dry-run first:

chmod +x ai_user_plan.sh
./ai_user_plan.sh --csv users.csv

Then apply:

sudo ./ai_user_plan.sh --csv users.csv --apply

Tip: For JSON input:

./ai_user_plan.sh --json users.json --apply

Step 3: Review and guardrails

Before you ever run with --apply:

  • Version-control your input files (CSV/JSON) and the script.

  • Run in dry-run mode and store the plan:

./ai_user_plan.sh --csv users.csv | tee plan.txt
  • Sanity-check usernames and shells. The script sanitizes usernames to a-z0-9._-, but you should still review group names and shells.

  • Run it on a staging VM first.

Security hardening tips:

  • Avoid embedding plaintext passwords in files. If you must, restrict permissions (chmod 600) and consider generating one-time passwords out-of-band.

  • Prefer locked accounts and SSO where possible; leave temp_password empty.

  • Use sudo with precise rules so the script can run only the needed commands.


Step 4: Schedule and observe

Use cron/cronie to run nightly with a fresh, normalized file:

  • Example cron entry (Debian/openSUSE):
# m h dom mon dow user  command
15 1 * * * root /usr/local/sbin/ai_user_plan.sh --json /srv/provision/users.json --apply >> /var/log/ai-user-mgmt.log 2>&1
  • Example crontab (RHEL/Fedora: service is crond; same crontab syntax):
15 1 * * * root /usr/local/sbin/ai_user_plan.sh --json /srv/provision/users.json --apply >> /var/log/ai-user-mgmt.log 2>&1

Logs also go to syslog via logger -t ai-user-mgmt for central collection.


Real-world example: From messy HR to executed plan

1) HR sends this (messy) snippet:

Name,Login,Sector,Needs Docker?,Status
"Jane D.","JDoe","Engineering","yes","active"
"Dan Lee","dlee ","Engineering","no","terminated"

2) AI normalization prompt (OpenAI-compatible flow shown; adjust to your API):

export AI_API_ENDPOINT="https://your-llm-endpoint/v1/chat/completions"
export AI_API_KEY="YOUR_API_KEY"

cat > prompt.txt <<'EOF'
Transform the following CSV into a JSON array with fields:

- username (lowercase a-z0-9._-; derive from "Login" trimming spaces)

- full_name (from "Name")

- groups (array; include "engineering" if Sector == Engineering; include "docker" if Needs Docker? == yes)

- shell ("/bin/bash" default)

- state: "present" if Status == active else "absent"

- temp_password: null

Output JSON only.
EOF

raw="$(jq -Rs . < <(cat prompt.txt; echo; cat hr.csv))"

curl -s -H "Authorization: Bearer $AI_API_KEY" -H "Content-Type: application/json" -d '{
  "model":"your-model","temperature":0,
  "messages":[{"role":"user","content": '"$raw"'}]
}' "$AI_API_ENDPOINT" | jq -r '.choices[0].message.content' > users.json

3) Dry-run the plan:

./ai_user_plan.sh --json users.json

Sample output:

# Planning user changes (dry-run by default)
# Source: users.json
+ groupadd -f engineering
+ groupadd -f docker
+ useradd -m -c "Jane D." -s /bin/bash -G engineering,docker jdoe
+ chpasswd (redacted) | chage -d 0 jdoe
User created: jdoe
+ userdel -r dlee
User removed: dlee

4) Apply when satisfied:

sudo ./ai_user_plan.sh --json users.json --apply

Governance and safety checklist

  • Backups: snapshot /etc/passwd, /etc/group, and home directories before first run.

  • Audit: keep inputs, dry-run plans, and logs in version control or an audit bucket.

  • Idempotence: schedule frequent runs; re-applications won’t harm existing users.

  • Rollback: for accidental removals, restore from backup and re-run with corrected input.


Conclusion and Call to Action

You don’t need a giant IAM product to get safer, faster Linux user management. Let AI shape messy inputs into a clean desired state, and let Bash execute small, reviewable, idempotent changes.

Next steps:

  • Install jq, miller, curl, and cron/cronie using your package manager.

  • Save the ai_user_plan.sh script and run a dry-run against a lab VM.

  • Add an AI normalization step if your HR data is messy.

  • Wire it into cron and start capturing logs.

When you’re ready, extend this pattern: enforce exact group membership, add systemd timers, or integrate with your ticketing system. One small script—big impact.