From 7517310e3be6a24eaa0787e65a7a488ecdb6c9c8 Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Thu, 19 Feb 2026 23:06:54 +0100 Subject: [PATCH 1/4] add instance IDs for forgejo auth sources Extend the Forgejo authentication sources to hold instance IDs that uniquely identify a forgejo instance. These instance IDs are used to generate non-matching `incoming_labels` even if the user names on different forgejo-instances are identical. By default the disambiguation is done with an md5 hash of the forgejo authentication source URL. The configuration allows to override this with a custom string to keep `incoming_labels` readable. --- dump_things_service/auth/forgejo.py | 14 ++++++-- dump_things_service/config.py | 1 + dump_things_service/tests/test_auth.py | 50 +++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/dump_things_service/auth/forgejo.py b/dump_things_service/auth/forgejo.py index 41b5d37..d088ff4 100644 --- a/dump_things_service/auth/forgejo.py +++ b/dump_things_service/auth/forgejo.py @@ -9,6 +9,7 @@ will emit a complete repository-record including the complete owner-record. """ from __future__ import annotations +import hashlib import logging import time from functools import wraps @@ -74,6 +75,7 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache): organization: str, team: str, label_type: str, + instance_id: str | None = None, repository: str | None = None, ): """ @@ -90,6 +92,8 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache): :param team: The name of the team :param label_type: 'team' or 'user', determines how the incoming label is created. + :param instance_id: Optional instance ID. If present, will be used to + disambig :param repository: Optional repository. If this is provided, access will only be granted if the team has access to the repository. """ @@ -98,6 +102,7 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache): self.organization = organization self.team = team self.label_type = label_type + self.instance_id = instance_id self.repository = repository def _get_json_from_endpoint( @@ -210,6 +215,11 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache): ) return permissions + def _instance_label(self) -> str: + return self.instance_id or hashlib.md5( + self.api_url.encode() + ).hexdigest() + @MethodCache.cache_temporary(duration=60) def authenticate( self, @@ -272,7 +282,7 @@ class ForgejoAuthenticationSource(AuthenticationSource, MethodCache): ), user_id=user_info['email'], incoming_label= - f'forgejo-team-{organization["name"]}-{team["name"]}' + f'forgejo-{self._instance_label()}-team-{organization["name"]}-{team["name"]}' if self.label_type == 'team' - else f'forgejo-user-{user_info["login"]}', + else f'forgejo-{self._instance_label()}-user-{user_info["login"]}', ) diff --git a/dump_things_service/config.py b/dump_things_service/config.py index 0c69306..59b8a16 100644 --- a/dump_things_service/config.py +++ b/dump_things_service/config.py @@ -121,6 +121,7 @@ class ForgejoAuthConfig(StrictModel): organization: str team: str label_type: Literal['team', 'user'] + instance_id: str | None = None repository: str | None = None diff --git a/dump_things_service/tests/test_auth.py b/dump_things_service/tests/test_auth.py index cb0241f..1e5bf2e 100644 --- a/dump_things_service/tests/test_auth.py +++ b/dump_things_service/tests/test_auth.py @@ -42,12 +42,14 @@ team_1 = json.loads(team_template.format(id=1, action='none')) team_2 = json.loads(team_template.format(id=2, action='none')) team_3 = json.loads(team_template.format(id=3, action='write')) + def setup_http_server(http_server) -> None: - http_server.expect_request('/api/v1/user').respond_with_json(user_1) - http_server.expect_request('/api/v1/user/teams').respond_with_json([team_1, team_3]) - http_server.expect_request('/api/v1/orgs/org_1').respond_with_json(org_1) - http_server.expect_request('/api/v1/orgs/org_1/teams').respond_with_json([team_1, team_2, team_3]) - http_server.expect_request('/api/v1/repos/org_1/repo_1/teams').respond_with_json([team_1, team_2, team_3]) + for instance in ('1', '2'): + http_server.expect_request(f'/api/v{instance}/user').respond_with_json(user_1) + http_server.expect_request(f'/api/v{instance}/user/teams').respond_with_json([team_1, team_3]) + http_server.expect_request(f'/api/v{instance}/orgs/org_1').respond_with_json(org_1) + http_server.expect_request(f'/api/v{instance}/orgs/org_1/teams').respond_with_json([team_1, team_2, team_3]) + http_server.expect_request(f'/api/v{instance}/repos/org_1/repo_1/teams').respond_with_json([team_1, team_2, team_3]) @pytest.mark.parametrize('repository', ['repo_1', None]) @@ -102,3 +104,41 @@ def test_forgejo_auth_curator(httpserver, label_type, repository): zones_access=True, ) assert r.user_id == 'user_1@example.com' + + +@pytest.mark.parametrize('label_type', ['user', 'team']) +def test_forgejo_disambiguate(httpserver, label_type): + setup_http_server(httpserver) + + forgejo_authentication_results = [ + ForgejoAuthenticationSource( + api_url=httpserver.url_for(f'/api/v{instance}'), + organization='org_1', + team='team_1', + label_type=label_type, + ).authenticate(token='something') + for instance in ('1', '2') + ] + + assert ( + forgejo_authentication_results[0].incoming_label + != forgejo_authentication_results[1].incoming_label + ) + + forgejo_authentication_results = [ + ForgejoAuthenticationSource( + api_url=httpserver.url_for(f'/api/v{instance}'), + organization='org_1', + team='team_1', + label_type=label_type, + instance_id=f'instance_{instance}', + ).authenticate(token='something') + for instance in ('1', '2') + ] + + assert ( + forgejo_authentication_results[0].incoming_label + != forgejo_authentication_results[1].incoming_label + ) + assert 'instance_1' in forgejo_authentication_results[0].incoming_label + assert 'instance_2' in forgejo_authentication_results[1].incoming_label -- 2.52.0 From b234acace8b27657f55b8f34ea24c72d6b619bf6 Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Thu, 19 Feb 2026 23:14:42 +0100 Subject: [PATCH 2/4] update CHANGELOG.md and README.md Describe the disambiguation of identical user IDs on different Forgejo authentication sources. --- CHANGELOG.md | 9 +++++++++ README.md | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a570fda..14557c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 5.5.0 (2026-02-19) + +## New features + +- Forgejo authentication sources now generate incoming labels that are unique + to the user and the Forgejo instance. This keeps incoming areas of users on + different Forgejo instances separate, even if the user names are identical. + + # 5.4.0 (2026-02-02) ## New features diff --git a/README.md b/README.md index b940489..530b3dd 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,10 @@ collections: # is `user`, the incoming label will be # `forgejo-user-` label_type: team + # An optional instance id. This is used to disambiguate identical + # user IDs on different Forgejo instances. If not set, a hash of + # `url` will be used instead. + instance_id: forgejo-server-1 # An optional repository. The token will only be authorized # if the team has access to the repository. Note: if `repository` # is set, the token must have at least repository read -- 2.52.0 From 14e5f70ee7231f6f4eef9e5f15eea235adf17cd0 Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Thu, 19 Feb 2026 23:16:31 +0100 Subject: [PATCH 3/4] bump version to 5.5.0 --- dump_things_service/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dump_things_service/__about__.py b/dump_things_service/__about__.py index 271e4c0..714c807 100644 --- a/dump_things_service/__about__.py +++ b/dump_things_service/__about__.py @@ -1 +1 @@ -__version__ = '5.4.0' +__version__ = '5.5.0' -- 2.52.0 From 96f7819a824ccdd9467ea599fe17d23af9489695 Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Thu, 19 Feb 2026 23:23:14 +0100 Subject: [PATCH 4/4] fix tests for Forgejo auth sources --- dump_things_service/tests/test_auth.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dump_things_service/tests/test_auth.py b/dump_things_service/tests/test_auth.py index 1e5bf2e..e63438d 100644 --- a/dump_things_service/tests/test_auth.py +++ b/dump_things_service/tests/test_auth.py @@ -63,13 +63,14 @@ def test_forgejo_auth_team(httpserver, label_type, repository): team='team_1', label_type=label_type, repository=repository, + instance_id='inst_1', ) r = forgejo_auth_source.authenticate(token='something') if label_type == 'team': - assert r.incoming_label == 'forgejo-team-org_1-team_1' + assert r.incoming_label == 'forgejo-inst_1-team-org_1-team_1' else: - assert r.incoming_label == 'forgejo-user-user_1' + assert r.incoming_label == 'forgejo-inst_1-user-user_1' assert r.token_permission == TokenPermission( curated_read=True, incoming_read=True, @@ -88,14 +89,15 @@ def test_forgejo_auth_curator(httpserver, label_type, repository): organization='org_1', team='team_3', label_type=label_type, + instance_id='inst_1', repository=repository, ) r = forgejo_auth_source.authenticate(token='something') if label_type == 'team': - assert r.incoming_label == 'forgejo-team-org_1-team_3' + assert r.incoming_label == 'forgejo-inst_1-team-org_1-team_3' else: - assert r.incoming_label == 'forgejo-user-user_1' + assert r.incoming_label == 'forgejo-inst_1-user-user_1' assert r.token_permission == TokenPermission( curated_read=True, incoming_read=True, -- 2.52.0