Recognize legacy sudo grants for any account name

The legacy command wrote the caller's unvalidated name into both the
sudoers filename and the rule. Cleanup applied the current lower-case
account pattern to that suffix, so an exact legacy grant for an account
such as Alice was classified as administrator policy, left active, and
the machine-wide migration marker was written anyway.

Match a legacy grant by its exact filename and rule relationship instead
of the account policy, and cover it in the lifecycle suite through both
the unit cleanup and the real migration runner.

The account pattern itself stays lower-case: sudoers reads an upper-case
word such as ALICE as a User_Alias reference, and ALL as every user, so
such names must never reach the generated rule. Pin that with a test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Afonso OliveiraandClaude Fable 5.1 committed 2026-09-16 23:01:13 +01:00
1 parent c46f321680
commit 3eb3142313
4 files changed
+34 -18

No files matched your search

+18 -13
View File
@@ -56,9 +56,10 @@ resolve_account() {
entry=$(/usr/bin/getent passwd "$((10#$uid))") || return 1
IFS=: read -r ACCOUNT_NAME _ ACCOUNT_UID _ _ _ _ <<<"$entry"
[[ $ACCOUNT_UID == "$((10#$uid))" ]] || return 1
# Sudoers names and the legacy filename both have metacharacters. Omarchy
# accounts use this portable subset; refusing anything else is safer than
# attempting to quote privileged policy syntax.
# Sudoers has metacharacters, and it reads an upper-case word such as ALICE
# as an alias reference rather than a user. Accounts use this portable
# lower-case subset; refusing anything else is safer than attempting to
# quote privileged policy syntax.
valid_account_name "$ACCOUNT_NAME" || return 1
ACCOUNT_UID=$((10#$uid))
}
@@ -109,6 +110,8 @@ read_grant() {
valid_account_name "$GRANT_NAME" || return 2
}
# Returns 0 for a rule this command generated, 1 for anything else under the
# owned prefix (preserved as administrator policy), and 2 when unreadable.
classify_generated_rule() {
local file=$1 suffix contents name
@@ -116,18 +119,20 @@ classify_generated_rule() {
contents=$(/usr/bin/cat -- "$file") || return 2
suffix=${file##*/99-omarchy-nopasswd-}
if [[ $suffix =~ ^[0-9]+$ ]]; then
name=${contents%' ALL=(ALL) NOPASSWD: ALL'}
if valid_account_name "$name" && [[ $contents == "$name ALL=(ALL) NOPASSWD: ALL" ]]; then
return 0
fi
name=${contents%%' ALL=(ALL) NOTAFTER='*}
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
elif valid_account_name "$suffix" && [[ $contents == "$suffix ALL=(ALL) NOPASSWD: ALL" ]]; then
# The legacy command wrote the caller's unvalidated name into both the
# filename and the rule. That exact relationship is its fingerprint, so an
# account the current policy would reject still has its old grant removed.
if [[ $contents == "$suffix ALL=(ALL) NOPASSWD: ALL" ]]; then
return 0
else
return 1
fi
[[ $suffix =~ ^[0-9]+$ ]] || return 1
name=${contents%' ALL=(ALL) NOPASSWD: ALL'}
if valid_account_name "$name" && [[ $contents == "$name ALL=(ALL) NOPASSWD: ALL" ]]; then
return 0
fi
name=${contents%%' ALL=(ALL) NOTAFTER='*}
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
}
cleanup_uid_locked() {