Skip to content

Commit 7395dd3

Browse files
committed
first try at an issuer
1 parent 7171f67 commit 7395dd3

28 files changed

+3225
-0
lines changed

.github/workflows/ci.yaml

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
name: wipac ci/cd
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
tags-ignore:
8+
- '**'
9+
10+
env:
11+
py_version: '3.13'
12+
REGISTRY_IMAGE: ghcr.io/wipacrepo/scitoken-issuer
13+
14+
jobs:
15+
16+
flake8:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
ref: ${{ github.sha }} # lock to triggered commit (github.ref is dynamic)
22+
- uses: astral-sh/ruff-action@v3
23+
24+
py-versions:
25+
runs-on: ubuntu-latest
26+
outputs:
27+
matrix: ${{ steps.versions.outputs.matrix }}
28+
steps:
29+
- uses: actions/checkout@v4
30+
with:
31+
ref: ${{ github.sha }} # lock to triggered commit (github.ref is dynamic)
32+
- id: versions
33+
uses: WIPACrepo/wipac-dev-py-versions-action@v2.5
34+
35+
mypy:
36+
needs: [ py-versions ]
37+
runs-on: ubuntu-latest
38+
strategy:
39+
max-parallel: 4
40+
fail-fast: false
41+
matrix:
42+
py3: ${{ fromJSON(needs.py-versions.outputs.matrix) }}
43+
steps:
44+
- uses: actions/checkout@v4
45+
with:
46+
ref: ${{ github.sha }} # lock to triggered commit (github.ref is dynamic)
47+
- uses: actions/setup-python@v5
48+
with:
49+
python-version: ${{ matrix.py3 }}
50+
- uses: WIPACrepo/wipac-dev-mypy-action@v2.0
51+
52+
tests:
53+
needs: [py-versions]
54+
runs-on: ubuntu-latest
55+
strategy:
56+
max-parallel: 4
57+
fail-fast: false
58+
matrix:
59+
version: ${{ fromJSON(needs.py-versions.outputs.matrix) }}
60+
services:
61+
keycloak:
62+
image: ghcr.io/wipacrepo/keycloak-rest-services:test-keycloak-master
63+
env:
64+
KEYCLOAK_ADMIN: admin
65+
KEYCLOAK_ADMIN_PASSWORD: admin
66+
CMD: start-dev
67+
ports:
68+
- 8080:8080
69+
mongo:
70+
image: mongo:8
71+
ports:
72+
- 27017:27017
73+
env:
74+
CI_TESTING: "true"
75+
IDP_ADDRESS: http://localhost:8080
76+
USERNAME: admin
77+
PASSWORD: admin
78+
steps:
79+
- uses: actions/checkout@v4
80+
with:
81+
ref: ${{ github.sha }} # lock to triggered commit (github.ref is dynamic)
82+
- uses: actions/setup-python@v5
83+
with:
84+
python-version: ${{ matrix.version }}
85+
- run: |
86+
pip install --upgrade pip wheel setuptools
87+
pip install -e .[tests]
88+
pytest -v --log-level debug --tb=short
89+
90+
docker-build:
91+
name: "Docker Image"
92+
runs-on: ubuntu-latest
93+
steps:
94+
# Note: we need to checkout the repository at the workflow sha in case during the workflow
95+
# the branch was updated. To keep PSR working with the configured release branches,
96+
# we force a checkout of the desired release branch but at the workflow sha HEAD.
97+
- name: Setup | Checkout Repository at workflow sha
98+
uses: actions/checkout@v4
99+
with:
100+
fetch-depth: 0
101+
ref: ${{ github.sha }}
102+
- name: Build Docker Image
103+
uses: docker/build-push-action@v4
104+
with:
105+
context: .
106+
push: false
107+
108+
release:
109+
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
110+
needs: [flake8, tests, docker-build]
111+
runs-on: ubuntu-latest
112+
concurrency: release
113+
114+
permissions:
115+
id-token: write
116+
contents: write
117+
118+
outputs:
119+
released: ${{ steps.release.outputs.released }}
120+
tag: ${{ steps.release.outputs.tag }}
121+
122+
steps:
123+
# Note: we need to checkout the repository at the workflow sha in case during the workflow
124+
# the branch was updated. To keep PSR working with the configured release branches,
125+
# we force a checkout of the desired release branch but at the workflow sha HEAD.
126+
- name: Setup | Checkout Repository at workflow sha
127+
uses: actions/checkout@v4
128+
with:
129+
fetch-depth: 0
130+
ref: ${{ github.sha }}
131+
132+
- name: Setup | Force correct release branch on workflow sha
133+
run: |
134+
git checkout -B ${{ github.ref_name }} ${{ github.sha }}
135+
136+
- name: Action | Semantic Version Release
137+
id: release
138+
# Adjust tag with desired version if applicable.
139+
uses: python-semantic-release/python-semantic-release@v9.8.1
140+
with:
141+
github_token: ${{ secrets.GITHUB_TOKEN }}
142+
git_committer_name: "github-actions"
143+
git_committer_email: "actions@users.noreply.github.com"
144+
145+
- name: Publish | Upload to GitHub Release Assets
146+
uses: python-semantic-release/publish-action@v9.16.1
147+
if: steps.release.outputs.released == 'true'
148+
with:
149+
github_token: ${{ secrets.GITHUB_TOKEN }}
150+
tag: ${{ steps.release.outputs.tag }}
151+
152+
docker_release:
153+
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' && needs.release.outputs.released == 'true' }}
154+
needs: [release]
155+
runs-on: ubuntu-latest
156+
strategy:
157+
fail-fast: false
158+
matrix:
159+
platform:
160+
- linux/amd64
161+
- linux/arm64
162+
163+
permissions:
164+
packages: write
165+
166+
steps:
167+
# Note: we checkout the just created tag here
168+
- name: Setup | Checkout Repository at tag
169+
uses: actions/checkout@v4
170+
with:
171+
fetch-depth: 0
172+
ref: ${{ needs.release.outputs.tag }}
173+
174+
- name: Prepare
175+
run: |
176+
platform=${{ matrix.platform }}
177+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
178+
179+
- name: Docker meta
180+
id: docker_meta
181+
uses: docker/metadata-action@v5
182+
with:
183+
images: ${{ env.REGISTRY_IMAGE }}
184+
185+
- name: Login to GitHub Container Registry
186+
uses: docker/login-action@v3
187+
with:
188+
registry: ghcr.io
189+
username: ${{ github.actor }}
190+
password: ${{ secrets.GITHUB_TOKEN }}
191+
192+
- name: Set up Docker Buildx
193+
uses: docker/setup-buildx-action@v3
194+
195+
- name: Push Docker Image
196+
id: build
197+
uses: docker/build-push-action@v6
198+
with:
199+
context: .
200+
platforms: ${{ matrix.platform }}
201+
build-args: |
202+
SETUPTOOLS_SCM_PRETEND_VERSION=${{ needs.release.outputs.tag }}
203+
tags: ${{ env.REGISTRY_IMAGE }}
204+
labels: ${{ steps.docker_meta.outputs.labels }}
205+
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
206+
207+
- name: Export digest
208+
run: |
209+
mkdir -p ${{ runner.temp }}/digests
210+
digest="${{ steps.build.outputs.digest }}"
211+
touch "${{ runner.temp }}/digests/${digest#sha256:}"
212+
213+
- name: Upload digest
214+
uses: actions/upload-artifact@v4
215+
with:
216+
name: digests-${{ env.PLATFORM_PAIR }}
217+
path: ${{ runner.temp }}/digests/*
218+
if-no-files-found: error
219+
retention-days: 1
220+
221+
docker_merge:
222+
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' && needs.release.outputs.released == 'true' }}
223+
needs: [release, docker_release]
224+
runs-on: ubuntu-latest
225+
226+
permissions:
227+
packages: write
228+
229+
steps:
230+
- name: Download digests
231+
uses: actions/download-artifact@v4
232+
with:
233+
path: ${{ runner.temp }}/digests
234+
pattern: digests-*
235+
merge-multiple: true
236+
237+
- name: Login to Docker Hub
238+
uses: docker/login-action@v3
239+
with:
240+
registry: ghcr.io
241+
username: ${{ github.actor }}
242+
password: ${{ secrets.GITHUB_TOKEN }}
243+
244+
- name: Set up Docker Buildx
245+
uses: docker/setup-buildx-action@v3
246+
247+
- name: Docker meta
248+
id: docker_meta
249+
uses: docker/metadata-action@v5
250+
with:
251+
images: |
252+
${{ env.REGISTRY_IMAGE }}
253+
tags: |
254+
type=semver,pattern={{major}},value=${{ needs.release.outputs.tag }}
255+
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release.outputs.tag }}
256+
type=semver,pattern={{major}}.{{minor}}.{{patch}},value=${{ needs.release.outputs.tag }}
257+
258+
- name: Create manifest list and push
259+
working-directory: ${{ runner.temp }}/digests
260+
run: |
261+
docker buildx imagetools create \
262+
$(echo $DOCKER_METADATA_OUTPUT_JSON | jq -cr '.tags | map("-t " + .) | join(" ")') \
263+
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
264+
# $(echo $DOCKER_METADATA_OUTPUT_JSON | jq -r '.annotations | map("--annotation \"" + . + "\"") | join(" ")')

Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM python:3.13
2+
3+
RUN groupadd -g 1000 app && useradd -m -g 1000 -u 1000 app
4+
5+
RUN mkdir /mnt/data
6+
7+
RUN mkdir /app
8+
WORKDIR /app
9+
10+
COPY src /app/src
11+
COPY pyproject.toml /app/pyproject.toml
12+
13+
RUN chown -R app:app /app
14+
15+
USER app
16+
17+
ENV VIRTUAL_ENV=/app/venv
18+
19+
RUN python3.13 -m venv $VIRTUAL_ENV
20+
21+
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
22+
23+
RUN --mount=source=.git,target=.git,type=bind pip install -e .
24+
25+
CMD ["python", "-m", "wlcg_token_claims"]

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,89 @@
11
# scitoken-issuer
22
IceCube / WIPAC SciToken Issuer
3+
4+
## Running the Issuer
5+
6+
After installing the package, run:
7+
8+
python -m scitoken_issuer
9+
10+
and it will start a web server capable of issuing tokens.
11+
12+
## Key Environment Variables
13+
14+
* IDP_ADDRESS - the full address to the upstream identity provider
15+
* IDP_CLIENT_ID - the upstream identity provider client id
16+
* IDP_CLIENT_SECRET - the upstream identity provider client secret
17+
* ISSUER_ADDRESS - the full address to this issuer
18+
* AUDIENCE - aud to add to tokens
19+
* POSIX_PATH - base path for group information lookup
20+
21+
If you use LDAP:
22+
* USE_LDAP - boolean
23+
* LDAP_URL - full address to ldap server
24+
* LDAP_USER_BASE - user OU
25+
* LDAP_GROUP_BASE - group OU
26+
27+
## Code Structure
28+
29+
Tests are in tests/
30+
Code is in src/scitoken_issuer/
31+
32+
* config.py - configuration variables from the environment
33+
* server.py - main server code
34+
* group_validation.py - validating scopes against a POSIX filesystem
35+
36+
37+
38+
## OpenID Details
39+
40+
OpenID flows supported:
41+
42+
* authorization code flow (with client secret)
43+
* device code flow
44+
* refresh flow
45+
46+
### OpenID URLs
47+
48+
* openid config `/.well-known/openid-configuration`
49+
* public certs in jwks format `/auth/certs`
50+
* token endpoint `/auth/token`
51+
* authorization endpoint `/auth/authorize`
52+
* user info endpoint `/auth/userinfo`
53+
* device auth endpoint `/auth/device/code`
54+
* client registration `/auth/client/registration`
55+
56+
57+
### authorization code flow
58+
59+
For browsers.
60+
61+
1. user requests token with scopes
62+
2. redirect to identity provider (authorization code flow)
63+
3. user logs in with IdP
64+
4. redirect back to token service
65+
5. do code exchange with IdP to gain identity token
66+
6. do scope auth
67+
7. return refresh + access tokens (or deny request)
68+
69+
### device code flow
70+
71+
For command line clients.
72+
73+
1. user requests token with scopes, via device grant
74+
2. return device + user code, local verification url
75+
2. verification url redirects to identity provider (authorization code flow)
76+
3. user logs in with IdP
77+
4. redirect back to token service
78+
5. do code exchange with IdP to gain identity token
79+
6. do scope auth
80+
7. return refresh + access tokens (or deny request) on /oauth/token poll
81+
82+
### refresh flow
83+
84+
For refresh tokens.
85+
86+
1. api request to token endpoint with refresh
87+
2. do we check with IdP here? hold a refresh token for IdP?
88+
3. do scope auth
89+
4. return refresh + access tokens (or deny request)

old_scitoken_issuer/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# version is a human-readable version number.
2+
3+
# version_info is a four-tuple for programmatic comparison. The first
4+
# three numbers are the components of the version number. The fourth
5+
# is zero for an official release, positive for a development branch,
6+
# or negative for a release candidate or beta (after the base version
7+
# number has been incremented)
8+
__version__ = "1.0.0"
9+
version_info = (
10+
int(__version__.split(".")[0]),
11+
int(__version__.split(".")[1]),
12+
int(__version__.split(".")[2]),
13+
0,
14+
)

0 commit comments

Comments
 (0)