nisse-review: repository patterns, and the wall checked every round
At bring-up, nisse-review read no repositories: Gitea's /user/repos leaves out a public repository's Read collaborators, so dev/nisse never showed up. `repositories` now holds patterns (dev/nisse, dev/*, */*) matched against every repository the account can read, public ones included (an organization's or user's list, or the instance's search); unset, it is /user/repos as before. A configured repository it can't read is left out with an oplog line. The read-only wall (R3) is checked every round, not only at startup: an account granted push or admin while running stops (exit 3) rather than review on. nisse-core's Gitea client gains GiteaNotFound for 404s, owner_repositories and visible_repositories. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
52c6704b17
commit
2139ddd05c
12 files changed
+283
-44
No files matched your search
@@ -0,0 +1,79 @@
|
||||
"""What nisse-review reviews, and the wall around it (R1, R3).
|
||||
|
||||
The repositories are worked out again every round: ``repositories`` in the
|
||||
configuration is a list of patterns ("dev/nisse", "dev/*", "*/*"), matched
|
||||
against what the account can read, public repositories included; unset, it
|
||||
is every repository the account was given (``/user/repos``). Access is
|
||||
managed in Gitea for private repositories and in the configuration for
|
||||
public ones.
|
||||
|
||||
The wall is checked as often: an account that can push to, or administer,
|
||||
any repository it reviews is refused, at startup and in every round, so a
|
||||
grant made while it runs stops it.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from fnmatch import fnmatchcase
|
||||
|
||||
from nisse_core.gitea import GiteaClient, GiteaNotFound
|
||||
from nisse_core.oplog import OpLog
|
||||
|
||||
_GLOB = frozenset("*?[")
|
||||
|
||||
|
||||
class AccessRefused(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _glob(text: str) -> bool:
|
||||
return any(c in _GLOB for c in text)
|
||||
|
||||
|
||||
def reviewed(
|
||||
gitea: GiteaClient, patterns: Iterable[str] | None, oplog: OpLog
|
||||
) -> list[str]:
|
||||
"""The repositories to review, sorted."""
|
||||
if patterns is None:
|
||||
return sorted(gitea.readable_repositories())
|
||||
found: set[str] = set()
|
||||
for pattern in patterns:
|
||||
owner, _, name = pattern.partition("/")
|
||||
if not _glob(owner) and not _glob(name):
|
||||
found.add(pattern) # checked by ``check``: it must be readable
|
||||
continue
|
||||
try:
|
||||
candidates = (
|
||||
gitea.visible_repositories()
|
||||
if _glob(owner)
|
||||
else gitea.owner_repositories(owner)
|
||||
)
|
||||
except GiteaNotFound:
|
||||
oplog.emit("access.no_owner", cause="no such owner", pattern=pattern)
|
||||
continue
|
||||
found |= {repo for repo in candidates if fnmatchcase(repo, pattern)}
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def check(gitea: GiteaClient, repos: list[str], oplog: OpLog) -> list[str]:
|
||||
"""The repositories it can read, after the wall: refused outright if it
|
||||
could write to any of them. A configured one it can't read is left out."""
|
||||
readable: list[str] = []
|
||||
too_much: list[str] = []
|
||||
for repo in repos:
|
||||
try:
|
||||
permissions = gitea.repository(repo).get("permissions") or {}
|
||||
except GiteaNotFound:
|
||||
oplog.emit(
|
||||
"access.unreadable", cause="not found or not readable", repo=repo
|
||||
)
|
||||
continue
|
||||
if permissions.get("push") or permissions.get("admin"):
|
||||
too_much.append(repo)
|
||||
elif permissions.get("pull", True):
|
||||
readable.append(repo)
|
||||
if too_much:
|
||||
raise AccessRefused(
|
||||
f"{gitea.whoami()} can write to {', '.join(too_much)}: nisse-review's "
|
||||
"account must have read access only (Part R, R3)"
|
||||
)
|
||||
return readable
|
||||
Reference in new issue
Block a user