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
2 changes: 0 additions & 2 deletions cypress/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down
22 changes: 17 additions & 5 deletions integration-tests/testkit/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -89,15 +96,20 @@ const createSession = async (
],
});

await internalApi.ensureUser.mutate({
const { user } = await internalApi.ensureUser.mutate({
superTokensUserId,
email,
oidcIntegrationId,
firstName: null,
lastName: null,
});

const sessionData = createSessionPayload(superTokensUserId, email);
const sessionData = createSessionPayload({
superTokensUserId,
userId: user.id,
oidcIntegrationId,
email,
});
const payload = {
enableAntiCsrf: false,
userId: superTokensUserId,
Expand Down
11 changes: 11 additions & 0 deletions integration-tests/testkit/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ export function getOrganization(organizationSlug: string, authToken: string) {
reportingOperations
enablingUsageBasedBreakingChanges
}
me {
id
user {
id
}
role {
id
name
permissions
}
}
}
}
`),
Expand Down
76 changes: 48 additions & 28 deletions integration-tests/testkit/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
149 changes: 42 additions & 107 deletions integration-tests/tests/api/oidc-integrations/crud.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")`,
}),
]),
);
},
);
});
});

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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: {
Expand All @@ -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 });

Expand Down
27 changes: 15 additions & 12 deletions integration-tests/tests/api/organization/members.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/services/api/src/modules/auth/lib/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function parseResourceIdentifier(resource: string) {
export type UserActor = {
type: 'user';
user: User;
oidcIntegrationId: string | null;
};

export type OrganizationAccessTokenActor = {
Expand Down
Loading
Loading