mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-08-13 12:33:30 +02:00
wiki-guard: trust repo/org members with write access in all rule checks
This commit is contained in:
parent
dc1b06006f
commit
2b5a19d34a
2 changed files with 301 additions and 38 deletions
46
.github/workflows/issue.yml
vendored
46
.github/workflows/issue.yml
vendored
|
|
@ -2,6 +2,12 @@ name: "Update issues"
|
|||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
|
|
@ -10,6 +16,46 @@ jobs:
|
|||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const item = context.payload.issue || context.payload.pull_request;
|
||||
|
||||
// Close on sight anything opened by a user on the wiki Blacklist
|
||||
// page (maintained by the Wiki Guard workflow).
|
||||
let blacklist = [];
|
||||
try {
|
||||
const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`);
|
||||
if (res.ok) {
|
||||
blacklist = (await res.text()).split("\n")
|
||||
.filter(l => l.startsWith("- "))
|
||||
.map(l => l.slice(2).trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Failed to fetch the blacklist: ${e}`);
|
||||
}
|
||||
if (blacklist.includes(item.user.login.toLowerCase())) {
|
||||
if (context.payload.pull_request) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: item.number,
|
||||
state: "closed"
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned"
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.payload.pull_request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = context.payload.issue;
|
||||
if (issue.title.startsWith("Report bugs to")) {
|
||||
// Tracking issue for a third-party dns/deploy/notify api:
|
||||
|
|
|
|||
293
.github/workflows/wiki-guard.yml
vendored
293
.github/workflows/wiki-guard.yml
vendored
|
|
@ -1,7 +1,19 @@
|
|||
name: Restore Wiki Pages Deleted by Others
|
||||
name: Wiki Guard
|
||||
|
||||
# Rules enforced here:
|
||||
# - Only the maintainer and write-access members may delete or rename wiki
|
||||
# pages. Anyone else doing so gets blacklisted and the page restored to
|
||||
# its last good revision.
|
||||
# - Only the maintainer and write-access members may edit the Blacklist
|
||||
# wiki page. Anyone else touching it gets blacklisted and the page
|
||||
# reverted.
|
||||
# - Any wiki change made by a blacklisted identity is reverted.
|
||||
# A "good" revision is one authored by the maintainer, by this bot, or by
|
||||
# a non-blacklisted user -- restoring from the deleted commit's parent is
|
||||
# NOT safe, because vandals replace a page before destroying it and the
|
||||
# parent would launder their version into a bot commit.
|
||||
# The gollum event only fires on page create/update, never on deletion,
|
||||
# so deletions can only be caught by polling the wiki git history.
|
||||
# so violations are caught by polling the wiki git history.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
|
@ -18,7 +30,7 @@ concurrency:
|
|||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
restore:
|
||||
guard:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
|
|
@ -28,68 +40,273 @@ jobs:
|
|||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Restore pages deleted by non-maintainer
|
||||
id: restore
|
||||
- name: Enforce wiki rules
|
||||
id: guard
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Logins with write (push) access to the repository, including
|
||||
# organization members -- they may delete/rename pages and edit
|
||||
# the blacklist just like the maintainer. If the API call fails,
|
||||
# the list stays empty and enforcement falls back to
|
||||
# maintainer-only, which is the safe direction.
|
||||
gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \
|
||||
-q '.[] | select(.permissions.push) | .login' 2>/dev/null \
|
||||
| tr 'A-Z' 'a-z' | sort -u > writers.txt || true
|
||||
cd wiki
|
||||
git config core.quotePath false
|
||||
|
||||
# Any author email under this domain is the maintainer and may delete pages.
|
||||
# Any author email under this domain is the maintainer.
|
||||
OWNER_DOMAIN="neilpang.com"
|
||||
# Our own enforcement commits.
|
||||
BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BL_PAGE="Blacklist.md"
|
||||
# Rolling window; the cron runs every 10 minutes, so 7 days gives
|
||||
# ample overlap without re-judging old changes the maintainer
|
||||
# already accepted.
|
||||
WINDOW="7 days ago"
|
||||
|
||||
: > ../restored.txt
|
||||
: > ../actions.txt
|
||||
: > ../bl_new.txt
|
||||
|
||||
# Paths deleted within the recent window (rolling; the cron runs
|
||||
# every 10 minutes, so 7 days gives ample overlap without
|
||||
# resurrecting old deletions the maintainer already accepted).
|
||||
# --no-renames makes a rename count as a deletion of the old path,
|
||||
# so a page renamed by a non-maintainer is restored under its
|
||||
# original name (the new name stays -- creating pages is allowed).
|
||||
git log --since="7 days ago" --diff-filter=D --no-renames --name-only --format= \
|
||||
is_owner() {
|
||||
case "$1" in
|
||||
*@"$OWNER_DOMAIN") return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_bot() {
|
||||
[ "$1" = "$BOT_EMAIL" ]
|
||||
}
|
||||
|
||||
author_email() {
|
||||
git show -s --format=%ae "$1" | tr 'A-Z' 'a-z'
|
||||
}
|
||||
|
||||
# Identity of a commit author: the GitHub login when the email is a
|
||||
# users.noreply.github.com address, otherwise the email itself.
|
||||
identity_of() {
|
||||
case "$1" in
|
||||
*+*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_blacklisted() {
|
||||
grep -Fxq "$1" ../bl_all.txt
|
||||
}
|
||||
|
||||
# Trusted committers: the maintainer (by email domain), this bot,
|
||||
# and anyone whose GitHub login has write access to the repo.
|
||||
is_trusted() {
|
||||
if is_owner "$1" || is_bot "$1"; then
|
||||
return 0
|
||||
fi
|
||||
grep -Fxq "$(identity_of "$1")" ../writers.txt
|
||||
}
|
||||
|
||||
# Newest commit on file $1 authored by a non-blacklisted user.
|
||||
last_good_for() {
|
||||
for g in $(git log --format=%H --no-renames -- "$1"); do
|
||||
gae="$(author_email "$g")"
|
||||
if is_trusted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
gid="$(identity_of "$gae")"
|
||||
if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -e "$BL_PAGE" ]; then
|
||||
page_existed=1
|
||||
else
|
||||
page_existed=""
|
||||
fi
|
||||
|
||||
# ---- 1. Last good version of the blacklist page: the newest
|
||||
# revision authored by the maintainer or by this bot. Everything
|
||||
# else on that page is tampering and is discarded.
|
||||
bl_good_commit=""
|
||||
for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
bl_good_commit="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then
|
||||
git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt
|
||||
else
|
||||
{
|
||||
echo "# Blacklist"
|
||||
echo ""
|
||||
echo "Users listed below violated the wiki rules (deleted or renamed"
|
||||
echo "pages, or tampered with this page). Their new issues and pull"
|
||||
echo "requests are closed on sight and their wiki edits are reverted"
|
||||
echo "automatically. Only the maintainer and write-access members"
|
||||
echo "may edit this page."
|
||||
echo ""
|
||||
echo "To pardon a user while their violation is still inside the"
|
||||
echo "scan window, replace their entry with: pardon: username"
|
||||
echo ""
|
||||
} > ../bl_page.txt
|
||||
fi
|
||||
sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt
|
||||
sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt
|
||||
|
||||
bl_add() {
|
||||
if grep -Fxq "$1" ../bl_pardon.txt; then
|
||||
return 0
|
||||
fi
|
||||
if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then
|
||||
printf '%s\n' "$1" >> ../bl_new.txt
|
||||
printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- 2. Blacklist everyone who deleted or renamed a page.
|
||||
# --no-renames makes a rename count as a deletion of the old path.
|
||||
for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
# ---- 3. Blacklist everyone else who touched the blacklist page.
|
||||
# The revert of their tampering falls out of steps 5 and 6.
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
sort -u ../bl_new.txt > ../bl_new_u.txt
|
||||
cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt
|
||||
|
||||
# ---- 4. Restore pages that are currently missing because a
|
||||
# non-maintainer deleted them, using the last good revision.
|
||||
git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \
|
||||
| sort -u \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ -e "$f" ]; then
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then
|
||||
continue
|
||||
fi
|
||||
# The most recent commit (in full history) that deleted this path.
|
||||
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
|
||||
if [ -z "$del" ]; then
|
||||
continue
|
||||
fi
|
||||
ae="$(git show -s --format=%ae "$del" | tr 'A-Z' 'a-z')"
|
||||
case "$ae" in
|
||||
*@"$OWNER_DOMAIN")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
an="$(git show -s --format=%an "$del")"
|
||||
echo "Restoring: $f (deleted in $del by $an <$ae>)"
|
||||
git checkout "$del^" -- "$f"
|
||||
printf '%s\n' "- \`$f\` deleted in $del by $an <$ae>" >> ../restored.txt
|
||||
ae="$(author_email "$del")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -s ../restored.txt ]; then
|
||||
# ---- 5. Revert every recent change made by a blacklisted
|
||||
# identity: each touched file goes back to its newest revision
|
||||
# authored by a non-blacklisted user; a file that has no such
|
||||
# revision (they created it) is removed.
|
||||
if [ -s ../bl_all.txt ]; then
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
id="$(identity_of "$ae")"
|
||||
if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
git show --name-only --no-renames --format= "$c" \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
want="$(git rev-parse "$good:$f")"
|
||||
have="$(git hash-object -- "$f" 2>/dev/null || echo missing)"
|
||||
if [ "$want" != "$have" ]; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt
|
||||
fi
|
||||
elif [ -e "$f" ]; then
|
||||
git rm -q -- "$f"
|
||||
printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# ---- 6. Regenerate the blacklist page: the last good text plus
|
||||
# any newly blacklisted identities. This both reverts tampering
|
||||
# and records new violators; manual edits by the maintainer are
|
||||
# preserved as the new good text.
|
||||
cp ../bl_page.txt ../bl_page_new.txt
|
||||
if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then
|
||||
echo >> ../bl_page_new.txt
|
||||
fi
|
||||
while IFS= read -r id; do
|
||||
if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then
|
||||
printf -- '- %s\n' "$id" >> ../bl_page_new.txt
|
||||
fi
|
||||
done < ../bl_new_u.txt
|
||||
if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then
|
||||
cp ../bl_page_new.txt "$BL_PAGE"
|
||||
git add -- "$BL_PAGE"
|
||||
if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then
|
||||
printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 7. Commit, push, notify.
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "Restore pages deleted by non-maintainer"
|
||||
git config user.email "$BOT_EMAIL"
|
||||
git commit -m "wiki-guard: restore pages and enforce blacklist"
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
fi
|
||||
if [ -s ../actions.txt ]; then
|
||||
{
|
||||
echo "The following wiki pages were deleted or renamed by someone other than the maintainer and have been restored automatically:"
|
||||
echo "The wiki guard handled the following rule violations:"
|
||||
echo ""
|
||||
cat ../restored.txt
|
||||
cat ../actions.txt
|
||||
echo ""
|
||||
echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist"
|
||||
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
|
||||
} > ../restore-msg.txt
|
||||
echo "restored=true" >> "$GITHUB_OUTPUT"
|
||||
} > ../guard-msg.txt
|
||||
echo "acted=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No unauthorized deletions found."
|
||||
echo "restored=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No rule violations found."
|
||||
echo "acted=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
if: steps.restore.outputs.restored == 'true'
|
||||
if: steps.guard.outputs.acted == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki pages restored after unauthorized deletion"
|
||||
content-filepath: ./restore-msg.txt
|
||||
title: "Wiki guard: rule violations handled"
|
||||
content-filepath: ./guard-msg.txt
|
||||
assignees: Neilpang
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue