ops-jrz1/scripts/dev-add.sh
Dan 3c8f961cdc docs: user-facing docs with SERVER.md symlink pattern
- SERVER.md: symlinked from /etc/user-docs (always current)
- AGENTS.md: user's file, points to SERVER.md, editable
- README.md: welcome doc, copied once
- readme.txt: whaddup cuz

dev-add.sh provisions all four, only overwrites SERVER.md symlink.
2026-01-22 22:04:51 -08:00

368 lines
12 KiB
Bash
Executable file

#!/usr/bin/env bash
# dev-add.sh - Add or update a dev user account
# Usage: dev-add.sh <username> <ssh-pubkey>
#
# Idempotent: safe to re-run. Updates SSH key and config if user exists.
#
# Creates/updates:
# - Unix user account with SSH key
# - Adds to devs group (Slack token access)
# - Git config (user.name, user.email)
# - Forgejo account for git.clarun.xyz access
# - Outputs onboarding instructions
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
usage() {
echo "Usage: $0 <username> <ssh-pubkey>"
echo ""
echo "Arguments:"
echo " username - Dev's username (alphanumeric, 3-16 chars)"
echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)"
echo ""
echo "Example:"
echo " $0 alice 'ssh-ed25519 AAAA... alice@laptop'"
exit 1
}
validate_username() {
local username="$1"
if [[ ! "$username" =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then
log_error "Invalid username: must be 3-16 chars, start with letter, alphanumeric/underscore/dash only"
exit 1
fi
}
validate_ssh_key() {
local key="$1"
if [[ ! "$key" =~ ^ssh-(ed25519|rsa|ecdsa) ]]; then
log_error "Invalid SSH key: must start with ssh-ed25519, ssh-rsa, or ssh-ecdsa"
exit 1
fi
}
create_user() {
local username="$1"
local ssh_key="$2"
local user_exists=false
# Check if user already exists
if id "$username" &>/dev/null; then
user_exists=true
log_info "Updating existing user '$username'..."
else
log_info "Creating user '$username'..."
# Create user with home directory
# NixOS: don't specify shell (uses default), group is 'users'
useradd -m -g users "$username"
fi
# Make home directory private (not world-readable)
chmod 700 "/home/$username"
# Add to devs group for Slack token access
if ! groups "$username" | grep -q '\bdevs\b'; then
usermod -aG devs "$username"
log_info "Added to devs group"
fi
# Add to systemd-journal group for log access
if ! groups "$username" | grep -q '\bsystemd-journal\b'; then
usermod -aG systemd-journal "$username"
log_info "Added to systemd-journal group"
fi
# Set up SSH directory and login key (authorized_keys)
local ssh_dir="/home/$username/.ssh"
mkdir -p "$ssh_dir"
echo "$ssh_key" > "$ssh_dir/authorized_keys"
chmod 700 "$ssh_dir"
chmod 600 "$ssh_dir/authorized_keys"
chown -R "$username:users" "$ssh_dir"
if [[ "$user_exists" == true ]]; then
log_info "Updated SSH login key"
fi
# Generate server-side keypair for git access (if not exists)
local server_key="$ssh_dir/id_ed25519"
if [[ ! -f "$server_key" ]]; then
log_info "Generating server-side SSH key for git access..."
sudo -u "$username" ssh-keygen -t ed25519 \
-f "$server_key" \
-N '' \
-C "$username@jrz1-server-DO-NOT-REUSE" \
>/dev/null
log_info "Server-side SSH key generated"
else
log_info "Server-side SSH key already exists"
fi
# Ensure strict permissions
chmod 700 "$ssh_dir"
chmod 600 "$server_key"
chmod 644 "$server_key.pub"
chown -R "$username:users" "$ssh_dir"
# Set up user's shell config (may not exist on NixOS)
# .profile = login shells (SSH), .bashrc = interactive non-login
local profile="/home/$username/.profile"
{
echo '# bun global packages (preferred - faster installs)'
echo "export PATH=\"\$HOME/.bun/bin:\$PATH\""
echo ''
echo '# npm global packages (fallback)'
echo "export PATH=\"\$HOME/.npm-global/bin:\$PATH\""
echo ''
echo '# Slack bot development tokens (if readable)'
echo '[ -r /etc/slack-dev.env ] && source /etc/slack-dev.env'
} > "$profile"
chown "$username:users" "$profile"
# Pre-create npm global directory and configure npm prefix
local npm_global="/home/$username/.npm-global"
local npmrc="/home/$username/.npmrc"
mkdir -p "$npm_global"
echo "prefix=$npm_global" > "$npmrc"
chown "$username:users" "$npm_global" "$npmrc"
# Provision user documentation from /etc/user-docs/
# SERVER.md = symlink (always current, system-maintained)
# AGENTS.md, README.md = copy (user can customize)
local docs_dir="/etc/user-docs"
if [[ -d "$docs_dir" ]]; then
# Symlink SERVER.md (system-maintained, always current)
if [[ -f "$docs_dir/SERVER.md" ]]; then
ln -sf "$docs_dir/SERVER.md" "/home/$username/SERVER.md"
log_info "Linked SERVER.md"
fi
# Copy user-editable docs (only if they don't exist)
for doc in AGENTS.md README.md readme.txt; do
if [[ -f "$docs_dir/$doc" && ! -f "/home/$username/$doc" ]]; then
cp "$docs_dir/$doc" "/home/$username/$doc"
chown "$username:users" "/home/$username/$doc"
log_info "Created $doc"
fi
done
else
log_warn "/etc/user-docs not found - skipping doc provisioning"
log_warn "Deploy NixOS config to create user docs"
fi
# Set up git config for proper commit attribution
local gitconfig="/home/$username/.gitconfig"
cat > "$gitconfig" << EOF
[user]
name = $username
email = $username@clarun.xyz
[init]
defaultBranch = main
EOF
chown "$username:users" "$gitconfig"
log_info "Git config created"
# Provision Forgejo account for git.clarun.xyz access
provision_forgejo "$username" "$ssh_key"
if [[ "$user_exists" == true ]]; then
log_info "User updated"
else
log_info "User created with SSH access"
fi
}
# Helper function to upload a single SSH key to Forgejo
upload_forgejo_key() {
local username="$1"
local pubkey="$2"
local title="$3"
local token="$4"
local forgejo_url="http://localhost:3000"
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 --max-time 30 \
-X POST "$forgejo_url/api/v1/admin/users/$username/keys" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"$title\",
\"key\": \"$pubkey\",
\"read_only\": false
}" 2>/dev/null) || true
if [[ "$http_code" == "201" ]]; then
log_info "Forgejo key '$title' added"
elif [[ "$http_code" == "422" ]]; then
log_info "Forgejo key '$title' already exists"
else
log_warn "Forgejo key '$title' upload returned HTTP $http_code"
fi
}
provision_forgejo() {
local username="$1"
local login_key="$2"
local token_file="/run/secrets/forgejo-api-token"
local forgejo_url="http://localhost:3000"
# Check if token file exists
if [[ ! -r "$token_file" ]]; then
log_warn "Forgejo API token not found at $token_file"
log_warn "Skipping Forgejo provisioning - user can self-register at https://git.clarun.xyz"
return 0
fi
local token
token=$(cat "$token_file")
# Try to create Forgejo user (ignore if already exists)
local random_pass
random_pass=$(head -c 16 /dev/urandom | base64 | tr -d '/+=' | head -c 16)
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 --max-time 30 \
-X POST "$forgejo_url/api/v1/admin/users" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{
\"username\": \"$username\",
\"email\": \"$username@clarun.xyz\",
\"password\": \"$random_pass\",
\"must_change_password\": true,
\"send_notify\": false
}" 2>/dev/null) || true
local user_created=false
if [[ "$http_code" == "201" ]]; then
log_info "Forgejo user created"
user_created=true
elif [[ "$http_code" == "422" ]]; then
log_info "Forgejo user already exists"
else
log_warn "Forgejo user creation returned HTTP $http_code (may already exist)"
fi
# Write credentials file (only if we created the user and know the password)
if [[ "$user_created" == true ]]; then
local creds_file="/home/$username/.forgejo-credentials"
cat > "$creds_file" << EOF
{
"service": "forgejo",
"url": "https://git.clarun.xyz",
"username": "$username",
"initial_password": "$random_pass",
"must_change_password": true,
"note": "Delete this file after first login: rm ~/.forgejo-credentials"
}
EOF
chmod 600 "$creds_file"
chown "$username:users" "$creds_file"
log_info "Credentials written to ~/.forgejo-credentials"
fi
# Upload BOTH keys to Forgejo:
# 1. Login key (user's laptop key) - for direct laptop→git access
upload_forgejo_key "$username" "$login_key" "$username-laptop" "$token"
# 2. Server-side key - for git access from within the server
local server_pubkey_file="/home/$username/.ssh/id_ed25519.pub"
if [[ -r "$server_pubkey_file" ]]; then
local server_pubkey
server_pubkey=$(cat "$server_pubkey_file")
upload_forgejo_key "$username" "$server_pubkey" "$username-devserver" "$token"
else
log_warn "Server-side key not found at $server_pubkey_file"
fi
}
print_onboarding() {
local username="$1"
local server_ip
# NixOS: use ip command instead of hostname -I
server_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
echo ""
echo "=========================================="
echo " Dev Environment Ready: $username"
echo "=========================================="
echo ""
echo "## SSH Config (~/.ssh/config on your laptop)"
echo ""
echo " Host dev-server"
echo " HostName ${server_ip:-<server-ip>}"
echo " User $username"
echo " LocalForward 8080 127.0.0.1:8080"
echo ""
echo "## Quick Start"
echo ""
echo "1. SSH in:"
echo " ssh dev-server"
echo ""
echo "2. Install AI tools (pick one or more):"
echo " bun install -g @anthropic-ai/claude-code # Claude"
echo " bun install -g @google/gemini-cli # Gemini"
echo ""
echo "3. Authenticate and start coding:"
echo " claude # or: gemini"
echo " # Follow prompts to authenticate"
echo ""
echo "## Git Access"
echo ""
echo " Clone repos: git clone forgejo@git.clarun.xyz:org/repo.git"
echo ""
echo " Forgejo account: $username"
echo " Credentials file: ~/.forgejo-credentials"
echo " Web UI: https://git.clarun.xyz"
echo ""
echo " Note: Change password on first login, then delete credentials file"
echo ""
echo "## Tools Available"
echo " System: python3, uv, git, bun, node, opencode, bd"
echo " Install more: bun install -g <package>"
echo " nix profile install nixpkgs#<package>"
echo ""
echo "=========================================="
}
main() {
if [[ $# -lt 2 ]]; then
usage
fi
local username="$1"
local ssh_key="$2"
# Must run as root
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
validate_username "$username"
validate_ssh_key "$ssh_key"
# Check devs group exists (created by NixOS config)
if ! getent group devs >/dev/null; then
log_error "Required group 'devs' does not exist"
log_error "Ensure users.groups.devs = {} is in NixOS config and deployed"
exit 1
fi
create_user "$username" "$ssh_key"
print_onboarding "$username"
log_info "Dev user '$username' setup complete!"
}
main "$@"