Skip to content

Commit 39868f9

Browse files
committed
Reusable callable type for claim checking code
1 parent 479b429 commit 39868f9

File tree

9 files changed

+139
-182
lines changed

9 files changed

+139
-182
lines changed

tests/common/db/oidc.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,8 @@ class Meta:
8181
organization_url_name = factory.Faker("pystr", max_chars=12)
8282
project_id = factory.Faker("uuid4")
8383
activestate_project_name = factory.Faker("pystr", max_chars=12)
84-
project_path = f"{organization_url_name}/{activestate_project_name}"
8584
user_id = factory.Faker("uuid4")
8685
branch_id = factory.Faker("uuid4")
87-
project_visibility = factory.Faker("random_element", elements=[True, False])
88-
sub = factory.Faker("pystr", max_chars=12)
8986

9087

9188
class PendingActiveStatePublisherFactory(WarehouseFactory):
@@ -99,7 +96,5 @@ class Meta:
9996
organization_url_name = factory.Faker("pystr", max_chars=12)
10097
project_id = factory.Faker("uuid4")
10198
activestate_project_name = factory.Faker("pystr", max_chars=12)
102-
project_path = f"{organization_url_name}/{activestate_project_name}"
10399
user_id = factory.Faker("uuid4")
104-
project_visibility = factory.Faker("random_element", elements=[True, False])
105100
added_by = factory.SubFactory(UserFactory)

tests/conftest.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -351,18 +351,6 @@ def oidc_service(db_session):
351351
)
352352

353353

354-
# @pytest.fixture
355-
# def activestate_oidc_service(db_session):
356-
# return oidc_services.NullOIDCPublisherService(
357-
# db_session,
358-
# pretend.stub(),
359-
# ACTIVESTATE_OIDC_ISSUER_URL,
360-
# pretend.stub(),
361-
# pretend.stub(),
362-
# pretend.stub(),
363-
# )
364-
365-
366354
@pytest.fixture
367355
def macaroon_service(db_session):
368356
return macaroon_services.DatabaseMacaroonService(db_session)

tests/unit/oidc/models/test_activestate.py

Lines changed: 97 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
PROJECT_ID = "00000000-0000-1000-8000-000000000001"
3030
USER_ID = "00000000-0000-1000-8000-000000000002"
3131
BRANCH_ID = "00000000-0000-1000-8000-000000000003"
32+
# This follows the format of the subject that ActiveState sends us. We don't
33+
# validate the format when verifying the JWT. That should happen when the
34+
# Publisher is configured. We just need to make sure that the subject matches
35+
#
36+
# Technically, the branch should only be present if a branch was provided in the JWT
37+
# claims
38+
SUBJECT = "org:fake_org_id:project:fake_project_id:branch_id:fake_branch_id"
3239

3340

3441
def test_lookup_strategies():
@@ -40,65 +47,63 @@ def test_lookup_strategies():
4047

4148

4249
def new_signed_claims(
43-
sub: str = "fake:fake:fake:fake",
50+
sub: str = SUBJECT,
4451
organization_id: str = ORG_ID,
4552
org_url_name: str = "fakeorg",
4653
project_id: str = PROJECT_ID,
4754
project_name: str = "fakeproject",
4855
project_path: str = "fakeorg/fakeproject",
49-
user_id: str = "00000000-0000-1000-8000-000000000002",
56+
user_id: str = USER_ID,
5057
project_visibility: str = "public",
5158
branch_id: str | None = None,
5259
) -> SignedClaims:
5360
project_name = "fakeproject"
5461
org_url_name = "fakeorg"
55-
claims: SignedClaims = {
56-
"sub": sub,
57-
"organization_id": organization_id,
58-
"organization_url_name": org_url_name,
59-
"project_id": project_id,
60-
"project_name": project_name,
61-
"project_path": project_path,
62-
"user_id": user_id,
63-
"project_visibility": project_visibility,
64-
}
62+
claims = SignedClaims(
63+
{
64+
"sub": sub,
65+
"organization_id": organization_id,
66+
"organization_url_name": org_url_name,
67+
"project_id": project_id,
68+
"project_name": project_name,
69+
"project_path": project_path,
70+
"user_id": user_id,
71+
"project_visibility": project_visibility,
72+
}
73+
)
6574
if branch_id:
6675
claims["branch_id"] = branch_id
6776
return claims
6877

6978

7079
class TestActiveStatePublisher:
7180
def test_publisher_name(self):
72-
publisher = ActiveStatePublisher(
73-
organization_id=ORG_ID,
74-
project_id=PROJECT_ID,
75-
user_id=USER_ID,
76-
project_visibility="public",
77-
)
81+
publisher = ActiveStatePublisher()
7882

7983
assert publisher.publisher_name == "ActiveState"
8084

8185
def test_publisher_url(self):
86+
org_name = "fakeorg"
87+
project_name = "fakeproject"
8288
publisher = ActiveStatePublisher(
83-
organization_id=ORG_ID,
84-
project_id=PROJECT_ID,
85-
user_id=USER_ID,
86-
project_visibility="public",
89+
organization_url_name=org_name, activestate_project_name=project_name
8790
)
8891

89-
assert publisher.publisher_url == "https://platform.activestate.com/oauth/oidc"
92+
assert (
93+
publisher.publisher_url()
94+
== f"https://platform.activestate.com/{org_name}/{project_name}"
95+
)
9096

91-
def test_stringifies_as_email(self):
97+
def test_stringifies_as_project_url(self):
98+
org_name = "fakeorg"
99+
project_name = "fakeproject"
92100
publisher = ActiveStatePublisher(
93-
organization_id=ORG_ID,
94-
project_id=PROJECT_ID,
95-
user_id=USER_ID,
96-
project_visibility="public",
101+
organization_url_name=org_name, activestate_project_name=project_name
97102
)
98103

99104
assert (
100105
str(publisher)
101-
== f"https://platform.activestate.com/{publisher.project_path}"
106+
== f"https://platform.activestate.com/{org_name}/{project_name}"
102107
)
103108

104109
def test_activestate_publisher_all_known_claims(self):
@@ -107,7 +112,6 @@ def test_activestate_publisher_all_known_claims(self):
107112
"organization_id",
108113
"project_id",
109114
"user_id",
110-
"project_visibility",
111115
"sub",
112116
# optional verifiable claims
113117
"branch_id",
@@ -118,18 +122,15 @@ def test_activestate_publisher_all_known_claims(self):
118122
"exp",
119123
"aud",
120124
# unchecked claims
125+
"project_visibility",
121126
"project_name",
122127
"project_path",
123128
"organization_url_name",
124129
}
125130

126131
def test_activestate_publisher_unaccounted_claims(self, monkeypatch):
127132
publisher = ActiveStatePublisher(
128-
sub="fake:fake:fake:fake:fake:fake",
129-
organization_id=ORG_ID,
130-
project_id=PROJECT_ID,
131-
user_id=USER_ID,
132-
project_visibility="public",
133+
sub=SUBJECT,
133134
)
134135

135136
scope = pretend.stub()
@@ -143,7 +144,6 @@ def test_activestate_publisher_unaccounted_claims(self, monkeypatch):
143144
)
144145
monkeypatch.setattr(_core, "sentry_sdk", sentry_sdk)
145146

146-
# We don't care if these actually verify, only that they're present.
147147
signed_claims = {
148148
claim_name: "fake" for claim_name in ActiveStatePublisher.all_known_claims()
149149
}
@@ -158,13 +158,27 @@ def test_activestate_publisher_unaccounted_claims(self, monkeypatch):
158158
]
159159
assert scope.fingerprint == ["another-fake-claim", "fake-claim"]
160160

161-
def test_activestate_publisher_missing_claims(self, monkeypatch):
161+
@pytest.mark.parametrize(
162+
("claim_to_drop", "valid"),
163+
[
164+
("organization_id", False),
165+
("project_id", False),
166+
("user_id", False),
167+
("branch_id", True),
168+
("organization_url_name", True),
169+
("project_visibility", True),
170+
("project_name", True),
171+
("project_path", True),
172+
],
173+
)
174+
def test_activestate_publisher_missing_claims(
175+
self, monkeypatch, claim_to_drop: str, valid: bool
176+
):
162177
publisher = ActiveStatePublisher(
163-
sub="fake:fake:fake:fake:fake:fake",
164-
organization_id="fake",
165-
project_id="fake",
166-
user_id="fake",
167-
project_visibility="fake",
178+
sub=SUBJECT,
179+
organization_id=ORG_ID,
180+
project_id=PROJECT_ID,
181+
user_id=USER_ID,
168182
)
169183

170184
scope = pretend.stub()
@@ -178,21 +192,21 @@ def test_activestate_publisher_missing_claims(self, monkeypatch):
178192
)
179193
monkeypatch.setattr(_core, "sentry_sdk", sentry_sdk)
180194

181-
signed_claims = {
182-
claim_name: "fake" for claim_name in ActiveStatePublisher.all_known_claims()
183-
}
184-
signed_claims["sub"] = "fake:fake:fake:fake:fake:fake"
185-
# Pop the first signed claim, so that it's the first one to fail.
186-
signed_claims.pop("organization_id")
187-
assert "organization_id" not in signed_claims
195+
signed_claims = new_signed_claims()
196+
# I don't set branch_id in the claims by default, so no need to remove it
197+
if claim_to_drop != "branch_id":
198+
# Remove a required claim to test that the code detects it's missing
199+
signed_claims.pop(claim_to_drop)
200+
assert claim_to_drop not in signed_claims
188201
assert publisher.__required_verifiable_claims__
189-
assert not publisher.verify_claims(signed_claims=signed_claims)
190-
assert sentry_sdk.capture_message.calls == [
191-
pretend.call(
192-
"JWT for ActiveStatePublisher is missing claim: organization_id"
193-
)
194-
]
195-
assert scope.fingerprint == ["organization_id"]
202+
assert publisher.verify_claims(signed_claims=signed_claims) is valid
203+
if not valid:
204+
assert sentry_sdk.capture_message.calls == [
205+
pretend.call(
206+
"JWT for ActiveStatePublisher is missing claim: " + claim_to_drop
207+
)
208+
]
209+
assert scope.fingerprint == [claim_to_drop]
196210

197211
@pytest.mark.parametrize(
198212
("expect", "actual", "valid"),
@@ -205,11 +219,10 @@ def test_activestate_publisher_org_id_verified(
205219
self, expect: str, actual: str, valid: bool
206220
):
207221
publisher = ActiveStatePublisher(
208-
sub="fake:fake:fake:fake",
222+
sub=SUBJECT,
209223
organization_id=actual,
210224
project_id=PROJECT_ID,
211225
user_id=USER_ID,
212-
project_visibility="public",
213226
)
214227

215228
signed_claims = new_signed_claims(organization_id=expect)
@@ -218,66 +231,68 @@ def test_activestate_publisher_org_id_verified(
218231
@pytest.mark.parametrize(
219232
("expect", "actual", "valid"),
220233
[
221-
(PROJECT_ID, PROJECT_ID, True),
222-
(PROJECT_ID, ORG_ID, False),
234+
(BRANCH_ID, BRANCH_ID, True),
235+
(BRANCH_ID, PROJECT_ID, False),
236+
# If it's configured in the publisher, it must be present in the claim
237+
(BRANCH_ID, None, False),
238+
# If it's not configured in the publisher, we don't care what it is
239+
# in the claim
240+
(None, None, True),
241+
(None, PROJECT_ID, True),
223242
],
224243
)
225-
def test_activestate_publisher_project_id_verified(
244+
def test_activestate_publisher_branch_id_verified(
226245
self, expect: str, actual: str, valid: bool
227246
):
228247
publisher = ActiveStatePublisher(
229-
sub="fake:fake:fake:fake",
248+
sub=SUBJECT,
230249
organization_id=ORG_ID,
231-
project_id=actual,
250+
project_id=PROJECT_ID,
232251
user_id=USER_ID,
233-
project_visibility="public",
252+
branch_id=expect,
234253
)
235254

236-
signed_claims = new_signed_claims(project_id=expect)
255+
signed_claims = new_signed_claims(branch_id=actual)
237256
assert publisher.verify_claims(signed_claims=signed_claims) is valid
238257

239258
@pytest.mark.parametrize(
240259
("expect", "actual", "valid"),
241260
[
242-
(USER_ID, USER_ID, True),
243-
(USER_ID, ORG_ID, False),
261+
(PROJECT_ID, PROJECT_ID, True),
262+
(PROJECT_ID, ORG_ID, False),
244263
],
245264
)
246-
def test_activestate_publisher_user_id_verified(
265+
def test_activestate_publisher_project_id_verified(
247266
self, expect: str, actual: str, valid: bool
248267
):
249268
publisher = ActiveStatePublisher(
250-
sub="fake:fake:fake:fake",
269+
sub=SUBJECT,
251270
organization_id=ORG_ID,
252-
project_id=PROJECT_ID,
253-
user_id=actual,
254-
project_visibility="public",
271+
project_id=actual,
272+
user_id=USER_ID,
255273
)
256274

257-
signed_claims = new_signed_claims(user_id=expect)
275+
signed_claims = new_signed_claims(project_id=expect)
258276
assert publisher.verify_claims(signed_claims=signed_claims) is valid
259277

260278
@pytest.mark.parametrize(
261279
("expect", "actual", "valid"),
262280
[
263-
("public", "public", True),
264-
("private", "private", True),
265-
("public", "private", False),
266-
("private", "public", False),
281+
(USER_ID, USER_ID, True),
282+
(USER_ID, ORG_ID, False),
267283
],
268284
)
269-
def test_activestate_publisher_project_vis_verified(
285+
def test_activestate_publisher_user_id_verified(
270286
self, expect: str, actual: str, valid: bool
271287
):
272288
publisher = ActiveStatePublisher(
273-
sub="fake:fake:fake:fake",
289+
sub=SUBJECT,
274290
organization_id=ORG_ID,
275291
project_id=PROJECT_ID,
276-
user_id=USER_ID,
277-
project_visibility=actual,
292+
user_id=actual,
278293
)
279294

280-
signed_claims = new_signed_claims(project_visibility=expect)
295+
signed_claims = new_signed_claims(user_id=expect)
281296
assert publisher.verify_claims(signed_claims=signed_claims) is valid
282297

283298
@pytest.mark.parametrize(
@@ -317,7 +332,6 @@ def test_activestate_publisher_project_vis_verified(
317332
)
318333
def test_activestate_publisher_sub(self, expected: str, actual: str, valid: bool):
319334
check = ActiveStatePublisher.__required_verifiable_claims__["sub"]
320-
# claims: SignedClaims = new_signed_claims(sub=actual)
321335
assert check(expected, actual, pretend.stub()) is valid
322336

323337

@@ -337,8 +351,6 @@ def test_reify_does_not_exist_yet(self, db_request):
337351
)
338352
publisher = pending_publisher.reify(db_request.db)
339353

340-
# If an OIDC publisher for this pending publisher does not already exist,
341-
# a new one is created and the pending publisher is marked for deletion.
342354
assert isinstance(publisher, ActiveStatePublisher)
343355
assert pending_publisher in db_request.db.deleted
344356
assert publisher.organization_id == pending_publisher.organization_id
@@ -350,12 +362,9 @@ def test_reify_already_exists(self, db_request):
350362
organization_id=existing_publisher.organization_id,
351363
project_id=existing_publisher.project_id,
352364
branch_id=existing_publisher.branch_id,
353-
project_visibility=existing_publisher.project_visibility,
354365
sub=existing_publisher.sub,
355366
)
356367
publisher = pending_publisher.reify(db_request.db)
357368

358-
# If an OIDC publisher for this pending publisher already exists,
359-
# it is returned and the pending publisher is marked for deletion.
360369
assert existing_publisher == publisher
361370
assert pending_publisher in db_request.db.deleted

0 commit comments

Comments
 (0)