"""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``). Patterns are matched against Gitea's search, which needs only the token's repository scope: listing an organization's repositories would need ``read:organization`` too. 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 = {p for p in patterns if not _glob(p)} # ``check`` sees they're readable globs = [p for p in patterns if _glob(p)] if globs: visible = gitea.visible_repositories() for pattern in globs: matched = {repo for repo in visible if fnmatchcase(repo, pattern)} if not matched: oplog.emit("access.no_match", cause="matches nothing", pattern=pattern) found |= matched 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