mirror of
https://github.com/tildeclub/launcher.git
synced 2026-10-25 08:25:38 +00:00
Harden script approvals and add migration support
This commit is contained in:
7 files changed
+962
-338
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shell:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Shell syntax and smoke tests
|
||||||
|
run: sh tests/smoke.sh
|
||||||
|
- name: Approval and migration integration tests
|
||||||
|
run: sudo sh tests/integration.sh
|
||||||
@@ -4,20 +4,17 @@ BINDIR ?= $(PREFIX)/bin
|
|||||||
MANDIR ?= $(PREFIX)/share/man
|
MANDIR ?= $(PREFIX)/share/man
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@echo Installing the executable to $(BINDIR)
|
@set -eu; \
|
||||||
@mkdir -p $(BINDIR)
|
mkdir -p "$(BINDIR)" "$(MANDIR)/man1"; \
|
||||||
@cp -f $(BASENAME) $(BINDIR)/$(BASENAME)
|
install -m 0755 "$(BASENAME)" "$(BINDIR)/.$(BASENAME).new"; \
|
||||||
@chmod 755 $(BINDIR)/$(BASENAME)
|
mv -f "$(BINDIR)/.$(BASENAME).new" "$(BINDIR)/$(BASENAME)"; \
|
||||||
@echo Installing the manual page to $(MANDIR)/man1
|
install -m 0644 "$(BASENAME).1" "$(MANDIR)/man1/.$(BASENAME).1.new"; \
|
||||||
@mkdir -p $(MANDIR)/man1
|
mv -f "$(MANDIR)/man1/.$(BASENAME).1.new" "$(MANDIR)/man1/$(BASENAME).1"
|
||||||
@cp -f $(BASENAME).1 $(MANDIR)/man1/$(BASENAME).1
|
|
||||||
@chmod 644 $(MANDIR)/man1/$(BASENAME).1
|
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
@echo Removing the executable from $(BINDIR)
|
@rm -f "$(BINDIR)/$(BASENAME)" "$(MANDIR)/man1/$(BASENAME).1"
|
||||||
@rm -f $(BINDIR)/$(BASENAME)
|
|
||||||
@echo Removing the manual page from $(MANDIR)/man1
|
|
||||||
@rm -f $(BINDIR)/man1/$(BASENAME).1
|
|
||||||
|
|
||||||
.PHONY: install uninstall
|
test:
|
||||||
|
@sh tests/smoke.sh
|
||||||
|
|
||||||
|
.PHONY: install uninstall test
|
||||||
@@ -1,25 +1,123 @@
|
|||||||
# `tilde` - user script tilde-launcher
|
# tilde launcher
|
||||||
|
|
||||||
```
|
`tilde` is a small POSIX shell utility for submitting and approving programs
|
||||||
Usage: tilde [options] [command]
|
that can be run by other users. Approved commands remain available under
|
||||||
|
`/tilde/bin`. This version targets Linux with GNU coreutils, `runuser`,
|
||||||
|
`timeout`, `flock`, `getent`, `sha256sum`, and a sendmail-compatible MTA.
|
||||||
|
|
||||||
Options:
|
## Commands
|
||||||
-h, --help Show this help message and exit
|
|
||||||
-v, --version Show version information
|
|
||||||
|
|
||||||
Commands:
|
```text
|
||||||
list Show a list of approved user scripts
|
tilde list Show published scripts
|
||||||
submit Start the submission flow for your own script
|
tilde about <script> Show its description and author
|
||||||
about <script> Show description and details of a specific script
|
tilde submit Submit ~/bin/<script>
|
||||||
approve Enter the approval queue for pending scripts (requires root)
|
tilde update <script> Submit a new revision of your published script
|
||||||
revoke <script> Revoke a previously approved script (requires root)
|
tilde pending List your submissions awaiting review
|
||||||
run <script> Run an approved script
|
tilde status <script> Display approval status
|
||||||
|
tilde run <script> [args] Run an approved script with arguments
|
||||||
Please add /tilde/bin to your PATH to use approved scripts without this wrapper.
|
sudo tilde approve Inspect and approve submitted scripts
|
||||||
|
sudo tilde revoke <name> Revoke an approved script
|
||||||
|
sudo tilde migrate [name] Review and migrate legacy approvals
|
||||||
|
sudo tilde init Create and secure storage directories
|
||||||
```
|
```
|
||||||
|
|
||||||
approved scripts are placed in /tilde/bin and listed with `tilde list`
|
`description` is an alias for `about`. Previously published scripts continue
|
||||||
|
to work from `/tilde/bin` without the wrapper. Add that directory to `PATH` to
|
||||||
|
run them directly.
|
||||||
|
|
||||||
submissions use sendmail to notify an admin.
|
## How approvals work
|
||||||
|
|
||||||
admins can use `sudo tilde approve` and `sudo tilde revoke <script_name>`.
|
New submissions are **copies** of an executable in the user's `~/bin`, not
|
||||||
|
symlinks to live code. The submitted file and description remain user-writable;
|
||||||
|
they are not considered trusted approval state. The administrator captures
|
||||||
|
both files **under the submitter's UID** in a private review directory, sees
|
||||||
|
the exact snapshot, and approves that snapshot. No submitted program is
|
||||||
|
executed during approval.
|
||||||
|
|
||||||
|
Approved files are stored in root-owned `/tilde/approved/<name>/<sha256>`.
|
||||||
|
`/tilde/bin/<name>` points to the approved version. Approval records live in
|
||||||
|
root-owned `/tilde/metadata`; rejected submission fingerprints are tracked in
|
||||||
|
`/tilde/rejected`. Published descriptions remain in
|
||||||
|
`/tilde/descriptions`. Previous versions are retained without ordinary user
|
||||||
|
read/execute permissions after an update or revocation. Each approval/revocation
|
||||||
|
is also sent to syslog with the tag `tilde-launcher`.
|
||||||
|
|
||||||
|
An administrator can approve, reject (with a reason sent to the author), or
|
||||||
|
skip a submission. A rejected submission may be replaced with `tilde submit`.
|
||||||
|
|
||||||
|
A submission is limited to 50 MiB with a 16 KiB description. Single-file
|
||||||
|
scripts and executables work best. Scripts that load adjacent assets or rely on
|
||||||
|
`dirname "$0"` may require an adjusted wrapper or an app-specific packaging
|
||||||
|
approach; do not migrate those without checking them first.
|
||||||
|
|
||||||
|
## Installing or upgrading
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/tildeclub/launcher.git
|
||||||
|
cd launcher
|
||||||
|
sh tests/smoke.sh
|
||||||
|
sudo make install
|
||||||
|
sudo tilde init
|
||||||
|
```
|
||||||
|
|
||||||
|
Review existing `/tilde` ownership and permissions before running `init`.
|
||||||
|
It refuses pre-existing, unsafe symlink directories and group/world-writable
|
||||||
|
administrative directories instead of silently overwriting them. The parent
|
||||||
|
`/tilde/pending-submissions` is set to sticky mode `1733`; existing user
|
||||||
|
submissions are not deleted. `/tilde/review` is root-only (`0700`).
|
||||||
|
|
||||||
|
**Important:** `make install` does not replace any previously approved
|
||||||
|
`/tilde/bin` symlinks. They remain live links to user code until explicitly
|
||||||
|
migrated. Administrators should plan a manual review:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo tilde migrate # review each legacy link
|
||||||
|
sudo tilde migrate some-script # or migrate one at a time
|
||||||
|
```
|
||||||
|
|
||||||
|
A migration checks the target is under its owner's home `bin` directory,
|
||||||
|
captures it under the author's UID, and asks for approval. Existing users'
|
||||||
|
source files are not modified. If a legacy symlink relies on adjacent files,
|
||||||
|
leave it untouched until it has a suitable standalone replacement.
|
||||||
|
|
||||||
|
Do **not** point `/tilde/approved`, `/tilde/metadata`, `/tilde/rejected`, or `/tilde/review` at
|
||||||
|
user-controlled storage. The administrative directories must be root-owned,
|
||||||
|
not symlinks, and not writable by other users. `runuser`, `timeout`, and `flock`
|
||||||
|
are required for approval and migration. A working `/usr/sbin/sendmail` is
|
||||||
|
recommended for notices; failure to send mail is reported without undoing a
|
||||||
|
completed submission or approval.
|
||||||
|
|
||||||
|
To verify the deployment:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo tilde init
|
||||||
|
tilde --version
|
||||||
|
tilde list
|
||||||
|
tilde status some-script
|
||||||
|
sudo journalctl -t tilde-launcher --since today
|
||||||
|
```
|
||||||
|
|
||||||
|
For an optional full test on an isolated test machine or CI runner, execute
|
||||||
|
`sudo sh tests/integration.sh`. It creates and removes a temporary system
|
||||||
|
account and tests under `/tmp`, without using your live `/tilde` tree.
|
||||||
|
|
||||||
|
## Recovery and rollback
|
||||||
|
|
||||||
|
Before installing, archive `/tilde` and preserve the old launcher binary and
|
||||||
|
manual page. Because existing approvals are not migrated automatically,
|
||||||
|
reverting the command alone restores its original behaviour for unmigrated
|
||||||
|
scripts. Migrated scripts are immutable copies; reverting to the old launcher
|
||||||
|
does **not** restore the old user-owned symlinks. Restore the corresponding
|
||||||
|
`/tilde/bin` links, descriptions, and metadata from the backup if you need to
|
||||||
|
undo a migration. Avoid restoring old files over an active installation without
|
||||||
|
first stopping administrative changes.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh tests/smoke.sh
|
||||||
|
sudo sh tests/integration.sh # isolated test, needs root and useradd
|
||||||
|
```
|
||||||
|
|
||||||
|
Please run ShellCheck and test on a non-production machine before deploying.
|
||||||
|
No GitHub commit or migration occurs merely by applying a patch.
|
||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Full test of the approval path. Requires root, runuser, and getent.
|
||||||
|
set -eu
|
||||||
|
[ "$(id -u)" -eq 0 ] || { echo 'Run as root' >&2; exit 1; }
|
||||||
|
for cmd in runuser timeout flock sha256sum getent; do
|
||||||
|
command -v "$cmd" >/dev/null || { echo "Missing $cmd" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
tmp=$(mktemp -d /tmp/tilde-launcher-test.XXXXXXXX)
|
||||||
|
user=launcher-test-$$
|
||||||
|
cleanup() {
|
||||||
|
userdel "$user" >/dev/null 2>&1 || :
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
trap 'cleanup' EXIT HUP INT TERM
|
||||||
|
chmod 0755 "$tmp"
|
||||||
|
root=$tmp/tilde
|
||||||
|
home=$tmp/home
|
||||||
|
useradd -M -d "$home" -s /bin/sh "$user"
|
||||||
|
uid=$(id -u "$user")
|
||||||
|
gid=$(id -g "$user")
|
||||||
|
mkdir -m 0755 "$home"
|
||||||
|
mkdir -m 0755 "$home/bin"
|
||||||
|
chown -R "$uid:$gid" "$home"
|
||||||
|
cat > "$tmp/sendmail" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
cat >/dev/null
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$tmp/sendmail"
|
||||||
|
sed -e "s|^ROOT=/tilde$|ROOT=$root|" \
|
||||||
|
-e "s|/usr/sbin/sendmail|$tmp/sendmail|g" tilde > "$tmp/launcher"
|
||||||
|
chmod 0755 "$tmp/launcher"
|
||||||
|
"$tmp/launcher" init >/dev/null
|
||||||
|
cat > "$home/bin/example" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
printf 'v1:%s\n' "$*"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$home/bin/example"
|
||||||
|
chown "$uid:$gid" "$home/bin/example"
|
||||||
|
printf 'y\nexample\nA test script\ny\n' |
|
||||||
|
runuser -u "$user" -- env HOME="$home" "$tmp/launcher" submit >/dev/null
|
||||||
|
printf 'a\n' | "$tmp/launcher" approve >/dev/null
|
||||||
|
[ "$("$tmp/launcher" run example argument)" = 'v1:argument' ]
|
||||||
|
[ "$("$tmp/launcher" about example | head -n 1)" = 'A test script' ]
|
||||||
|
# Changing the user's script cannot change what everyone else executes.
|
||||||
|
cat > "$home/bin/example" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
printf 'v2:%s\n' "$*"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$home/bin/example"
|
||||||
|
chown "$uid:$gid" "$home/bin/example"
|
||||||
|
[ "$("$tmp/launcher" run example argument)" = 'v1:argument' ]
|
||||||
|
printf 'Updated description\ny\ny\n' |
|
||||||
|
runuser -u "$user" -- env HOME="$home" "$tmp/launcher" update example >/dev/null
|
||||||
|
printf 'a\n' | "$tmp/launcher" approve >/dev/null
|
||||||
|
[ "$("$tmp/launcher" run example argument)" = 'v2:argument' ]
|
||||||
|
# The retired version is not executable by ordinary users.
|
||||||
|
# Verify the old version's permissions without making any assumptions about its hash.
|
||||||
|
for previous in "$root/approved/example"/*; do
|
||||||
|
[ -f "$previous" ] || continue
|
||||||
|
if [ "$(readlink -f "$root/bin/example")" != "$previous" ]; then
|
||||||
|
[ "$(stat -c %a "$previous")" = 700 ]
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Cancellation must not revoke anything.
|
||||||
|
printf 'n\n' | "$tmp/launcher" revoke example >/dev/null
|
||||||
|
[ -x "$root/bin/example" ]
|
||||||
|
printf 'y\nSecurity review\n' | "$tmp/launcher" revoke example >/dev/null
|
||||||
|
[ ! -e "$root/bin/example" ] && [ ! -L "$root/bin/example" ]
|
||||||
|
[ "$(stat -c %a "$(find "$root/approved/example" -maxdepth 1 -type f | tail -n 1)")" = 700 ]
|
||||||
|
# Reject a submission, then allow a corrected resubmission.
|
||||||
|
cat > "$home/bin/rejected" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo rejected-v1
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$home/bin/rejected"
|
||||||
|
chown "$uid:$gid" "$home/bin/rejected"
|
||||||
|
printf 'y\nrejected\nFirst version\ny\n' |
|
||||||
|
runuser -u "$user" -- env HOME="$home" "$tmp/launcher" submit >/dev/null
|
||||||
|
printf 'r\nNeeds revision\n' | "$tmp/launcher" approve >/dev/null
|
||||||
|
[ ! -e "$root/bin/rejected" ]
|
||||||
|
runuser -u "$user" -- env HOME="$home" "$tmp/launcher" status rejected | grep -q 'rejected'
|
||||||
|
printf 'y\nrejected\nFixed version\ny\ny\n' |
|
||||||
|
runuser -u "$user" -- env HOME="$home" "$tmp/launcher" submit >/dev/null
|
||||||
|
printf 'a\n' | "$tmp/launcher" approve >/dev/null
|
||||||
|
[ "$("$tmp/launcher" run rejected)" = 'rejected-v1' ]
|
||||||
|
# Migrating a legacy symlink requires review and freezes the code.
|
||||||
|
cat > "$home/bin/legacy" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo legacy-v1
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$home/bin/legacy"
|
||||||
|
chown "$uid:$gid" "$home/bin/legacy"
|
||||||
|
ln -s "$home/bin/legacy" "$root/bin/legacy"
|
||||||
|
printf 'A legacy script\n' > "$root/descriptions/legacy"
|
||||||
|
printf 'a\n' | "$tmp/launcher" migrate legacy >/dev/null
|
||||||
|
[ "$("$tmp/launcher" run legacy)" = 'legacy-v1' ]
|
||||||
|
printf '#!/bin/sh\necho legacy-v2\n' > "$home/bin/legacy"
|
||||||
|
[ "$("$tmp/launcher" run legacy)" = 'legacy-v1' ]
|
||||||
|
printf 'Integration tests passed.\n'
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
sh -n tilde
|
||||||
|
[ "$(./tilde --version)" = 0.2.0 ]
|
||||||
|
./tilde --help | grep -q 'migrate'
|
||||||
|
if ./tilde run '../etc/passwd' >/dev/null 2>&1; then
|
||||||
|
printf 'Path traversal was accepted\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ./tilde revoke '../etc/passwd' >/dev/null 2>&1; then
|
||||||
|
printf 'Unsafe revocation was accepted\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf 'Smoke tests passed.\n'
|
||||||
@@ -1,306 +1,690 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# tilde - manage user-submitted scripts and apps
|
# tilde - manage user-submitted scripts and apps
|
||||||
|
|
||||||
# Copyright 2018, Ben Harris <ben@tilde.team>
|
# Copyright 2018, Ben Harris <ben@tilde.team>
|
||||||
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# GPL-3.0-or-later. See LICENSE for the full licence text.
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License at <http://www.gnu.org/licenses/> for
|
|
||||||
# more details.
|
|
||||||
|
|
||||||
# Usage: tilde [-h|--help]
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
PROGNAME=${0##*/}
|
set -u
|
||||||
VERSION="0.1.0"
|
umask 077
|
||||||
user=$(whoami)
|
# Preserve users' PATH, especially for "tilde run". Sanitize only root commands.
|
||||||
hostname=$(hostname -f)
|
if [ "$(/usr/bin/id -u)" -eq 0 ]; then
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
# check coreutils and wrap stat for portability
|
export PATH
|
||||||
if stat -c"%U" /dev/null >/dev/null 2>/dev/null ; then
|
|
||||||
# GNU environment
|
|
||||||
stat_func () {
|
|
||||||
stat -c '%U' "$1"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
# BSD environment
|
|
||||||
stat_func () {
|
|
||||||
stat -f %Su "$1"
|
|
||||||
}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
isroot() {
|
PROGNAME=${0##*/}
|
||||||
[ "$(id -u)" = "0" ]
|
VERSION=0.2.0
|
||||||
}
|
ROOT=/tilde
|
||||||
|
BIN=$ROOT/bin
|
||||||
|
DESCRIPTIONS=$ROOT/descriptions
|
||||||
|
PENDING=$ROOT/pending-submissions
|
||||||
|
APPROVED=$ROOT/approved
|
||||||
|
META=$ROOT/metadata
|
||||||
|
REVIEW=$ROOT/review
|
||||||
|
REJECTED=$ROOT/rejected
|
||||||
|
LOCK=$ROOT/.launcher.lock
|
||||||
|
MAX_SCRIPT=52428800
|
||||||
|
MAX_DESCRIPTION=16384
|
||||||
|
WORK=
|
||||||
|
|
||||||
error_exit() {
|
fail() {
|
||||||
printf "%s\n" "${1:-"unknown Error"}" >&2
|
printf '%s: %s\n' "$PROGNAME" "$1" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
signal_exit() { # Handle trapped signals
|
warn() {
|
||||||
case $1 in
|
printf '%s: %s\n' "$PROGNAME" "$1" >&2
|
||||||
INT)
|
}
|
||||||
error_exit "program interrupted by user"
|
|
||||||
;;
|
cleanup() {
|
||||||
TERM)
|
if [ -n "$WORK" ]; then
|
||||||
printf "\n%s: program terminated" "$PROGNAME" >&2
|
rm -rf -- "$WORK"
|
||||||
exit
|
WORK=
|
||||||
;;
|
fi
|
||||||
*)
|
}
|
||||||
error_exit "$PROGNAME: terminating on unknown signal"
|
trap 'cleanup' EXIT
|
||||||
;;
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM HUP
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
[ "$(id -u)" -eq 0 ] || fail 'this command requires root'
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_name() {
|
||||||
|
case ${1-} in
|
||||||
|
''|.*|*[!a-zA-Z0-9_.+-]*) return 1 ;;
|
||||||
|
*) return 0 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt_confirm() {
|
check_name() {
|
||||||
while true; do
|
valid_name "${1-}" || fail 'invalid script name (use letters, numbers, _, -, + or .; not starting with .)'
|
||||||
printf "%s [y/n]: " "${1:-continue?}"
|
case $1 in
|
||||||
read -r REPLY
|
tilde|about|description|list|ls|submit|update|approve|revoke|run|help|apropos|pending|status|migrate|init|version)
|
||||||
case $REPLY in
|
fail "$1 is a reserved name" ;;
|
||||||
[yY]) printf "\n" ; return 0 ;;
|
esac
|
||||||
[nN]) printf "\n" ; return 1 ;;
|
}
|
||||||
*) printf " \033[31m %s \n\033[0m" "invalid input" ;;
|
|
||||||
|
valid_user() {
|
||||||
|
case ${1-} in
|
||||||
|
''|.*|*[!a-zA-Z0-9_.-]*) return 1 ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Never trust pre-existing filesystem entries when operating as root.
|
||||||
|
trusted_dir() {
|
||||||
|
[ -d "$1" ] && [ ! -L "$1" ] || fail "not a directory: $1"
|
||||||
|
[ "$(stat -c %u -- "$1")" = 0 ] || fail "directory must be owned by root: $1"
|
||||||
|
mode=$(stat -c %a -- "$1") || fail "cannot stat $1"
|
||||||
|
[ $((0$mode & 0022)) -eq 0 ] || fail "directory is writable by group/others: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
storage_ready() {
|
||||||
|
trusted_dir "$ROOT"
|
||||||
|
trusted_dir "$BIN"
|
||||||
|
trusted_dir "$DESCRIPTIONS"
|
||||||
|
trusted_dir "$APPROVED"
|
||||||
|
trusted_dir "$META"
|
||||||
|
trusted_dir "$REJECTED"
|
||||||
|
trusted_dir "$REVIEW"
|
||||||
|
[ $((0$(stat -c %a -- "$REVIEW") & 0077)) -eq 0 ] || fail 'review directory must be private (0700)'
|
||||||
|
[ -d "$PENDING" ] && [ ! -L "$PENDING" ] || fail "invalid pending directory"
|
||||||
|
[ "$(stat -c %u -- "$PENDING")" = 0 ] || fail "pending directory must be root-owned"
|
||||||
|
# The pending parent must be sticky when it is publicly writable.
|
||||||
|
mode=$(stat -c %a -- "$PENDING") || fail 'cannot stat pending directory'
|
||||||
|
[ $((0$mode & 01000)) -ne 0 ] || fail 'pending directory needs the sticky bit'
|
||||||
|
}
|
||||||
|
|
||||||
|
init_storage() {
|
||||||
|
require_root
|
||||||
|
for dir in "$ROOT" "$BIN" "$DESCRIPTIONS" "$APPROVED" "$META" "$REJECTED" "$REVIEW" "$PENDING"; do
|
||||||
|
[ ! -L "$dir" ] || fail "refusing symlink: $dir"
|
||||||
|
if [ -e "$dir" ]; then
|
||||||
|
[ -d "$dir" ] || fail "not a directory: $dir"
|
||||||
|
if [ "$dir" = "$PENDING" ]; then
|
||||||
|
[ "$(stat -c %u -- "$dir")" = 0 ] || fail "directory must be root-owned: $dir"
|
||||||
|
else
|
||||||
|
trusted_dir "$dir"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
mkdir -m 0755 -- "$dir" || fail "cannot create $dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
chmod 0700 "$REVIEW" || fail 'cannot secure review directory'
|
||||||
|
chmod 1733 "$PENDING" || fail 'cannot secure pending directory'
|
||||||
|
storage_ready
|
||||||
|
printf 'Launcher directories are ready. Existing scripts were not changed.\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
admin_lock() {
|
||||||
|
require_root
|
||||||
|
storage_ready
|
||||||
|
[ ! -L "$LOCK" ] || fail 'lock file must not be a symlink'
|
||||||
|
(umask 077; : >> "$LOCK") || fail 'cannot open lock file'
|
||||||
|
[ "$(stat -c %u -- "$LOCK")" = 0 ] || fail 'lock file must be root-owned'
|
||||||
|
exec 9>"$LOCK" || fail 'cannot lock launcher'
|
||||||
|
flock -n 9 || fail 'another launcher administration is in progress'
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt() {
|
||||||
|
while :; do
|
||||||
|
printf '%s [y/n]: ' "$1"
|
||||||
|
IFS= read -r answer || return 1
|
||||||
|
case $answer in
|
||||||
|
y|Y) return 0 ;;
|
||||||
|
n|N) return 1 ;;
|
||||||
|
*) printf 'Please answer y or n.\n' ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ask_line() {
|
||||||
|
printf '%s' "$1"
|
||||||
|
IFS= read -r INPUT || fail 'input cancelled'
|
||||||
|
}
|
||||||
|
|
||||||
help_message() {
|
help_message() {
|
||||||
cat <<-EOF
|
cat <<EOF
|
||||||
Usage: $PROGNAME [options] [command]
|
Usage: $PROGNAME [option] [command]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help
|
||||||
-v, --version Show version information
|
-v, --version Show version
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
list Show a list of approved user scripts
|
list List approved scripts
|
||||||
submit Start the submission flow for your own script
|
about <script> Show description and author (alias: description)
|
||||||
about <script> Show description and details of a specific script
|
submit Submit a new script from ~/bin
|
||||||
approve Enter the approval queue for pending scripts (requires root)
|
update <script> Submit an update to one of your approved scripts
|
||||||
revoke <script> Revoke a previously approved script (requires root)
|
pending List your submitted scripts awaiting review
|
||||||
run <script> Run an approved script
|
status <script> Show the approval status of a script
|
||||||
|
run <script> Run an approved script (passes along arguments)
|
||||||
|
approve Review and approve pending submissions (root)
|
||||||
|
revoke <script> Revoke an approved script (root)
|
||||||
|
migrate [script] Review and migrate legacy symlinks (root)
|
||||||
|
init Prepare secured /tilde directories (root)
|
||||||
|
|
||||||
Please add /tilde/bin to your PATH to use approved scripts without this wrapper.
|
Add /tilde/bin to PATH to run approved scripts directly.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_script_name() {
|
metadata_value() {
|
||||||
if [ -z "$1" ]; then
|
key=$1
|
||||||
error_exit "please enter a script name"
|
file=$2
|
||||||
fi
|
[ -f "$file" ] && [ ! -L "$file" ] || return 1
|
||||||
|
sed -n "s/^${key}=//p" "$file" | head -n 1
|
||||||
|
}
|
||||||
|
|
||||||
if command -v "$1"; then
|
approved_author() {
|
||||||
if [ "$(command -v "$1")" != "/home/$user/bin/$1" ]; then
|
metadata_value author "$META/$1"
|
||||||
error_exit "$1 already exists. rename your script and try again."
|
}
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -x "/tilde/bin/$1" ]; then
|
approved_digest() {
|
||||||
error_exit "$1 is already taken. rename your script and try again."
|
metadata_value sha256 "$META/$1"
|
||||||
fi
|
}
|
||||||
|
|
||||||
case $1 in
|
# The user's own writable directory is never trusted as an approval record.
|
||||||
about|description|list|ls|submit|about|help|apropos|submit|approve)
|
user_pending_dir() {
|
||||||
error_exit "$1 is a subcommand of tilde. rename your script and try again."
|
owner=$1
|
||||||
;;
|
valid_user "$owner" || fail "invalid username: $owner"
|
||||||
*)
|
dir=$PENDING/$owner
|
||||||
return
|
[ -d "$dir" ] && [ ! -L "$dir" ] || fail "missing user submissions directory: $dir"
|
||||||
;;
|
[ "$(stat -c %U -- "$dir")" = "$owner" ] || fail "submission directory ownership mismatch: $dir"
|
||||||
|
printf '%s\n' "$dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_user_dir() {
|
||||||
|
owner=$1
|
||||||
|
valid_user "$owner" || fail 'unsupported account name'
|
||||||
|
[ -d "$PENDING" ] && [ ! -L "$PENDING" ] || fail 'launcher has not been initialized by an admin'
|
||||||
|
dir=$PENDING/$owner
|
||||||
|
if [ ! -e "$dir" ] && [ ! -L "$dir" ]; then
|
||||||
|
mkdir -m 0700 -- "$dir" || fail 'cannot create your submissions directory (ask an admin if your name is taken)'
|
||||||
|
fi
|
||||||
|
user_pending_dir "$owner" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
send_notice() {
|
||||||
|
recipient=$1
|
||||||
|
subject=$2
|
||||||
|
body=$3
|
||||||
|
valid_user "$recipient" || { warn 'skipping mail to invalid account'; return 1; }
|
||||||
|
if ! {
|
||||||
|
printf 'To: %s\nSubject: %s\n\n%s\n' "$recipient" "$subject" "$body"
|
||||||
|
} | /usr/sbin/sendmail -t; then
|
||||||
|
warn "email to $recipient failed; the filesystem changes were retained"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
submit_script() {
|
||||||
|
operation=$1
|
||||||
|
[ "$(id -u)" -ne 0 ] || fail 'submit as your own user, not root'
|
||||||
|
storage_ready
|
||||||
|
current_user=$(id -un) || fail 'cannot identify user'
|
||||||
|
valid_user "$current_user" || fail 'unsupported account name'
|
||||||
|
if [ "$operation" = update ]; then
|
||||||
|
name=${2-}
|
||||||
|
check_name "$name"
|
||||||
|
[ "$(approved_author "$name" 2>/dev/null || :)" = "$current_user" ] || fail 'only the approved author can submit an update'
|
||||||
|
[ -L "$BIN/$name" ] || fail 'script is not currently approved'
|
||||||
|
else
|
||||||
|
printf 'Hello, %s! Ready to submit your script?\n' "$current_user"
|
||||||
|
printf 'Place an executable in ~/bin that supports --help.\n'
|
||||||
|
prompt 'Continue?' || return 0
|
||||||
|
ask_line 'Script name: '
|
||||||
|
name=$INPUT
|
||||||
|
check_name "$name"
|
||||||
|
[ ! -e "$BIN/$name" ] && [ ! -L "$BIN/$name" ] || fail 'name already approved; use update if it is yours'
|
||||||
|
[ ! -f "$META/$name" ] || fail 'name already has an approval record'
|
||||||
|
fi
|
||||||
|
source=$HOME/bin/$name
|
||||||
|
if [ "$operation" = submit ]; then
|
||||||
|
located=$(command -v "$name" 2>/dev/null || :)
|
||||||
|
if [ -n "$located" ] && [ "$(readlink -f -- "$located" 2>/dev/null || :)" != "$(readlink -f -- "$source" 2>/dev/null || :)" ]; then
|
||||||
|
fail "$name is already an existing command; use another name"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -f "$source" ] && [ -x "$source" ] || fail "executable not found: $source"
|
||||||
|
if [ "$(wc -c < "$source")" -gt "$MAX_SCRIPT" ]; then
|
||||||
|
fail 'script is too large (limit: 50 MiB)'
|
||||||
|
fi
|
||||||
|
if [ "$operation" = update ]; then
|
||||||
|
printf 'Updating %s (the currently approved version keeps working).\n' "$name"
|
||||||
|
fi
|
||||||
|
ask_line 'Description: '
|
||||||
|
description=$INPUT
|
||||||
|
[ -n "$description" ] || fail 'description is required'
|
||||||
|
[ "${#description}" -le "$MAX_DESCRIPTION" ] || fail 'description is too long'
|
||||||
|
prompt 'Send this version to the admins for review?' || return 0
|
||||||
|
ensure_user_dir "$current_user"
|
||||||
|
parent=$PENDING/$current_user
|
||||||
|
target=$parent/$name
|
||||||
|
if [ -e "$target" ] || [ -L "$target" ]; then
|
||||||
|
[ -d "$target" ] && [ ! -L "$target" ] || fail 'unsafe existing submission directory'
|
||||||
|
prompt 'Replace your previous pending submission?' || return 0
|
||||||
|
fi
|
||||||
|
stage=$(mktemp -d "$parent/.submit.XXXXXXXX") || fail 'cannot stage submission'
|
||||||
|
if ! cp -- "$source" "$stage/$name" ||
|
||||||
|
! chmod 0700 "$stage/$name" ||
|
||||||
|
! printf '%s\n' "$description" > "$stage/description.txt"; then
|
||||||
|
rm -rf -- "$stage"
|
||||||
|
fail 'cannot stage submission'
|
||||||
|
fi
|
||||||
|
if [ -e "$target" ]; then
|
||||||
|
rm -rf -- "$target" || fail 'cannot replace your pending submission'
|
||||||
|
fi
|
||||||
|
if ! mv -- "$stage" "$target"; then
|
||||||
|
rm -rf -- "$stage"
|
||||||
|
fail 'cannot save submission'
|
||||||
|
fi
|
||||||
|
send_notice root 'tilde script submission' "Submission from $current_user: $name. Review with sudo tilde approve." || :
|
||||||
|
printf 'Submitted %s for review.\n' "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Capture potentially hostile user-controlled files *as that user*, never as root.
|
||||||
|
# Review and publication use only the resulting root-owned immutable snapshot.
|
||||||
|
capture() {
|
||||||
|
author=$1
|
||||||
|
path=$2
|
||||||
|
dest=$3
|
||||||
|
maximum=$4
|
||||||
|
[ -f "$path" ] || return 1
|
||||||
|
timeout 60s runuser -u "$author" -- head -c "$((maximum + 1))" -- "$path" > "$dest" || return 1
|
||||||
|
[ "$(wc -c < "$dest")" -le "$maximum" ] || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_submission() {
|
||||||
|
author=$1
|
||||||
|
name=$2
|
||||||
|
folder=$(user_pending_dir "$author") || return 1
|
||||||
|
submission=$folder/$name
|
||||||
|
[ -d "$submission" ] && [ ! -L "$submission" ] || return 1
|
||||||
|
WORK=$(mktemp -d "$REVIEW/.review.XXXXXXXX") || fail 'cannot stage review'
|
||||||
|
capture "$author" "$submission/$name" "$WORK/script" "$MAX_SCRIPT" || return 1
|
||||||
|
capture "$author" "$submission/description.txt" "$WORK/description" "$MAX_DESCRIPTION" || return 1
|
||||||
|
[ -s "$WORK/script" ] && [ -s "$WORK/description" ] || return 1
|
||||||
|
# A submitted binary or script must be runnable by the submitter.
|
||||||
|
runuser -u "$author" -- test -x "$submission/$name" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_event() {
|
||||||
|
logger -t tilde-launcher -- "$1" 2>/dev/null || :
|
||||||
|
}
|
||||||
|
|
||||||
|
write_metadata() {
|
||||||
|
name=$1
|
||||||
|
author=$2
|
||||||
|
digest=$3
|
||||||
|
temp=$(mktemp "$META/.record.XXXXXXXX") || return 1
|
||||||
|
if ! printf 'author=%s\nsha256=%s\napproved_at=%s\nstate=approved\n' \
|
||||||
|
"$author" "$digest" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$temp" ||
|
||||||
|
! chmod 0644 "$temp" || ! mv -f -- "$temp" "$META/$name"; then
|
||||||
|
rm -f -- "$temp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
publish() {
|
||||||
|
author=$1
|
||||||
|
name=$2
|
||||||
|
digest=$(sha256sum "$WORK/script" | cut -d ' ' -f 1) || fail 'cannot hash script'
|
||||||
|
folder=$APPROVED/$name
|
||||||
|
[ ! -L "$folder" ] || fail 'approved directory must not be a symlink'
|
||||||
|
if [ ! -e "$folder" ]; then
|
||||||
|
mkdir -m 0755 -- "$folder" || fail 'cannot create approved directory'
|
||||||
|
fi
|
||||||
|
trusted_dir "$folder"
|
||||||
|
version=$folder/$digest
|
||||||
|
[ ! -L "$version" ] || fail 'approved version must not be a symlink'
|
||||||
|
if [ ! -e "$version" ]; then
|
||||||
|
temp=$(mktemp "$folder/.version.XXXXXXXX") || fail 'cannot stage approved version'
|
||||||
|
if ! cp -- "$WORK/script" "$temp" || ! chmod 0755 "$temp" || ! mv -- "$temp" "$version"; then
|
||||||
|
rm -f -- "$temp"
|
||||||
|
fail 'cannot publish approved version'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
[ -f "$version" ] &&
|
||||||
|
[ "$(stat -c %u -- "$version")" = 0 ] &&
|
||||||
|
[ $((0$(stat -c %a -- "$version") & 0022)) -eq 0 ] &&
|
||||||
|
[ "$(sha256sum "$version" | cut -d ' ' -f 1)" = "$digest" ] || fail 'existing approved version has changed'
|
||||||
|
fi
|
||||||
|
dtmp=$(mktemp "$DESCRIPTIONS/.description.XXXXXXXX") || fail 'cannot stage description'
|
||||||
|
if ! cp -- "$WORK/description" "$dtmp" || ! chmod 0644 "$dtmp" || ! mv -f -- "$dtmp" "$DESCRIPTIONS/$name"; then
|
||||||
|
rm -f -- "$dtmp"
|
||||||
|
fail 'cannot publish description'
|
||||||
|
fi
|
||||||
|
old_target=$(readlink -f -- "$BIN/$name" 2>/dev/null || :)
|
||||||
|
chmod 0755 -- "$version" || fail 'cannot activate approved version'
|
||||||
|
write_metadata "$name" "$author" "$digest" || fail 'cannot save approval metadata'
|
||||||
|
linktmp=$BIN/.link.$name.$$
|
||||||
|
[ ! -e "$linktmp" ] && [ ! -L "$linktmp" ] || fail 'temporary link collision'
|
||||||
|
ln -s -- "$version" "$linktmp" || fail 'cannot create approved link'
|
||||||
|
if ! mv -Tf -- "$linktmp" "$BIN/$name"; then
|
||||||
|
rm -f -- "$linktmp"
|
||||||
|
fail 'cannot activate approved version'
|
||||||
|
fi
|
||||||
|
# Previously approved revisions are retained but no longer user-executable.
|
||||||
|
case $old_target in
|
||||||
|
"$APPROVED/$name/"*)
|
||||||
|
if [ "$old_target" != "$version" ]; then
|
||||||
|
chmod 0700 -- "$old_target" || fail 'could not restrict retired version'
|
||||||
|
fi ;;
|
||||||
esac
|
esac
|
||||||
|
log_event "approved $name by $author sha256=$digest"
|
||||||
|
send_notice "$author" 'tilde script approved' "$name is approved and available at $BIN/$name" || :
|
||||||
|
printf 'Approved %s by %s.\n' "$name" "$author"
|
||||||
}
|
}
|
||||||
|
|
||||||
submission_checklist() {
|
review_snapshot() {
|
||||||
cat <<- _EOF_
|
author=$1
|
||||||
Requirements for submitting a user script or program:
|
name=$2
|
||||||
|
printf '\nScript: %s\nAuthor: %s\n' "$name" "$author"
|
||||||
- Placed in your ~/bin
|
printf 'SHA-256: %s\n' "$(sha256sum "$WORK/script" | cut -d ' ' -f 1)"
|
||||||
- Executable
|
printf 'Description:\n'
|
||||||
- Responds to help or --help
|
LC_ALL=C cat -v "$WORK/description"
|
||||||
- No name collisions with existing scripts or $PROGNAME subcommands
|
printf '\nSource preview (up to 80 lines / 8 KiB, control characters escaped):\n'
|
||||||
|
head -c 8192 "$WORK/script" | head -n 80 | LC_ALL=C cat -v
|
||||||
_EOF_
|
while :; do
|
||||||
}
|
printf '\n[a]pprove, [r]eject, [v]iew full source, [s]kip? '
|
||||||
|
IFS= read -r answer || return 1
|
||||||
mail_body() {
|
case $answer in
|
||||||
cat <<- _EOF_
|
a|A) return 0 ;;
|
||||||
Subject: tilde script submission from ${user}
|
r|R) return 2 ;;
|
||||||
From: ${user}@${hostname}
|
v|V) LC_ALL=C cat -v "$WORK/script" ;;
|
||||||
To: root@${hostname}
|
s|S) return 1 ;;
|
||||||
|
*) printf 'Choose a, r, v or s.\n' ;;
|
||||||
tilde script submission from ${user}
|
esac
|
||||||
|
|
||||||
script name: $1
|
|
||||||
|
|
||||||
description:
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
$2
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
You'll find the script and description in: /tilde/pending-submissions/$user/$1
|
|
||||||
|
|
||||||
Run this to see the approval queue:
|
|
||||||
sudo tilde approve
|
|
||||||
_EOF_
|
|
||||||
}
|
|
||||||
|
|
||||||
# Trap signals
|
|
||||||
trap "signal_exit TERM" TERM HUP
|
|
||||||
trap "signal_exit INT" INT
|
|
||||||
|
|
||||||
# Parse command-line
|
|
||||||
case $1 in
|
|
||||||
-h | --help)
|
|
||||||
help_message
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
|
|
||||||
-v | --version)
|
|
||||||
printf "%s\n" "$VERSION"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
|
|
||||||
list)
|
|
||||||
# List approved scripts
|
|
||||||
printf "Available scripts:\n\n"
|
|
||||||
for scr in /tilde/bin/*; do
|
|
||||||
if [ -f "$scr" ]; then
|
|
||||||
script_name=$(basename "$scr")
|
|
||||||
target=$(readlink -f "$scr")
|
|
||||||
printf "%s by %s\n" "$script_name" "$(stat_func "$target")"
|
|
||||||
cat "/tilde/descriptions/$script_name"
|
|
||||||
printf "\n"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
;;
|
}
|
||||||
|
|
||||||
about | description)
|
submission_fingerprint() {
|
||||||
# Show details about a specific script
|
sha256sum "$1" "$2" | cut -d ' ' -f 1 | sha256sum | cut -d ' ' -f 1
|
||||||
if [ -z "$2" ]; then
|
}
|
||||||
printf "Please provide a script name.\n"
|
|
||||||
exit 1
|
reject_snapshot() {
|
||||||
|
author=$1
|
||||||
|
name=$2
|
||||||
|
ask_line 'Reason for rejection: '
|
||||||
|
reason=$INPUT
|
||||||
|
[ -n "$reason" ] || { warn 'rejection cancelled: reason required'; return 0; }
|
||||||
|
dir=$REJECTED/$author
|
||||||
|
if [ ! -e "$dir" ]; then
|
||||||
|
mkdir -m 0755 -- "$dir" || fail 'cannot create rejection records'
|
||||||
fi
|
fi
|
||||||
|
trusted_dir "$dir"
|
||||||
|
fingerprint=$(submission_fingerprint "$WORK/script" "$WORK/description")
|
||||||
|
temp=$(mktemp "$dir/.rejection.XXXXXXXX") || fail 'cannot stage rejection record'
|
||||||
|
if ! printf 'fingerprint=%s\nrejected_at=%s\n' \
|
||||||
|
"$fingerprint" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$temp" ||
|
||||||
|
! chmod 0644 "$temp" || ! mv -f -- "$temp" "$dir/$name"; then
|
||||||
|
rm -f -- "$temp"
|
||||||
|
fail 'cannot save rejection record'
|
||||||
|
fi
|
||||||
|
log_event "rejected $name by $author"
|
||||||
|
send_notice "$author" 'tilde script rejected' "$name was not approved. Reason: $reason. You may submit a revised version." || :
|
||||||
|
printf 'Rejected %s by %s.\n' "$name" "$author"
|
||||||
|
}
|
||||||
|
|
||||||
if [ -f "/tilde/descriptions/$2" ]; then
|
approve_one() {
|
||||||
cat "/tilde/descriptions/$2"
|
author=$1
|
||||||
|
name=$2
|
||||||
|
check_name "$name"
|
||||||
|
if ! capture_submission "$author" "$name"; then
|
||||||
|
warn "could not safely read $author/$name; skipping"
|
||||||
|
cleanup
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
digest=$(sha256sum "$WORK/script" | cut -d ' ' -f 1)
|
||||||
|
existing_author=$(approved_author "$name" 2>/dev/null || :)
|
||||||
|
existing_digest=$(approved_digest "$name" 2>/dev/null || :)
|
||||||
|
if [ -n "$existing_author" ] && [ "$existing_author" != "$author" ]; then
|
||||||
|
warn "name $name belongs to $existing_author; skipping $author"
|
||||||
|
cleanup
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$digest" = "$existing_digest" ] && [ -L "$BIN/$name" ]; then
|
||||||
|
cleanup
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
rejected=$(metadata_value fingerprint "$REJECTED/$author/$name" 2>/dev/null || :)
|
||||||
|
fingerprint=$(submission_fingerprint "$WORK/script" "$WORK/description")
|
||||||
|
if [ -n "$rejected" ] && [ "$rejected" = "$fingerprint" ]; then
|
||||||
|
cleanup
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -e "$BIN/$name" ] || [ -L "$BIN/$name" ]; then
|
||||||
|
[ -n "$existing_author" ] || {
|
||||||
|
warn "$name is an untracked legacy script; use tilde migrate first"
|
||||||
|
cleanup
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
[ -L "$BIN/$name" ] || fail "refusing to replace a non-symlink: $BIN/$name"
|
||||||
|
fi
|
||||||
|
if review_snapshot "$author" "$name"; then
|
||||||
|
publish "$author" "$name"
|
||||||
else
|
else
|
||||||
printf "%s not found. Use '%s list' to see available user scripts.\n" "$2" "$PROGNAME"
|
action=$?
|
||||||
exit 1
|
[ "$action" -ne 2 ] || reject_snapshot "$author" "$name"
|
||||||
fi
|
fi
|
||||||
;;
|
cleanup
|
||||||
|
}
|
||||||
|
|
||||||
submit)
|
approve_queue() {
|
||||||
# Submit a user script
|
admin_lock
|
||||||
printf "Hello, %s! Ready to submit your script?\n" "$user"
|
printf 'Pending submissions:\n'
|
||||||
submission_checklist
|
for ownerdir in "$PENDING"/*; do
|
||||||
prompt_confirm "Are you ready to continue?" || exit
|
[ -d "$ownerdir" ] && [ ! -L "$ownerdir" ] || continue
|
||||||
|
author=${ownerdir##*/}
|
||||||
printf "Enter the name of your script: "
|
if ! valid_user "$author" || [ "$(stat -c %U -- "$ownerdir")" != "$author" ]; then
|
||||||
read -r script_name
|
warn "skipping unsafe submission directory: $ownerdir"
|
||||||
verify_script_name "$script_name"
|
continue
|
||||||
|
|
||||||
if [ ! -x "$HOME/bin/$script_name" ]; then
|
|
||||||
error_exit "$script_name not found in ~/bin"
|
|
||||||
fi
|
fi
|
||||||
|
for item in "$ownerdir"/*; do
|
||||||
printf "Enter a description for your script: \n"
|
[ -d "$item" ] && [ ! -L "$item" ] || continue
|
||||||
read -r description
|
name=${item##*/}
|
||||||
printf "\nYour script and description will be sent to the admins for approval.\n"
|
valid_name "$name" || { warn "skipping invalid name $name"; continue; }
|
||||||
prompt_confirm "Ready to submit?" || exit
|
case $name in
|
||||||
|
tilde|about|description|list|ls|submit|update|approve|revoke|run|help|apropos|pending|status|migrate|init|version)
|
||||||
# Submit now
|
continue ;;
|
||||||
mkdir -p "/tilde/pending-submissions/$user/$script_name"
|
esac
|
||||||
ln -s "$HOME/bin/$script_name" "/tilde/pending-submissions/$user/$script_name/$script_name"
|
approve_one "$author" "$name"
|
||||||
printf "%s\n" "$description" > "/tilde/pending-submissions/$user/$script_name/description.txt"
|
|
||||||
mail_body "$script_name" "$description" | /usr/sbin/sendmail root
|
|
||||||
printf "Script submitted. Thank you! :)\n"
|
|
||||||
;;
|
|
||||||
|
|
||||||
approve)
|
|
||||||
# Approve pending scripts (requires root)
|
|
||||||
isroot || \
|
|
||||||
error_exit "Re-run this as root to access the approval queue."
|
|
||||||
printf "Welcome to the approval queue\n\n"
|
|
||||||
|
|
||||||
for user_submission in /tilde/pending-submissions/*; do
|
|
||||||
user=$(basename "$user_submission")
|
|
||||||
for script in "$user_submission"/*; do
|
|
||||||
script_name=$(basename "$script")
|
|
||||||
[ -f "$script/approved" ] && continue
|
|
||||||
|
|
||||||
printf "Script name: %s\n" "$script_name"
|
|
||||||
cat "$script/description.txt"
|
|
||||||
prompt_confirm "Approve?" || continue
|
|
||||||
|
|
||||||
ln -s "$(readlink -f "$script/$script_name")" "/tilde/bin/$script_name"
|
|
||||||
cp "$script/description.txt" "/tilde/descriptions/$script_name"
|
|
||||||
touch "$script/approved"
|
|
||||||
chmod 664 "/tilde/descriptions/$script_name"
|
|
||||||
printf "Your submission of %s has been approved and is now available at /tilde/bin/%s\n" "$script_name" "$script_name" \
|
|
||||||
| sendmail "$user"
|
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
printf "~~Done for now~~\n"
|
printf 'Approval queue complete.\n'
|
||||||
;;
|
}
|
||||||
|
|
||||||
revoke)
|
migrate_one() {
|
||||||
# Revoke an approved script (requires root)
|
name=$1
|
||||||
isroot || \
|
check_name "$name"
|
||||||
error_exit "Re-run this as root to access the revoke menu."
|
link=$BIN/$name
|
||||||
[ -f "/tilde/bin/$2" ] || \
|
[ -L "$link" ] || { warn "$name is not a legacy symlink; skipping"; return 0; }
|
||||||
error_exit "$2 isn't an approved script"
|
if [ -n "$(approved_author "$name" 2>/dev/null || :)" ]; then
|
||||||
|
warn "$name has already been migrated"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
source=$(readlink -f -- "$link") || fail "broken legacy symlink: $name"
|
||||||
|
[ -f "$source" ] || fail "legacy target not a file: $name"
|
||||||
|
author=$(stat -c %U -- "$source") || fail 'cannot inspect author'
|
||||||
|
valid_user "$author" || fail 'cannot determine legacy author'
|
||||||
|
home=$(getent passwd "$author" | cut -d: -f6)
|
||||||
|
case $source in
|
||||||
|
"$home"/bin/*) : ;;
|
||||||
|
*) warn "legacy target is outside $author's ~/bin: $source"; return 0 ;;
|
||||||
|
esac
|
||||||
|
[ -f "$DESCRIPTIONS/$name" ] && [ ! -L "$DESCRIPTIONS/$name" ] || {
|
||||||
|
warn "missing or unsafe legacy description for $name"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
[ "$(stat -c %u -- "$DESCRIPTIONS/$name")" = 0 ] || {
|
||||||
|
warn "legacy description $name is not root-owned"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
WORK=$(mktemp -d "$REVIEW/.review.XXXXXXXX") || fail 'cannot stage migration'
|
||||||
|
if ! capture "$author" "$source" "$WORK/script" "$MAX_SCRIPT" ||
|
||||||
|
! cp -- "$DESCRIPTIONS/$name" "$WORK/description"; then
|
||||||
|
warn "could not capture legacy $name"
|
||||||
|
cleanup
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[ "$(wc -c < "$WORK/description")" -le "$MAX_DESCRIPTION" ] || fail 'legacy description is too large'
|
||||||
|
printf '\nLegacy link: %s -> %s\n' "$name" "$source"
|
||||||
|
printf 'The reviewed copy will be immutable. Scripts requiring adjacent files may need changes.\n'
|
||||||
|
if review_snapshot "$author" "$name"; then
|
||||||
|
publish "$author" "$name"
|
||||||
|
fi
|
||||||
|
cleanup
|
||||||
|
}
|
||||||
|
|
||||||
prompt_confirm "revoke $2?"
|
migrate_legacy() {
|
||||||
printf "please provide a reason: "
|
admin_lock
|
||||||
read -r reason
|
if [ -n "${1-}" ]; then
|
||||||
|
migrate_one "$1"
|
||||||
|
else
|
||||||
|
for item in "$BIN"/*; do
|
||||||
|
[ -L "$item" ] || continue
|
||||||
|
migrate_one "${item##*/}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
original_script=$(readlink -f "/tilde/bin/$2")
|
revoke_script() {
|
||||||
author=$(stat_func "$original_script")
|
admin_lock
|
||||||
rm "/tilde/bin/$2"
|
name=${1-}
|
||||||
rm "/tilde/descriptions/$2"
|
check_name "$name"
|
||||||
rm -rf "/tilde/pending-submissions/$author/$2"
|
link=$BIN/$name
|
||||||
|
[ -L "$link" ] || fail "$name is not an approved symlink"
|
||||||
|
author=$(approved_author "$name" 2>/dev/null || :)
|
||||||
|
if [ -z "$author" ]; then
|
||||||
|
source=$(readlink -f -- "$link") || fail 'cannot find legacy script author'
|
||||||
|
author=$(stat -c %U -- "$source") || fail 'cannot find script owner'
|
||||||
|
fi
|
||||||
|
valid_user "$author" || fail 'invalid script owner'
|
||||||
|
prompt "Revoke $name by $author?" || { printf 'Cancelled.\n'; return 0; }
|
||||||
|
ask_line 'Reason: '
|
||||||
|
reason=$INPUT
|
||||||
|
[ -n "$reason" ] || fail 'a reason is required'
|
||||||
|
old_target=$(readlink -f -- "$link" 2>/dev/null || :)
|
||||||
|
# Retire the previously published file before removing its public link.
|
||||||
|
case $old_target in
|
||||||
|
"$APPROVED/$name/"*)
|
||||||
|
chmod 0700 -- "$old_target" || fail 'could not restrict revoked executable' ;;
|
||||||
|
esac
|
||||||
|
# Only unlink exact, validated entries. Do not recurse into user submissions.
|
||||||
|
rm -- "$link" || fail 'could not revoke executable'
|
||||||
|
if [ -e "$DESCRIPTIONS/$name" ] || [ -L "$DESCRIPTIONS/$name" ]; then
|
||||||
|
rm -- "$DESCRIPTIONS/$name" || warn 'could not remove description'
|
||||||
|
fi
|
||||||
|
if [ -e "$META/$name" ] || [ -L "$META/$name" ]; then
|
||||||
|
rm -- "$META/$name" || warn 'could not remove metadata'
|
||||||
|
fi
|
||||||
|
log_event "revoked $name by $author"
|
||||||
|
send_notice "$author" 'tilde script revoked' "$name was revoked. Reason: $reason. You may submit a revised version." || :
|
||||||
|
printf '%s revoked. Older reviewed copies were retained in %s.\n' "$name" "$APPROVED/$name"
|
||||||
|
}
|
||||||
|
|
||||||
printf "your script %s has been returned because: %s\nfeel free to resubmit\n" "$2" "$reason" \
|
list_scripts() {
|
||||||
| sendmail "$author"
|
printf 'Available scripts:\n\n'
|
||||||
|
for item in "$BIN"/*; do
|
||||||
|
[ -x "$item" ] && [ -f "$item" ] || continue
|
||||||
|
name=${item##*/}
|
||||||
|
author=$(approved_author "$name" 2>/dev/null || :)
|
||||||
|
if [ -z "$author" ]; then
|
||||||
|
author=$(stat -c %U -- "$item" 2>/dev/null || printf unknown)
|
||||||
|
fi
|
||||||
|
printf '%s by %s\n' "$name" "$author"
|
||||||
|
if [ -f "$DESCRIPTIONS/$name" ]; then
|
||||||
|
cat -- "$DESCRIPTIONS/$name"
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
printf "%s revoked and returned to author" "$2"
|
about_script() {
|
||||||
;;
|
name=${1-}
|
||||||
|
check_name "$name"
|
||||||
|
[ -f "$DESCRIPTIONS/$name" ] || fail "$name not found; use '$PROGNAME list'"
|
||||||
|
cat -- "$DESCRIPTIONS/$name"
|
||||||
|
author=$(approved_author "$name" 2>/dev/null || :)
|
||||||
|
[ -z "$author" ] || printf '\nAuthor: %s\n' "$author"
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_scripts() {
|
||||||
|
current_user=$(id -un)
|
||||||
|
valid_user "$current_user" || fail 'unsupported account name'
|
||||||
|
[ -d "$PENDING/$current_user" ] || { printf 'No submissions.\n'; return 0; }
|
||||||
|
folder=$(user_pending_dir "$current_user") || return 1
|
||||||
|
found=0
|
||||||
|
for item in "$folder"/*; do
|
||||||
|
[ -d "$item" ] && [ ! -L "$item" ] || continue
|
||||||
|
name=${item##*/}
|
||||||
|
valid_name "$name" || continue
|
||||||
|
current=$(approved_digest "$name" 2>/dev/null || :)
|
||||||
|
submitted=$(sha256sum "$item/$name" 2>/dev/null | cut -d ' ' -f 1)
|
||||||
|
if [ -n "$current" ] && [ "$current" = "$submitted" ] && [ -L "$BIN/$name" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
rejected=$(metadata_value fingerprint "$REJECTED/$current_user/$name" 2>/dev/null || :)
|
||||||
|
fingerprint=$(submission_fingerprint "$item/$name" "$item/description.txt" 2>/dev/null || :)
|
||||||
|
if [ -n "$fingerprint" ] && [ "$fingerprint" = "$rejected" ]; then
|
||||||
|
printf '%s (rejected; revise with submit)\n' "$name"
|
||||||
|
else
|
||||||
|
printf '%s (awaiting review)\n' "$name"
|
||||||
|
fi
|
||||||
|
found=1
|
||||||
|
done
|
||||||
|
[ "$found" -eq 1 ] || printf 'No pending submissions.\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
status_script() {
|
||||||
|
name=${1-}
|
||||||
|
check_name "$name"
|
||||||
|
author=$(approved_author "$name" 2>/dev/null || :)
|
||||||
|
if [ -n "$author" ] && [ -L "$BIN/$name" ]; then
|
||||||
|
printf '%s: approved (author: %s)\n' "$name" "$author"
|
||||||
|
elif [ -L "$BIN/$name" ]; then
|
||||||
|
printf '%s: approved (legacy symlink; needs migration)\n' "$name"
|
||||||
|
else
|
||||||
|
printf '%s: not approved\n' "$name"
|
||||||
|
fi
|
||||||
|
current_user=$(id -un)
|
||||||
|
if [ -d "$PENDING/$current_user/$name" ]; then
|
||||||
|
submission=$PENDING/$current_user/$name
|
||||||
|
fingerprint=$(submission_fingerprint "$submission/$name" "$submission/description.txt" 2>/dev/null || :)
|
||||||
|
rejected=$(metadata_value fingerprint "$REJECTED/$current_user/$name" 2>/dev/null || :)
|
||||||
|
if [ -n "$fingerprint" ] && [ "$fingerprint" = "$rejected" ]; then
|
||||||
|
printf 'Latest submission: rejected (submit a revision).\n'
|
||||||
|
elif [ -n "$author" ] && [ "$author" = "$current_user" ] &&
|
||||||
|
[ "$(sha256sum "$submission/$name" 2>/dev/null | cut -d ' ' -f 1)" = "$(approved_digest "$name" 2>/dev/null || :)" ]; then
|
||||||
|
printf 'Submitted version matches the current approval.\n'
|
||||||
|
else
|
||||||
|
printf 'A submission is awaiting review for %s.\n' "$current_user"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case ${1-} in
|
||||||
|
-h|--help|'') help_message ;;
|
||||||
|
-v|--version) printf '%s\n' "$VERSION" ;;
|
||||||
|
init) init_storage ;;
|
||||||
|
list|ls) list_scripts ;;
|
||||||
|
about|description) about_script "${2-}" ;;
|
||||||
|
submit) submit_script submit ;;
|
||||||
|
update) submit_script update "${2-}" ;;
|
||||||
|
pending) pending_scripts ;;
|
||||||
|
status) status_script "${2-}" ;;
|
||||||
|
approve) approve_queue ;;
|
||||||
|
migrate) migrate_legacy "${2-}" ;;
|
||||||
|
revoke) revoke_script "${2-}" ;;
|
||||||
run)
|
run)
|
||||||
# Run an approved script
|
name=${2-}
|
||||||
if [ -z "$2" ]; then
|
check_name "$name"
|
||||||
printf "Please provide a script name.\n"
|
[ -f "$BIN/$name" ] && [ -x "$BIN/$name" ] || fail "$name not found or not approved"
|
||||||
help_message
|
shift 2
|
||||||
exit 1
|
exec "$BIN/$name" "$@"
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -x "/tilde/bin/$2" ]; then
|
|
||||||
prog="/tilde/bin/$2"
|
|
||||||
shift 2 # Shift twice to remove both 'run' and 'tcoin'
|
|
||||||
exec "$prog" "$@"
|
|
||||||
else
|
|
||||||
printf "%s not found or not approved yet. Use '%s list' to see available user scripts.\n" "$2" "$PROGNAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
"")
|
|
||||||
help_message
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
printf "Invalid command: %s\n" "$1"
|
|
||||||
help_message
|
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
|
*) fail "invalid command: $1 (use --help)" ;;
|
||||||
esac
|
esac
|
||||||
@@ -1,76 +1,91 @@
|
|||||||
.TH TILDE 1 "August 2023" "Tilde User Scripts" "User Commands"
|
.TH TILDE 1 "October 2026" "Tilde User Scripts" "User Commands"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
tilde \- manage user-submitted scripts and apps
|
tilde \- submit, review and run approved community scripts
|
||||||
|
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B tilde
|
.B tilde
|
||||||
[\fIOPTION\fR] [\fICOMMAND\fR]
|
[\fIOPTION\fR] [\fICOMMAND\fR]
|
||||||
|
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
The \fBtilde\fR command is a script management tool for managing user-submitted scripts and apps on the system. It allows users to submit their scripts for approval by administrators, who can then approve or revoke them for system-wide usage.
|
The tilde launcher provides a submission and administrator approval process
|
||||||
|
for community scripts. Approved executables are accessible from /tilde/bin.
|
||||||
By default, approved scripts are stored in the directory /tilde/bin, and their descriptions are kept in /tilde/descriptions.
|
New approvals use reviewed, root-owned copies rather than mutable links to
|
||||||
|
the submitting user's home directory.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.TP
|
.TP
|
||||||
.B \-h, \-\-help
|
.B -h, --help
|
||||||
Show usage information and exit.
|
Show usage and exit.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-v, \-\-version
|
.B -v, --version
|
||||||
Show version information and exit.
|
Show the version and exit.
|
||||||
|
|
||||||
.SH COMMANDS
|
.SH COMMANDS
|
||||||
The following commands are supported:
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B list
|
.B list
|
||||||
Show a list of approved user scripts along with their descriptions.
|
List approved scripts with author and description.
|
||||||
|
|
||||||
.TP
|
|
||||||
.B submit
|
|
||||||
Start the submission flow for your own script. The script should be placed in the ~/bin directory, be executable, and respond to help or --help.
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B about \fIscript\fR
|
.B about \fIscript\fR
|
||||||
Show the description and details of a specific script.
|
Show its description and author. Alias: description.
|
||||||
|
.TP
|
||||||
|
.B submit
|
||||||
|
Submit an executable in ~/bin for approval.
|
||||||
|
.TP
|
||||||
|
.B update \fIscript\fR
|
||||||
|
Submit a new revision of a script that you own. The approved version remains
|
||||||
|
available until the revision is accepted.
|
||||||
|
.TP
|
||||||
|
.B pending
|
||||||
|
List your submissions that are not yet approved.
|
||||||
|
.TP
|
||||||
|
.B status \fIscript\fR
|
||||||
|
Show the script's approval and submission status.
|
||||||
|
.TP
|
||||||
|
.B run \fIscript\fR [\fIarguments\fR]
|
||||||
|
Run an approved script, forwarding its arguments.
|
||||||
.TP
|
.TP
|
||||||
.B approve
|
.B approve
|
||||||
Enter the approval queue for pending scripts. This command is only accessible to the root user. Administrators can review pending submissions and approve them for system-wide usage.
|
Interactively approve, reject or skip pending submissions (root only).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B revoke \fIscript\fR
|
.B revoke \fIscript\fR
|
||||||
Revoke a previously approved script. This command is only accessible to the root user.
|
Revoke a previously approved script after confirmation (root only).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B run \fIscript\fR
|
.B migrate [\fIscript\fR]
|
||||||
Run an approved script by specifying its name. The script must be available in the /tilde/bin directory.
|
Interactively review and migrate legacy user-owned symlinks (root only).
|
||||||
|
When no name is given, review all legacy links.
|
||||||
.SH PATH CONFIGURATION
|
.TP
|
||||||
To use approved scripts without invoking the \fBtilde\fR command, you can add /tilde/bin to your PATH environment variable. By doing so, the scripts in /tilde/bin will be accessible directly from the command line.
|
.B init
|
||||||
|
Initialize secured launcher directories (root only). Does not alter
|
||||||
You can add /tilde/bin to your PATH by adding the following line to your shell configuration file (e.g., .bashrc or .zshrc):
|
existing approved links.
|
||||||
|
.SH FILES
|
||||||
.BP
|
.TP
|
||||||
\fBexport PATH=\$PATH:/tilde/bin\fR
|
.I /tilde/bin
|
||||||
|
Published command names.
|
||||||
Then, restart your shell or run the following command to apply the changes:
|
.TP
|
||||||
|
.I /tilde/approved
|
||||||
.BP
|
Root-owned reviewed versions.
|
||||||
\fBsource ~/.bashrc\fR (or \fBsource ~/.zshrc\fR for Zsh)
|
.TP
|
||||||
|
.I /tilde/descriptions
|
||||||
.SH SEE ALSO
|
Published descriptions.
|
||||||
The \fBtilde\fR command is part of the tilde-scripts package. More information about the tilde-scripts package can be found on the tilde.team website.
|
.TP
|
||||||
|
.I /tilde/metadata
|
||||||
|
Root-owned approval records.
|
||||||
|
.TP
|
||||||
|
.I /tilde/rejected
|
||||||
|
Root-owned rejection fingerprints.
|
||||||
|
.TP
|
||||||
|
.I /tilde/pending-submissions
|
||||||
|
User-managed pending snapshots.
|
||||||
|
.TP
|
||||||
|
.I /tilde/review
|
||||||
|
Private root-owned temporary review area.
|
||||||
|
.SH NOTES
|
||||||
|
An administrator approves an immutable snapshot of a submitted script. The
|
||||||
|
script is captured under the submitter's user ID; it is not executed as root.
|
||||||
|
Legacy links are not changed until an administrator explicitly migrates them.
|
||||||
|
Single-file scripts are supported; executables requiring files beside them may
|
||||||
|
need packaging changes before migration.
|
||||||
|
.PP
|
||||||
|
To run approved scripts directly, append /tilde/bin to your PATH.
|
||||||
.SH AUTHOR
|
.SH AUTHOR
|
||||||
\fBBen Harris\fR <\fBben@tilde.team\fR>
|
Ben Harris <ben@tilde.team>
|
||||||
|
|
||||||
.SH COPYRIGHT
|
.SH COPYRIGHT
|
||||||
Copyright (C) 2018 Ben Harris.
|
Copyright (C) 2018 Ben Harris.
|
||||||
|
|
||||||
.SH LICENSE
|
.SH LICENSE
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
GPL-3.0-or-later. See LICENSE.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License at <http://www.gnu.org/licenses/> for more details.
|
|
||||||
Reference in new issue
Block a user