Skip to content

Commit 76e4040

Browse files
authored
refactor: authorization model to use .github/allowed_repos.json (#2)
* refactor: authorization model to use .github/allowed_repos.json - Introduced .github/allowed_repos.json as the source of truth for authorized repositories, replacing the ALLOWED_REPOS secret. - Updated workflows and documentation to reflect the new file-based allowlist approach. - Removed ALLOWED_REPOS references from Makefile and README, enhancing clarity on authorization process. This change improves maintainability and security by centralizing the allowlist within the repository. Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com> * tests: Enhance Makefile and README with new testing targets and documentation - Added `test-allowlist` target to run allowlist validation and authorization logic tests without Docker. - Introduced `test-all` target to execute both allowlist tests and the full act workflow test. - Updated help section in Makefile to reflect new targets and improved descriptions for existing commands. - Enhanced README with instructions for running allowlist tests and clarified descriptions for make targets. These changes improve the testing framework and documentation for better usability and clarity. Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com> --------- Signed-off-by: Jonathan Kilzi <jkilzi@redhat.com>
1 parent 751b176 commit 76e4040

File tree

6 files changed

+198
-49
lines changed

6 files changed

+198
-49
lines changed

.github/workflows/generate-and-publish.yml

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# Output: Generated client is placed in the 'generated-client' directory
99
#
1010
# Authorization: Calls from this repository are automatically authorized.
11-
# External repositories must be listed in the ALLOWED_REPOS secret.
11+
# External repositories must be listed in .github/allowed_repos.json (source of truth).
1212
#
1313
# Publishing Authentication (in order of precedence):
1414
# 1. npm Trusted Publishing (OIDC) - preferred, no long-lived tokens needed
@@ -55,19 +55,16 @@ on:
5555
type: boolean
5656
default: false
5757
secrets:
58-
ALLOWED_REPOS:
59-
description: 'JSON array of authorized repository names'
60-
required: true
6158
NPM_TOKEN:
62-
description: 'npm access token - fallback for when OIDC is unavailable (local testing with act-cli)'
59+
description: 'npm access token - fallback for when OIDC is unavailable (e.g.: local testing with act-cli)'
6360
required: false
6461

6562
jobs:
6663
# ===========================================================================
6764
# Job 1: Authorization Check
6865
# ===========================================================================
6966
# This repository is automatically authorized (for CI/testing).
70-
# External repositories must be listed in ALLOWED_REPOS secret.
67+
# External repositories must be listed in .github/allowed_repos.json.
7168
# ===========================================================================
7269
authorize:
7370
name: Verify Authorization
@@ -78,30 +75,30 @@ jobs:
7875
- name: Verify caller is authorized
7976
id: check
8077
env:
81-
ALLOWED_REPOS: ${{ secrets.ALLOWED_REPOS }}
8278
CALLER_REPO: ${{ github.repository }}
8379
THIS_REPO: "kubev2v/migration-planner-client-generator"
80+
ALLOWED_REPOS_URL: "https://raw.githubusercontent.com/kubev2v/migration-planner-client-generator/main/.github/allowed_repos.json"
8481
run: |
82+
set -e
8583
# Allow calls from this repository (for CI/testing)
8684
if [ "$CALLER_REPO" = "$THIS_REPO" ]; then
8785
echo "authorized=true" >> $GITHUB_OUTPUT
8886
echo "✅ Repository is authorized (same-repo call)"
8987
exit 0
9088
fi
9189
92-
# For external callers, validate ALLOWED_REPOS secret
90+
# Fetch allowed repos list (source of truth in this repo)
91+
ALLOWED_REPOS=$(curl -sL "$ALLOWED_REPOS_URL") || true
9392
if [ -z "$ALLOWED_REPOS" ]; then
94-
echo "::error::ALLOWED_REPOS secret is not configured"
93+
echo "::error::Failed to fetch .github/allowed_repos.json"
9594
exit 1
9695
fi
9796
98-
# Validate secret is valid JSON
9997
if ! echo "$ALLOWED_REPOS" | jq empty 2>/dev/null; then
100-
echo "::error::ALLOWED_REPOS secret is not valid JSON"
98+
echo "::error::allowed_repos.json is not valid JSON"
10199
exit 1
102100
fi
103101
104-
# Check if caller repo is in the allowed list (exact match)
105102
if echo "$ALLOWED_REPOS" | jq -e --arg repo "$CALLER_REPO" 'index($repo) != null' > /dev/null; then
106103
echo "authorized=true" >> $GITHUB_OUTPUT
107104
echo "✅ Repository is authorized"

AGENTS.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ This is a **GitOps repository** providing a reusable GitHub Actions workflow for
1212

1313
```
1414
.
15-
├── .github/workflows/
16-
│ ├── generate-and-publish.yml # Main reusable workflow (workflow_call)
17-
│ └── test.yml # CI test workflow
15+
├── .github/
16+
│ ├── allowed_repos.json # Source of truth: repos allowed to call the workflow
17+
│ └── workflows/
18+
│ ├── generate-and-publish.yml # Main reusable workflow (workflow_call)
19+
│ └── test.yml # CI test workflow
1820
├── .actrc # act-cli configuration
1921
├── .gitignore
2022
├── LICENSE # Apache-2.0
@@ -32,8 +34,8 @@ This is a **GitOps repository** providing a reusable GitHub Actions workflow for
3234

3335
2. **Authorization Model**
3436
- **Self-authorization**: This repository is automatically authorized (for CI/testing)
35-
- External repos validated via `jq` exact-match against `ALLOWED_REPOS` secret
36-
- Secret contains JSON array: `["org/repo1", "org/repo2"]`
37+
- External repos validated via `.github/allowed_repos.json` (source of truth in this repo)
38+
- Workflow fetches the file from `main`; file is a JSON array: `["org/repo1", "org/repo2"]`
3739
- Fails fast before any generation occurs
3840

3941
## Critical Constraints
@@ -79,11 +81,9 @@ This workflow uses **npm Trusted Publishing (OIDC)** as the primary authenticati
7981
### Required Secrets
8082

8183
**In this repository:**
82-
- `ALLOWED_REPOS` - JSON array of authorized external repository names (this repo is auto-authorized)
8384
- `NPM_TOKEN` - *(optional)* npm access token, only needed for local testing fallback
8485

8586
**In calling repositories:**
86-
- `ALLOWED_REPOS` - Must be passed through to the reusable workflow
8787
- Calling workflow must have `permissions: id-token: write` for OIDC
8888
- Use `secrets: inherit` to pass OIDC permissions to the reusable workflow
8989

@@ -133,7 +133,6 @@ For local publish testing:
133133
```bash
134134
# .secrets file for local publish testing
135135
NPM_TOKEN=npm_your-real-granular-token-here
136-
ALLOWED_REPOS=["kubev2v/migration-planner-client-generator"]
137136
```
138137

139138
### CI Feature Toggle
@@ -166,9 +165,9 @@ When publishing is enabled, the test package is automatically unpublished after
166165

167166
## Security Considerations
168167

169-
1. **Never expose the ALLOWED_REPOS list** - Error messages should not reveal authorized repos
170-
2. **Use exact string matching** - `jq index()` prevents partial name attacks
171-
3. **Validate JSON before parsing** - Use `jq empty` to verify format
168+
1. **Use exact string matching** - `jq index()` prevents partial name attacks
169+
2. **Validate JSON before parsing** - Use `jq empty` to verify format before use
170+
3. **Allowed list is in repo** - `.github/allowed_repos.json` is the source of truth; only maintainers can change it
172171
4. **Secrets are masked** - GitHub automatically masks secret values in logs
173172

174173
## Related Repositories
@@ -182,9 +181,9 @@ When publishing is enabled, the test package is automatically unpublished after
182181

183182
### Adding a New Authorized Repository
184183

185-
1. Update the `ALLOWED_REPOS` secret in GitHub Settings
184+
1. Edit `.github/allowed_repos.json` in this repository
186185
2. Add the repo to the JSON array: `["existing/repo", "new/repo"]`
187-
3. No code changes required
186+
3. Commit and push to `main` (workflow fetches from `main`)
188187

189188
### Updating Generator Settings
190189

@@ -199,6 +198,5 @@ When publishing is enabled, the test package is automatically unpublished after
199198

200199
1. If calling from this repo: Should auto-authorize; check workflow syntax
201200
2. If calling from external repo:
202-
- Check `ALLOWED_REPOS` secret is valid JSON
203-
- Verify exact repository name match (case-sensitive)
204-
- Ensure calling workflow passes `ALLOWED_REPOS` secret through
201+
- Ensure the repo is listed in `.github/allowed_repos.json` (exact `owner/repo` match, case-sensitive)
202+
- Check that the file is valid JSON and fetchable from `main` (raw GitHub URL)

Makefile

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,29 @@
44
# Local testing with act-cli
55
# =============================================================================
66

7-
.PHONY: help test test-publish test-verbose setup-secrets clean check-act
7+
.PHONY: help test test-publish test-verbose test-allowlist test-all setup-secrets clean check-act
88

99
# Default target
1010
help:
1111
@echo "OpenAPI Client Generator - Local Testing"
1212
@echo ""
1313
@echo "Usage:"
14-
@echo " make setup-secrets Create .secrets file template"
15-
@echo " make test Run test workflow (dry-run mode, no npm publish)"
16-
@echo " make test-publish Run test workflow with actual npm publishing"
14+
@echo " make setup-secrets Create .secrets file template"
15+
@echo " make test Run test workflow via act (dry-run mode, no npm publish)"
16+
@echo " make test-publish Run test workflow with actual npm publishing"
1717
@echo " make test-verbose Run test workflow with verbose output"
18-
@echo " make clean Remove generated artifacts"
19-
@echo " make check-act Verify act-cli is installed"
18+
@echo " make test-allowlist Run allowlist validation and authorize logic tests (no Docker)"
19+
@echo " make test-all Run test-allowlist then test (act)"
20+
@echo " make clean Remove generated artifacts"
21+
@echo " make check-act Verify act-cli is installed"
2022
@echo ""
2123
@echo "Variables:"
2224
@echo " GITHUB_REPOSITORY Override github.repository context (default: kubev2v/migration-planner-client-generator)"
2325
@echo " TEST_NPM_PUBLISH Set to 'true' to enable npm publishing (default: empty/dry-run)"
2426
@echo ""
2527
@echo "Prerequisites:"
26-
@echo " - act-cli: brew install act"
27-
@echo " - Docker: must be running"
28+
@echo " - act-cli: brew install act (for make test)"
29+
@echo " - Docker (or Podman): must be running (for make test)"
2830

2931
# -----------------------------------------------------------------------------
3032
# Setup
@@ -45,9 +47,6 @@ setup-secrets:
4547
echo '# (OIDC/Trusted Publishing does not work locally, so token auth is used as fallback)' >> .secrets; \
4648
echo 'NPM_TOKEN=fake-token-for-testing' >> .secrets; \
4749
echo '' >> .secrets; \
48-
echo '# Allowed repositories (JSON array)' >> .secrets; \
49-
echo '# Include this repo for local testing' >> .secrets; \
50-
echo 'ALLOWED_REPOS=["kubev2v/migration-planner-client-generator"]' >> .secrets; \
5150
echo "✅ Created .secrets file"; \
5251
echo ""; \
5352
echo "Edit .secrets if you need to customize the values."; \
@@ -78,6 +77,17 @@ ACT_FLAGS = --secret-file .secrets \
7877
--var TEST_NPM_PUBLISH=$(TEST_NPM_PUBLISH) \
7978
-P ubuntu-latest=catthehacker/ubuntu:act-latest
8079

80+
# Run allowlist and authorize logic tests (no Docker/act)
81+
test-allowlist:
82+
@echo "🧪 Running allowlist and authorize logic tests..."
83+
@./tests/validate_allowed_repos.sh
84+
@./tests/test_authorize_logic.sh
85+
@echo "✅ test-allowlist passed"
86+
87+
# Run allowlist tests then full act workflow test
88+
test-all: test-allowlist test
89+
@echo "✅ test-all passed"
90+
8191
# Run test workflow (dry-run mode - no actual publishing)
8292
test: check-act
8393
@if [ ! -f .secrets ]; then \

README.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ This workflow uses **npm Trusted Publishing (OIDC)** as the primary authenticati
7171

7272
| Secret | Required | Description |
7373
|--------|----------|-------------|
74-
| `ALLOWED_REPOS` | Yes | JSON array of authorized repository names |
7574
| `NPM_TOKEN` | No | Only needed for local testing with act-cli (OIDC fallback) |
7675

76+
Authorization is controlled by `.github/allowed_repos.json` in this repository (see [Authorization Model](#authorization-model)).
77+
7778
## Generator Configuration
7879

7980
The workflow uses hardcoded generator settings that **cannot be modified by callers**:
@@ -96,26 +97,27 @@ These settings match the configuration in [kubev2v/migration-planner-ui](https:/
9697
The workflow includes a mandatory authorization check before generating and publishing clients.
9798

9899
1. **Self-Authorization**: Calls from this repository (`kubev2v/migration-planner-client-generator`) are automatically authorized for CI/testing purposes
99-
2. **Secret-based Allowlist**: External repositories must be listed in the `ALLOWED_REPOS` secret (JSON array)
100+
2. **File-based Allowlist**: External repositories must be listed in `.github/allowed_repos.json` in this repository (source of truth)
100101
3. **Exact Match**: Uses `jq` for precise string matching (no partial matches)
101102
4. **Fail-Fast**: Unauthorized requests are rejected before any generation occurs
102103

103-
### Configuring Authorization
104+
The workflow fetches the allowlist from the `main` branch at run time, so updates take effect as soon as they are merged.
105+
106+
### Adding an Authorized Repository
104107

105-
Set the `ALLOWED_REPOS` secret in this repository with a JSON array of external repositories:
108+
Edit `.github/allowed_repos.json` in this repository and add the repo to the JSON array:
106109

107110
```json
108-
["kubev2v/migration-planner", "kubev2v/migration-planner-ui"]
111+
["kubev2v/migration-planner", "kubev2v/assisted-migration-agent"]
109112
```
110113

111-
> **Note**: You don't need to add this repository to `ALLOWED_REPOS` - it's automatically authorized.
114+
> **Note**: You don't need to add this repository to the file—it's automatically authorized.
112115

113116
### Security Features
114117

115-
- Allowlist is stored as a secret (never exposed in logs or workflow file)
118+
- Single source of truth in the repo; only maintainers can change the list
116119
- Exact string matching prevents partial name attacks
117120
- JSON format is validated before use
118-
- Error messages don't reveal which repos are authorized
119121

120122
## Local Development
121123

@@ -140,11 +142,23 @@ make test-publish
140142
make clean
141143
```
142144

145+
### Allowlist tests (no Docker)
146+
147+
Run allowlist validation and authorize logic tests without act/Docker:
148+
149+
```bash
150+
make test-allowlist
151+
```
152+
153+
These tests validate `.github/allowed_repos.json` (valid JSON, array of strings) and that the same allow/deny logic as the workflow behaves correctly (same-repo allowed, list lookup, exact match).
154+
143155
### Make Targets
144156

145157
| Command | Description |
146158
|---------|-------------|
147-
| `make test` | Run workflow in dry-run mode (tests generation/build only) |
159+
| `make test` | Run workflow in dry-run mode via act (tests generation/build only) |
160+
| `make test-allowlist` | Run allowlist and authorize logic tests (no Docker) |
161+
| `make test-all` | Run test-allowlist then make test |
148162
| `make test-publish` | Run workflow with actual npm publishing + cleanup |
149163
| `make test-verbose` | Run workflow with verbose output |
150164
| `make setup-secrets` | Create `.secrets` file template |
@@ -166,12 +180,16 @@ When enabled, test packages are automatically unpublished after successful publi
166180
```
167181
.
168182
├── .github/
183+
│ ├── allowed_repos.json # Allowed callers (source of truth)
169184
│ └── workflows/
170185
│ ├── generate-and-publish.yml # Reusable workflow
171186
│ └── test.yml # CI test workflow
187+
├── tests/
188+
│ ├── validate_allowed_repos.sh # Validate allowlist JSON
189+
│ └── test_authorize_logic.sh # Test authorize allow/deny logic
172190
├── .actrc # act-cli configuration
173191
├── .gitignore # Ignores generated-client/, secrets, etc.
174-
├── Makefile # Local testing commands (make test, make clean)
192+
├── Makefile # Local testing commands (make test, make test-allowlist, etc.)
175193
├── AGENTS.md # AI agent guidelines
176194
├── LICENSE # Apache-2.0
177195
└── README.md # This file

tests/test_authorize_logic.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# Test authorize logic (same as workflow: same-repo shortcut + allowlist check)
4+
# =============================================================================
5+
# Uses local .github/allowed_repos.json (no curl). Verifies:
6+
# - Same repo (THIS_REPO) is always authorized
7+
# - Repo in allowed_repos.json is authorized
8+
# - Repo not in list is denied
9+
# =============================================================================
10+
set -e
11+
12+
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
13+
ALLOWED_REPOS_FILE="${REPO_ROOT}/.github/allowed_repos.json"
14+
THIS_REPO="kubev2v/migration-planner-client-generator"
15+
16+
assert_authorized() {
17+
local caller_repo="$1"
18+
local list_content="$2"
19+
if [ "$caller_repo" = "$THIS_REPO" ]; then
20+
return 0
21+
fi
22+
echo "$list_content" | jq -e --arg repo "$caller_repo" 'index($repo) != null' >/dev/null
23+
}
24+
25+
assert_unauthorized() {
26+
local caller_repo="$1"
27+
local list_content="$2"
28+
if [ "$caller_repo" = "$THIS_REPO" ]; then
29+
echo "Expected unauthorized but same-repo is always allowed: $caller_repo"
30+
return 1
31+
fi
32+
! echo "$list_content" | jq -e --arg repo "$caller_repo" 'index($repo) != null' >/dev/null 2>&1
33+
}
34+
35+
list=$(cat "$ALLOWED_REPOS_FILE")
36+
if ! echo "$list" | jq empty 2>/dev/null; then
37+
echo "::error::allowed_repos.json is not valid JSON (run validate_allowed_repos.sh first)"
38+
exit 1
39+
fi
40+
41+
errors=0
42+
43+
# Same repo is always authorized (no list lookup)
44+
if assert_authorized "$THIS_REPO" "[]"; then
45+
echo "✅ Same-repo ($THIS_REPO) is authorized (shortcut)"
46+
else
47+
echo "::error::Same-repo should be authorized"
48+
errors=$((errors + 1))
49+
fi
50+
51+
# First repo in the list should be authorized when used as caller
52+
first_repo=$(echo "$list" | jq -r '.[0]')
53+
if [ -n "$first_repo" ] && [ "$first_repo" != "null" ]; then
54+
if assert_authorized "$first_repo" "$list"; then
55+
echo "✅ Caller in list ($first_repo) is authorized"
56+
else
57+
echo "::error::Caller in list ($first_repo) should be authorized"
58+
errors=$((errors + 1))
59+
fi
60+
fi
61+
62+
# Random repo not in list should be unauthorized
63+
if assert_unauthorized "other-org/other-repo" "$list"; then
64+
echo "✅ Caller not in list (other-org/other-repo) is denied"
65+
else
66+
echo "::error::Caller not in list should be denied"
67+
errors=$((errors + 1))
68+
fi
69+
70+
# Partial match should be denied (exact match only)
71+
if echo "$list" | jq -e --arg repo "kubev2v/migration-planner-fake" 'index($repo) != null' >/dev/null 2>&1; then
72+
echo "::error::Partial repo name should not match"
73+
errors=$((errors + 1))
74+
else
75+
echo "✅ Partial repo name does not match (exact match only)"
76+
fi
77+
78+
if [ $errors -gt 0 ]; then
79+
echo "::error::$errors assertion(s) failed"
80+
exit 1
81+
fi
82+
83+
echo "✅ All authorize logic tests passed"

0 commit comments

Comments
 (0)