From a48e2090c798ce8fb92cdd1f0287f2a2ca03f6f3 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Mon, 8 Dec 2025 22:27:10 +0900 Subject: [PATCH 1/5] implement account linking for new users --- .../api/src/modules/auth/lib/authz.ts | 1 + .../modules/auth/lib/supertokens-strategy.ts | 22 +++++++++++++++---- .../src/modules/shared/providers/storage.ts | 5 ++++- packages/services/server/src/supertokens.ts | 22 ++++++++++++++----- packages/services/storage/src/index.ts | 21 +++++++++++++++--- 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/packages/services/api/src/modules/auth/lib/authz.ts b/packages/services/api/src/modules/auth/lib/authz.ts index 6963ef2162e..d8f8ee8e53e 100644 --- a/packages/services/api/src/modules/auth/lib/authz.ts +++ b/packages/services/api/src/modules/auth/lib/authz.ts @@ -51,6 +51,7 @@ function parseResourceIdentifier(resource: string) { export type UserActor = { type: 'user'; user: User; + oidcIntegrationId: string | null; }; export type OrganizationAccessTokenActor = { diff --git a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts index 11e6d609ad6..c6b561062e3 100644 --- a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts +++ b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts @@ -11,15 +11,24 @@ import { AuthNStrategy, AuthorizationPolicyStatement, Session, UserActor } from export class SuperTokensCookieBasedSession extends Session { public superTokensUserId: string; + public userId: string | undefined; + public oidcIntegrationId: string | null | undefined; private organizationMembers: OrganizationMembers; private storage: Storage; constructor( - args: { superTokensUserId: string; email: string }, + args: { + superTokensUserId: string; + userId: string | undefined; + oidcIntegrationId: string | null | undefined; + email: string; + }, deps: { organizationMembers: OrganizationMembers; storage: Storage; logger: Logger }, ) { super({ logger: deps.logger }); this.superTokensUserId = args.superTokensUserId; + this.userId = args.userId; + this.oidcIntegrationId = args.oidcIntegrationId; this.organizationMembers = deps.organizationMembers; this.storage = deps.storage; } @@ -109,9 +118,9 @@ export class SuperTokensCookieBasedSession extends Session { } public async getActor(): Promise { - const user = await this.storage.getUserBySuperTokenId({ - superTokensUserId: this.superTokensUserId, - }); + const user = this.userId + ? await this.storage.getUserById({ id: this.userId }) + : await this.storage.getUserBySuperTokenId({ superTokensUserId: this.superTokensUserId }); if (!user) { throw new AccessError('User not found'); @@ -120,6 +129,7 @@ export class SuperTokensCookieBasedSession extends Session { return { type: 'user', user, + oidcIntegrationId: this.oidcIntegrationId ?? null, }; } @@ -227,6 +237,8 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy; + }): Promise<{ + user: User; + action: 'created' | 'no_action'; + }>; getUserBySuperTokenId(_: { superTokensUserId: string }): Promise; getUserById(_: { id: string }): Promise; diff --git a/packages/services/server/src/supertokens.ts b/packages/services/server/src/supertokens.ts index 55b382786e2..b50c9d68448 100644 --- a/packages/services/server/src/supertokens.ts +++ b/packages/services/server/src/supertokens.ts @@ -189,17 +189,27 @@ export const backendConfig = (requirements: { ); } - input.accessTokenPayload = { + let payload = { version: '1', superTokensUserId: input.userId, email: user.emails[0], + userId: undefined as string | undefined, + oidcIntegrationId: undefined as string | null | undefined, }; + try { + const internalUser = await internalApi.ensureUser({ + superTokensUserId: user.id, + email: user.emails[0], + oidcIntegrationId: input.userContext['oidcId'] ?? null, + firstName: null, + lastName: null, + }); + payload.userId = internalUser.user.id; + payload.oidcIntegrationId = input.userContext['oidcId'] ?? null; + } catch {} - input.sessionDataInDatabase = { - version: '1', - superTokensUserId: input.userId, - email: user.emails[0], - }; + input.accessTokenPayload = structuredClone(payload); + input.sessionDataInDatabase = structuredClone(payload); return originalImplementation.createNewSession(input); }, diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index dc2a5aeb3c1..26c3aaf5ef9 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -609,8 +609,20 @@ export async function createStorage( }) { return tracedTransaction('ensureUserExists', pool, async t => { let action: 'created' | 'no_action' = 'no_action'; - let internalUser = await shared.getUserBySuperTokenId({ superTokensUserId }, t); - + const users = await t.any(sql`/* ensureUserExists */ + SELECT + ${userFields(sql`"users".`, sql`"stu".`)} + FROM + "users" + LEFT JOIN "supertokens_thirdparty_users" AS "stu" + ON ("stu"."user_id" = "users"."supertoken_user_id") + WHERE + "users"."email" = ${email}; + `); + let internalUser = users.length > 0 ? UserModel.parse(users[0]) : null; + if (!internalUser) { + internalUser = await shared.getUserBySuperTokenId({ superTokensUserId }, t); + } if (!internalUser) { internalUser = await shared.createUser( buildUserData({ @@ -636,7 +648,10 @@ export async function createStorage( ); } - return action; + return { + user: internalUser, + action, + }; }); }, async getUserBySuperTokenId({ superTokensUserId }) { From 7ae3a2ac26e625d648eefb7f2b8366631afe523d Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Mon, 8 Dec 2025 22:31:40 +0900 Subject: [PATCH 2/5] add OIDC integration check on access policy check --- .../api/src/modules/auth/lib/supertokens-strategy.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts index c6b561062e3..10daed8ee89 100644 --- a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts +++ b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts @@ -63,7 +63,12 @@ export class SuperTokensCookieBasedSession extends Session { user.id, organizationId, ); - const organization = await this.storage.getOrganization({ organizationId }); + const [organization, oidcIntegration] = await Promise.all([ + this.storage.getOrganization({ organizationId }), + this.storage.getOIDCIntegrationForOrganization({ + organizationId, + }), + ]); const organizationMembership = await this.organizationMembers.findOrganizationMembership({ organization, userId: user.id, @@ -108,6 +113,10 @@ export class SuperTokensCookieBasedSession extends Session { ]; } + if (oidcIntegration?.oidcUserAccessOnly && this.oidcIntegrationId !== oidcIntegration.id) { + return []; + } + this.logger.debug( 'Translate organization role assignments to policy statements. (userId=%s, organizationId=%s)', user.id, From bdd23329976328bcdbf709932e29c37e4aec93f9 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Mon, 8 Dec 2025 22:32:26 +0900 Subject: [PATCH 3/5] remove SuperTokens and OIDC user guards --- .../oidc-integrations/resolvers/User.ts | 2 +- .../providers/organization-manager.ts | 28 ++----- .../resolvers/Query/myDefaultOrganization.ts | 22 ------ packages/services/api/src/shared/entities.ts | 1 - packages/services/api/src/shared/helpers.ts | 6 +- packages/services/storage/src/index.ts | 79 ++++++++++--------- 6 files changed, 55 insertions(+), 83 deletions(-) diff --git a/packages/services/api/src/modules/oidc-integrations/resolvers/User.ts b/packages/services/api/src/modules/oidc-integrations/resolvers/User.ts index d25aff9c155..39db551ef04 100644 --- a/packages/services/api/src/modules/oidc-integrations/resolvers/User.ts +++ b/packages/services/api/src/modules/oidc-integrations/resolvers/User.ts @@ -1,5 +1,5 @@ import type { UserResolvers } from './../../../__generated__/types'; export const User: Pick = { - canSwitchOrganization: user => !user.oidcIntegrationId, + canSwitchOrganization: () => true, }; diff --git a/packages/services/api/src/modules/organization/providers/organization-manager.ts b/packages/services/api/src/modules/organization/providers/organization-manager.ts index a252a46c738..3bbe7f23791 100644 --- a/packages/services/api/src/modules/organization/providers/organization-manager.ts +++ b/packages/services/api/src/modules/organization/providers/organization-manager.ts @@ -297,20 +297,11 @@ export class OrganizationManager { user: { id: string; superTokensUserId: string | null; - oidcIntegrationId: string | null; }; }) { const { slug, user } = input; this.logger.info('Creating an organization (input=%o)', input); - if (user.oidcIntegrationId) { - this.logger.debug( - 'Failed to create organization as oidc user is not allowed to do so (input=%o)', - input, - ); - throw new HiveError('Cannot create organization with OIDC user.'); - } - const result = await this.storage.createOrganization({ slug, userId: user.id, @@ -652,13 +643,9 @@ export class OrganizationManager { async joinOrganization({ code }: { code: string }): Promise { this.logger.info('Joining an organization (code=%s)', code); - const user = await this.session.getViewer(); - const isOIDCUser = user.oidcIntegrationId !== null; - - if (isOIDCUser) { - return { - message: `You cannot join an organization with an OIDC account.`, - }; + const actor = await this.session.getActor(); + if (actor.type !== 'user') { + throw new Error('Only users can join organizations'); } const organization = await this.getOrganizationByInviteCode({ @@ -674,9 +661,10 @@ export class OrganizationManager { organizationId: organization.id, }); - if (oidcIntegration?.oidcUserAccessOnly && !isOIDCUser) { + if (oidcIntegration?.oidcUserAccessOnly && actor.oidcIntegrationId !== oidcIntegration.id) { return { - message: 'Non-OIDC users are not allowed to join this organization.', + message: + 'The user is not authorized through the OIDC integration required for the organization', }; } } @@ -685,7 +673,7 @@ export class OrganizationManager { await this.storage.addOrganizationMemberViaInvitationCode({ code, - userId: user.id, + userId: actor.user.id, organizationId: organization.id, }); @@ -701,7 +689,7 @@ export class OrganizationManager { eventType: 'USER_JOINED', organizationId: organization.id, metadata: { - inviteeEmail: user.email, + inviteeEmail: actor.user.email, }, }), ]); diff --git a/packages/services/api/src/modules/organization/resolvers/Query/myDefaultOrganization.ts b/packages/services/api/src/modules/organization/resolvers/Query/myDefaultOrganization.ts index 27214cfa648..cc3e4b806b4 100644 --- a/packages/services/api/src/modules/organization/resolvers/Query/myDefaultOrganization.ts +++ b/packages/services/api/src/modules/organization/resolvers/Query/myDefaultOrganization.ts @@ -1,5 +1,4 @@ import { Session } from '../../../auth/lib/authz'; -import { OIDCIntegrationsProvider } from '../../../oidc-integrations/providers/oidc-integrations.provider'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { OrganizationManager } from '../../providers/organization-manager'; import type { QueryResolvers } from './../../../../__generated__/types'; @@ -12,27 +11,6 @@ export const myDefaultOrganization: NonNullable = { */ export function batchBy( /** Function to determine the batch group. */ - buildBatchKey: (arg: TItem) => string, + buildBatchKey: (arg: TItem) => unknown, /** Loader for each batch group. */ loader: (args: TItem[]) => Promise[]>, /** Maximum amount of items per batch, if it is exceeded a new batch for a given batchKey is created. */ maxBatchSize = Infinity, ) { - let batchGroups = new Map>(); + let batchGroups = new Map>(); let didSchedule = false; function startLoadingBatch(currentBatch: BatchGroup): void { @@ -226,7 +226,7 @@ export function batchBy( ); } - function getBatchGroup(batchKey: string) { + function getBatchGroup(batchKey: unknown) { // get the batch collection for the batch key let currentBatch = batchGroups.get(batchKey); // if it does not exist or the batch is full, create a new batch diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index 26c3aaf5ef9..73d8d74f88c 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -444,32 +444,53 @@ export async function createStorage( return UserModel.parse(record); }, + getUserById: batchBy( + (item: { id: string; connection: Connection }) => item.connection, + async input => { + const userIds = input.map(i => i.id); + const records = await input[0].connection.any(sql`/* getUserById */ + SELECT + ${userFields(sql`"users".`, sql`"stu".`)} + FROM + "users" + LEFT JOIN "supertokens_thirdparty_users" AS "stu" + ON ("stu"."user_id" = "users"."supertoken_user_id") + WHERE + "users"."id" = ANY(${sql.array(userIds, 'uuid')}) + `); + + const mappings = new Map(); + for (const record of records) { + const user = UserModel.parse(record); + mappings.set(user.id, user); + } + + return userIds.map(async id => mappings.get(id) ?? null); + }, + ), async createUser( { - superTokensUserId, email, fullName, displayName, - oidcIntegrationId, }: { - superTokensUserId: string; email: string; fullName: string; displayName: string; - oidcIntegrationId: string | null; }, connection: Connection, ) { - await connection.query( + const { id } = await connection.one<{ id: string }>( sql`/* createUser */ INSERT INTO users - ("email", "supertoken_user_id", "full_name", "display_name", "oidc_integration_id") + ("email", "full_name", "display_name") VALUES - (${email}, ${superTokensUserId}, ${fullName}, ${displayName}, ${oidcIntegrationId}) + (${email}, ${fullName}, ${displayName}) + RETURNING id `, ); - const user = await this.getUserBySuperTokenId({ superTokensUserId }, connection); + const user = await shared.getUserById({ id, connection }); if (!user) { throw new Error('Something went wrong.'); } @@ -559,9 +580,7 @@ export async function createStorage( }; function buildUserData(input: { - superTokensUserId: string; email: string; - oidcIntegrationId: string | null; firstName: string | null; lastName: string | null; }) { @@ -572,11 +591,9 @@ export async function createStorage( : input.email.split('@')[0].slice(0, 25).padEnd(2, '1'); return { - superTokensUserId: input.superTokensUserId, email: input.email, displayName: name, fullName: name, - oidcIntegrationId: input.oidcIntegrationId, }; } @@ -626,9 +643,7 @@ export async function createStorage( if (!internalUser) { internalUser = await shared.createUser( buildUserData({ - superTokensUserId, email, - oidcIntegrationId: oidcIntegration?.id ?? null, firstName, lastName, }), @@ -637,6 +652,16 @@ export async function createStorage( action = 'created'; } + if (users.length === 1 && internalUser.superTokensUserId != null) { + await t.query(sql`/* ensureUserExists */ + UPDATE "users" + SET + "supertoken_user_id" = NULL, + "oidc_integration_id" = NULL + WHERE "id" = ${internalUser.id}; + `); + } + if (oidcIntegration !== null) { // Add user to OIDC linked integration await shared.addOrganizationMemberViaOIDCIntegrationId( @@ -657,27 +682,9 @@ export async function createStorage( async getUserBySuperTokenId({ superTokensUserId }) { return shared.getUserBySuperTokenId({ superTokensUserId }, pool); }, - getUserById: batch(async input => { - const userIds = input.map(i => i.id); - const records = await pool.any(sql`/* getUserById */ - SELECT - ${userFields(sql`"users".`, sql`"stu".`)} - FROM - "users" - LEFT JOIN "supertokens_thirdparty_users" AS "stu" - ON ("stu"."user_id" = "users"."supertoken_user_id") - WHERE - "users"."id" = ANY(${sql.array(userIds, 'uuid')}) - `); - - const mappings = new Map(); - for (const record of records) { - const user = UserModel.parse(record); - mappings.set(user.id, user); - } - - return userIds.map(async id => mappings.get(id) ?? null); - }), + async getUserById({ id }) { + return shared.getUserById({ id, connection: pool }); + }, async updateUser({ id, displayName, fullName }) { await pool.query(sql`/* updateUser */ UPDATE "users" @@ -5410,7 +5417,7 @@ export const UserModel = zod.object({ createdAt: zod.string(), displayName: zod.string(), fullName: zod.string(), - superTokensUserId: zod.string(), + superTokensUserId: zod.string().nullable(), isAdmin: zod .boolean() .nullable() From 13ec62ac091fa46ca595421096b3b9344619cd06 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Wed, 10 Dec 2025 18:15:14 +0900 Subject: [PATCH 4/5] fix auth in tests --- integration-tests/testkit/auth.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/integration-tests/testkit/auth.ts b/integration-tests/testkit/auth.ts index 107aedc849e..0bf93dc620d 100644 --- a/integration-tests/testkit/auth.ts +++ b/integration-tests/testkit/auth.ts @@ -57,10 +57,17 @@ const signUpUserViaEmail = async ( } }; -const createSessionPayload = (superTokensUserId: string, email: string) => ({ +const createSessionPayload = (payload: { + superTokensUserId: string; + userId: string; + oidcIntegrationId: string | null; + email: string; +}) => ({ version: '1', - superTokensUserId, - email, + superTokensUserId: payload.superTokensUserId, + userId: payload.userId, + oidcIntegrationId: payload.oidcIntegrationId, + email: payload.email, }); const CreateSessionModel = z.object({ @@ -89,7 +96,7 @@ const createSession = async ( ], }); - await internalApi.ensureUser.mutate({ + const { user } = await internalApi.ensureUser.mutate({ superTokensUserId, email, oidcIntegrationId, @@ -97,7 +104,12 @@ const createSession = async ( lastName: null, }); - const sessionData = createSessionPayload(superTokensUserId, email); + const sessionData = createSessionPayload({ + superTokensUserId, + userId: user.id, + oidcIntegrationId, + email, + }); const payload = { enableAntiCsrf: false, userId: superTokensUserId, From 7c8bf626aef4a90baf2956ba44128596404d4872 Mon Sep 17 00:00:00 2001 From: Iha Shin Date: Wed, 10 Dec 2025 21:56:08 +0900 Subject: [PATCH 5/5] update oidc tests to match the latest requirements --- cypress/e2e/app.cy.ts | 2 - integration-tests/testkit/flow.ts | 11 ++ integration-tests/testkit/seed.ts | 76 +++++---- .../tests/api/oidc-integrations/crud.spec.ts | 149 +++++------------- .../tests/api/organization/members.spec.ts | 27 ++-- 5 files changed, 116 insertions(+), 149 deletions(-) diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index a5e0d3cf538..554f1a647e1 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -64,8 +64,6 @@ describe('oidc', () => { cy.get('button[value="login"]').click(); cy.get(`a[href="/${slug}"]`).should('exist'); - // Organization picker should not be visible - cy.get('[data-cy="organization-picker-current"]').should('not.exist'); }); }); diff --git a/integration-tests/testkit/flow.ts b/integration-tests/testkit/flow.ts index c36b252d03b..cd2e89c2764 100644 --- a/integration-tests/testkit/flow.ts +++ b/integration-tests/testkit/flow.ts @@ -158,6 +158,17 @@ export function getOrganization(organizationSlug: string, authToken: string) { reportingOperations enablingUsageBasedBreakingChanges } + me { + id + user { + id + } + role { + id + name + permissions + } + } } } `), diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index ac1a8783a92..289e6216a26 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -889,49 +889,69 @@ export function initSeed() { }, }; }, - async inviteAndJoinMember( - inviteToken: string = ownerToken, - memberRoleId: string | undefined = undefined, - resources: GraphQLSchema.ResourceAssignmentInput | undefined = undefined, - ) { + async inviteAndJoinMember(options?: { + inviteToken?: string; + memberRoleId?: string | undefined; + oidcIntegrationId?: string | undefined; + resources?: GraphQLSchema.ResourceAssignmentInput | undefined; + }) { + const { inviteToken, memberRoleId, oidcIntegrationId, resources } = Object.assign( + options ?? {}, + { + inviteToken: ownerToken, + }, + ); const memberEmail = userEmail(generateUnique()); - const memberToken = await authenticate(memberEmail).then(r => r.access_token); + const memberToken = await authenticate(memberEmail, oidcIntegrationId).then( + r => r.access_token, + ); - const invitationResult = await inviteToOrganization( - { - organization: { - bySelector: { - organizationSlug: organization.slug, + if (!oidcIntegrationId) { + const invitationResult = await inviteToOrganization( + { + organization: { + bySelector: { + organizationSlug: organization.slug, + }, }, + email: memberEmail, + memberRoleId, + resources, }, - email: memberEmail, - memberRoleId, - resources, - }, - inviteToken, - ).then(r => r.expectNoGraphQLErrors()); - - const code = - invitationResult.inviteToOrganizationByEmail.ok?.createdOrganizationInvitation.code; + inviteToken, + ).then(r => r.expectNoGraphQLErrors()); + const code = + invitationResult.inviteToOrganizationByEmail.ok?.createdOrganizationInvitation + .code; + + if (!code) { + throw new Error( + `Could not create invitation for ${memberEmail} to join org ${organization.slug}`, + ); + } - if (!code) { - throw new Error( - `Could not create invitation for ${memberEmail} to join org ${organization.slug}`, + const joinResult = await joinOrganization(code, memberToken).then(r => + r.expectNoGraphQLErrors(), ); + + if (joinResult.joinOrganization.__typename !== 'OrganizationPayload') { + throw new Error( + `Member ${memberEmail} could not join organization ${organization.slug}`, + ); + } } - const joinResult = await joinOrganization(code, memberToken).then(r => + const orgAfterJoin = await getOrganization(organization.slug, memberToken).then(r => r.expectNoGraphQLErrors(), ); + const member = orgAfterJoin.organization?.me; - if (joinResult.joinOrganization.__typename !== 'OrganizationPayload') { + if (!member) { throw new Error( - `Member ${memberEmail} could not join organization ${organization.slug}`, + `Could not retrieve membership for ${memberEmail} in ${organization.slug} after joining`, ); } - const member = joinResult.joinOrganization.organization.me; - return { member, memberEmail, diff --git a/integration-tests/tests/api/oidc-integrations/crud.spec.ts b/integration-tests/tests/api/oidc-integrations/crud.spec.ts index 2cbb2d9e1b5..484a59e4ba7 100644 --- a/integration-tests/tests/api/oidc-integrations/crud.spec.ts +++ b/integration-tests/tests/api/oidc-integrations/crud.spec.ts @@ -540,78 +540,6 @@ describe('delete', () => { ]), ); }); - - test.concurrent( - 'success: upon integration deletion oidc members are also deleted', - async ({ expect }) => { - const seed = initSeed(); - const { ownerToken, createOrg } = await seed.createOwner(); - const { organization } = await createOrg(); - - const createResult = await execute({ - document: 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()); - - const oidcIntegrationId = createResult.createOIDCIntegration.ok!.createdOIDCIntegration.id; - - const MeQuery = graphql(` - query Me { - me { - id - } - } - `); - - const { access_token: memberAccessToken } = await seed.authenticate( - seed.generateEmail(), - oidcIntegrationId, - ); - const meResult = await execute({ - document: MeQuery, - authToken: memberAccessToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(meResult).toEqual({ - me: { - id: expect.any(String), - }, - }); - - await execute({ - document: DeleteOIDCIntegrationMutation, - variables: { - input: { - oidcIntegrationId, - }, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); - - const refetchedMeResult = await execute({ - document: MeQuery, - authToken: memberAccessToken, - }).then(r => r.expectGraphQLErrors()); - - expect(refetchedMeResult).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - message: `No access (reason: "User not found")`, - }), - ]), - ); - }, - ); }); }); @@ -777,45 +705,50 @@ describe('restrictions', () => { return result.createOIDCIntegration.ok!.createdOIDCIntegration.id; } - test.concurrent('non-oidc users cannot join an organization (default)', async ({ expect }) => { - const seed = initSeed(); - const { ownerToken, createOrg } = await seed.createOwner(); - const { organization, inviteMember, joinMemberUsingCode } = await createOrg(); + test.concurrent( + 'users authorized with non-OIDC method cannot join an organization (default)', + async ({ expect }) => { + const seed = initSeed(); + const { ownerToken, createOrg } = await seed.createOwner(); + const { organization, inviteMember, joinMemberUsingCode } = await createOrg(); - await configureOIDC({ - ownerToken, - organizationId: organization.id, - }); + await configureOIDC({ + ownerToken, + organizationId: organization.id, + }); - const refetchedOrg = await execute({ - document: OrganizationWithOIDCIntegration, - variables: { - organizationSlug: organization.slug, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); + const refetchedOrg = await execute({ + document: OrganizationWithOIDCIntegration, + variables: { + organizationSlug: organization.slug, + }, + authToken: ownerToken, + }).then(r => r.expectNoGraphQLErrors()); - expect(refetchedOrg.organization?.oidcIntegration?.oidcUserAccessOnly).toEqual(true); + expect(refetchedOrg.organization?.oidcIntegration?.oidcUserAccessOnly).toEqual(true); - const invitation = await inviteMember('example@example.com'); - const invitationCode = invitation.ok?.createdOrganizationInvitation.code; + const invitation = await inviteMember('example@example.com'); + const invitationCode = invitation.ok?.createdOrganizationInvitation.code; - if (!invitationCode) { - throw new Error('No invitation code'); - } + if (!invitationCode) { + throw new Error('No invitation code'); + } - const nonOidcAccount = await seed.authenticate(userEmail('non-oidc-user')); - const joinResult = await joinMemberUsingCode(invitationCode, nonOidcAccount.access_token).then( - r => r.expectNoGraphQLErrors(), - ); + const nonOidcAccount = await seed.authenticate(userEmail('non-oidc-user')); + const joinResult = await joinMemberUsingCode( + invitationCode, + nonOidcAccount.access_token, + ).then(r => r.expectNoGraphQLErrors()); - expect(joinResult.joinOrganization).toEqual( - expect.objectContaining({ - __typename: 'OrganizationInvitationError', - message: 'Non-OIDC users are not allowed to join this organization.', - }), - ); - }); + expect(joinResult.joinOrganization).toEqual( + expect.objectContaining({ + __typename: 'OrganizationInvitationError', + message: + 'The user is not authorized through the OIDC integration required for the organization', + }), + ); + }, + ); test.concurrent('non-oidc users can join an organization (opt-in)', async ({ expect }) => { const seed = initSeed(); @@ -925,10 +858,8 @@ test.concurrent( const seed = initSeed(); const { createOrg, ownerToken } = await seed.createOwner(); const { organization, inviteAndJoinMember } = await createOrg(); - const { createMemberRole, assignMemberRole, updateMemberRole, memberToken, member } = - await inviteAndJoinMember(); - await execute({ + const createOIDCIntegrationResult = await execute({ document: CreateOIDCIntegrationMutation, variables: { input: { @@ -942,7 +873,11 @@ test.concurrent( }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); + const oidcIntegrationId = + createOIDCIntegrationResult.createOIDCIntegration.ok?.createdOIDCIntegration.id; + const { createMemberRole, assignMemberRole, updateMemberRole, memberToken, member } = + await inviteAndJoinMember({ oidcIntegrationId }); const role = await createMemberRole([]); await assignMemberRole({ roleId: role.id, userId: member.id }); diff --git a/integration-tests/tests/api/organization/members.spec.ts b/integration-tests/tests/api/organization/members.spec.ts index 99b6c39dac7..024ebb3e671 100644 --- a/integration-tests/tests/api/organization/members.spec.ts +++ b/integration-tests/tests/api/organization/members.spec.ts @@ -154,18 +154,21 @@ test.concurrent('invite user with assigned resouces', async ({ expect }) => { const m = await org.inviteAndJoinMember(); const role = await m.createMemberRole(['organization:describe', 'project:describe']); - const member = await org.inviteAndJoinMember(undefined, role.id, { - mode: ResourceAssignmentModeType.Granular, - projects: [ - { - projectId: project1.id, - targets: { mode: ResourceAssignmentModeType.Granular, targets: [] }, - }, - { - projectId: project3.id, - targets: { mode: ResourceAssignmentModeType.Granular, targets: [] }, - }, - ], + const member = await org.inviteAndJoinMember({ + memberRoleId: role.id, + resources: { + mode: ResourceAssignmentModeType.Granular, + projects: [ + { + projectId: project1.id, + targets: { mode: ResourceAssignmentModeType.Granular, targets: [] }, + }, + { + projectId: project3.id, + targets: { mode: ResourceAssignmentModeType.Granular, targets: [] }, + }, + ], + }, }); const result = await org.projects(member.memberToken);