Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import { graphql } from 'testkit/gql';
import { ResourceAssignmentModeType } from 'testkit/gql/graphql';
import { execute } from 'testkit/graphql';
import { initSeed } from 'testkit/seed';

const AssignedResourcesSpec_CreateOIDCIntegrationMutation = graphql(`
mutation AssignedResourcesSpec_CreateOIDCIntegrationMutation(
$input: CreateOIDCIntegrationInput!
) {
createOIDCIntegration(input: $input) {
ok {
createdOIDCIntegration {
id
defaultResourceAssignment {
mode
}
}
}
}
}
`);

const AssignedResourcesSpec_ReadDefaultTest = graphql(`
query AssignedResourcesSpec_ReadDefaultTest($organizationSlug: String!) {
organization(reference: { bySelector: { organizationSlug: $organizationSlug } }) {
id
oidcIntegration {
defaultResourceAssignment {
mode
projects {
project {
id
slug
}
targets {
mode
targets {
target {
id
slug
}
services {
mode
services
}
appDeployments {
mode
appDeployments
}
}
}
}
}
}
}
}
`);

const AssignedResourcesSpec_UpdateDefaultMutation = graphql(`
mutation AssignedResourcesSpec_UpdateDefaultMutation(
$input: UpdateOIDCDefaultResourceAssignmentInput!
) {
updateOIDCDefaultResourceAssignment(input: $input) {
ok {
updatedOIDCIntegration {
id
defaultResourceAssignment {
mode
projects {
project {
id
slug
}
targets {
mode
targets {
target {
id
slug
}
services {
mode
services
}
appDeployments {
mode
appDeployments
}
}
}
}
}
}
}
error {
message
}
}
}
`);

async function setup() {
const { ownerToken, createOrg } = await initSeed().createOwner();
const { organization, createOrganizationAccessToken } = await createOrg();

const result = await execute({
document: AssignedResourcesSpec_CreateOIDCIntegrationMutation,
variables: {
input: {
organizationId: organization.id,
clientId: 'foo',
clientSecret: 'foofoofoofoo',
tokenEndpoint: 'http://localhost:8888/oauth/token',
userinfoEndpoint: 'http://localhost:8888/oauth/userinfo',
authorizationEndpoint: 'http://localhost:8888/oauth/authorize',
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());

// no default exists at creation
expect(result.createOIDCIntegration.ok?.createdOIDCIntegration.defaultResourceAssignment).toBe(
null,
);
return {
organization,
ownerToken,
oidcIntegrationId: result.createOIDCIntegration.ok?.createdOIDCIntegration.id!,
createOrganizationAccessToken,
};
}

describe('read OIDC', () => {
describe('permissions="organization:integrations"', () => {
test.concurrent('success', async ({ expect }) => {
const { organization, ownerToken, oidcIntegrationId } = await setup();

await execute({
document: AssignedResourcesSpec_UpdateDefaultMutation,
variables: {
input: {
oidcIntegrationId,
resources: {
mode: ResourceAssignmentModeType.All,
},
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());

const read = await execute({
document: AssignedResourcesSpec_ReadDefaultTest,
variables: {
organizationSlug: organization.slug,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());

expect(read).toEqual({
organization: {
id: expect.stringMatching('.+'),
oidcIntegration: {
defaultResourceAssignment: {
mode: 'ALL',
projects: null,
},
},
},
});
});
});

describe('permissions missing "organization:integrations"', () => {
test.concurrent('fail', async ({ expect }) => {
const { organization, ownerToken, oidcIntegrationId, createOrganizationAccessToken } =
await setup();
const { privateAccessKey: readToken } = await createOrganizationAccessToken({
permissions: ['organization:read'],
resources: { mode: ResourceAssignmentModeType.All },
});

await execute({
document: AssignedResourcesSpec_UpdateDefaultMutation,
variables: {
input: {
oidcIntegrationId,
resources: {
mode: ResourceAssignmentModeType.All,
},
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());

await execute({
document: AssignedResourcesSpec_ReadDefaultTest,
variables: {
organizationSlug: organization.slug,
},
authToken: readToken,
}).then(r => r.expectGraphQLErrors());
});
});
});

describe('update OIDC default assigned resources', () => {
describe('permissions="oidc:modify"', () => {
test.concurrent('success', async ({ expect }) => {
const { organization, ownerToken, oidcIntegrationId } = await setup();

const update = await execute({
document: AssignedResourcesSpec_UpdateDefaultMutation,
variables: {
input: {
oidcIntegrationId,
resources: {
mode: ResourceAssignmentModeType.All,
},
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());

expect(update).toEqual({
updateOIDCDefaultResourceAssignment: {
error: null,
ok: {
updatedOIDCIntegration: {
defaultResourceAssignment: {
mode: 'ALL',
projects: null,
},
id: expect.stringMatching('.+'),
},
},
},
});
});
});

describe('permissions missing "oidc:modify"', () => {
test.concurrent('fails', async ({ expect }) => {
const { createOrganizationAccessToken, ownerToken, oidcIntegrationId } = await setup();

const { privateAccessKey: accessToken } = await createOrganizationAccessToken({
permissions: ['organization:read'],
resources: {
mode: ResourceAssignmentModeType.All,
},
});

const update = await execute({
document: AssignedResourcesSpec_UpdateDefaultMutation,
variables: {
input: {
oidcIntegrationId,
resources: {
mode: ResourceAssignmentModeType.All,
},
},
},
authToken: accessToken,
}).then(r => r.expectGraphQLErrors());
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type MigrationExecutor } from '../pg-migrator';

export default {
name: '2025.10.30T00-00-00.granular-oidc-role-permissions.ts',
run: ({ sql }) => sql`
ALTER TABLE "oidc_integrations"
ADD COLUMN "default_assigned_resources" JSONB
;
`,
} satisfies MigrationExecutor;
1 change: 1 addition & 0 deletions packages/migrations/src/run-pg-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
await import('./actions/2025.05.15T00-00-01.organization-member-pagination'),
await import('./actions/2025.05.28T00-00-00.schema-log-by-ids'),
await import('./actions/2025.10.16T00-00-00.schema-log-by-commit-ordered'),
await import('./actions/2025.10.30T00-00-00.granular-oidc-role-permissions'),
],
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { ResourceAssignmentModel } from '../../organization/lib/resource-assignment-model';
import { ResourceAssignmentModel } from '@hive/storage/resource-assignment-model';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to move because now storage uses the model and i didnt want to make services a dependency of storage.


export const AuditLogModel = z.union([
z.object({
Expand Down
3 changes: 2 additions & 1 deletion packages/services/api/src/modules/oidc-integrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createModule } from 'graphql-modules';
import { ResourceAssignments } from '../organization/providers/resource-assignments';
import { OIDCIntegrationsProvider } from './providers/oidc-integrations.provider';
import { resolvers } from './resolvers.generated';
import typeDefs from './module.graphql';
Expand All @@ -8,5 +9,5 @@ export const oidcIntegrationsModule = createModule({
dirname: __dirname,
typeDefs,
resolvers,
providers: [OIDCIntegrationsProvider],
providers: [OIDCIntegrationsProvider, ResourceAssignments],
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { gql } from 'graphql-modules';

export default gql`
type ProjectForResourceSelector {
id: ID!
slug: String!
type: ProjectType!
targets: [TargetForResourceSelector!]!
}

type TargetForResourceSelector {
id: ID!
slug: String!
services: [String!]!
appDeployments: [String!]!
}

extend type Organization {
viewerCanManageOIDCIntegration: Boolean!
oidcIntegration: OIDCIntegration
projectsForResourceSelector: [ProjectForResourceSelector]
}

extend type User {
Expand All @@ -19,6 +34,7 @@ export default gql`
authorizationEndpoint: String!
oidcUserAccessOnly: Boolean!
defaultMemberRole: MemberRole!
defaultResourceAssignment: ResourceAssignment
}

extend type Mutation {
Expand All @@ -29,6 +45,30 @@ export default gql`
updateOIDCDefaultMemberRole(
input: UpdateOIDCDefaultMemberRoleInput!
): UpdateOIDCDefaultMemberRoleResult!
updateOIDCDefaultResourceAssignment(
input: UpdateOIDCDefaultResourceAssignmentInput!
): UpdateOIDCDefaultResourceAssignmentResult!
}

"""
@oneOf
"""
type UpdateOIDCDefaultResourceAssignmentResult {
ok: UpdateOIDCDefaultResourceAssignmentOk
error: UpdateOIDCDefaultResourceAssignmentError
}

type UpdateOIDCDefaultResourceAssignmentOk {
updatedOIDCIntegration: OIDCIntegration!
}

type UpdateOIDCDefaultResourceAssignmentError implements Error {
message: String!
}

input UpdateOIDCDefaultResourceAssignmentInput {
oidcIntegrationId: ID!
resources: ResourceAssignmentInput!
}

extend type Subscription {
Expand Down
Loading
Loading