Skip to content

Commit 6e13101

Browse files
authored
feat(secrethub): add project and org. name to OIDC token claims (#303)
## 📝 Description Adds `prj` (project name) and `org` (organization name) claims to the OIDC token. More in the [public discussion](#204). See also [this task](renderedtext/tasks#7975). ## ✅ Checklist - [x] I have tested this change - [x] This change requires documentation update
1 parent 74f99be commit 6e13101

File tree

13 files changed

+168
-64
lines changed

13 files changed

+168
-64
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ console.bash:
199199
docker compose $(DOCKER_COMPOSE_OPTS) build --build-arg BUILDKIT_INLINE_CACHE=$(BUILDKIT_INLINE_CACHE) --build-arg MIX_ENV=$(MIX_ENV) app
200200
docker compose $(DOCKER_COMPOSE_OPTS) run $(DOCKER_COMPOSE_RUN_OPTS) --rm app /bin/bash
201201

202+
console.sh:
203+
docker compose $(DOCKER_COMPOSE_OPTS) build --build-arg BUILDKIT_INLINE_CACHE=$(BUILDKIT_INLINE_CACHE) --build-arg MIX_ENV=$(MIX_ENV) app
204+
docker compose $(DOCKER_COMPOSE_OPTS) run $(DOCKER_COMPOSE_RUN_OPTS) --rm app /bin/sh
205+
202206
#
203207
# The default test.ex.setup target does nothing.
204208
# Each application's Makefile should override it as it sees fit.

docs/docs/reference/openid.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description: OpenID Connect (OIDC) token reference
33
---
44

5-
# OIDC Tokens
5+
# OIDC Tokens
66

77
import Tabs from '@theme/Tabs';
88
import TabItem from '@theme/TabItem';
@@ -23,9 +23,9 @@ Sure, here is the reordered list presented in a table with three columns: Claim,
2323

2424
| Claim | Description | Example |
2525
|-------------|-----------------------------------------------------------|--------------------------------------|
26-
| iss | The issuer of the token. The full URL of the organization | `https://<org-url>.semaphoreci.com` |
27-
| aud | The intended audience of the token. The full URL of the organization | `https://<org-url>.semaphoreci.com` |
28-
| sub | The subject of the token. A combination of org, project, repository, and git reference for which this token was issued<br/>Template:<br/> `org:<org-url>:`<br/>`project:{project-id}:`<br/>`repo:{repo-name}:`<br/>`ref_type:{branch or pr or tag}:`<br/>`ref:{git_reference}` | `org:{org-name}:`<br/>`project:936a5312-a3b8-4921-8b3f-2cec8baac574:`<br/>`repo:web:`<br/>`ref_type:branch:`<br/>`ref:refs/heads/main` |
26+
| iss | The issuer of the token. The full URL of the organization | `https://<org-name>.semaphoreci.com` |
27+
| aud | The intended audience of the token. The full URL of the organization | `https://<org-name>.semaphoreci.com` |
28+
| sub | The subject of the token. A combination of org, project, repository, and git reference for which this token was issued<br/>Template:<br/> `org:<org-name>:`<br/>`project:{project-id}:`<br/>`repo:{repo-name}:`<br/>`ref_type:{branch or pr or tag}:`<br/>`ref:{git_reference}` | `org:{org-name}:`<br/>`project:936a5312-a3b8-4921-8b3f-2cec8baac574:`<br/>`repo:web:`<br/>`ref_type:branch:`<br/>`ref:refs/heads/main` |
2929
| exp | The UNIX timestamp when the token expires | `1660317851` |
3030
| iat | The UNIX timestamp when the token was issued | `1660317851` |
3131
| nbf | The UNIX timestamp before which the token is not valid | `1660317851` |
@@ -38,7 +38,9 @@ Sure, here is the reordered list presented in a table with three columns: Claim,
3838
| tag | The name of the git tag for which the token was issued | `v1.0.0` |
3939
| repo | The name of the repository for which the token was issued | `web` |
4040
| repo_slug | Specifies the repository's name in the format `owner_name/repository_name` for the current Semaphore project. It is associated with the environment variable `SEMAPHORE_GIT_REPO_SLUG` | `semaphoreci/docs` |
41-
| org_id | The organization ID of the owner of the project for which the token was issued | `d7dd33ad-9317-498c-9cc6-51c250749be7` |
41+
| org | The organization name for which the token was issued | `semaphore` |
42+
| org_id | The organization ID for which the token was issued | `d7dd33ad-9317-498c-9cc6-51c250749be7` |
43+
| prj | The project name for which the token was issued | `docs` |
4244
| prj_id | The project ID for which the token was issued | `1e1fcfb5-09c0-487e-b051-2d0b5514c42a` |
4345
| wf_id | The ID of the workflow for which the token was issued | `1be81412-6ab8-4fc0-9d0d-7af33335a6ec` |
4446
| ppl_id | The pipeline ID for which the token was issued | `1e1fcfb5-09c0-487e-b051-2d0b5514c42a` |

secrethub/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ include ../Makefile
44

55
DOCKER_BUILD_PATH=..
66
APP_NAME=secrethub
7-
TMP_REPO_DIR=/tmp/internal_api
7+
TMP_REPO_DIR?=/tmp/internal_api
88
INTERNAL_API_BRANCH ?= master
99
PUBLIC_API_BRANCH ?= master
1010
PROTOC_TAG=1.12.1-3.17.3-0.7.1

secrethub/lib/internal_api/secrethub.pb.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,8 @@ defmodule InternalApi.Secrethub.GenerateOpenIDConnectTokenRequest do
719719
job_type: String.t(),
720720
git_pull_request_branch: String.t(),
721721
repo_slug: String.t(),
722-
triggerer: String.t()
722+
triggerer: String.t(),
723+
project_name: String.t()
723724
}
724725

725726
defstruct [
@@ -741,7 +742,8 @@ defmodule InternalApi.Secrethub.GenerateOpenIDConnectTokenRequest do
741742
:job_type,
742743
:git_pull_request_branch,
743744
:repo_slug,
744-
:triggerer
745+
:triggerer,
746+
:project_name
745747
]
746748

747749
field :org_id, 1, type: :string
@@ -763,6 +765,7 @@ defmodule InternalApi.Secrethub.GenerateOpenIDConnectTokenRequest do
763765
field :git_pull_request_branch, 17, type: :string
764766
field :repo_slug, 18, type: :string
765767
field :triggerer, 19, type: :string
768+
field :project_name, 20, type: :string
766769
end
767770

768771
defmodule InternalApi.Secrethub.GenerateOpenIDConnectTokenResponse do

secrethub/lib/secrethub/open_id_connect/jwt.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ defmodule Secrethub.OpenIDConnect.JWT do
9999
domain = Application.fetch_env!(:secrethub, :domain)
100100

101101
common_claims = %{
102+
"org" => req.org_username,
102103
"org_id" => req.org_id,
104+
"prj" => req.project_name,
103105
"prj_id" => req.project_id,
104106
"wf_id" => req.workflow_id,
105107
"ppl_id" => req.pipeline_id,

secrethub/lib/secrethub/open_id_connect/jwt_claim.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,22 @@ defmodule Secrethub.OpenIDConnect.JWTClaim do
242242
is_mandatory: false,
243243
is_active: true
244244
},
245+
"org" => %__MODULE__{
246+
name: "org",
247+
description: "Organization name",
248+
is_system_claim: true,
249+
is_aws_tag: false,
250+
is_mandatory: false,
251+
is_active: false
252+
},
253+
"prj" => %__MODULE__{
254+
name: "prj",
255+
description: "Project name associated with the workflow",
256+
is_system_claim: true,
257+
is_aws_tag: false,
258+
is_mandatory: false,
259+
is_active: false
260+
},
245261
"https://aws.amazon.com/tags" => %__MODULE__{
246262
name: "https://aws.amazon.com/tags",
247263
description:

secrethub/lib/secrethub/open_id_connect/jwt_configuration.ex

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ defmodule Secrethub.OpenIDConnect.JWTConfiguration do
115115
create_default_org_config(org_id)
116116

117117
config ->
118-
{:ok, config}
118+
{:ok, populate_missing_standard_claims(config)}
119119
end
120120
end
121121

@@ -163,7 +163,7 @@ defmodule Secrethub.OpenIDConnect.JWTConfiguration do
163163

164164
case Repo.one(query) do
165165
nil -> get_org_config(org_id)
166-
config -> {:ok, config}
166+
config -> {:ok, populate_missing_standard_claims(config)}
167167
end
168168
end
169169

@@ -229,7 +229,9 @@ defmodule Secrethub.OpenIDConnect.JWTConfiguration do
229229
updated_claims =
230230
Enum.map(claims, fn claim -> enforce_default_claim_values(claim, standard_claims) end)
231231

232-
put_change(changeset, :claims, updated_claims)
232+
missing_claims = find_missing_standard_claims(updated_claims)
233+
234+
put_change(changeset, :claims, updated_claims ++ missing_claims)
233235
end
234236

235237
defp enforce_default_claim_values(claim = %{"name" => name}, standard_claims)
@@ -292,4 +294,29 @@ defmodule Secrethub.OpenIDConnect.JWTConfiguration do
292294
end
293295

294296
defp filter_supported_fields(claim), do: claim
297+
298+
# Populates missing standard claims (from JWTClaim) in the JWTConfig
299+
defp populate_missing_standard_claims(config = %{claims: claims}) do
300+
missing_claims = find_missing_standard_claims(claims)
301+
302+
%{config | claims: claims ++ missing_claims}
303+
end
304+
305+
defp find_missing_standard_claims(claims) do
306+
standard_claims = Secrethub.OpenIDConnect.JWTClaim.standard_claims()
307+
present_claim_names = MapSet.new(Enum.map(claims, & &1["name"]))
308+
309+
standard_claims
310+
|> Enum.reject(fn {name, _claim} -> name in present_claim_names end)
311+
|> Enum.map(fn {_name, claim} ->
312+
%{
313+
"name" => claim.name,
314+
"description" => claim.description,
315+
"is_system_claim" => claim.is_system_claim,
316+
"is_aws_tag" => claim.is_aws_tag,
317+
"is_mandatory" => claim.is_mandatory,
318+
"is_active" => claim.is_active
319+
}
320+
end)
321+
end
295322
end

secrethub/test/secrethub/internal_grpc_api_test.exs

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,8 @@ defmodule Secrethub.InternalGrpcApi.Test do
809809
workflow_id: Ecto.UUID.generate(),
810810
pipeline_id: Ecto.UUID.generate(),
811811
job_id: Ecto.UUID.generate(),
812-
job_type: "pipeline_job"
812+
job_type: "pipeline_job",
813+
project_name: "my-project"
813814
)
814815

815816
{:ok, channel} = GRPC.Stub.connect("localhost:50051")
@@ -826,13 +827,16 @@ defmodule Secrethub.InternalGrpcApi.Test do
826827
assert_in_delta Map.get(jwt.fields, "exp") + req.expires_in, now, 5
827828

828829
assert Map.get(jwt.fields, "prj_id") == req.project_id
830+
assert Map.get(jwt.fields, "org_id") == org_id
829831
assert Map.get(jwt.fields, "wf_id") == req.workflow_id
830832
assert Map.get(jwt.fields, "ppl_id") == req.pipeline_id
831833
assert Map.get(jwt.fields, "job_id") == req.job_id
832834
assert Map.get(jwt.fields, "job_type") == req.job_type
833835
assert Map.get(jwt.fields, "aud") == "https://testera.localhost"
834836
assert Map.get(jwt.fields, "iss") == "https://testera.localhost"
835837
assert Map.get(jwt.fields, "sub") == "project:front:pipeline:semaphore.yml"
838+
assert Map.get(jwt.fields, "prj") == req.project_name
839+
assert Map.get(jwt.fields, "org") == req.org_username
836840
refute Map.has_key?(jwt.fields, "https://aws.amazon.com/tags")
837841
end
838842

@@ -925,7 +929,8 @@ defmodule Secrethub.InternalGrpcApi.Test do
925929
git_ref_type: "branch",
926930
job_type: "debug_job",
927931
repo_slug: "renderedtext/front",
928-
triggerer: "h:f-i:f"
932+
triggerer: "h:f-i:f",
933+
project_name: "front"
929934
)
930935

931936
with_mock Secrethub, on_prem?: fn -> true end do
@@ -946,6 +951,9 @@ defmodule Secrethub.InternalGrpcApi.Test do
946951
# Project related claims should be present
947952
assert Map.get(jwt.fields, "prj_id") == req.project_id
948953
assert Map.get(jwt.fields, "job_type") == req.job_type
954+
assert Map.get(jwt.fields, "org_id") == org_id
955+
refute Map.has_key?(jwt.fields, "prj")
956+
refute Map.has_key?(jwt.fields, "org")
949957

950958
# AWS tags should be filtered
951959
aws_tags = Map.get(jwt.fields, "https://aws.amazon.com/tags")
@@ -1128,17 +1136,19 @@ defmodule Secrethub.InternalGrpcApi.Test do
11281136
project_id: project_id
11291137
)
11301138

1131-
{:ok, get_response} = SecretService.Stub.get_jwt_config(channel, get_req)
1132-
[stored_claim] = get_response.claims
1133-
refute is_nil(stored_claim), "Expected original valid claim to still be present"
1134-
assert stored_claim.name == "test_claim"
1135-
assert stored_claim.description == "Test claim"
1136-
assert stored_claim.is_active == true
1137-
# can't update for non system claims
1138-
assert stored_claim.is_mandatory == false
1139-
assert stored_claim.is_aws_tag == false
1140-
# can't update for non system claims
1141-
assert stored_claim.is_system_claim == false
1139+
{:ok, response} = SecretService.Stub.get_jwt_config(channel, get_req)
1140+
1141+
assert Enum.any?(response.claims, fn claim ->
1142+
claim == %InternalApi.Secrethub.ClaimConfig{
1143+
name: "test_claim",
1144+
description: "Test claim",
1145+
is_active: true,
1146+
# can't update for non system claims
1147+
is_mandatory: false,
1148+
is_aws_tag: false,
1149+
is_system_claim: false
1150+
}
1151+
end)
11421152
end
11431153
end
11441154

@@ -1190,18 +1200,17 @@ defmodule Secrethub.InternalGrpcApi.Test do
11901200
assert response.project_id == project_id
11911201
assert response.is_active == true
11921202

1193-
[stored_claim] = response.claims
1194-
1195-
assert stored_claim.name == expected_claim.name,
1196-
"Expected claim name to be #{expected_claim.name}, got #{stored_claim.name}"
1197-
1198-
assert stored_claim.description == expected_claim.description
1199-
assert stored_claim.is_active == expected_claim.is_active
1200-
# can't update for non system claims
1201-
assert stored_claim.is_mandatory == false
1202-
assert stored_claim.is_aws_tag == expected_claim.is_aws_tag
1203-
# can't update for non system claims
1204-
assert stored_claim.is_system_claim == false
1203+
assert Enum.any?(response.claims, fn claim ->
1204+
Map.take(claim, Map.keys(expected_claim)) == %InternalApi.Secrethub.ClaimConfig{
1205+
name: expected_claim.name,
1206+
description: expected_claim.description,
1207+
is_active: expected_claim.is_active,
1208+
# can't update for non system claims
1209+
is_mandatory: false,
1210+
is_aws_tag: false,
1211+
is_system_claim: false
1212+
}
1213+
end)
12051214
end
12061215

12071216
test "returns org config for non-existent project", %{org_id: org_id, channel: channel} do
@@ -1239,15 +1248,18 @@ defmodule Secrethub.InternalGrpcApi.Test do
12391248
assert response.org_id == org_id
12401249
refute is_nil(response.project_id), "Expected project_id not to be nil"
12411250

1242-
[stored_claim] = response.claims
1243-
assert stored_claim.name == "sub2"
1244-
assert stored_claim.description == "Subject identifier"
1245-
assert stored_claim.is_active == true
1246-
# can't update for non system claims
1247-
assert stored_claim.is_mandatory == false
1248-
assert stored_claim.is_aws_tag == false
1249-
# can't update for non system claims
1250-
assert stored_claim.is_system_claim == false
1251+
expected_claim = %{
1252+
name: "sub2",
1253+
description: "Subject identifier",
1254+
is_active: true,
1255+
is_mandatory: false,
1256+
is_aws_tag: false,
1257+
is_system_claim: false
1258+
}
1259+
1260+
assert Enum.any?(response.claims, fn claim ->
1261+
Map.take(claim, Map.keys(expected_claim)) == expected_claim
1262+
end)
12511263
end
12521264

12531265
test ".get_jwt_config returns error for empty org_id" do

0 commit comments

Comments
 (0)