Skip to content

Commit 29fefc6

Browse files
authored
OIDC default resource assignments (#7135)
1 parent 7fe1c27 commit 29fefc6

File tree

16 files changed

+658
-33
lines changed

16 files changed

+658
-33
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { graphql } from 'testkit/gql';
2+
import { ResourceAssignmentModeType } from 'testkit/gql/graphql';
3+
import { execute } from 'testkit/graphql';
4+
import { initSeed } from 'testkit/seed';
5+
6+
const AssignedResourcesSpec_CreateOIDCIntegrationMutation = graphql(`
7+
mutation AssignedResourcesSpec_CreateOIDCIntegrationMutation(
8+
$input: CreateOIDCIntegrationInput!
9+
) {
10+
createOIDCIntegration(input: $input) {
11+
ok {
12+
createdOIDCIntegration {
13+
id
14+
defaultResourceAssignment {
15+
mode
16+
}
17+
}
18+
}
19+
}
20+
}
21+
`);
22+
23+
const AssignedResourcesSpec_ReadDefaultTest = graphql(`
24+
query AssignedResourcesSpec_ReadDefaultTest($organizationSlug: String!) {
25+
organization(reference: { bySelector: { organizationSlug: $organizationSlug } }) {
26+
id
27+
oidcIntegration {
28+
defaultResourceAssignment {
29+
mode
30+
projects {
31+
project {
32+
id
33+
slug
34+
}
35+
targets {
36+
mode
37+
targets {
38+
target {
39+
id
40+
slug
41+
}
42+
services {
43+
mode
44+
services
45+
}
46+
appDeployments {
47+
mode
48+
appDeployments
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
`);
58+
59+
const AssignedResourcesSpec_UpdateDefaultMutation = graphql(`
60+
mutation AssignedResourcesSpec_UpdateDefaultMutation(
61+
$input: UpdateOIDCDefaultResourceAssignmentInput!
62+
) {
63+
updateOIDCDefaultResourceAssignment(input: $input) {
64+
ok {
65+
updatedOIDCIntegration {
66+
id
67+
defaultResourceAssignment {
68+
mode
69+
projects {
70+
project {
71+
id
72+
slug
73+
}
74+
targets {
75+
mode
76+
targets {
77+
target {
78+
id
79+
slug
80+
}
81+
services {
82+
mode
83+
services
84+
}
85+
appDeployments {
86+
mode
87+
appDeployments
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}
95+
error {
96+
message
97+
}
98+
}
99+
}
100+
`);
101+
102+
async function setup() {
103+
const { ownerToken, createOrg } = await initSeed().createOwner();
104+
const { organization, createOrganizationAccessToken } = await createOrg();
105+
106+
const result = await execute({
107+
document: AssignedResourcesSpec_CreateOIDCIntegrationMutation,
108+
variables: {
109+
input: {
110+
organizationId: organization.id,
111+
clientId: 'foo',
112+
clientSecret: 'foofoofoofoo',
113+
tokenEndpoint: 'http://localhost:8888/oauth/token',
114+
userinfoEndpoint: 'http://localhost:8888/oauth/userinfo',
115+
authorizationEndpoint: 'http://localhost:8888/oauth/authorize',
116+
},
117+
},
118+
authToken: ownerToken,
119+
}).then(r => r.expectNoGraphQLErrors());
120+
121+
// no default exists at creation
122+
expect(result.createOIDCIntegration.ok?.createdOIDCIntegration.defaultResourceAssignment).toBe(
123+
null,
124+
);
125+
return {
126+
organization,
127+
ownerToken,
128+
oidcIntegrationId: result.createOIDCIntegration.ok?.createdOIDCIntegration.id!,
129+
createOrganizationAccessToken,
130+
};
131+
}
132+
133+
describe('read OIDC', () => {
134+
describe('permissions="organization:integrations"', () => {
135+
test.concurrent('success', async ({ expect }) => {
136+
const { organization, ownerToken, oidcIntegrationId } = await setup();
137+
138+
await execute({
139+
document: AssignedResourcesSpec_UpdateDefaultMutation,
140+
variables: {
141+
input: {
142+
oidcIntegrationId,
143+
resources: {
144+
mode: ResourceAssignmentModeType.All,
145+
},
146+
},
147+
},
148+
authToken: ownerToken,
149+
}).then(r => r.expectNoGraphQLErrors());
150+
151+
const read = await execute({
152+
document: AssignedResourcesSpec_ReadDefaultTest,
153+
variables: {
154+
organizationSlug: organization.slug,
155+
},
156+
authToken: ownerToken,
157+
}).then(r => r.expectNoGraphQLErrors());
158+
159+
expect(read).toEqual({
160+
organization: {
161+
id: expect.stringMatching('.+'),
162+
oidcIntegration: {
163+
defaultResourceAssignment: {
164+
mode: 'ALL',
165+
projects: null,
166+
},
167+
},
168+
},
169+
});
170+
});
171+
});
172+
173+
describe('permissions missing "organization:integrations"', () => {
174+
test.concurrent('fail', async ({ expect }) => {
175+
const { organization, ownerToken, oidcIntegrationId, createOrganizationAccessToken } =
176+
await setup();
177+
const { privateAccessKey: readToken } = await createOrganizationAccessToken({
178+
permissions: ['organization:read'],
179+
resources: { mode: ResourceAssignmentModeType.All },
180+
});
181+
182+
await execute({
183+
document: AssignedResourcesSpec_UpdateDefaultMutation,
184+
variables: {
185+
input: {
186+
oidcIntegrationId,
187+
resources: {
188+
mode: ResourceAssignmentModeType.All,
189+
},
190+
},
191+
},
192+
authToken: ownerToken,
193+
}).then(r => r.expectNoGraphQLErrors());
194+
195+
await execute({
196+
document: AssignedResourcesSpec_ReadDefaultTest,
197+
variables: {
198+
organizationSlug: organization.slug,
199+
},
200+
authToken: readToken,
201+
}).then(r => r.expectGraphQLErrors());
202+
});
203+
});
204+
});
205+
206+
describe('update OIDC default assigned resources', () => {
207+
describe('permissions="oidc:modify"', () => {
208+
test.concurrent('success', async ({ expect }) => {
209+
const { organization, ownerToken, oidcIntegrationId } = await setup();
210+
211+
const update = await execute({
212+
document: AssignedResourcesSpec_UpdateDefaultMutation,
213+
variables: {
214+
input: {
215+
oidcIntegrationId,
216+
resources: {
217+
mode: ResourceAssignmentModeType.All,
218+
},
219+
},
220+
},
221+
authToken: ownerToken,
222+
}).then(r => r.expectNoGraphQLErrors());
223+
224+
expect(update).toEqual({
225+
updateOIDCDefaultResourceAssignment: {
226+
error: null,
227+
ok: {
228+
updatedOIDCIntegration: {
229+
defaultResourceAssignment: {
230+
mode: 'ALL',
231+
projects: null,
232+
},
233+
id: expect.stringMatching('.+'),
234+
},
235+
},
236+
},
237+
});
238+
});
239+
});
240+
241+
describe('permissions missing "oidc:modify"', () => {
242+
test.concurrent('fails', async ({ expect }) => {
243+
const { createOrganizationAccessToken, ownerToken, oidcIntegrationId } = await setup();
244+
245+
const { privateAccessKey: accessToken } = await createOrganizationAccessToken({
246+
permissions: ['organization:read'],
247+
resources: {
248+
mode: ResourceAssignmentModeType.All,
249+
},
250+
});
251+
252+
const update = await execute({
253+
document: AssignedResourcesSpec_UpdateDefaultMutation,
254+
variables: {
255+
input: {
256+
oidcIntegrationId,
257+
resources: {
258+
mode: ResourceAssignmentModeType.All,
259+
},
260+
},
261+
},
262+
authToken: accessToken,
263+
}).then(r => r.expectGraphQLErrors());
264+
});
265+
});
266+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { type MigrationExecutor } from '../pg-migrator';
2+
3+
export default {
4+
name: '2025.10.30T00-00-00.granular-oidc-role-permissions.ts',
5+
run: ({ sql }) => sql`
6+
ALTER TABLE "oidc_integrations"
7+
ADD COLUMN "default_assigned_resources" JSONB
8+
;
9+
`,
10+
} satisfies MigrationExecutor;

packages/migrations/src/run-pg-migrations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,5 +168,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
168168
await import('./actions/2025.05.15T00-00-01.organization-member-pagination'),
169169
await import('./actions/2025.05.28T00-00-00.schema-log-by-ids'),
170170
await import('./actions/2025.10.16T00-00-00.schema-log-by-commit-ordered'),
171+
await import('./actions/2025.10.30T00-00-00.granular-oidc-role-permissions'),
171172
],
172173
});

packages/services/api/src/modules/oidc-integrations/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createModule } from 'graphql-modules';
2+
import { ResourceAssignments } from '../organization/providers/resource-assignments';
23
import { OIDCIntegrationsProvider } from './providers/oidc-integrations.provider';
34
import { resolvers } from './resolvers.generated';
45
import typeDefs from './module.graphql';
@@ -8,5 +9,5 @@ export const oidcIntegrationsModule = createModule({
89
dirname: __dirname,
910
typeDefs,
1011
resolvers,
11-
providers: [OIDCIntegrationsProvider],
12+
providers: [OIDCIntegrationsProvider, ResourceAssignments],
1213
});

packages/services/api/src/modules/oidc-integrations/module.graphql.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default gql`
1919
authorizationEndpoint: String!
2020
oidcUserAccessOnly: Boolean!
2121
defaultMemberRole: MemberRole!
22+
defaultResourceAssignment: ResourceAssignment
2223
}
2324
2425
extend type Mutation {
@@ -29,6 +30,30 @@ export default gql`
2930
updateOIDCDefaultMemberRole(
3031
input: UpdateOIDCDefaultMemberRoleInput!
3132
): UpdateOIDCDefaultMemberRoleResult!
33+
updateOIDCDefaultResourceAssignment(
34+
input: UpdateOIDCDefaultResourceAssignmentInput!
35+
): UpdateOIDCDefaultResourceAssignmentResult!
36+
}
37+
38+
"""
39+
@oneOf
40+
"""
41+
type UpdateOIDCDefaultResourceAssignmentResult {
42+
ok: UpdateOIDCDefaultResourceAssignmentOk
43+
error: UpdateOIDCDefaultResourceAssignmentError
44+
}
45+
46+
type UpdateOIDCDefaultResourceAssignmentOk {
47+
updatedOIDCIntegration: OIDCIntegration!
48+
}
49+
50+
type UpdateOIDCDefaultResourceAssignmentError implements Error {
51+
message: String!
52+
}
53+
54+
input UpdateOIDCDefaultResourceAssignmentInput {
55+
oidcIntegrationId: ID!
56+
resources: ResourceAssignmentInput!
3257
}
3358
3459
extend type Subscription {

0 commit comments

Comments
 (0)