Skip to content

Commit 6dc7f1c

Browse files
gmondelloclaude
andcommitted
Auto-generate custom role permissions reference from IAM metadata
Adds a generator + workflow that rebuild the custom-role permissions reference from custom-role-metadata.yaml in docker/iam-services (the source of truth), replacing the manual updates done by hand. - scripts/generate-custom-role-permissions.mjs rewrites the tables between generation markers; frontmatter, intro, and "Next steps" are hand-maintained. - .github/workflows/sync-custom-role-permissions.yml regenerates the page and opens a PR when triggered by docker/iam-services. The regenerated page adds the Licensing and Cloud Sandboxes sections and the DHI "Manage customizations" row (from iam-services#1172 and #1173), superseding the manual #26324. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 547efd2 commit 6dc7f1c

3 files changed

Lines changed: 257 additions & 8 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Sync custom role permissions
2+
3+
# Regenerates the custom-role permissions reference from the IAM source of truth
4+
# (custom-role-metadata.yaml in docker/iam-services) and opens a PR for review.
5+
# Triggered by the "Sync docs permissions" workflow in docker/iam-services when
6+
# that file changes on main; can also be run manually.
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
ref:
12+
description: 'The git ref in docker/iam-services to read custom-role-metadata.yaml from.'
13+
type: string
14+
required: false
15+
default: 'main'
16+
17+
concurrency:
18+
group: sync-custom-role-permissions
19+
cancel-in-progress: true
20+
21+
jobs:
22+
sync:
23+
name: Sync custom role permissions reference
24+
runs-on: ubuntu-latest
25+
steps:
26+
- id: setup
27+
uses: docker/actions/setup@707e5d98a430d92e8e6c2d34faa5bfb2a9149e9b
28+
with:
29+
app_id: ${{ secrets.HUB_PLATFORM_APP_ID }}
30+
app_private_key: ${{ secrets.HUB_PLATFORM_APP_PRIVATE_KEY }}
31+
32+
- name: Checkout docs
33+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
34+
with:
35+
token: ${{ steps.setup.outputs.token }}
36+
show-progress: false
37+
38+
- name: Fetch custom-role-metadata.yaml from iam-services
39+
env:
40+
GH_TOKEN: ${{ steps.setup.outputs.token }}
41+
REF: ${{ inputs.ref }}
42+
run: |
43+
gh api \
44+
"repos/docker/iam-services/contents/internal/services/access_control/config/custom-role-metadata.yaml?ref=${REF}" \
45+
-H "Accept: application/vnd.github.raw+json" \
46+
> /tmp/custom-role-metadata.yaml
47+
48+
- name: Generate permissions reference
49+
run: |
50+
node scripts/generate-custom-role-permissions.mjs \
51+
--metadata /tmp/custom-role-metadata.yaml \
52+
--doc content/manuals/security/roles-and-permissions/custom-roles/permissions-reference.md
53+
54+
- name: Create PR
55+
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0
56+
with:
57+
token: ${{ steps.setup.outputs.token }}
58+
title: '⚙️ Sync custom role permissions reference'
59+
body: |
60+
Automatically generated from
61+
[`custom-role-metadata.yaml`](https://github.com/docker/iam-services/blob/main/internal/services/access_control/config/custom-role-metadata.yaml)
62+
in `docker/iam-services` by the **Sync custom role permissions**
63+
workflow, triggered by a change to that file.
64+
65+
Only the content between the generation markers in
66+
`permissions-reference.md` is updated. If this change is unexpected
67+
or shouldn't ship yet, close this PR — the workflow will re-open it
68+
the next time the source changes.
69+
commit-message: 'Sync custom role permissions reference'
70+
sign-commits: true
71+
add-paths: |
72+
content/manuals/security/roles-and-permissions/custom-roles/permissions-reference.md
73+
branch: chore/sync-custom-role-permissions
74+
delete-branch: true

‎content/manuals/security/roles-and-permissions/custom-roles/permissions-reference.md‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Custom roles use permissions from organization management, Docker Hub,
2020
billing, AI Governance, Docker Hardened Images, and Docker Offload. Use
2121
the following tables to [create or edit a custom role](manage.md).
2222

23+
<!-- BEGIN GENERATED: custom-role-permissions -->
24+
2325
## Organization management
2426

2527
| Permission | Description |
@@ -45,11 +47,11 @@ the following tables to [create or edit a custom role](manage.md).
4547

4648
## Docker Hub
4749

48-
| Permission | Description |
49-
| :--------------------------------- | :------------------------------------------------------------------------------------------- |
50-
| View repositories | View repository details and contents |
51-
| Manage repositories | Full repository management including settings, webhooks, privacy, Dockerfile, immutable tags |
52-
| Manage repository team permissions | Add and remove teams from a repository, and manage team access level |
50+
| Permission | Description |
51+
| :--------------------------------- | :-------------------------------------------------------------------------------------------- |
52+
| View repositories | View repository details and contents |
53+
| Manage repositories | Full repository management including settings, webhooks, privacy, Dockerfile, immutable tags. |
54+
| Manage repository team permissions | Add and remove teams from a repository, and manage team access level |
5355

5456
## Billing
5557

@@ -58,6 +60,12 @@ the following tables to [create or edit a custom role](manage.md).
5860
| View billing | View organization billing information |
5961
| Manage billing | Complete access to managing organization billing |
6062

63+
## Licensing
64+
65+
| Permission | Description |
66+
| :-------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
67+
| Manage licenses | Assign and revoke licenses for members and manage auto-assign rules. Does not include purchasing or changing the number of licenses. |
68+
6169
## AI Governance
6270

6371
| Permission | Description |
@@ -69,16 +77,25 @@ the following tables to [create or edit a custom role](manage.md).
6977

7078
## DHI (Docker Hardened Images)
7179

72-
| Permission | Description |
73-
| :----------------- | :----------------------------------------------- |
74-
| Create DHI mirrors | Create Docker Hardened Image mirror repositories |
80+
| Permission | Description |
81+
| :-------------------- | :-------------------------------------------------------------- |
82+
| Create DHI mirrors | Create Docker Hardened Image mirror repositories |
83+
| Manage customizations | Create, update, and delete Docker Hardened Image customizations |
7584

7685
## Docker Offload
7786

7887
| Permission | Description |
7988
| :---------------- | :--------------------------------------------- |
8089
| Offload Read-Only | View Offload account status, leases, and zones |
8190

91+
## Cloud Sandboxes
92+
93+
| Permission | Description |
94+
| :------------------ | :------------------------------------ |
95+
| Use Cloud Sandboxes | Create and use Docker Cloud Sandboxes |
96+
97+
<!-- END GENERATED: custom-role-permissions -->
98+
8299
## Next steps
83100

84101
- [Manage custom roles](manage.md): Create, assign, and delete custom
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Regenerates the permission tables in the custom-role permissions reference
2+
// from the IAM source of truth (`custom-role-metadata.yaml` in
3+
// docker/iam-services). Run by the "Sync custom role permissions" workflow when
4+
// that file changes; the result is opened as a PR for review.
5+
//
6+
// Only the region between the generation markers is rewritten — the frontmatter,
7+
// intro, and "Next steps" are hand-maintained. Table columns are left
8+
// unpadded here; prettier aligns them afterwards.
9+
//
10+
// Usage:
11+
// node scripts/generate-custom-role-permissions.mjs \
12+
// --metadata path/to/custom-role-metadata.yaml \
13+
// --doc content/manuals/security/roles-and-permissions/custom-roles/permissions-reference.md
14+
// add --check to exit non-zero (without writing) when the doc is out of date.
15+
16+
import { readFileSync, writeFileSync } from 'node:fs';
17+
18+
const BEGIN = '<!-- BEGIN GENERATED: custom-role-permissions -->';
19+
const END = '<!-- END GENERATED: custom-role-permissions -->';
20+
21+
function parseArgs(argv) {
22+
const args = {};
23+
for (let i = 2; i < argv.length; i += 1) {
24+
const arg = argv[i];
25+
if (arg === '--check') args.check = true;
26+
else if (arg === '--metadata') args.metadata = argv[(i += 1)];
27+
else if (arg === '--doc') args.doc = argv[(i += 1)];
28+
}
29+
if (!args.metadata || !args.doc) {
30+
throw new Error('Usage: --metadata <yaml> --doc <md> [--check]');
31+
}
32+
return args;
33+
}
34+
35+
// Minimal parser for the fixed shape of custom-role-metadata.yaml:
36+
// groups:
37+
// - name: <group>
38+
// relations:
39+
// - name: <permission>
40+
// description: <text>
41+
// relation: <relation>
42+
// Commented (`#`) and blank lines are ignored. Values after the first ": " are
43+
// taken verbatim (descriptions may contain colons and commas) with surrounding
44+
// quotes stripped. A dedicated parser avoids adding a YAML dependency to the
45+
// docs repo for this single, controlled file.
46+
function parseMetadata(yaml) {
47+
const groups = [];
48+
let group = null;
49+
let relation = null;
50+
51+
for (const rawLine of yaml.split('\n')) {
52+
const line = rawLine.replace(/\s+$/, '');
53+
const trimmed = line.trim();
54+
if (trimmed === '' || trimmed.startsWith('#')) continue;
55+
56+
const indent = line.length - line.trimStart().length;
57+
const field = (key) => {
58+
const prefix = `${key}:`;
59+
const idx = trimmed.indexOf(prefix);
60+
if (idx !== 0) return null;
61+
let value = trimmed.slice(prefix.length).trim();
62+
if (
63+
(value.startsWith('"') && value.endsWith('"')) ||
64+
(value.startsWith("'") && value.endsWith("'"))
65+
) {
66+
value = value.slice(1, -1);
67+
}
68+
return value;
69+
};
70+
71+
// A group starts at ` - name:` (indent 2); a relation at ` - name:`
72+
// (indent 6). Distinguish by indentation.
73+
if (trimmed.startsWith('- name:')) {
74+
const name = field.call(null, 'name') ?? trimmed.slice('- name:'.length).trim();
75+
if (indent <= 2) {
76+
group = { name, relations: [] };
77+
groups.push(group);
78+
relation = null;
79+
} else {
80+
relation = { name };
81+
if (group) group.relations.push(relation);
82+
}
83+
continue;
84+
}
85+
86+
const description = field('description');
87+
if (description !== null && relation) {
88+
relation.description = description;
89+
continue;
90+
}
91+
}
92+
93+
return groups;
94+
}
95+
96+
// Render a GFM table with left-aligned, space-padded columns to match the
97+
// hand-formatted style already in the docs (markdown is excluded from the
98+
// repo's prettier run, so there is no formatter to align it afterwards).
99+
function renderTable(rows) {
100+
const headers = ['Permission', 'Description'];
101+
const widths = headers.map((header, col) =>
102+
Math.max(header.length, ...rows.map((row) => row[col].length)),
103+
);
104+
const line = (cells) =>
105+
`| ${cells.map((cell, col) => cell.padEnd(widths[col])).join(' | ')} |`;
106+
const separator = `| ${widths
107+
.map((width) => `:${'-'.repeat(Math.max(width - 1, 2))}`)
108+
.join(' | ')} |`;
109+
return [line(headers), separator, ...rows.map(line)].join('\n');
110+
}
111+
112+
function renderTables(groups) {
113+
const sections = groups
114+
.filter((group) => group.relations.length > 0)
115+
.map((group) => {
116+
const rows = group.relations.map((rel) => [
117+
rel.name,
118+
rel.description ?? '',
119+
]);
120+
return `## ${group.name}\n\n${renderTable(rows)}`;
121+
});
122+
return sections.join('\n\n');
123+
}
124+
125+
function main() {
126+
const args = parseArgs(process.argv);
127+
const groups = parseMetadata(readFileSync(args.metadata, 'utf8'));
128+
const doc = readFileSync(args.doc, 'utf8');
129+
130+
const beginIdx = doc.indexOf(BEGIN);
131+
const endIdx = doc.indexOf(END);
132+
if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
133+
throw new Error(
134+
`Generation markers not found in ${args.doc}. Expected:\n${BEGIN}\n...\n${END}`,
135+
);
136+
}
137+
138+
const before = doc.slice(0, beginIdx + BEGIN.length);
139+
const after = doc.slice(endIdx);
140+
const next = `${before}\n\n${renderTables(groups)}\n\n${after}`;
141+
142+
if (next === doc) {
143+
console.log('Custom role permissions reference is up to date.');
144+
return;
145+
}
146+
147+
if (args.check) {
148+
console.error(
149+
'Custom role permissions reference is out of date. Run the generator to update it.',
150+
);
151+
process.exit(1);
152+
}
153+
154+
writeFileSync(args.doc, next);
155+
console.log(`Updated ${args.doc}.`);
156+
}
157+
158+
main();

0 commit comments

Comments
 (0)