diff --git a/packages/nopy-cubes-core/cubes/user/add/README.md b/packages/nopy-cubes-core/cubes/user/add/README.md index f07d5c3..2b1ab64 100644 --- a/packages/nopy-cubes-core/cubes/user/add/README.md +++ b/packages/nopy-cubes-core/cubes/user/add/README.md @@ -47,22 +47,19 @@ This cube creates a new user account with a modern shell environment (Fish), SSH credential nobody had seen, and replaying that run produced a different one. - **GROUPS** (string, default: `''`) - - Comma-separated list of additional groups (e.g., `"docker,sudo"`) + - Space-separated list of additional groups (e.g., `"docker sudo"`) - Common groups: - `docker` - Run Docker without sudo - `sudo` - Administrative privileges - `www-data` - Web server file access -- **PUBKEY** (string, **required** — no default) +- **PUBKEY** (string, default: `''`) - SSH public key to authorize for the user - - Should be your public key for passwordless SSH access - - There is deliberately no default. It used to be a specific personal key, so - accepting the default authorized *someone else's* key on the new account. - No key would be a sensible guess, so the cube asks instead. - - Because it is required, `--use-defaults` refuses to run this cube unless - `PUBKEY` comes from `env` in `.nopyrc.json`, a dependency, or a hook. - - Submitting an empty value at the prompt authorizes no key at all (the account - is still created, with password login only). + - Empty (the default) authorizes no key at all — the account is created with + password login only. Some users simply do not need one. + - The default is deliberately empty, never a specific key. It used to be a + personal key, so accepting the default authorized *someone else's* key on + the new account. ## Dependencies @@ -87,6 +84,9 @@ After deployment: ## Notes +- If the user already exists, the cube does nothing at all — rerunning it would + reset the password and overwrite `~/.config/fish`, so an existing account is + left untouched. - The user's home directory is created at `/home/{USER}` - Fish configuration is stored in `/home/{USER}/.config/fish/` - Oh My Fish provides package management: `omf install ` diff --git a/packages/nopy-cubes-core/cubes/user/add/deploy.py b/packages/nopy-cubes-core/cubes/user/add/deploy.py index b4ff3e7..6323848 100644 --- a/packages/nopy-cubes-core/cubes/user/add/deploy.py +++ b/packages/nopy-cubes-core/cubes/user/add/deploy.py @@ -1,6 +1,6 @@ from pyinfra import host from pyinfra.operations import server, files, apt -from io import StringIO +from pyinfra.facts.server import Users # Define the username, password, and public key for the new admin user USER = host.data.USER @@ -11,83 +11,89 @@ PASSWORD = host.data.PASSWORD # line, so an absent key means no key rather than a blank one. PUBKEY = host.data.PUBKEY PUBKEYS = [PUBKEY] if PUBKEY and str(PUBKEY).strip() else [] -GROUPS = list(filter(None, map(str.strip, str(host.data.GROUPS).split()))) +GROUPS = str(host.data.GROUPS).split() FISH_PATH = "/usr/bin/fish" FISH_CONFIG_DIR = f"{HOME_DIR}/.config/fish" FISH_CONFIG_FILE = f"{FISH_CONFIG_DIR}/config.fish" FISH_RC_DIR = f"{FISH_CONFIG_DIR}/rc" SSH_AGENT_SCRIPT = f"{FISH_RC_DIR}/ssh-agent.fish" -apt.packages( - name='Ensure fish shell is installed', - packages=[ 'fish'], - _sudo=True -) +# An existing user is left entirely alone — everything below would reset the +# password and overwrite ~/.config/fish, clobbering whatever the user has +# changed since their account was created. +if host.get_fact(Users).get(USER): + host.noop(f"User {USER} already exists") +else: + apt.packages( + name='Ensure fish shell is installed', + packages=[ 'fish'], + _sudo=True + ) -# Ensure the user exists with a login shell -server.user( - name=f"Create user {USER} [{GROUPS}]", - present=True, - user=USER, - password=PASSWORD, - create_home=True, - groups=GROUPS, - shell=FISH_PATH, - public_keys=PUBKEYS, - _sudo=True -) + # Ensure the user exists with a login shell + server.user( + name=f"Create user {USER} [{GROUPS}]", + present=True, + user=USER, + password=PASSWORD, + create_home=True, + groups=GROUPS, + shell=FISH_PATH, + public_keys=PUBKEYS, + _sudo=True + ) -for dir in [f"{HOME_DIR}/.ssh", FISH_RC_DIR, TMP_DIR]: - files.directory( - name=f"Ensure {dir} directory exists", - path=dir, + for dir in [f"{HOME_DIR}/.ssh", FISH_RC_DIR, TMP_DIR]: + files.directory( + name=f"Ensure {dir} directory exists", + path=dir, + present=True, + mode=700, + user=USER, + group=USER, + _sudo=True, + _sudo_user=USER, + _use_sudo_login=True + ) + + files.file( + name="Ensure .ssh/config exists", + path=f"{HOME_DIR}/.ssh/config", present=True, - mode=700, user=USER, group=USER, + _sudo=True + ) + + server.shell( + name=f"Install OMF(Oh My Fish) for {USER}", + commands=[ + f"curl https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install > install-omf", + f"fish install-omf --yes --noninteractive", + ], _sudo=True, _sudo_user=USER, _use_sudo_login=True ) -files.file( - name="Ensure .ssh/config exists", - path=f"{HOME_DIR}/.ssh/config", - present=True, - user=USER, - group=USER, - _sudo=True -) + files.put( + name="Add SSH agent auto-load script to Fish rc directory", + src="ssh-agent.fish", + dest=SSH_AGENT_SCRIPT, + user=USER, + group=USER, + mode="755", # Make it executable + _sudo=True, -server.shell( - name=f"Install OMF(Oh My Fish) for {USER}", - commands=[ - f"curl https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install > install-omf", - f"fish install-omf --yes --noninteractive", - ], - _sudo=True, - _sudo_user=USER, - _use_sudo_login=True -) + ) -files.put( - name="Add SSH agent auto-load script to Fish rc directory", - src="ssh-agent.fish", - dest=SSH_AGENT_SCRIPT, - user=USER, - group=USER, - mode="755", # Make it executable - _sudo=True, - -) - -files.put( - name="Add custom config.fish", - src="config.fish", - dest=FISH_CONFIG_FILE, - user=USER, - group=USER, - mode="755", # Make it executable - _sudo=True, -) \ No newline at end of file + files.put( + name="Add custom config.fish", + src="config.fish", + dest=FISH_CONFIG_FILE, + user=USER, + group=USER, + mode="755", # Make it executable + _sudo=True, + ) diff --git a/packages/nopy-cubes-core/cubes/user/add/manifest.mjs b/packages/nopy-cubes-core/cubes/user/add/manifest.mjs index 077d65c..f0cc03b 100644 --- a/packages/nopy-cubes-core/cubes/user/add/manifest.mjs +++ b/packages/nopy-cubes-core/cubes/user/add/manifest.mjs @@ -17,12 +17,14 @@ export default Manifest({ PASSWORD: z.string().describe('Password for the new user account').default('changeme'), GROUPS: z .string() - .describe('Comma-separated list of additional groups (e.g., "docker,sudo")') + .describe('Space-separated list of additional groups (e.g., "docker sudo")') + .default(''), + // Empty by default, never a specific key: this used to carry a personal + // key, which meant an unattended run authorised someone else's key on the + // new account. Empty means no key is authorised — some users need none. + PUBKEY: z + .string() + .describe('SSH public key to authorize for the user (empty for none)') .default(''), - // No default on purpose. This used to carry a specific personal key, which - // meant an unattended run authorised someone else's key on the new account. - // Leaving it required makes `--use-defaults` refuse by name instead of - // guessing, and there is no key that would be a sensible guess. - PUBKEY: z.string().describe('SSH public key to authorize for the user'), }), });