Skip to content

Commit

Permalink
Merge pull request #175 from vit-zikmund/restrict_to
Browse files Browse the repository at this point in the history
feat(github): Allow to configure which orgs/repos the github auth applies to
  • Loading branch information
athornton authored Dec 13, 2024
2 parents fc81f36 + 332270b commit a87ea84
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/source/auth-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ This token represents a special identity of an "application installation", actin
* `api_url` (`str` = `"https://api.github.com"`): Base URL for the GitHub API (enterprise servers have API at `"https://<custom-hostname>/api/v3/"`).
* `api_timeout` (`float | tuple[float, float]` = `(10.0, 20.0)`): Timeout for the GitHub API calls ([details](https://requests.readthedocs.io/en/stable/user/advanced/#timeouts)).
* `api_version` (`str | None` = `"2022-11-28"`): Target GitHub API version; set to `None` to use GitHub's latest (rather experimental).
* `restrict_to` (`dict[str, list[str] | None] | None` = `None`): Optional (but highly recommended) dictionary of GitHub organizations/users the authentication is restricted to. Each key (organization name) in the dictionary can contain a list of further restricted repository names. When the list is empty (or null), only the organizations are considered.
* `cache` (`dict`): Cache configuration section
* `token_max_size` (`int` = `32`): Max number of entries in the token -> user LRU cache. This cache holds the authentication data for a token. Evicted tokens will need to be re-authenticated.
* `auth_max_size` (`int` = `32`): Max number of [un]authorized org/repos TTL(LRU) for each user. Evicted repos will need to get re-authorized.
Expand Down
23 changes: 23 additions & 0 deletions giftless/auth/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ class Config:
api_version: str | None
# GitHub API requests timeout
api_timeout: float | tuple[float, float]
# Orgs and repos this instance is restricted to
restrict_to: dict[str, list[str] | None] | None
# cache config above
cache: CacheConfig

Expand All @@ -261,6 +263,12 @@ class Schema(ma.Schema):
load_default="2022-11-28", allow_none=True
)
api_timeout = RequestsTimeout(load_default=(5.0, 10.0))
restrict_to = ma.fields.Dict(
keys=ma.fields.String(),
values=ma.fields.List(ma.fields.String(), allow_none=True),
load_default=None,
allow_none=True,
)
# always provide default CacheConfig when not present in the input
cache = ma.fields.Nested(
CacheConfig.Schema(),
Expand Down Expand Up @@ -314,12 +322,27 @@ def __post_init__(self, request: flask.Request) -> None:
org_repo_getter = itemgetter("organization", "repo")
self.org, self.repo = org_repo_getter(request.view_args or {})
self.user, self.token = self._extract_auth(request)
self._check_restricted_to()

self._api_url = self.cfg.api_url
self._api_headers["Authorization"] = f"Bearer {self.token}"
if self.cfg.api_version:
self._api_headers["X-GitHub-Api-Version"] = self.cfg.api_version

def _check_restricted_to(self) -> None:
restrict_to = self.cfg.restrict_to
if restrict_to:
try:
rest_repos = restrict_to[self.org]
except KeyError:
raise Unauthorized(
f"Unauthorized GitHub organization '{self.org}'"
) from None
if rest_repos and self.repo not in rest_repos:
raise Unauthorized(
f"Unauthorized GitHub repository '{self.org}/{self.repo}'"
)

def __enter__(self) -> "CallContext":
self._session = self._exit_stack.enter_context(requests.Session())
self._session.headers.update(self._api_headers)
Expand Down
20 changes: 20 additions & 0 deletions tests/auth/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,26 @@ def mock_installation_repos(
return cast(responses.BaseResponse, ret)


def test_call_context_restrict_to_org_only(app: flask.Flask) -> None:
cfg = gh.Config.from_dict({"restrict_to": {ORG: None}})
with auth_request_context(app):
ctx = gh.CallContext(cfg, flask.request)
assert ctx is not None
with auth_request_context(app, org="bogus"):
with pytest.raises(Unauthorized):
gh.CallContext(cfg, flask.request)


def test_call_context_restrict_to_org_and_repo(app: flask.Flask) -> None:
cfg = gh.Config.from_dict({"restrict_to": {ORG: [REPO]}})
with auth_request_context(app):
ctx = gh.CallContext(cfg, flask.request)
assert ctx is not None
with auth_request_context(app, repo="bogus"):
with pytest.raises(Unauthorized):
gh.CallContext(cfg, flask.request)


def test_call_context_api_get_no_session(app: flask.Flask) -> None:
with auth_request_context(app):
ctx = gh.CallContext(DEFAULT_CONFIG, flask.request)
Expand Down

0 comments on commit a87ea84

Please sign in to comment.