cubes: user:add gets optional PUBKEY, space-separated GROUPS, exists guard
PUBKEY defaults to empty now — empty means no key is authorised, and some users need none. GROUPS was always split on whitespace by deploy.py, so the comma-separated prompt label and README were documenting a bug; both now say space-separated. The deploy script checks the Users fact up front and noops when the user exists, since rerunning reset the password and overwrote ~/.config/fish. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ce5atB2tXDXyz2jd9s1bqE
This commit is contained in:
co-authored by
Claude Fable 5
parent
f1cc9effa0
commit
643d7379ba
@@ -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 <package>`
|
||||
|
||||
@@ -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,22 +11,28 @@ 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(
|
||||
# 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(
|
||||
# Ensure the user exists with a login shell
|
||||
server.user(
|
||||
name=f"Create user {USER} [{GROUPS}]",
|
||||
present=True,
|
||||
user=USER,
|
||||
@@ -36,9 +42,9 @@ server.user(
|
||||
shell=FISH_PATH,
|
||||
public_keys=PUBKEYS,
|
||||
_sudo=True
|
||||
)
|
||||
)
|
||||
|
||||
for dir in [f"{HOME_DIR}/.ssh", FISH_RC_DIR, TMP_DIR]:
|
||||
for dir in [f"{HOME_DIR}/.ssh", FISH_RC_DIR, TMP_DIR]:
|
||||
files.directory(
|
||||
name=f"Ensure {dir} directory exists",
|
||||
path=dir,
|
||||
@@ -51,16 +57,16 @@ for dir in [f"{HOME_DIR}/.ssh", FISH_RC_DIR, TMP_DIR]:
|
||||
_use_sudo_login=True
|
||||
)
|
||||
|
||||
files.file(
|
||||
files.file(
|
||||
name="Ensure .ssh/config exists",
|
||||
path=f"{HOME_DIR}/.ssh/config",
|
||||
present=True,
|
||||
user=USER,
|
||||
group=USER,
|
||||
_sudo=True
|
||||
)
|
||||
)
|
||||
|
||||
server.shell(
|
||||
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",
|
||||
@@ -69,9 +75,9 @@ server.shell(
|
||||
_sudo=True,
|
||||
_sudo_user=USER,
|
||||
_use_sudo_login=True
|
||||
)
|
||||
)
|
||||
|
||||
files.put(
|
||||
files.put(
|
||||
name="Add SSH agent auto-load script to Fish rc directory",
|
||||
src="ssh-agent.fish",
|
||||
dest=SSH_AGENT_SCRIPT,
|
||||
@@ -80,9 +86,9 @@ files.put(
|
||||
mode="755", # Make it executable
|
||||
_sudo=True,
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
files.put(
|
||||
files.put(
|
||||
name="Add custom config.fish",
|
||||
src="config.fish",
|
||||
dest=FISH_CONFIG_FILE,
|
||||
@@ -90,4 +96,4 @@ files.put(
|
||||
group=USER,
|
||||
mode="755", # Make it executable
|
||||
_sudo=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user