FFFF Add restricted flag to scope down perms by injoongy · Pull Request #1 · injoongy/flyctl-hackathon · GitHub
[go: up one dir, main page]

Skip to content

Add restricted flag to scope down perms - #1

Open
injoongy wants to merge 3 commits into
masterfrom
injoong/add-restricted-login
Open

injoongy wants to merge 3 commits into
masterfrom
injoong/add-restricted-login

Conversation

@injoongy
@injoongy injoongy commented Oct 3, 2026
Copy link
Copy Markdown
Owner

Add fly auth login --restricted

Adds a way to log in with org-scoped tokens that can do less than a full login, for example a session that can't delete machines or apps.

This automates the community forum recipe of creating an org token and attenuating it with fly tokens attenuate.

Why a flag on login, not just attenuation

A normal login saves an OAuth user token (fo1_) plus short-lived per-org macaroons that flyctl creates from it every 10 minutes. Attenuating those macaroons alone doesn't restrict anything, for two reasons:

  • GraphQL still accepts the fo1_ token, which can't be attenuated.
  • fetchOrgTokens replaces the attenuated macaroons with fresh, unrestricted ones once they expire.

So a restricted login has to save macaroons only.

What --restricted does

  1. Runs the normal login (web or -i), but never saves the resulting user token.
  2. Asks which orgs to include (multi-select, or --org a,b).
  3. Asks for a permission preset: No deletes (rwcC) or Read-only (r). Any mask can be passed with --mask. Without a TTY it uses "No deletes".
  4. For each org, creates a deploy_organization token (the same kind fly tokens create org makes) with an expiry. The default is 30 days; --expiry changes it.
  5. Adds an Organization{ID, Mask} caveat to each token and saves only the attenuated bundle.
  6. Revokes the user token. If revocation fails, it warns instead of erroring.

With no user token saved, fetchOrgTokens does nothing, so the restrictions stay in place.

fly auth login --restricted
fly auth login --restricted --org acme --mask r --expiry 24h

--org, --mask and --expiry return an error unless --restricted is also set.

Changes

  • internal/command/auth/restricted.go: picks the orgs and mask, then creates and attenuates the tokens.
  • internal/command/auth/login.go: new flags and the restricted save path.
  • internal/command/auth/webauth/webauth.go: adds SaveTokenFor(ctx, token, userToken), which saves one token but looks up the user with another. SaveToken now calls it, so existing callers behave the same.
  • internal/command/auth/restricted_test.go: tests for checking the mask, looking up orgs by slug, and creating a token against a fake API. The last one confirms the narrowed token still allows writes and rejects deletes.

Known limitations / needs manual verification

  • Revoking the fo1_ session doesn't also revoke the org tokens we just created.
  • fly auth logout revokes the restricted bundle (it uses the same LogOut mutation as fly tokens revoke supplied).
  • Deploys under "No deletes". Blue-green deploys destroy old machines, so they will likely fail. Whether releasing a machine lease counts as d is decided server-side.
  • Commands that need a user token (whoami, orgs list, ...) may fail, the same as with FLY_API_TOKEN set to an org token.
  • Each restricted login leaves server-side org tokens, which show up in fly tokens list until they expire or are revoked.

Future work

RunWebLogin already sends lock_organization / lock_app metadata, driven by the FLY_TOKEN_LOCK_* env vars. A server-side version could have the browser consent page issue a macaroon-only session directly. Then no full-access token would ever reach the client, and no extra tokens would pile up.

🤖 Generated with Claude Code

The forum link is a placeholder, so swap in the real thread URL. Tick the manual checklist items once you've tried a real login.

@github-actions
github-actions Bot commented Oct 3, 2026 •
Copy link
Copy Markdown

🔬 Flycrocosm E2E: plan

This PR adds fly auth login --restricted (with --org, --mask, --expiry), which logs in, saves only attenuated org-scoped macaroons instead of the user token, and revokes the user token.

What I'll check

All logins use the email/password path (--email dev@fly.local --password …, stdin not a TTY) with an isolated FLY_CONFIG_DIR. The harness pins FLY_ACCESS_TOKEN, which overrides a saved login, so I'll read the restricted token from the saved config and pass it with --access-token.

  1. Flag guards. fly auth login --org personal, --mask r and --expiry 1h without --restricted each exit non-zero with --<flag> requires --restricted. With --restricted, --mask rwx, --org nope and --expiry 0s fail with their own errors (invalid mask, organization nope not found, --expiry must be positive), and no token is saved.
  2. Restricted login saves macaroons only. fly auth login --restricted --org personal --expiry 1h with no --mask exits 0, prints restricted to personal with permissions …, expiring in 1h0m0s (the "No deletes" default) and successfully logged in as dev@fly.local, with no "Failed revoking" warning. The saved access_token holds only fm2_ macaroons, no fo1_ user token, and fly tokens list shows a new flyctl restricted login token for the org.
  3. "No deletes" is enforced by the API. On a test app e2e-pr-1-restricted with one machine in dc1, the restricted token can list machines and stop/start the machine, but fly machine destroy --force and fly apps destroy are rejected as unauthorized. The app and machine still exist afterwards.
  4. --mask r is read-only. After a second login with --restricted --org personal --mask r, fly apps list and fly machine list succeed, while fly machine stop and fly apps create e2e-pr-1-ro are rejected.

What I won't check

  • The browser login and the interactive org/preset prompts: the run has no TTY, so only the --email/--password path and flag-driven selection are exercised. The token handling after login is the same code.
  • fly auth logout and deploys under a restricted session (the unticked items in the description). Logout through the harness wrapper could revoke the run's own token, and deploy behavior under "No deletes" is a known open question rather than a claim.

Testing on Flycrocosm 127ec8d (topology standard), flyctl-hackathon head 37b5a9805. The result follows in a separate comment.
Claude Code: 16 turns, 90s, $1.54

@github-actions
github-actions Bot commented Oct 3, 2026 •
Copy link
Copy Markdown

❌ Flycrocosm E2E: the PR does not match its plan

recording

Full recording (mp4)

fly auth login --restricted: 1 of the planned checks fails

Verdict: fail. The restrictions themselves work, but the default "No deletes" preset
breaks back-to-back machine commands. The result was the same on three consecutive runs
(about 145s each).

The failing check

Plan item 3 expected the r 10000 estricted token to stop and then start a machine.

Expected Observed
fly machine stop <id> (rwcC) exits 0, machine stops exits 0, machine stops, but prints failed to release lease for machine <id>: unauthorized
fly machine start <id> (rwcC), once stopped exits 0, machine starts exits 1: failed to obtain lease: ... lease currently held by <uuid>@tokens.fly.io, expires at ...
  • The API treats releasing a machine lease as a delete, so a token without d cannot
    release the 120s lease flyctl takes for stop, start, destroy and similar commands.
    The next lease-taking command on that machine fails until the lease expires.
  • This answers the open question in the description ("Whether releasing a machine lease
    counts as d"): it does, and it affects plain machine stop/start, not only deploys.
  • Control: the harness's unrestricted token runs the same stop → start sequence without
    error in the same script.
  • start itself is allowed under rwcC: after the script clears the stuck lease with the
    harness token, the restricted fly machine start succeeds (and again fails to release
    its own lease).

What passed

  • Flag guards. --org, --mask and --expiry without --restricted each fail with
    --<flag> requires --restricted. With --restricted, --mask rwx, --org nope and
    --expiry 0s fail with invalid mask, organization nope not found and
    --expiry must be positive, and nothing is saved.
  • Restricted login. --restricted --org personal --expiry 1h with no --mask and no
    TTY prints restricted to personal with permissions rwcC, expiring in 1h0m0s and
    successfully logged in as dev@fly.local, with no "Failed revoking" warning. The saved
    access_token is two fm2_ tokens. fly tokens list shows exactly one new
    flyctl restricted login token, expiring in 1h.
  • "No deletes" is enforced. With the saved token, fly machine list and
    fly machine stop work; fly machine destroy --force and fly apps destroy are
    rejected as unauthorized, and the machine and app still exist.
  • --mask r is read-only. The login prints permissions r, expiring in 720h0m0s.
    fly apps list and fly machine list work; fly machine stop and
    fly apps create e2e-pr-1-ro are rejected as unauthorized.

Not verified, and caveats

  • User token not saved. Flycrocosm's email/password login returns an fm2_ token, not
    fo1_, so the prefix check cannot tell the user token from the org macaroons. The
    evidence that the saved bundle is the attenuated one is that deletes are rejected with it.
  • User token revoked. The script never sees the user token, so it only checks that no
    "Failed revoking" warning is printed.
  • Not exercised, as stated in the plan: browser login, the interactive org and preset
    prompts, fly auth logout, and deploys under a restricted session.
  • Test image. The API returned a 500 resolving nginx:alpine (about 14s per attempt),
    so the test machine runs registry.fly.io/dashboard-demo:e2e from the local registry,
    with nginx:alpine as the fallback.
  • From reading the diff, not tested. When a restricted login fails after
    authentication (bad --mask, --org or --expiry), restrictToken returns before
    revokeUserToken, so that session's user token is neither saved nor revoked.
  • Cleanup. The harness token is not allowed to revoke tokens, so the script does a
    plain login in its temporary config dir and revokes the two org tokens it created
    (cleanup: 2 tokens revoked).
Provenance
Flycrocosm 127ec8d (dirty), topology standard
flyctl-hackathon head 37b5a9805, base 33ed7a3a3
Built binary=bin/flyctl.real version=flyctl.real v2026.10.3-dev.1791010324 linux/amd64 Commit: <none> BuildDate: 2026-10-03T19:52:04+13:00 BranchName: dev
Test demo.sh sha256 d3f130da3c84
Independent run asciinema recording, exit 1
Generated test (demo.sh)
#!/usr/bin/env bash
# Demo/verification for `fly auth login --restricted` (PR 1) against Flycrocosm.
set -euo pipefail

EMAIL="dev@fly.local"
PASSWORD="1q2w3e4r"
ORG="personal"
APP="e2e-pr-1-restricted"
RO_APP="e2e-pr-1-ro"
TOKEN_NAME="flyctl restricted login"
IMAGES="registry.fly.io/dashboard-demo:e2e nginx:alpine"
DENIED='unauthorized|not authorized|forbidden'

WORK="$(mktemp -d)"
# Logins save into an isolated config dir, never the real one.
export FLY_CONFIG_DIR="$WORK/fly"
mkdir -p "$FLY_CONFIG_DIR"
CONFIG="$FLY_CONFIG_DIR/config.yml"
TOKENS_BEFORE="$WORK/tokens.before"

FAILURES=0
fail() { echo "FAIL: $*"; exit 1; }
# Record a failed check but keep running; the script still exits non-zero.
soft_fail() { echo "FAIL: $*"; FAILURES=$((FAILURES + 1)); }
pass() { echo "PASS: $*"; }

OUT=""
RC=0
# run CMD...: no TTY on stdin/stdout, capture combined output in OUT and exit code in RC.
capture() {
  set +e
  OUT="$("$@" 2>&1 </dev/null)"
  RC=$?
  set -e
}
run() {
  capture "$@"
  [ -z "$OUT" ] || printf '%s\n' "$OUT" | sed 's/^/    /'
}
# list_machines TOKEN: `fly machine list --json` with TOKEN, printed as "id state region".
list_machines() {
  capture fly machine list --app "$APP" --json --access-token "$1"
  if [ "$RC" -eq 0 ]; then
    OUT="$(jq -r '.[] | "\(.id) \(.state) \(.region)"' <<<"$OUT")"
  fi
  [ -z "$OUT" ] || printf '%s\n' "$OUT" | sed 's/^/    /'
}
quiet() { "$@" >/dev/null 2>&1 </dev/null; }

expect_ok() { [ "$RC" -eq 0 ] || fail "$1 (exit $RC)"; }
expect_out() { grep -qF -- "$1" <<<"$OUT" || fail "expected output to contain: $1"; }
expect_denied() {
  [ "$RC" -ne 0 ] || fail "$1 succeeded, expected the API to reject it"
  grep -qiE -- "$DENIED" <<<"$OUT" || fail "$1 failed, but not with an authorization error"
}

login() { run fly auth login --email "$EMAIL" --password "$PASSWORD" "$@"; }
saved_token() { [ -f "$CONFIG" ] && sed -n 's/^access_token: *//p' "$CONFIG" | tr -d "\"'" || true; }
# IDs of the org's "flyctl restricted login" tokens (harness token).
restricted_token_ids() {
  local list
  list="$(fly tokens list --scope org --org "$ORG" </dev/null 2>&1)" || { echo "$list" >&2; return 1; }
  { grep -F "$TOKEN_NAME" <<<"$list" || true; } | awk '{print $1}' | sort
}
machine_state() { fly machine list --app "$APP" --json </dev/null 2>/dev/null | jq -r --arg id "$MID" '.[] | select(.id == $id) | .state'; }
wait_state() {
  local want="$1" state=""
  for _ in $(seq 1 30); do
    state="$(machine_state || true)"
    [ "$state" = "$want" ] && return 0
    sleep 2
  done
  fail "machine $MID is '$state', expected '$want'"
}
# Drop any lease left on the machine (harness token), so a later rejection is
# about permissions and not about a held lease.
clear_lease() { quiet fly machine leases clear "$MID" --app "$APP" || true; }
app_exists() { fly apps list --json </dev/null 2>/dev/null | jq -e --arg n "$1" 'any(.[]; (.Name // .name) == $n)' >/dev/null; }

cleanup() {
  set +e
  quiet fly apps destroy "$APP" --yes
  quiet fly apps destroy "$RO_APP" --yes
  # Revoke the org tokens this run created. The harness token may not revoke
  # tokens, so use a plain login saved in the isolated config dir.
  local ids=""
  [ -f "$TOKENS_BEFORE" ] && ids="$(restricted_token_ids 2>/dev/null | comm -13 "$TOKENS_BEFORE" -)"
  if [ -n "$ids" ] && quiet fly auth login --email "$EMAIL" --password "$PASSWORD"; then
    # shellcheck disable=SC2086
    fly tokens revoke $ids --access-token "$(saved_token)" </dev/null 2>&1 | grep "tokens revoked" | sed 's/^/cleanup: /'
  fi
  rm -rf "$WORK"
}
trap cleanup EXIT

echo "==> PR binary under test"
run fly version
expect_ok "fly version"

# ---------------------------------------------------------------------------
echo
echo "==> 1. Flag guards: --org/--mask/--expiry need --restricted"
for args in "--org $ORG" "--mask r" "--expiry 1h"; do
  flagname="${args%% *}"
  echo "--> fly auth login --email $EMAIL --password *** $args"
  # shellcheck disable=SC2086
  login $args
  [ "$RC" -ne 0 ] || fail "login with $flagname but without --restricted exited 0"
  expect_out "$flagname requires --restricted"
  pass "$flagname without --restricted is rejected"
done

echo
echo "==> 1b. Invalid values with --restricted fail and save nothing"
echo "--> fly auth login --restricted --org $ORG --mask rwx"
login --restricted --org "$ORG" --mask rwx
[ "$RC" -ne 0 ] || fail "--mask rwx exited 0"
expect_out "invalid mask"
pass "--mask rwx is rejected as an invalid mask"

echo "--> fly auth login --restricted --org nope"
login --restricted --org nope
[ "$RC" -ne 0 ] || fail "--org nope exited 0"
expect_out "organization nope not found"
pass "--org nope is rejected as not found"

echo "--> fly auth login --restricted --org $ORG --expiry 0s"
login --restricted --org "$ORG" --expiry 0s
[ "$RC" -ne 0 ] || fail "--expiry 0s exited 0"
expect_out "--expiry must be positive"
pass "--expiry 0s is rejected"

[ -z "$(saved_token)" ] || fail "a token was saved to $CONFIG by a failed login"
pass "no access token was saved by the failed logins"

# ---------------------------------------------------------------------------
echo
echo "==> 2. Restricted login (default preset) saves macaroons only"
restricted_token_ids > "$TOKENS_BEFORE" || fail "fly tokens list failed with the harness token"
echo "--> fly auth login --restricted --org $ORG --expiry 1h  (no --mask, no TTY)"
login --restricted --org "$ORG" --expiry 1h
expect_ok "restricted login"
expect_out "restricted to $ORG with permissions rwcC, expiring in 1h0m0s"
expect_out "successfully logged in as $EMAIL"
! grep -qF "Failed revoking" <<<"$OUT" || fail "the user token could not be revoked"
pass "login succeeded with the \"No deletes\" (rwcC) default and no revocation warning"

ND_TOKEN="$(saved_token)"
[ -n "$ND_TOKEN" ] || fail "no access_token saved in $CONFIG"
! grep -q "fo1_" "$CONFIG" || fail "an fo1_ user token was saved in $CONFIG"
n_total=0
IFS=',' read -ra parts <<<"${ND_TOKEN#FlyV1 }"
for p in "${parts[@]}"; do
  n_total=$((n_total + 1))
  [[ "$p" == fm2_* ]] || fail "saved access_token has a non-macaroon part (prefix ${p:0:4})"
done
echo "    saved access_token: $n_total token(s), all fm2_ macaroons"
pass "saved config holds only fm2_ macaroons, no fo1_ user token"

echo "--> fly tokens list --scope org --org $ORG   (harness token)"
restricted_token_ids > "$WORK/tokens.after" || fail "fly tokens list failed with the harness token"
new_ids="$(comm -13 "$TOKENS_BEFORE" "$WORK/tokens.after")"
n_new="$(grep -c . <<<"$new_ids" || true)"
echo "    new \"$TOKEN_NAME\" tokens: ${new_ids:-none}"
[ "$n_new" -eq 1 ] || fail "expected 1 new \"$TOKEN_NAME\" org token, found $n_new"
pass "one new \"$TOKEN_NAME\" token exists for $ORG"

row="$(fly tokens list --scope org --org "$ORG" </dev/null 2>/dev/null | grep -F "$new_ids")"
expires="$(awk -F'│' '{print $4}' <<<"$row" | sed 's/ UTC//' | xargs)"
revoked="$(awk -F'│' '{print $5}' <<<"$row" | xargs)"
echo "    expires at: $expires   revoked at: ${revoked:-never}"
ttl=$(( $(date -u -d "$expires" +%s) - $(date -u +%s) ))
[ "$ttl" -gt 3300 ] && [ "$ttl" -le 3600 ] || fail "token expires in ${ttl}s, expected about 3600s (--expiry 1h)"
[ -z "$revoked" ] || fail "the new org token is already revoked"
pass "the org token expires in 1h and is not revoked"

# ---------------------------------------------------------------------------
echo
echo "==> 3. \"No deletes\" is enforced by the API"
echo "--> setup (harness token): app $APP with one machine in dc1"
quiet fly apps destroy "$APP" --yes || true
quiet fly apps destroy "$RO_APP" --yes || true
run fly apps create "$APP" --org "$ORG"
expect_ok "creating $APP"
# Prefer an image already in the local registry; resolving Docker Hub images
# depends on the stack's outbound network.
for image in $IMAGES; do
  run fly machine run "$image" --app "$APP" --region dc1 \
    --vm-size shared-cpu-1x --vm-memory 256 --detach
  [ "$RC" -ne 0 ] || break
done
expect_ok "creating the machine"
MID="$(fly machine list --app "$APP" --json </dev/null | jq -r '.[0].id')"
[ -n "$MID" ] && [ "$MID" != null ] || fail "no machine found in $APP"
wait_state started
echo "    machine $MID is started"

echo "--> control (harness token): stop, then start as soon as it is stopped"
run fly machine stop "$MID" --app "$APP"
expect_ok "machine stop with the harness token"
wait_state stopped
run fly machine start "$MID" --app "$APP"
expect_ok "machine start with the harness token"
wait_state started
pass "an unrestricted token can stop and then start the machine"

echo "--> fly machine list   (restricted token)"
list_machines "$ND_TOKEN"
expect_ok "machine list with the restricted token"
expect_out "$MID"
pass "restricted token can list machines"

echo "--> fly machine stop $MID   (restricted token)"
run fly machine stop "$MID" --app "$APP" --access-token "$ND_TOKEN"
expect_ok "machine stop with the restricted token"
wait_state stopped
pass "restricted token can stop the machine"

STOP_OUT="$OUT"

echo "--> fly machine start $MID   (restricted token, once it is stopped)"
run fly machine start "$MID" --app "$APP" --access-token "$ND_TOKEN"
if [ "$RC" -eq 0 ]; then
  wait_state started
  pass "restricted token can start the machine"
else
  soft_fail "restricted token cannot start the machine it just stopped (exit $RC)"
  if grep -q "failed to release lease" <<<"$STOP_OUT" && grep -q "lease currently held" <<<"$OUT"; then
    echo "      cause: the stop could not release its machine lease (unauthorized without 'd'),"
    echo "      so the machine stays locked until that 120s lease expires."
  fi
  # Keep going so the remaining checks are still shown; the run fails at the end.
  echo "--> clear the stuck lease (harness token), then retry the start (restricted token)"
  run fly machine leases clear "$MID" --app "$APP"
  expect_ok "clearing the lease with the harness token"
  run fly machine start "$MID" --app "$APP" --access-token "$ND_TOKEN"
  expect_ok "machine start with the restricted token after clearing the lease"
  wait_state started
  echo "    start itself is allowed once the lease is cleared"
fi
clear_lease

echo "--> fly machine destroy $MID --force   (restricted token)"
run fly machine destroy "$MID" --force --app "$APP" --access-token "$ND_TOKEN"
expect_denied "machine destroy"
[ -n "$(machine_state)" ] || fail "machine $MID is gone after the rejected destroy"
pass "machine destroy is rejected and the machine still exists"
clear_lease

echo "--> fly apps destroy $APP --yes   (restricted token)"
run fly apps destroy "$APP" --yes --access-token "$ND_TOKEN"
expect_denied "apps destroy"
app_exists "$APP" || fail "app $APP is gone after the rejected destroy"
pass "apps destroy is rejected and the app still exists"

# ---------------------------------------------------------------------------
echo
echo "==> 4. --mask r is read-only"
echo "--> fly auth login --restricted --org $ORG --mask r"
login --restricted --org "$ORG" --mask r
expect_ok "read-only restricted login"
expect_out "restricted to $ORG with permissions r, expiring in 720h0m0s"
expect_out "successfully logged in as $EMAIL"
RO_TOKEN="$(saved_token)"
[ -n "$RO_TOKEN" ] || fail "no access_token saved in $CONFIG"
! grep -q "fo1_" "$CONFIG" || fail "an fo1_ user token was saved in $CONFIG"
[ "$RO_TOKEN" != "$ND_TOKEN" ] || fail "the second login did not replace the saved token"
pass "read-only login succeeded with the 30 day default expiry"

echo "--> fly apps list   (read-only token)"
run fly apps list --access-token "$RO_TOKEN"
expect_ok "apps list with the read-only token"
expect_out "$APP"
pass "read-only token can list apps"

clear_lease
echo "--> fly machine list   (read-only token)"
list_machines "$RO_TOKEN"
expect_ok "machine list with the read-only token"
expect_out "$MID"
pass "read-only token can list machines"

echo "--> fly machine stop $MID   (read-only token)"
run fly machine stop "$MID" --app "$APP" --access-token "$RO_TOKEN"
expect_denied "machine stop"
[ "$(machine_state)" = started ] || fail "machine $MID is no longer started after the rejected stop"
pass "machine stop is rejected and the machine is still started"

echo "--> fly apps create $RO_APP   (read-only token)"
run fly apps create "$RO_APP" --org "$ORG" --access-token "$RO_TOKEN"
expect_denied "apps create"
! app_exists "$RO_APP" || fail "app $RO_APP was created with the read-only token"
pass "apps create is rejected and $RO_APP does not exist"

echo
echo "(ran in ${SECONDS}s)"
if [ "$FAILURES" -gt 0 ]; then
  echo "==> $FAILURES check(s) failed"
  exit 1
fi
echo "==> All checks passed"
Test output
--> control (harness token): stop, then start as soon as it is stopped
    Sending kill signal to machine 024d899f4e8793...
    024d899f4e8793 has been successfully stopped
    024d899f4e8793 has been started
PASS: an unrestricted token can stop and then start the machine
--> fly machine list   (restricted token)
    024d899f4e8793 started dc1
PASS: restricted token can list machines
--> fly machine stop 024d899f4e8793   (restricted token)
    Sending kill signal to machine 024d899f4e8793...
    024d899f4e8793 has been successfully stopped
    failed to release lease for machine 024d899f4e8793: unauthorized
PASS: restricted token can stop the machine
--> fly machine start 024d899f4e8793   (restricted token, once it is stopped)
    Waiting on lease for machine 024d899f4e8793...
    Error: failed to obtain lease: failed to get lease on VM 024d899f4e8793: machine ID 024d899f4e8793 lease currently held by d696615f-9397-52ed-b355-5a9047f0d6f4@tokens.fly.io, expires at 2026-10-03T07:09:43Z (Request ID: 01M409G24J4W1SRF0K71P1RZDP-dc0)
FAIL: restricted token cannot start the machine it just stopped (exit 1)
      cause: the stop could not release its machine lease (unauthorized without 'd'),
      so the machine stays locked until that 120s lease expires.
--> clear the stuck lease (harness token), then retry the start (restricted token)
    clearing lease for machine 024d899f4e8793
    Lease(s) cleared
    024d899f4e8793 has been started
    failed to release lease for machine 024d899f4e8793: unauthorized
    start itself is allowed once the lease is cleared
--> fly machine destroy 024d899f4e8793 --force   (restricted token)
    machine 024d899f4e8793 was found and is currently in started state, attempting to destroy...
    failed to release lease for machine 024d899f4e8793: unauthorized
    Error: could not destroy machine 024d899f4e8793: failed to destroy VM 024d899f4e8793: unauthorized (Request ID: 01M409G3XY7VP42VW5A80EVJWB-dc0)
PASS: machine destroy is rejected and the machine still exists
--> fly apps destroy e2e-pr-1-restricted --yes   (restricted token)
    Error: unauthorized (Request ID: 01M409G45FRXVFSPDGDAFFXZ8D-dc0)
PASS: apps destroy is rejected and the app still exists

==> 4. --mask r is read-only
--> fly auth login --restricted --org personal --mask r
    restricted to personal with permissions r, expiring in 720h0m0s
    successfully logged in as dev@fly.local
    
    ! Environment variable FLY_ACCESS_TOKEN is set, so flyctl will continue using it instead of the access token just saved. Unset FLY_ACCESS_TOKEN to use the new access token.
PASS: read-only login succeeded with the 30 day default expiry
--> fly apps list   (read-only token)
     NAME                │ OWNER    │ STATUS    │ LATEST DEPLOY 
     dashboard-demo      │ personal │ pending   │               
     e2e-pr-1-restricted │ personal │ suspended │               
PASS: read-only token can list apps
--> fly machine list   (read-only token)
    024d899f4e8793 started dc1
PASS: read-only token can list machines
--> fly machine stop 024d899f4e8793   (read-only token)
    Waiting on lease for machine 024d899f4e8793...
    Error: failed to obtain lease: failed to get lease on VM 024d899f4e8793: unauthorized (Request ID: 01M409HZHPQ7626Q1B4E0RYTQZ-dc0)
PASS: machine stop is rejected and the machine is still started
--> fly apps create e2e-pr-1-ro   (read-only token)
    Error: unauthorized (Request ID: 01M409HZQAQM3XREJDN9KDMNM3-dc0)
PASS: apps create is rejected and e2e-pr-1-ro does not exist

(ran in 145s)
==> 1 check(s) failed
cleanup: 2 tokens revoked

Claude Code: 78 turns, 1100s, $5.75

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0