Skip to content

Commit a87ea84

Browse files
authored
Merge pull request #175 from vit-zikmund/restrict_to
feat(github): Allow to configure which orgs/repos the github auth applies to
2 parents fc81f36 + 332270b commit a87ea84

File tree

3 files changed

+44
-0
lines changed

3 files changed

+44
-0
lines changed

docs/source/auth-providers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ This token represents a special identity of an "application installation", actin
233233
* `api_url` (`str` = `"https://api.github.com"`): Base URL for the GitHub API (enterprise servers have API at `"https://<custom-hostname>/api/v3/"`).
234234
* `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)).
235235
* `api_version` (`str | None` = `"2022-11-28"`): Target GitHub API version; set to `None` to use GitHub's latest (rather experimental).
236+
* `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.
236237
* `cache` (`dict`): Cache configuration section
237238
* `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.
238239
* `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.

giftless/auth/github.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ class Config:
252252
api_version: str | None
253253
# GitHub API requests timeout
254254
api_timeout: float | tuple[float, float]
255+
# Orgs and repos this instance is restricted to
256+
restrict_to: dict[str, list[str] | None] | None
255257
# cache config above
256258
cache: CacheConfig
257259

@@ -261,6 +263,12 @@ class Schema(ma.Schema):
261263
load_default="2022-11-28", allow_none=True
262264
)
263265
api_timeout = RequestsTimeout(load_default=(5.0, 10.0))
266+
restrict_to = ma.fields.Dict(
267+
keys=ma.fields.String(),
268+
values=ma.fields.List(ma.fields.String(), allow_none=True),
269+
load_default=None,
270+
allow_none=True,
271+
)
264272
# always provide default CacheConfig when not present in the input
265273
cache = ma.fields.Nested(
266274
CacheConfig.Schema(),
@@ -314,12 +322,27 @@ def __post_init__(self, request: flask.Request) -> None:
314322
org_repo_getter = itemgetter("organization", "repo")
315323
self.org, self.repo = org_repo_getter(request.view_args or {})
316324
self.user, self.token = self._extract_auth(request)
325+
self._check_restricted_to()
317326

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

332+
def _check_restricted_to(self) -> None:
333+
restrict_to = self.cfg.restrict_to
334+
if restrict_to:
335+
try:
336+
rest_repos = restrict_to[self.org]
337+
except KeyError:
338+
raise Unauthorized(
339+
f"Unauthorized GitHub organization '{self.org}'"
340+
) from None
341+
if rest_repos and self.repo not in rest_repos:
342+
raise Unauthorized(
343+
f"Unauthorized GitHub repository '{self.org}/{self.repo}'"
344+
)
345+
323346
def __enter__(self) -> "CallContext":
324347
self._session = self._exit_stack.enter_context(requests.Session())
325348
self._session.headers.update(self._api_headers)

tests/auth/test_github.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,26 @@ def mock_installation_repos(
410410
return cast(responses.BaseResponse, ret)
411411

412412

413+
def test_call_context_restrict_to_org_only(app: flask.Flask) -> None:
414+
cfg = gh.Config.from_dict({"restrict_to": {ORG: None}})
415+
with auth_request_context(app):
416+
ctx = gh.CallContext(cfg, flask.request)
417+
assert ctx is not None
418+
with auth_request_context(app, org="bogus"):
419+
with pytest.raises(Unauthorized):
420+
gh.CallContext(cfg, flask.request)
421+
422+
423+
def test_call_context_restrict_to_org_and_repo(app: flask.Flask) -> None:
424+
cfg = gh.Config.from_dict({"restrict_to": {ORG: [REPO]}})
425+
with auth_request_context(app):
426+
ctx = gh.CallContext(cfg, flask.request)
427+
assert ctx is not None
428+
with auth_request_context(app, repo="bogus"):
429+
with pytest.raises(Unauthorized):
430+
gh.CallContext(cfg, flask.request)
431+
432+
413433
def test_call_context_api_get_no_session(app: flask.Flask) -> None:
414434
with auth_request_context(app):
415435
ctx = gh.CallContext(DEFAULT_CONFIG, flask.request)

0 commit comments

Comments
 (0)