diff --git a/integration-tests/testkit/schema-policy.ts b/integration-tests/testkit/schema-policy.ts index fc9e71dd35..9602e7e986 100644 --- a/integration-tests/testkit/schema-policy.ts +++ b/integration-tests/testkit/schema-policy.ts @@ -1,40 +1,6 @@ import { RuleInstanceSeverityLevel, SchemaPolicyInput } from 'testkit/gql/graphql'; import { graphql } from './gql'; -export const TargetCalculatedPolicy = graphql(` - query TargetCalculatedPolicy($selector: TargetSelectorInput!) { - target(selector: $selector) { - id - schemaPolicy { - mergedRules { - ...SchemaPolicyRuleInstanceFields - } - projectPolicy { - id - rules { - ...SchemaPolicyRuleInstanceFields - } - } - organizationPolicy { - id - allowOverrides - rules { - ...SchemaPolicyRuleInstanceFields - } - } - } - } - } - - fragment SchemaPolicyRuleInstanceFields on SchemaPolicyRuleInstance { - rule { - id - } - severity - configuration - } -`); - export const OrganizationAndProjectsWithSchemaPolicy = graphql(` query OrganizationAndProjectsWithSchemaPolicy($organization: String!) { organization(selector: { organizationSlug: $organization }) { diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index 009b6cb1ae..f8b00100c7 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -495,19 +495,6 @@ export function initSeed() { secret, ); }, - - async updateSchemaVersionStatus(versionId: string, valid: boolean) { - return await updateSchemaVersionStatus( - { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - valid, - versionId, - }, - secret, - ).then(r => r.expectNoGraphQLErrors()); - }, async publishSchema(options: { sdl: string; headerName?: 'x-api-token' | 'authorization'; @@ -708,6 +695,22 @@ export function initSeed() { return result.target?.schemaVersions.edges.map(edge => edge.node); }, + async updateSchemaVersionStatus( + versionId: string, + valid: boolean, + ttarget: TargetOverwrite = target, + ) { + return await updateSchemaVersionStatus( + { + organizationSlug: organization.slug, + projectSlug: project.slug, + targetSlug: ttarget.slug, + valid, + versionId, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); + }, }; }, async inviteAndJoinMember(inviteToken: string = ownerToken) { diff --git a/integration-tests/tests/api/artifacts-cdn.spec.ts b/integration-tests/tests/api/artifacts-cdn.spec.ts index 916c7356ba..9459866cb9 100644 --- a/integration-tests/tests/api/artifacts-cdn.spec.ts +++ b/integration-tests/tests/api/artifacts-cdn.spec.ts @@ -662,7 +662,7 @@ describe('CDN token', () => { expect(deleteResult).toEqual( expect.arrayContaining([ expect.objectContaining({ - message: `No access (reason: "Missing target:settings permission")`, + message: `No access (reason: "Missing permission for performing 'cdnAccessToken:delete' on resource")`, }), ]), ); diff --git a/integration-tests/tests/api/collections/document-collections.spec.ts b/integration-tests/tests/api/collections/document-collections.spec.ts index 1a49e699fc..9ccdc8fef0 100644 --- a/integration-tests/tests/api/collections/document-collections.spec.ts +++ b/integration-tests/tests/api/collections/document-collections.spec.ts @@ -140,7 +140,7 @@ describe('Document Collections', () => { ).rejects.toEqual( expect.objectContaining({ message: expect.stringContaining( - `No access (reason: "Missing target:registry:write permission")`, + `No access (reason: "Missing permission for performing 'laboratory:createCollection' on resource")`, ), }), ); @@ -172,7 +172,7 @@ describe('Document Collections', () => { ).rejects.toEqual( expect.objectContaining({ message: expect.stringContaining( - 'No access (reason: "Missing target:registry:write permission")', + `No access (reason: "Missing permission for performing 'laboratory:modifyCollection' on resource")`, ), }), ); @@ -202,7 +202,7 @@ describe('Document Collections', () => { ).rejects.toEqual( expect.objectContaining({ message: expect.stringContaining( - `No access (reason: "Missing target:registry:write permission")`, + `No access (reason: "Missing permission for performing 'laboratory:deleteCollection' on resource")`, ), }), ); diff --git a/integration-tests/tests/api/oidc-integrations/crud.spec.ts b/integration-tests/tests/api/oidc-integrations/crud.spec.ts index 1770bbb564..cfb6c7e240 100644 --- a/integration-tests/tests/api/oidc-integrations/crud.spec.ts +++ b/integration-tests/tests/api/oidc-integrations/crud.spec.ts @@ -150,7 +150,7 @@ describe('create', () => { expect(errors).toEqual( expect.arrayContaining([ expect.objectContaining({ - message: `No access (reason: "Missing organization:integrations permission")`, + message: `No access (reason: "Missing permission for performing 'oidc:modify' on resource")`, }), ]), ); @@ -545,7 +545,7 @@ describe('delete', () => { expect(errors).toEqual( expect.arrayContaining([ expect.objectContaining({ - message: `No access (reason: "Missing organization:integrations permission")`, + message: `No access (reason: "Missing permission for performing 'oidc:modify' on resource")`, }), ]), ); @@ -742,7 +742,7 @@ describe('update', () => { expect(errors).toEqual( expect.arrayContaining([ expect.objectContaining({ - message: `No access (reason: "Missing organization:integrations permission")`, + message: `No access (reason: "Missing permission for performing 'oidc:modify' on resource")`, }), ]), ); diff --git a/integration-tests/tests/api/policy/policy-access.spec.ts b/integration-tests/tests/api/policy/policy-access.spec.ts index d6560a67ad..2f60034acd 100644 --- a/integration-tests/tests/api/policy/policy-access.spec.ts +++ b/integration-tests/tests/api/policy/policy-access.spec.ts @@ -4,76 +4,6 @@ import { execute } from '../../../testkit/graphql'; import { initSeed } from '../../../testkit/seed'; describe('Policy Access', () => { - describe('Target', () => { - const query = graphql(` - query TargetSchemaPolicyAccess($selector: TargetSelectorInput!) { - target(selector: $selector) { - schemaPolicy { - mergedRules { - severity - } - } - } - } - `); - - test.concurrent( - 'should successfully fetch Target.schemaPolicy if the user has access to SETTINGS', - async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); - const { organization, createProject, inviteAndJoinMember } = await createOrg(); - const { project, target } = await createProject(ProjectType.Single); - const adminRole = organization.memberRoles.find(r => r.name === 'Admin'); - - if (!adminRole) { - throw new Error('Admin role not found'); - } - - const { member, memberToken, assignMemberRole } = await inviteAndJoinMember(); - await assignMemberRole({ - roleId: adminRole.id, - userId: member.user.id, - }); - - const result = await execute({ - document: query, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: memberToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(result.target?.schemaPolicy?.mergedRules).not.toBeNull(); - }, - ); - - test.concurrent( - 'should fail to fetch Target.schemaPolicy if the user lacks access to SETTINGS', - async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); - const { organization, createProject, inviteAndJoinMember } = await createOrg(); - const { project, target } = await createProject(ProjectType.Single); - const { memberToken } = await inviteAndJoinMember(); - - await execute({ - document: query, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: memberToken, - }).then(r => r.expectGraphQLErrors()); - }, - ); - }); - describe('Project', () => { const query = graphql(` query ProjectSchemaPolicyAccess($selector: ProjectSelectorInput!) { diff --git a/integration-tests/tests/api/policy/policy-crud.spec.ts b/integration-tests/tests/api/policy/policy-crud.spec.ts index 745a046921..1c55a521d5 100644 --- a/integration-tests/tests/api/policy/policy-crud.spec.ts +++ b/integration-tests/tests/api/policy/policy-crud.spec.ts @@ -1,235 +1,16 @@ import { ProjectType } from 'testkit/gql/graphql'; import { execute } from '../../../testkit/graphql'; import { - DESCRIPTION_RULE, EMPTY_RULE_CONFIG_POLICY, INVALID_RULE_CONFIG_POLICY, INVALID_RULE_POLICY, LONGER_VALID_POLICY, OrganizationAndProjectsWithSchemaPolicy, - TargetCalculatedPolicy, VALID_POLICY, } from '../../../testkit/schema-policy'; import { initSeed } from '../../../testkit/seed'; describe('Policy CRUD', () => { - describe('Target level', () => { - test.concurrent( - 'Should return empty policy when project and org does not have one', - async ({ expect }) => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, createProject } = await createOrg(); - const { project, target } = await createProject(ProjectType.Single); - - const result = await execute({ - document: TargetCalculatedPolicy, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(result.target?.schemaPolicy).toBeNull(); - }, - ); - - test('Should return a valid policy when only org has a policy', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, createProject, setOrganizationSchemaPolicy } = await createOrg(); - const { project, target } = await createProject(ProjectType.Single); - - const upsertResult = await setOrganizationSchemaPolicy(VALID_POLICY, true); - expect(upsertResult.error).toBeNull(); - - const result = await execute({ - document: TargetCalculatedPolicy, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(result.target?.schemaPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.organizationPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.projectPolicy).toBeNull(); - expect(result.target?.schemaPolicy?.mergedRules).toEqual( - result.target?.schemaPolicy?.organizationPolicy?.rules, - ); - }); - - test('Should return a valid policy when only project has a policy', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, createProject } = await createOrg(); - const { project, target, setProjectSchemaPolicy } = await createProject(ProjectType.Single); - - await setProjectSchemaPolicy(VALID_POLICY); - - const result = await execute({ - document: TargetCalculatedPolicy, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(result.target?.schemaPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.projectPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.organizationPolicy).toBeNull(); - expect(result.target?.schemaPolicy?.mergedRules).toEqual( - result.target?.schemaPolicy?.projectPolicy?.rules, - ); - }); - - test('Should return a valid policy when both project and org has policies - with no overrides', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, createProject, setOrganizationSchemaPolicy } = await createOrg(); - const { project, target, setProjectSchemaPolicy } = await createProject(ProjectType.Single); - - await setOrganizationSchemaPolicy(VALID_POLICY, true); - await setProjectSchemaPolicy({ rules: [DESCRIPTION_RULE] }); - - const result = await execute({ - document: TargetCalculatedPolicy, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(result.target?.schemaPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.projectPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.organizationPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.mergedRules).toMatchInlineSnapshot(` - [ - { - configuration: { - types: true, - }, - rule: { - id: require-description, - }, - severity: ERROR, - }, - { - configuration: { - style: inline, - }, - rule: { - id: description-style, - }, - severity: WARNING, - }, - ] - `); - }); - - test('Should return a valid policy when both project and org has policies - with overrides', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, createProject, setOrganizationSchemaPolicy } = await createOrg(); - const { project, target, setProjectSchemaPolicy } = await createProject(ProjectType.Single); - - await setOrganizationSchemaPolicy(VALID_POLICY, true); - await setProjectSchemaPolicy(LONGER_VALID_POLICY); - - const result = await execute({ - document: TargetCalculatedPolicy, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(result.target?.schemaPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.projectPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.organizationPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.mergedRules).toMatchInlineSnapshot(` - [ - { - configuration: { - FieldDefinition: true, - types: true, - }, - rule: { - id: require-description, - }, - severity: ERROR, - }, - { - configuration: { - style: inline, - }, - rule: { - id: description-style, - }, - severity: WARNING, - }, - ] - `); - }); - - test('Should ignore project policy when policy was set and org is not allowing overrides', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, createProject, setOrganizationSchemaPolicy } = await createOrg(); - const { project, target, setProjectSchemaPolicy } = await createProject(ProjectType.Single); - - // First, set the org policy while it still can - await setProjectSchemaPolicy(LONGER_VALID_POLICY); - - // Now, mark the org as not allowing overrides - await setOrganizationSchemaPolicy(VALID_POLICY, false); - - const result = await execute({ - document: TargetCalculatedPolicy, - variables: { - selector: { - organizationSlug: organization.slug, - projectSlug: project.slug, - targetSlug: target.slug, - }, - }, - authToken: ownerToken, - }).then(r => r.expectNoGraphQLErrors()); - - expect(result.target?.schemaPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.projectPolicy).toBeDefined(); - expect(result.target?.schemaPolicy?.organizationPolicy).toBeDefined(); - // Should have only org policy now - expect(result.target?.schemaPolicy?.mergedRules).toMatchInlineSnapshot(` - [ - { - configuration: { - types: true, - }, - rule: { - id: require-description, - }, - severity: ERROR, - }, - ] - `); - }); - }); - describe('Project level', () => { test.concurrent( 'creating a project should NOT create a record in the database for the policy', diff --git a/integration-tests/tests/api/schema/check.spec.ts b/integration-tests/tests/api/schema/check.spec.ts index 2804a3597e..dff7abbd8f 100644 --- a/integration-tests/tests/api/schema/check.spec.ts +++ b/integration-tests/tests/api/schema/check.spec.ts @@ -45,7 +45,9 @@ test.concurrent('can check a schema with target:registry:read access', async ({ `) .then(r => r.expectGraphQLErrors()); expect(checkResultErrors).toHaveLength(1); - expect(checkResultErrors[0].message).toMatch('target:registry:read'); + expect(checkResultErrors[0].message).toMatch( + `No access (reason: "Missing permission for performing 'schemaCheck:create' on resource")`, + ); // Check schema with read rights const checkResultValid = await readToken @@ -235,7 +237,7 @@ const ApproveFailedSchemaCheckMutation = graphql(/* GraphQL */ ` test.concurrent( 'successful check without previously published schema is persisted', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single); @@ -275,7 +277,7 @@ test.concurrent( }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -295,7 +297,7 @@ test.concurrent( test.concurrent( 'successful check with previously published schema is persisted', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single); @@ -353,7 +355,7 @@ test.concurrent( }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -373,7 +375,7 @@ test.concurrent( ); test.concurrent('failed check due to graphql validation is persisted', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single); @@ -412,7 +414,7 @@ test.concurrent('failed check due to graphql validation is persisted', async ({ }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -436,7 +438,7 @@ test.concurrent('failed check due to graphql validation is persisted', async ({ }); test.concurrent('failed check due to breaking change is persisted', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single); @@ -492,7 +494,7 @@ test.concurrent('failed check due to breaking change is persisted', async ({ exp }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -519,7 +521,7 @@ test.concurrent('failed check due to breaking change is persisted', async ({ exp }); test.concurrent('failed check due to policy error is persisted', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target, setProjectSchemaPolicy } = await createProject( ProjectType.Single, @@ -580,7 +582,7 @@ test.concurrent('failed check due to policy error is persisted', async ({ expect }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -621,7 +623,7 @@ test.concurrent('failed check due to policy error is persisted', async ({ expect test.concurrent( 'successful check with warnings and safe changes is persisted', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target, setProjectSchemaPolicy } = await createProject(ProjectType.Single); @@ -681,7 +683,7 @@ test.concurrent( }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -756,7 +758,7 @@ test.concurrent( ); test.concurrent('metadata is persisted', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single); @@ -803,7 +805,7 @@ test.concurrent('metadata is persisted', async ({ expect }) => { }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -911,7 +913,7 @@ test.concurrent( }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -1025,7 +1027,7 @@ test.concurrent('approve failed schema check with a comment', async ({ expect }) }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck).toMatchObject({ @@ -1156,7 +1158,7 @@ test.concurrent( }, id: newSchemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(newSchemaCheck.target?.schemaCheck).toMatchObject({ @@ -1297,7 +1299,7 @@ test.concurrent( }, id: newSchemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(newSchemaCheck.target?.schemaCheck).toMatchObject({ @@ -1433,7 +1435,7 @@ test.concurrent( }, id: newSchemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(newSchemaCheck.target?.schemaCheck).toMatchObject({ diff --git a/integration-tests/tests/api/schema/contracts-check.spec.ts b/integration-tests/tests/api/schema/contracts-check.spec.ts index 0f29e15d57..a3988b5175 100644 --- a/integration-tests/tests/api/schema/contracts-check.spec.ts +++ b/integration-tests/tests/api/schema/contracts-check.spec.ts @@ -503,7 +503,7 @@ test.concurrent( }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck?.target?.schemaCheck).toMatchObject({ @@ -699,7 +699,7 @@ test.concurrent( }, id: newSchemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(newSchemaCheck?.target?.schemaCheck).toMatchObject({ @@ -895,7 +895,7 @@ test.concurrent( }, id: newSchemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(newSchemaCheck?.target?.schemaCheck).toMatchObject({ @@ -1084,7 +1084,7 @@ test.concurrent( }, id: newSchemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(newSchemaCheck?.target?.schemaCheck).toMatchObject({ @@ -1328,7 +1328,7 @@ test.concurrent( }, id: schemaCheckId, }, - authToken: readToken.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(schemaCheck?.target?.schemaCheck).toMatchObject({ diff --git a/integration-tests/tests/api/schema/publish.spec.ts b/integration-tests/tests/api/schema/publish.spec.ts index 6575dba6d4..0a85733349 100644 --- a/integration-tests/tests/api/schema/publish.spec.ts +++ b/integration-tests/tests/api/schema/publish.spec.ts @@ -35,7 +35,9 @@ test.concurrent( .then(r => r.expectGraphQLErrors()); expect(resultErrors).toHaveLength(1); - expect(resultErrors[0].message).toMatch('target:registry:write'); + expect(resultErrors[0].message).toMatch( + `No access (reason: "Missing permission for performing 'schemaVersion:publish' on resource")`, + ); }, ); diff --git a/integration-tests/tests/api/target/tokens.spec.ts b/integration-tests/tests/api/target/tokens.spec.ts index 3263f7a71f..5e95e55faa 100644 --- a/integration-tests/tests/api/target/tokens.spec.ts +++ b/integration-tests/tests/api/target/tokens.spec.ts @@ -57,7 +57,7 @@ test.concurrent( }); await expect(tokenResult).rejects.toThrowError( - 'No access (reason: "Missing target:tokens:write permission")', + 'No access (reason: "Missing permission for performing \'targetAccessToken:create\' on resource")', ); }, ); diff --git a/integration-tests/tests/api/target/usage.spec.ts b/integration-tests/tests/api/target/usage.spec.ts index 8830be28b6..da484091a2 100644 --- a/integration-tests/tests/api/target/usage.spec.ts +++ b/integration-tests/tests/api/target/usage.spec.ts @@ -2350,7 +2350,7 @@ test.concurrent( test.concurrent( 'subscription operation is used for conditional breaking change detection', async ({ expect }) => { - const { createOrg } = await initSeed().createOwner(); + const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, @@ -2496,7 +2496,7 @@ test.concurrent( targetSlug: target.slug, }, }, - authToken: token.secret, + authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); const node = firstSchemaCheck.target?.schemaCheck?.breakingSchemaChanges?.nodes[0]; diff --git a/integration-tests/tests/models/single-legacy.spec.ts b/integration-tests/tests/models/single-legacy.spec.ts index d6492c6441..d025164e8e 100644 --- a/integration-tests/tests/models/single-legacy.spec.ts +++ b/integration-tests/tests/models/single-legacy.spec.ts @@ -322,11 +322,11 @@ describe('other', () => { test.concurrent('marking versions as valid', async () => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); - const { createTargetAccessToken, fetchVersions } = await createProject(ProjectType.Single, { - useLegacyRegistryModels: true, - }); - const { publishSchema, fetchLatestValidSchema, updateSchemaVersionStatus } = - await createTargetAccessToken({}); + const { createTargetAccessToken, fetchVersions, updateSchemaVersionStatus } = + await createProject(ProjectType.Single, { + useLegacyRegistryModels: true, + }); + const { publishSchema, fetchLatestValidSchema } = await createTargetAccessToken({}); // Initial schema let result = await publishSchema({ @@ -391,13 +391,11 @@ describe('other', () => { async () => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); - const { createTargetAccessToken, createCdnAccess, fetchVersions } = await createProject( - ProjectType.Single, - { + const { createTargetAccessToken, createCdnAccess, fetchVersions, updateSchemaVersionStatus } = + await createProject(ProjectType.Single, { useLegacyRegistryModels: true, - }, - ); - const { publishSchema, updateSchemaVersionStatus } = await createTargetAccessToken({}); + }); + const { publishSchema } = await createTargetAccessToken({}); const { fetchSchemaFromCDN, fetchMetadataFromCDN } = await createCdnAccess(); // Initial schema diff --git a/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts b/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts index 7584367707..8c7261fd8e 100644 --- a/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts +++ b/packages/migrations/test/2024.01.26T00.00.01.schema-check-purging.test.ts @@ -433,6 +433,7 @@ describe('schema check purging', async () => { ); await storage.approveFailedSchemaCheck({ + targetId: failedSchemaCheck.targetId, schemaCheckId: failedSchemaCheck.id, userId: user.id, comment: null, @@ -606,6 +607,7 @@ describe('schema check purging', async () => { ); await storage.approveFailedSchemaCheck({ + targetId: failedSchemaCheck.targetId, schemaCheckId: failedSchemaCheck.id, userId: user.id, comment: null, @@ -863,6 +865,7 @@ describe('schema check purging', async () => { const contracts = new Contracts(createLogger(), db, {} as any); await storage.approveFailedSchemaCheck({ + targetId: failedSchemaCheck.targetId, schemaCheckId: failedSchemaCheck.id, userId: user.id, contracts, @@ -1064,6 +1067,7 @@ describe('schema check purging', async () => { const contracts = new Contracts(createLogger(), db, {} as any); await storage.approveFailedSchemaCheck({ + targetId: failedSchemaCheck.targetId, schemaCheckId: failedSchemaCheck.id, userId: user.id, contracts, diff --git a/packages/services/api/src/context.ts b/packages/services/api/src/context.ts index 316d326dc2..a1bf617694 100644 --- a/packages/services/api/src/context.ts +++ b/packages/services/api/src/context.ts @@ -1,4 +1,5 @@ import type { FastifyRequest } from '@hive/service-common'; +import { Session } from './modules/auth/lib/authz'; export interface RegistryContext { req: FastifyRequest; @@ -6,6 +7,7 @@ export interface RegistryContext { user: any; headers: Record; request: Request; + session: Session; } declare global { diff --git a/packages/services/api/src/create.ts b/packages/services/api/src/create.ts index faf2a1cf2a..70bef8e2bc 100644 --- a/packages/services/api/src/create.ts +++ b/packages/services/api/src/create.ts @@ -1,4 +1,4 @@ -import { createApplication, Scope } from 'graphql-modules'; +import { CONTEXT, createApplication, Provider, Scope } from 'graphql-modules'; import { Redis } from 'ioredis'; import { adminModule } from './modules/admin'; import { alertsModule } from './modules/alerts'; @@ -6,14 +6,13 @@ import { WEBHOOKS_CONFIG, WebhooksConfig } from './modules/alerts/providers/toke import { appDeploymentsModule } from './modules/app-deployments'; import { APP_DEPLOYMENTS_ENABLED } from './modules/app-deployments/providers/app-deployments-enabled-token'; import { authModule } from './modules/auth'; +import { Session } from './modules/auth/lib/authz'; import { billingModule } from './modules/billing'; import { BILLING_CONFIG, BillingConfig } from './modules/billing/providers/tokens'; import { cdnModule } from './modules/cdn'; import { AwsClient } from './modules/cdn/providers/aws'; import { CDN_CONFIG, CDNConfig } from './modules/cdn/providers/tokens'; import { collectionModule } from './modules/collection'; -import { feedbackModule } from './modules/feedback'; -import { FEEDBACK_SLACK_CHANNEL, FEEDBACK_SLACK_TOKEN } from './modules/feedback/providers/tokens'; import { integrationsModule } from './modules/integrations'; import { GITHUB_APP_CONFIG, @@ -81,7 +80,6 @@ const modules = [ labModule, integrationsModule, alertsModule, - feedbackModule, cdnModule, adminModule, usageEstimationModule, @@ -110,7 +108,6 @@ export function createRegistry({ s3, s3Mirror, encryptionSecret, - feedback, billing, schemaConfig, supportConfig, @@ -146,10 +143,6 @@ export function createRegistry({ sessionToken?: string; } | null; encryptionSecret: string; - feedback: { - token: string; - channel: string; - }; app: { baseUrl: string; } | null; @@ -189,7 +182,7 @@ export function createRegistry({ const artifactStorageWriter = new ArtifactStorageWriter(s3Config, logger); - const providers = [ + const providers: Provider[] = [ ActivityManager, HttpClient, IdTranslator, @@ -271,16 +264,6 @@ export function createRegistry({ useValue: s3Config, scope: Scope.Singleton, }, - { - provide: FEEDBACK_SLACK_CHANNEL, - useValue: feedback.channel, - scope: Scope.Singleton, - }, - { - provide: FEEDBACK_SLACK_TOKEN, - useValue: feedback.token, - scope: Scope.Singleton, - }, { provide: OIDC_INTEGRATIONS_ENABLED, useValue: organizationOIDC, @@ -304,6 +287,14 @@ export function createRegistry({ { provide: PUB_SUB_CONFIG, scope: Scope.Singleton, useValue: pubSub }, encryptionSecretProvider(encryptionSecret), provideSchemaModuleConfig(schemaConfig), + { + provide: Session, + useFactory(context: { session: Session }) { + return context.session; + }, + scope: Scope.Operation, + deps: [CONTEXT], + }, ]; if (emailsEndpoint) { diff --git a/packages/services/api/src/modules/admin/providers/admin-manager.ts b/packages/services/api/src/modules/admin/providers/admin-manager.ts index 5cf79a5c17..993fbc9dd1 100644 --- a/packages/services/api/src/modules/admin/providers/admin-manager.ts +++ b/packages/services/api/src/modules/admin/providers/admin-manager.ts @@ -1,7 +1,7 @@ import { GraphQLError } from 'graphql'; import { Injectable, Scope } from 'graphql-modules'; import { atomic } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; +import { Session } from '../../auth/lib/authz'; import { OperationsReader } from '../../operations/providers/operations-reader'; import { Logger } from '../../shared/providers/logger'; import { Storage } from '../../shared/providers/storage'; @@ -19,7 +19,7 @@ export class AdminManager { constructor( logger: Logger, private storage: Storage, - private authManager: AuthManager, + private session: Session, private operationsReader: OperationsReader, ) { this.logger = logger.child({ source: 'AdminManager' }); @@ -27,7 +27,7 @@ export class AdminManager { async getStats(period: { from: Date; to: Date }) { this.logger.debug('Fetching admin stats'); - const user = await this.authManager.getCurrentUser(); + const user = await this.session.getViewer(); if (!user.isAdmin) { throw new GraphQLError('GO AWAY'); @@ -52,7 +52,7 @@ export class AdminManager { period.to, resolution, ); - const user = await this.authManager.getCurrentUser(); + const user = await this.session.getViewer(); if (!user.isAdmin) { throw new GraphQLError('GO AWAY'); @@ -76,7 +76,7 @@ export class AdminManager { period.from, period.to, ); - const user = await this.authManager.getCurrentUser(); + const user = await this.session.getViewer(); if (user.isAdmin) { const pairs = await this.storage.adminGetOrganizationsTargetPairs(); diff --git a/packages/services/api/src/modules/alerts/providers/alerts-manager.ts b/packages/services/api/src/modules/alerts/providers/alerts-manager.ts index 41194437af..d54b1f871e 100644 --- a/packages/services/api/src/modules/alerts/providers/alerts-manager.ts +++ b/packages/services/api/src/modules/alerts/providers/alerts-manager.ts @@ -1,9 +1,7 @@ import { Injectable, Scope } from 'graphql-modules'; import type { Alert, AlertChannel } from '../../../shared/entities'; import { cache } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { ProjectAccessScope } from '../../auth/providers/project-access'; -import { TargetAccessScope } from '../../auth/providers/target-access'; +import { Session } from '../../auth/lib/authz'; import { IntegrationsAccessContext } from '../../integrations/providers/integrations-access-context'; import { SlackIntegrationManager } from '../../integrations/providers/slack-integration-manager'; import { OrganizationManager } from '../../organization/providers/organization-manager'; @@ -25,7 +23,7 @@ export class AlertsManager { constructor( logger: Logger, - private authManager: AuthManager, + private session: Session, private slackIntegrationManager: SlackIntegrationManager, private slack: SlackCommunicationAdapter, private webhook: WebhookCommunicationAdapter, @@ -51,10 +49,13 @@ export class AlertsManager { input.projectId, input.type, ); - await this.authManager.ensureProjectAccess({ - scope: ProjectAccessScope.ALERTS, + await this.session.assertPerformAction({ + action: 'alert:modify', organizationId: input.organizationId, - projectId: input.projectId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); const channel = await this.storage.addAlertChannel({ @@ -87,11 +88,15 @@ export class AlertsManager { input.projectId, input.channelIds.length, ); - await this.authManager.ensureProjectAccess({ - scope: ProjectAccessScope.ALERTS, + await this.session.assertPerformAction({ + action: 'alert:modify', organizationId: input.organizationId, - projectId: input.projectId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); + const channels = await this.storage.deleteAlertChannels({ organizationId: input.organizationId, projectId: input.projectId, @@ -119,9 +124,13 @@ export class AlertsManager { selector.organizationId, selector.projectId, ); - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.READ, + await this.session.assertPerformAction({ + action: 'alert:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); return this.storage.getAlertChannels(selector); } @@ -139,10 +148,13 @@ export class AlertsManager { input.projectId, input.type, ); - await this.authManager.ensureProjectAccess({ - scope: ProjectAccessScope.ALERTS, + await this.session.assertPerformAction({ + action: 'alert:modify', organizationId: input.organizationId, - projectId: input.projectId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); return this.storage.addAlert({ @@ -165,9 +177,13 @@ export class AlertsManager { input.projectId, input.alerts.length, ); - await this.authManager.ensureProjectAccess({ - ...input, - scope: ProjectAccessScope.ALERTS, + await this.session.assertPerformAction({ + action: 'alert:modify', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); return this.storage.deleteAlerts({ organizationId: input.organizationId, @@ -182,9 +198,13 @@ export class AlertsManager { selector.organizationId, selector.projectId, ); - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.READ, + await this.session.assertPerformAction({ + action: 'alert:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); return this.storage.getAlerts(selector); } @@ -213,13 +233,6 @@ export class AlertsManager { event.schema.id, ); - await this.authManager.ensureTargetAccess({ - organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_WRITE, - }); - const [channels, alerts] = await Promise.all([ this.getChannels({ organizationId: organization, projectId: project }), this.getAlerts({ organizationId: organization, projectId: project }), diff --git a/packages/services/api/src/modules/app-deployments/providers/app-deployments-manager.ts b/packages/services/api/src/modules/app-deployments/providers/app-deployments-manager.ts index bc95741b83..73bca40f39 100644 --- a/packages/services/api/src/modules/app-deployments/providers/app-deployments-manager.ts +++ b/packages/services/api/src/modules/app-deployments/providers/app-deployments-manager.ts @@ -1,11 +1,9 @@ import { Injectable, Scope } from 'graphql-modules'; import { Target } from '../../../shared/entities'; import { batch } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { TargetAccessScope } from '../../auth/providers/scopes'; +import { Session } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; import { TargetManager } from '../../target/providers/target-manager'; -import { TokenStorage } from '../../token/providers/token-storage'; import { AppDeployments, type AppDeploymentRecord } from './app-deployments'; export type AppDeploymentStatus = 'pending' | 'active' | 'retired'; @@ -19,8 +17,7 @@ export class AppDeploymentsManager { constructor( logger: Logger, - private auth: AuthManager, - private tokenStorage: TokenStorage, + private session: Session, private targetManager: TargetManager, private appDeployments: AppDeployments, ) { @@ -34,13 +31,6 @@ export class AppDeploymentsManager { version: string; }, ): Promise { - await this.auth.ensureTargetAccess({ - organizationId: target.orgId, - projectId: target.projectId, - targetId: target.id, - scope: TargetAccessScope.READ, - }); - const appDeployment = await this.appDeployments.findAppDeployment({ targetId: target.id, name: appDeploymentInput.name, @@ -51,6 +41,16 @@ export class AppDeploymentsManager { return null; } + await this.session.assertPerformAction({ + action: 'appDeployment:describe', + organizationId: target.orgId, + params: { + organizationId: target.orgId, + projectId: target.projectId, + targetId: target.id, + }, + }); + return appDeployment; } @@ -72,19 +72,22 @@ export class AppDeploymentsManager { version: string; }; }) { - const token = this.auth.ensureApiToken(); - const tokenRecord = await this.tokenStorage.getToken({ token }); - - await this.auth.ensureTargetAccess({ - organizationId: tokenRecord.organization, - projectId: tokenRecord.project, - targetId: tokenRecord.target, - scope: TargetAccessScope.REGISTRY_WRITE, + const token = this.session.getLegacySelector(); + + await this.session.assertPerformAction({ + action: 'appDeployment:create', + organizationId: token.organizationId, + params: { + organizationId: token.organizationId, + projectId: token.projectId, + targetId: token.targetId, + appDeploymentName: args.appDeployment.name, + }, }); return await this.appDeployments.createAppDeployment({ - organizationId: tokenRecord.organization, - targetId: tokenRecord.target, + organizationId: token.organizationId, + targetId: token.targetId, appDeployment: args.appDeployment, }); } @@ -99,20 +102,23 @@ export class AppDeploymentsManager { body: string; }>; }) { - const token = this.auth.ensureApiToken(); - const tokenRecord = await this.tokenStorage.getToken({ token }); - - await this.auth.ensureTargetAccess({ - organizationId: tokenRecord.organization, - projectId: tokenRecord.project, - targetId: tokenRecord.target, - scope: TargetAccessScope.REGISTRY_WRITE, + const token = this.session.getLegacySelector(); + + await this.session.assertPerformAction({ + action: 'appDeployment:create', + organizationId: token.organizationId, + params: { + organizationId: token.organizationId, + projectId: token.projectId, + targetId: token.targetId, + appDeploymentName: args.appDeployment.name, + }, }); return await this.appDeployments.addDocumentsToAppDeployment({ - organizationId: tokenRecord.organization, - projectId: tokenRecord.project, - targetId: tokenRecord.target, + organizationId: token.organizationId, + projectId: token.projectId, + targetId: token.targetId, appDeployment: args.appDeployment, operations: args.documents, }); @@ -124,19 +130,22 @@ export class AppDeploymentsManager { version: string; }; }) { - const token = this.auth.ensureApiToken(); - const tokenRecord = await this.tokenStorage.getToken({ token }); - - await this.auth.ensureTargetAccess({ - organizationId: tokenRecord.organization, - projectId: tokenRecord.project, - targetId: tokenRecord.target, - scope: TargetAccessScope.REGISTRY_WRITE, + const token = this.session.getLegacySelector(); + + await this.session.assertPerformAction({ + action: 'appDeployment:publish', + organizationId: token.organizationId, + params: { + organizationId: token.organizationId, + projectId: token.projectId, + targetId: token.targetId, + appDeploymentName: args.appDeployment.name, + }, }); return await this.appDeployments.activateAppDeployment({ - organizationId: tokenRecord.organization, - targetId: tokenRecord.target, + organizationId: token.organizationId, + targetId: token.targetId, appDeployment: args.appDeployment, }); } @@ -150,11 +159,15 @@ export class AppDeploymentsManager { }) { const target = await this.targetManager.getTargetById({ targetId: args.targetId }); - await this.auth.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'appDeployment:retire', organizationId: target.orgId, - projectId: target.projectId, - targetId: target.id, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + organizationId: target.orgId, + projectId: target.projectId, + targetId: target.id, + appDeploymentName: args.appDeployment.name, + }, }); return await this.appDeployments.retireAppDeployment({ @@ -182,11 +195,14 @@ export class AppDeploymentsManager { target: Target, args: { cursor: string | null; first: number | null }, ) { - await this.auth.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'appDeployment:describe', organizationId: target.orgId, - projectId: target.projectId, - targetId: target.id, - scope: TargetAccessScope.READ, + params: { + organizationId: target.orgId, + projectId: target.projectId, + targetId: target.id, + }, }); return await this.appDeployments.getPaginatedAppDeployments({ diff --git a/packages/services/api/src/modules/auth/index.ts b/packages/services/api/src/modules/auth/index.ts index 03dc41bc72..c640d657a2 100644 --- a/packages/services/api/src/modules/auth/index.ts +++ b/packages/services/api/src/modules/auth/index.ts @@ -3,7 +3,6 @@ import { AuthManager } from './providers/auth-manager'; import { OrganizationAccess } from './providers/organization-access'; import { ProjectAccess } from './providers/project-access'; import { TargetAccess } from './providers/target-access'; -import { ApiTokenProvider } from './providers/tokens'; import { UserManager } from './providers/user-manager'; import { resolvers } from './resolvers.generated'; import typeDefs from './module.graphql'; @@ -13,12 +12,5 @@ export const authModule = createModule({ dirname: __dirname, typeDefs, resolvers, - providers: [ - AuthManager, - UserManager, - ApiTokenProvider, - OrganizationAccess, - ProjectAccess, - TargetAccess, - ], + providers: [AuthManager, UserManager, OrganizationAccess, ProjectAccess, TargetAccess], }); diff --git a/packages/services/api/src/modules/auth/lib/authz.spec.ts b/packages/services/api/src/modules/auth/lib/authz.spec.ts new file mode 100644 index 0000000000..7000732ca6 --- /dev/null +++ b/packages/services/api/src/modules/auth/lib/authz.spec.ts @@ -0,0 +1,211 @@ +import { AccessError } from '../../../shared/errors'; +import { NoopLogger } from '../../shared/providers/logger'; +import { AuthorizationPolicyStatement, Session } from './authz'; + +class TestSession extends Session { + policyStatements: Array; + constructor(policyStatements: Array) { + super({ logger: new NoopLogger() }); + this.policyStatements = policyStatements; + } + + public loadPolicyStatementsForOrganization( + _: string, + ): Promise> | Array { + return this.policyStatements; + } +} + +describe('Session.assertPerformAction', () => { + test('No policies results in rejection', async () => { + const session = new TestSession([]); + const result = await session + .assertPerformAction({ + organizationId: '50b84370-49fc-48d4-87cb-bde5a3c8fd2f', + action: 'organization:describe', + params: { + organizationId: '50b84370-49fc-48d4-87cb-bde5a3c8fd2f', + }, + }) + .catch(error => error); + expect(result).toBeInstanceOf(AccessError); + }); + test('Single allow policy on specific resource allows action', async () => { + const session = new TestSession([ + { + effect: 'allow', + resource: + 'hrn:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:organization/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + action: 'organization:describe', + }, + ]); + const result = await session + .assertPerformAction({ + organizationId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + action: 'organization:describe', + params: { + organizationId: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }) + .catch(error => error); + expect(result).toEqual(undefined); + }); + test('Single policy on wildcard resource id allows action', async () => { + const session = new TestSession([ + { + effect: 'allow', + resource: 'hrn:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:organization/*', + action: 'organization:describe', + }, + ]); + const result = await session + .assertPerformAction({ + organizationId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + action: 'organization:describe', + params: { + organizationId: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }) + .catch(error => error); + expect(result).toEqual(undefined); + }); + test('Single policy on wildcard organization allows action', async () => { + const session = new TestSession([ + { + effect: 'allow', + resource: 'hrn:*:organization/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + action: 'organization:describe', + }, + ]); + const result = await session + .assertPerformAction({ + organizationId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + action: 'organization:describe', + params: { + organizationId: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }) + .catch(error => error); + expect(result).toEqual(undefined); + }); + test('Single policy on wildcard organization and resource id allows action', async () => { + const session = new TestSession([ + { + effect: 'allow', + resource: 'hrn:*:organization/*', + action: 'organization:describe', + }, + ]); + const result = await session + .assertPerformAction({ + organizationId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + action: 'organization:describe', + params: { + organizationId: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }) + .catch(error => error); + expect(result).toEqual(undefined); + }); + test('Single policy on wildcard resource allows action', async () => { + const session = new TestSession([ + { + effect: 'allow', + resource: 'hrn:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:*', + action: 'organization:describe', + }, + ]); + await session.assertPerformAction({ + organizationId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + action: 'organization:describe', + params: { + organizationId: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }); + }); + test('Single policy on different organization disallows action', async () => { + const session = new TestSession([ + { + effect: 'allow', + resource: 'hrn:cccccccc-cccc-cccc-cccc-cccccccccccc:*', + action: 'organization:describe', + }, + ]); + const result = await session + .assertPerformAction({ + organizationId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + action: 'organization:describe', + params: { + organizationId: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }) + .catch(error => error); + expect(result).toBeInstanceOf(AccessError); + }); + test('A single deny policy always disallows action', async () => { + const session = new TestSession([ + { + effect: 'allow', + resource: 'hrn:*:*', + action: 'organization:describe', + }, + { + effect: 'deny', + resource: 'hrn:*:*', + action: 'organization:describe', + }, + ]); + const result = await session + .assertPerformAction({ + organizationId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + action: 'organization:describe', + params: { + organizationId: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }) + .catch(error => error); + expect(result).toBeInstanceOf(AccessError); + }); + + test('Allow on org level, but deny on project level for single resource', async () => { + const orgId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; + const bId = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'; + const cId = 'cccccccc-cccc-cccc-cccc-cccccccccccc'; + + const session = new TestSession([ + { + effect: 'allow', + action: 'project:describe', + resource: [`hrn:${orgId}:organization/${orgId}`], + }, + { + effect: 'deny', + action: 'project:describe', + resource: [`hrn:${orgId}:project/${cId}`], + }, + ]); + + const result1 = await session + .assertPerformAction({ + action: 'project:describe', + organizationId: orgId, + params: { + organizationId: orgId, + projectId: bId, + }, + }) + .catch(err => err); + expect(result1).toEqual(undefined); + const result2 = await session + .assertPerformAction({ + action: 'project:describe', + organizationId: orgId, + params: { + organizationId: orgId, + projectId: cId, + }, + }) + .catch(err => err); + expect(result2).toBeInstanceOf(AccessError); + }); +}); diff --git a/packages/services/api/src/modules/auth/lib/authz.ts b/packages/services/api/src/modules/auth/lib/authz.ts new file mode 100644 index 0000000000..639076cdc2 --- /dev/null +++ b/packages/services/api/src/modules/auth/lib/authz.ts @@ -0,0 +1,434 @@ +import stringify from 'fast-json-stable-stringify'; +import { FastifyReply, FastifyRequest } from '@hive/service-common'; +import type { User } from '../../../shared/entities'; +import { AccessError } from '../../../shared/errors'; +import { isUUID } from '../../../shared/is-uuid'; +import { Logger } from '../../shared/providers/logger'; + +export type AuthorizationPolicyStatement = { + effect: 'allow' | 'deny'; + action: ActionStrings | ActionStrings[]; + resource: string | string[]; +}; + +/** + * Parses a Hive Resource identifier into an object containing a organization path and resourceId path. + * e.g. `"hrn:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:target/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"` + * becomes + * ```json + * { + * "organizationId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + * "resourceId": "target/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + * } + * ``` + */ +function parseResourceIdentifier(resource: string) { + const parts = resource.split(':'); + if (parts.length < 2) { + throw new Error('Invalid resource identifier (1)'); + } + if (parts[0] !== 'hrn') { + throw new Error('Invalid resource identifier. Expected string to start with hrn: (2)'); + } + + if (!parts[1] || (!isUUID(parts[1]) && parts[1] !== '*')) { + throw new Error('Invalid resource identifier. Expected UUID or * (3)'); + } + const organizationId = parts[1]; + + if (!parts[2]) { + throw new Error('Invalid resource identifier. Expected type or * (4)'); + } + + // TODO: maybe some stricter validation of the resource id characters + + return { organizationId, resourceId: parts[2] }; +} + +/** + * Abstract session class that is implemented by various ways to identify a session. + * A session is a way to identify a user and their permissions for a specific organization. + * + * The `Session.loadPolicyStatementsForOrganization` method must be implemented by the subclass. + */ +export abstract class Session { + private policyStatementCache = new Map< + string, + Promise | Array + >(); + private performActionCache = new Map>(); + protected logger: Logger; + + constructor(args: { logger: Logger }) { + this.logger = args.logger.child({ + module: this.constructor.name, + }); + } + + /** Load policy statements for a specific organization. */ + protected abstract loadPolicyStatementsForOrganization( + organizationId: string, + ): Promise> | Array; + + /** Retrieve the current viewer. Implementations of the session need to implement this function */ + public getViewer(): Promise { + throw new AccessError('Authorization token is missing', 'UNAUTHENTICATED'); + } + + public isViewer(): boolean { + return false; + } + + /** Retrieve the access token of the request. */ + public getLegacySelector(): { + token: string; + organizationId: string; + projectId: string; + targetId: string; + } { + throw new AccessError('Authorization header is missing'); + } + + private async _loadPolicyStatementsForOrganization(organizationId: string) { + let result = this.policyStatementCache.get(organizationId); + if (result !== undefined) { + return result; + } + + result = this.loadPolicyStatementsForOrganization(organizationId); + this.policyStatementCache.set(organizationId, result); + return await result; + } + + public async assertPerformAction(args: { + action: TAction; + organizationId: string; + params: Parameters<(typeof actionDefinitions)[TAction]>[0]; + }): Promise { + this.logger.debug( + 'Assert performing action (action=%s, organizationId=%s, params=%o)', + args.action, + args.organizationId, + args.params, + ); + + const argsStr = stringify(args); + let result = this.performActionCache.get(argsStr); + if (result !== undefined) { + this.logger.debug( + 'Serve result from cache (action=%s, organizationId=%s, params=%o)', + args.action, + args.organizationId, + args.params, + ); + return result; + } + result = this._assertPerformAction(args); + this.performActionCache.set(argsStr, result); + return await result; + } + + /** + * Check whether a session is allowed to perform a specific action. + * Throws a AccessError if the action is not allowed. + */ + private async _assertPerformAction(args: { + action: TAction; + organizationId: string; + params: Parameters<(typeof actionDefinitions)[TAction]>[0]; + }): Promise { + const permissions = await this._loadPolicyStatementsForOrganization(args.organizationId); + + const resourceIdsForAction = actionDefinitions[args.action](args.params as any); + let isAllowed = false; + + for (const permission of permissions) { + const parsedResources = ( + Array.isArray(permission.resource) ? permission.resource : [permission.resource] + ).map(parseResourceIdentifier); + + /** If no resource matches, we skip this permission */ + if ( + !parsedResources.some(resource => { + if (resource.organizationId !== '*' && resource.organizationId !== args.organizationId) { + return false; + } + + for (const resourceActionId of resourceIdsForAction) { + if (isResourceIdMatch(resource.resourceId, resourceActionId)) { + return true; + } + } + + return false; + }) + ) { + continue; + } + + const actions = Array.isArray(permission.action) ? permission.action : [permission.action]; + + // check if action matches + for (const action of actions) { + if (isActionMatch(action, args.action)) { + if (permission.effect === 'deny') { + this.logger.debug( + 'Session not authorized to perform action. Action explicitly denied. (action=%s, organizationId=%s, params=%o)', + args.action, + args.organizationId, + args.params, + ); + throw new AccessError(`Missing permission for performing '${args.action}' on resource`); + } else { + isAllowed = true; + } + } + } + } + + if (!isAllowed) { + this.logger.debug( + 'Session not authorized to perform action. Action not allowed. (action=%s, organizationId=%s, params=%o)', + args.action, + args.organizationId, + args.params, + ); + + throw new AccessError(`Missing permission for performing '${args.action}' on resource`); + } + } + + /** + * Check whether a session is allowed to perform a specific action. + * Returns a boolean that indicates whether the action is allowed or not allowed. + */ + public async canPerformAction(args: { + action: TAction; + organizationId: string; + params: Parameters<(typeof actionDefinitions)[TAction]>[0]; + }): Promise { + return await this.assertPerformAction(args) + .then(() => true) + .catch(err => { + if (err instanceof AccessError) { + return false; + } + return Promise.reject(err); + }); + } + + /** Reset the permissions cache. */ + public reset() { + this.logger.debug('Reset cache.'); + this.performActionCache.clear(); + this.policyStatementCache.clear(); + } +} + +/** Check whether a action definition (using wildcards) matches a action */ +function isActionMatch(actionContainingWildcard: string, action: string) { + // any action + if (actionContainingWildcard === '*') { + return true; + } + // exact match + if (actionContainingWildcard === action) { + return true; + } + + const [actionScope] = action.split(':'); + const [userSpecifiedActionScope, userSpecifiedActionId] = actionContainingWildcard.split(':'); + + // wildcard match "scope:*" + if (actionScope === userSpecifiedActionScope && userSpecifiedActionId === '*') { + return true; + } + + return false; +} + +/** Check whether a resource id path (containing wildcards) matches a resource id path */ +function isResourceIdMatch( + /** The resource id path containing wildcards */ + resourceIdContainingWildcards: string, + /** The Resource id without wildcards */ + resourceId: string, +): boolean { + const wildcardIdParts = resourceIdContainingWildcards.split('/'); + const resourceIdParts = resourceId.split('/'); + + do { + const wildcardIdPart = wildcardIdParts.shift(); + const resourceIdPart = resourceIdParts.shift(); + + if (wildcardIdPart === '*' && wildcardIdParts.length === 0) { + return true; + } + + if (wildcardIdPart !== resourceIdPart) { + return false; + } + } while (wildcardIdParts.length || resourceIdParts.length); + + return true; +} + +function defaultOrgIdentity(args: { organizationId: string }) { + return [`organization/${args.organizationId}`]; +} + +function defaultProjectIdentity( + args: { projectId: string } & Parameters[0], +) { + return [...defaultOrgIdentity(args), `project/${args.projectId}`]; +} + +function defaultTargetIdentity( + args: { targetId: string } & Parameters[0], +) { + return [...defaultProjectIdentity(args), `target/${args.targetId}`]; +} + +function defaultAppDeploymentIdentity( + args: { appDeploymentName: string | null } & Parameters[0], +) { + const ids = defaultTargetIdentity(args); + + if (args.appDeploymentName !== null) { + ids.push(`target/${args.targetId}/appDeployment/${args.appDeploymentName}`); + } + + return ids; +} + +function schemaCheckOrPublishIdentity( + args: { serviceName: string | null } & Parameters[0], +) { + const ids = defaultTargetIdentity(args); + + if (args.serviceName !== null) { + ids.push(`target/${args.targetId}/service/${args.serviceName}`); + } + + return ids; +} + +/** + * Object map containing all possible actions + * and resource identifier builder functions required for checking whether an action can be performed. + * + * Used within the `Session.assertPerformAction` function for a fully type-safe experience. + * If you are adding new permissions to the existing system. + * This is the place to do so. + */ +const actionDefinitions = { + 'organization:describe': defaultOrgIdentity, + 'organization:modifySettings': defaultOrgIdentity, + 'organization:delete': defaultOrgIdentity, + 'gitHubIntegration:modify': defaultOrgIdentity, + 'slackIntegration:modify': defaultOrgIdentity, + 'oidc:modify': defaultOrgIdentity, + 'support:manageTickets': defaultOrgIdentity, + 'billing:describe': defaultOrgIdentity, + 'billing:update': defaultOrgIdentity, + 'targetAccessToken:describe': defaultTargetIdentity, + 'targetAccessToken:create': defaultTargetIdentity, + 'targetAccessToken:delete': defaultTargetIdentity, + 'cdnAccessToken:describe': defaultTargetIdentity, + 'cdnAccessToken:create': defaultTargetIdentity, + 'cdnAccessToken:delete': defaultTargetIdentity, + 'member:describe': defaultOrgIdentity, + 'member:assignRole': defaultOrgIdentity, + 'member:modifyRole': defaultOrgIdentity, + 'member:removeMember': defaultOrgIdentity, + 'member:manageInvites': defaultOrgIdentity, + 'project:create': defaultOrgIdentity, + 'project:describe': defaultProjectIdentity, + 'project:delete': defaultProjectIdentity, + 'project:modifySettings': defaultProjectIdentity, + 'alert:describe': defaultProjectIdentity, + 'alert:modify': defaultProjectIdentity, + 'schemaLinting:modifyOrganizationRules': defaultOrgIdentity, + 'schemaLinting:modifyProjectRules': defaultProjectIdentity, + 'target:create': defaultProjectIdentity, + 'target:delete': defaultTargetIdentity, + 'target:modifySettings': defaultTargetIdentity, + 'laboratory:describe': defaultTargetIdentity, + 'laboratory:createCollection': defaultTargetIdentity, + 'laboratory:modifyCollection': defaultTargetIdentity, + 'laboratory:deleteCollection': defaultTargetIdentity, + 'appDeployment:describe': defaultTargetIdentity, + 'appDeployment:create': defaultAppDeploymentIdentity, + 'appDeployment:publish': defaultAppDeploymentIdentity, + 'appDeployment:retire': defaultAppDeploymentIdentity, + 'schemaCheck:create': schemaCheckOrPublishIdentity, + 'schemaCheck:approve': schemaCheckOrPublishIdentity, + 'schemaVersion:publish': schemaCheckOrPublishIdentity, + 'schemaVersion:approve': defaultTargetIdentity, + 'schemaVersion:deleteService': schemaCheckOrPublishIdentity, + 'schema:loadFromRegistry': defaultTargetIdentity, + 'schema:compose': defaultTargetIdentity, +} satisfies ActionDefinitionMap; + +type ActionDefinitionMap = { + [key: `${string}:${string}`]: (args: any) => Array; +}; + +type Actions = keyof typeof actionDefinitions; + +type ActionStrings = Actions | '*'; + +/** Unauthenticated session that is returned by default. */ +class UnauthenticatedSession extends Session { + protected loadPolicyStatementsForOrganization( + _: string, + ): Promise> | Array { + return []; + } +} + +/** + * Strategy to authenticate a session from an incoming request. + * E.g. SuperTokens, JWT, etc. + */ +export abstract class AuthNStrategy { + /** + * Parse a session from an incoming request. + * Returns null if the strategy does not apply to the request. + * Returns a session if the strategy applies to the request. + * Rejects if the strategy applies to the request but the session could not be parsed. + */ + public abstract parse(args: { + req: FastifyRequest; + reply: FastifyReply; + }): Promise; +} + +/** Helper class to Authenticate an incoming request. */ +export class AuthN { + private strategies: Array>; + + constructor(deps: { + /** List of strategies for authentication a user */ + strategies: Array>; + }) { + this.strategies = deps.strategies; + } + + /** + * Returns the first successful `Session` created by a authentication strategy. + * If no authentication strategy succeeds a `UnauthenticatedSession` is returned instead. + */ + async authenticate(args: { req: FastifyRequest; reply: FastifyReply }): Promise { + for (const strategy of this.strategies) { + const session = await strategy.parse(args); + if (session) { + return session; + } + } + + return new UnauthenticatedSession({ + logger: args.req.log, + }); + } +} diff --git a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts new file mode 100644 index 0000000000..4b695fa21c --- /dev/null +++ b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts @@ -0,0 +1,344 @@ +import SessionNode from 'supertokens-node/recipe/session/index.js'; +import * as zod from 'zod'; +import type { FastifyReply, FastifyRequest, ServiceLogger } from '@hive/service-common'; +import { captureException } from '@sentry/node'; +import type { User } from '../../../shared/entities'; +import { AccessError, HiveError } from '../../../shared/errors'; +import { isUUID } from '../../../shared/is-uuid'; +import { Logger } from '../../shared/providers/logger'; +import type { Storage } from '../../shared/providers/storage'; +import { + OrganizationAccessScope, + ProjectAccessScope, + TargetAccessScope, +} from '../providers/scopes'; +import { AuthNStrategy, AuthorizationPolicyStatement, Session } from './authz'; + +export class SuperTokensCookieBasedSession extends Session { + public superTokensUserId: string; + private storage: Storage; + + constructor( + args: { superTokensUserId: string; email: string }, + deps: { storage: Storage; logger: Logger }, + ) { + super({ logger: deps.logger }); + this.superTokensUserId = args.superTokensUserId; + this.storage = deps.storage; + } + + protected async loadPolicyStatementsForOrganization( + organizationId: string, + ): Promise> { + const user = await this.getViewer(); + + if (!isUUID(organizationId)) { + return []; + } + + const member = await this.storage.getOrganizationMember({ + organizationId, + userId: user.id, + }); + + // owner of organization should have full right to do anything. + if (member?.isOwner) { + return [ + { + action: '*', + effect: 'allow', + resource: `hrn:${organizationId}:organization/${organizationId}`, + }, + ]; + } + + if (Array.isArray(member?.scopes)) { + return transformOrganizationMemberLegacyScopes({ organizationId, scopes: member.scopes }); + } + + return []; + } + + public async getViewer(): Promise { + const user = await this.storage.getUserBySuperTokenId({ + superTokensUserId: this.superTokensUserId, + }); + + if (!user) { + throw new AccessError('User not found'); + } + + return user; + } + + public isViewer() { + return true; + } +} + +export class SuperTokensUserAuthNStrategy extends AuthNStrategy { + private logger: ServiceLogger; + private storage: Storage; + + constructor(deps: { logger: ServiceLogger; storage: Storage }) { + super(); + this.logger = deps.logger.child({ module: 'SuperTokensUserAuthNStrategy' }); + this.storage = deps.storage; + } + + private async verifySuperTokensSession(args: { req: FastifyRequest; reply: FastifyReply }) { + this.logger.debug('Attempt verifying SuperTokens session'); + let session: SessionNode.SessionContainer | undefined; + + try { + session = await SessionNode.getSession(args.req, args.reply, { + sessionRequired: false, + antiCsrfCheck: false, + checkDatabase: true, + }); + this.logger.debug('Session resolution ended successfully'); + } catch (error) { + this.logger.debug('Session resolution failed'); + if (SessionNode.Error.isErrorFromSuperTokens(error)) { + // Check whether the email is already verified. + // If it is not then we need to redirect to the email verification page - which will trigger the email sending. + if (error.type === SessionNode.Error.INVALID_CLAIMS) { + throw new HiveError('Your account is not verified. Please verify your email address.', { + extensions: { + code: 'VERIFY_EMAIL', + }, + }); + } else if ( + error.type === SessionNode.Error.TRY_REFRESH_TOKEN || + error.type === SessionNode.Error.UNAUTHORISED + ) { + throw new HiveError('Invalid session', { + extensions: { + code: 'NEEDS_REFRESH', + }, + }); + } + } + + this.logger.error('Error while resolving user'); + console.log(error); + captureException(error); + + throw error; + } + + if (!session) { + this.logger.debug('No session found'); + return null; + } + + const payload = session.getAccessTokenPayload(); + + if (!payload) { + this.logger.error('No access token payload found'); + return null; + } + + const result = SuperTokenAccessTokenModel.safeParse(payload); + + if (result.success === false) { + this.logger.error('SuperTokens session payload is invalid'); + this.logger.debug('SuperTokens session payload: %s', JSON.stringify(payload)); + this.logger.debug( + 'SuperTokens session parsing errors: %s', + JSON.stringify(result.error.flatten().fieldErrors), + ); + throw new HiveError(`Invalid access token provided`); + } + + this.logger.debug('SuperTokens session resolved.'); + return result.data; + } + + async parse(args: { + req: FastifyRequest; + reply: FastifyReply; + }): Promise { + const session = await this.verifySuperTokensSession(args); + if (!session) { + return null; + } + + return new SuperTokensCookieBasedSession( + { + superTokensUserId: session.superTokensUserId, + email: session.email, + }, + { + storage: this.storage, + logger: args.req.log, + }, + ); + } +} + +const SuperTokenAccessTokenModel = zod.object({ + version: zod.literal('1'), + superTokensUserId: zod.string(), + /** + * Supertokens for some reason omits externalUserId from the access token payload if it is null. + */ + externalUserId: zod.optional(zod.union([zod.string(), zod.null()])), + email: zod.string(), +}); + +function transformOrganizationMemberLegacyScopes(args: { + organizationId: string; + scopes: Array; +}) { + const policies: Array = []; + for (const scope of args.scopes) { + switch (scope) { + case OrganizationAccessScope.READ: { + policies.push({ + effect: 'allow', + action: [ + 'support:manageTickets', + 'project:create', + 'project:describe', + 'organization:describe', + ], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case OrganizationAccessScope.SETTINGS: { + policies.push({ + effect: 'allow', + action: [ + 'organization:modifySettings', + 'schemaLinting:modifyOrganizationRules', + 'billing:describe', + 'billing:update', + ], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case OrganizationAccessScope.DELETE: { + policies.push({ + effect: 'allow', + action: ['organization:delete'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case OrganizationAccessScope.INTEGRATIONS: { + policies.push({ + effect: 'allow', + action: ['oidc:modify', 'gitHubIntegration:modify', 'slackIntegration:modify'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case OrganizationAccessScope.MEMBERS: { + policies.push({ + effect: 'allow', + action: [ + 'member:manageInvites', + 'member:removeMember', + 'member:assignRole', + 'member:modifyRole', + 'member:describe', + ], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case ProjectAccessScope.ALERTS: { + policies.push({ + effect: 'allow', + action: ['alert:modify', 'alert:describe'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case ProjectAccessScope.READ: { + policies.push({ + effect: 'allow', + action: ['project:describe'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case ProjectAccessScope.DELETE: { + policies.push({ + effect: 'allow', + action: ['project:delete'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case ProjectAccessScope.SETTINGS: { + policies.push({ + effect: 'allow', + action: ['project:delete', 'project:modifySettings', 'schemaLinting:modifyProjectRules'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case TargetAccessScope.READ: { + policies.push({ + effect: 'allow', + action: [ + 'appDeployment:describe', + 'laboratory:describe', + 'laboratory:createCollection', + 'laboratory:deleteCollection', + 'laboratory:modifyCollection', + 'schemaCheck:approve', + 'schemaVersion:approve', + ], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case TargetAccessScope.TOKENS_READ: { + policies.push({ + effect: 'allow', + action: ['cdnAccessToken:describe', 'targetAccessToken:describe', 'target:create'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case TargetAccessScope.TOKENS_WRITE: { + policies.push({ + effect: 'allow', + action: [ + 'targetAccessToken:create', + 'targetAccessToken:delete', + 'targetAccessToken:describe', + 'cdnAccessToken:create', + 'cdnAccessToken:delete', + 'cdnAccessToken:describe', + ], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case TargetAccessScope.SETTINGS: { + policies.push({ + effect: 'allow', + action: ['target:modifySettings'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + case TargetAccessScope.DELETE: { + policies.push({ + effect: 'allow', + action: ['target:delete'], + resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], + }); + break; + } + } + } + + return policies; +} diff --git a/packages/services/api/src/modules/auth/lib/target-access-token-strategy.ts b/packages/services/api/src/modules/auth/lib/target-access-token-strategy.ts new file mode 100644 index 0000000000..4cffd6c67b --- /dev/null +++ b/packages/services/api/src/modules/auth/lib/target-access-token-strategy.ts @@ -0,0 +1,190 @@ +import type { FastifyReply, FastifyRequest, ServiceLogger } from '@hive/service-common'; +import { Logger } from '../../shared/providers/logger'; +import { TokenStorage } from '../../token/providers/token-storage'; +import { TokensConfig } from '../../token/providers/tokens'; +import { + OrganizationAccessScope, + ProjectAccessScope, + TargetAccessScope, +} from '../providers/scopes'; +import { AuthNStrategy, Session, type AuthorizationPolicyStatement } from './authz'; + +export class TargetAccessTokenSession extends Session { + public readonly organizationId: string; + public readonly projectId: string; + public readonly targetId: string; + public readonly token: string; + + private policies: Array; + + constructor( + args: { + organizationId: string; + projectId: string; + targetId: string; + token: string; + policies: Array; + }, + deps: { + logger: Logger; + }, + ) { + super({ logger: deps.logger }); + this.organizationId = args.organizationId; + this.projectId = args.projectId; + this.targetId = args.targetId; + this.token = args.token; + this.policies = args.policies; + } + + protected loadPolicyStatementsForOrganization( + _: string, + ): Promise> | Array { + return this.policies; + } + + public getLegacySelector() { + return { + token: this.token, + organizationId: this.organizationId, + projectId: this.projectId, + targetId: this.targetId, + }; + } +} + +export class TargetAccessTokenStrategy extends AuthNStrategy { + private logger: ServiceLogger; + private tokensConfig: TokensConfig; + + constructor(deps: { logger: ServiceLogger; tokensConfig: TokensConfig }) { + super(); + this.logger = deps.logger.child({ module: 'OrganizationAccessTokenStrategy' }); + this.tokensConfig = deps.tokensConfig; + } + + async parse(args: { + req: FastifyRequest; + reply: FastifyReply; + }): Promise { + this.logger.debug('Attempt to resolve an API token from headers'); + let accessToken: string | undefined; + + for (const headerName in args.req.headers) { + if (headerName.toLowerCase() === 'x-api-token') { + const values = args.req.headers[headerName]; + const singleValue = Array.isArray(values) ? values[0] : values; + + if (singleValue && singleValue !== '') { + this.logger.debug( + 'Found X-API-Token header (length=%d, token=%s)', + singleValue.length, + maskToken(singleValue), + ); + accessToken = singleValue; + break; + } + } else if (headerName.toLowerCase() === 'authorization') { + const values = args.req.headers[headerName]; + const singleValue = Array.isArray(values) ? values[0] : values; + + if (singleValue && singleValue !== '') { + const bearer = singleValue.replace(/^Bearer\s+/i, ''); + + // Skip if bearer is missing or it's JWT generated by Auth0 (not API token) + if (bearer && bearer !== '' && !bearer.includes('.')) { + this.logger.debug( + 'Found Authorization header (length=%d, token=%s)', + bearer.length, + maskToken(bearer), + ); + accessToken = bearer; + break; + } + } + } + } + + if (!accessToken) { + this.logger.debug('No access token found'); + return null; + } + + // if (accessToken.length !== 32) { + // this.logger.debug('Invalid access token length.'); + // return null; + // } + + const tokens = new TokenStorage(this.logger, this.tokensConfig, { + requestId: args.req.headers['x-request-id'] as string, + } as any); + + const result = await tokens.getToken({ token: accessToken }); + + return new TargetAccessTokenSession( + { + organizationId: result.organization, + projectId: result.project, + targetId: result.target, + token: accessToken, + policies: transformAccessTokenLegacyScopes({ + organizationId: result.organization, + targetId: result.target, + scopes: result.scopes as Array< + OrganizationAccessScope | ProjectAccessScope | TargetAccessScope + >, + }), + }, + { + logger: args.req.log, + }, + ); + } +} + +function maskToken(token: string) { + if (token.length > 6) { + return token.substring(0, 3) + '*'.repeat(token.length - 6) + token.substring(token.length - 3); + } + + return '*'.repeat(token.length); +} + +function transformAccessTokenLegacyScopes(args: { + organizationId: string; + targetId: string; + scopes: Array; +}): Array { + const policies: Array = []; + for (const policy of args.scopes) { + switch (policy) { + case TargetAccessScope.REGISTRY_READ: { + policies.push({ + effect: 'allow', + action: ['schemaCheck:create'], + resource: [`hrn:${args.organizationId}:target/${args.targetId}`], + }); + break; + } + case TargetAccessScope.REGISTRY_WRITE: { + policies.push({ + effect: 'allow', + action: [ + 'appDeployment:describe', + 'appDeployment:create', + 'appDeployment:publish', + 'appDeployment:retire', + 'schemaVersion:publish', + 'schemaVersion:deleteService', + 'schema:loadFromRegistry', + 'schemaVersion:publish', + ], + resource: [`hrn:${args.organizationId}:target/${args.targetId}`], + }); + break; + } + } + } + + return policies; +} diff --git a/packages/services/api/src/modules/auth/providers/auth-manager.ts b/packages/services/api/src/modules/auth/providers/auth-manager.ts index fb3d8d2854..32284cceaa 100644 --- a/packages/services/api/src/modules/auth/providers/auth-manager.ts +++ b/packages/services/api/src/modules/auth/providers/auth-manager.ts @@ -1,10 +1,10 @@ -import { CONTEXT, Inject, Injectable, Scope } from 'graphql-modules'; +import { Injectable, Scope } from 'graphql-modules'; import type { User } from '../../../shared/entities'; import { AccessError } from '../../../shared/errors'; -import type { Listify, MapToArray } from '../../../shared/helpers'; import { share } from '../../../shared/helpers'; import { Storage } from '../../shared/providers/storage'; -import { TokenStorage } from '../../token/providers/token-storage'; +import { Session } from '../lib/authz'; +import { TargetAccessTokenSession } from '../lib/target-access-token-strategy'; import { OrganizationAccess, OrganizationAccessScope, @@ -12,7 +12,6 @@ import { } from './organization-access'; import { ProjectAccess, ProjectAccessScope, ProjectUserScopesSelector } from './project-access'; import { TargetAccess, TargetAccessScope, TargetUserScopesSelector } from './target-access'; -import { ApiToken } from './tokens'; import { UserManager } from './user-manager'; export interface OrganizationAccessSelector { @@ -33,13 +32,6 @@ export interface TargetAccessSelector { scope: TargetAccessScope; } -type SuperTokenSessionPayload = { - version: '1'; - superTokensUserId: string; - email: string; - externalUserId: string | null; -}; - /** * Responsible for auth checks. * Talks to Storage. @@ -49,81 +41,23 @@ type SuperTokenSessionPayload = { global: true, }) export class AuthManager { - private session: SuperTokenSessionPayload | null; - constructor( - @Inject(ApiToken) private apiToken: string, - @Inject(CONTEXT) context: any, private organizationAccess: OrganizationAccess, private projectAccess: ProjectAccess, private targetAccess: TargetAccess, private userManager: UserManager, - private tokenStorage: TokenStorage, private storage: Storage, - ) { - this.session = context.session; - } - - async ensureTargetAccess( - selector: Listify, - ): Promise { - if (this.apiToken) { - if (hasManyTargets(selector)) { - await Promise.all( - selector.targetId.map(target => - this.ensureTargetAccess({ - ...selector, - targetId: target, - }), - ), - ); - } else { - await this.targetAccess.ensureAccessForToken({ - ...(selector as TargetAccessSelector), - token: this.apiToken, - }); - } - } else if (hasManyTargets(selector)) { - await Promise.all( - selector.targetId.map(target => - this.ensureTargetAccess({ - ...selector, - targetId: target, - }), - ), - ); - } else { - const user = await this.getCurrentUser(); - await this.targetAccess.ensureAccessForUser({ - ...(selector as TargetAccessSelector), - userId: user.id, - }); - } - } - - async ensureProjectAccess(selector: ProjectAccessSelector): Promise { - if (this.apiToken) { - await this.projectAccess.ensureAccessForToken({ - ...selector, - token: this.apiToken, - }); - } else { - const user = await this.getCurrentUser(); - await this.projectAccess.ensureAccessForUser({ - ...selector, - userId: user.id, - }); - } - } + private session: Session, + ) {} async ensureOrganizationAccess(selector: OrganizationAccessSelector): Promise { - if (this.apiToken) { + if (this.session instanceof TargetAccessTokenSession) { await this.organizationAccess.ensureAccessForToken({ ...selector, - token: this.apiToken, + token: this.session.token, }); } else { - const user = await this.getCurrentUser(); + const user = await this.session.getViewer(); // If a user is an admin, we can allow access for all data if (user.isAdmin) { @@ -138,11 +72,11 @@ export class AuthManager { } async checkOrganizationAccess(selector: OrganizationAccessSelector): Promise { - if (this.apiToken) { + if (this.session instanceof TargetAccessTokenSession) { throw new Error('checkOrganizationAccess for token is not implemented yet'); } - const user = await this.getCurrentUser(); + const user = await this.session.getViewer(); return this.organizationAccess.checkAccessForUser({ ...selector, @@ -151,7 +85,7 @@ export class AuthManager { } async ensureOrganizationOwnership(selector: { organization: string }): Promise { - const user = await this.getCurrentUser(); + const user = await this.session.getViewer(); const isOwner = await this.organizationAccess.checkOwnershipForUser({ organizationId: selector.organization, userId: user.id, @@ -162,48 +96,23 @@ export class AuthManager { } } - ensureApiToken(): string | never { - if (this.apiToken) { - return this.apiToken; - } - - throw new AccessError('Authorization header is missing'); - } - getOrganizationOwnerByToken: () => Promise = share(async () => { - const token = this.ensureApiToken(); - const result = await this.tokenStorage.getToken({ token }); + const result = this.session.getLegacySelector(); await this.ensureOrganizationAccess({ - organizationId: result.organization, + organizationId: result.organizationId, scope: OrganizationAccessScope.READ, }); const member = await this.storage.getOrganizationOwner({ - organizationId: result.organization, + organizationId: result.organizationId, }); return member.user; }); - getCurrentUser: () => Promise<(User & { isAdmin: boolean }) | never> = share(async () => { - if (!this.session) { - throw new AccessError('Authorization token is missing', 'UNAUTHENTICATED'); - } - - const user = await this.storage.getUserBySuperTokenId({ - superTokensUserId: this.session.superTokensUserId, - }); - - if (!user) { - throw new AccessError('User not found'); - } - - return user; - }); - async getCurrentUserAccessScopes(organizationId: string) { - const user = await this.getCurrentUser(); + const user = await this.session.getViewer(); if (!user) { throw new AccessError('User not found'); @@ -228,17 +137,13 @@ export class AuthManager { } async updateCurrentUser(input: { displayName: string; fullName: string }): Promise { - const user = await this.getCurrentUser(); + const user = await this.session.getViewer(); return this.userManager.updateUser({ id: user.id, ...input, }); } - isUser() { - return !!this.session; - } - getMemberOrganizationScopes(selector: OrganizationUserScopesSelector) { return this.organizationAccess.getMemberScopes(selector); } @@ -257,9 +162,3 @@ export class AuthManager { this.targetAccess.resetAccessCache(); } } - -function hasManyTargets( - selector: Listify, -): selector is MapToArray { - return Array.isArray(selector.targetId); -} diff --git a/packages/services/api/src/modules/auth/providers/tokens.ts b/packages/services/api/src/modules/auth/providers/tokens.ts deleted file mode 100644 index 1502da99b3..0000000000 --- a/packages/services/api/src/modules/auth/providers/tokens.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { CONTEXT, FactoryProvider, InjectionToken, Scope } from 'graphql-modules'; -import type { RegistryContext } from './../../../context'; - -function maskToken(token: string) { - if (token.length > 6) { - return token.substring(0, 3) + '*'.repeat(token.length - 6) + token.substring(token.length - 3); - } - - return '*'.repeat(token.length); -} - -export const ApiToken = new InjectionToken('x-api-token'); -export const ApiTokenProvider: FactoryProvider = { - provide: ApiToken, - useFactory(context: RegistryContext) { - context.req.log.debug('Attempt to resolve an API token from headers'); - let token: string | undefined; - - for (const headerName in context.headers) { - if (headerName.toLowerCase() === 'x-api-token') { - const values = context.headers[headerName]; - const singleValue = Array.isArray(values) ? values[0] : values; - - if (singleValue && singleValue !== '') { - context.req.log.debug( - 'Found X-API-Token header (length=%d, token=%s)', - singleValue.length, - maskToken(singleValue), - ); - token = singleValue; - break; - } - } else if (headerName.toLowerCase() === 'authorization') { - const values = context.headers[headerName]; - const singleValue = Array.isArray(values) ? values[0] : values; - - if (singleValue && singleValue !== '') { - const bearer = singleValue.replace(/^Bearer\s+/i, ''); - - // Skip if bearer is missing or it's JWT generated by Auth0 (not API token) - if (bearer && bearer !== '' && !bearer.includes('.')) { - context.req.log.debug( - 'Found Authorization header (length=%d, token=%s)', - bearer.length, - maskToken(bearer), - ); - token = bearer; - break; - } - } - } - } - - return token; - }, - deps: [CONTEXT], - scope: Scope.Operation, -}; diff --git a/packages/services/api/src/modules/auth/resolvers/Query/me.ts b/packages/services/api/src/modules/auth/resolvers/Query/me.ts index 989ee29323..551e087a51 100644 --- a/packages/services/api/src/modules/auth/resolvers/Query/me.ts +++ b/packages/services/api/src/modules/auth/resolvers/Query/me.ts @@ -1,6 +1,6 @@ -import { AuthManager } from '../../providers/auth-manager'; +import { Session } from '../../lib/authz'; import type { QueryResolvers } from './../../../../__generated__/types'; export const me: NonNullable = (_, __, { injector }) => { - return injector.get(AuthManager).getCurrentUser(); + return injector.get(Session).getViewer(); }; diff --git a/packages/services/api/src/modules/billing/providers/billing.provider.ts b/packages/services/api/src/modules/billing/providers/billing.provider.ts index a0983d1f58..6c396a6772 100644 --- a/packages/services/api/src/modules/billing/providers/billing.provider.ts +++ b/packages/services/api/src/modules/billing/providers/billing.provider.ts @@ -2,6 +2,8 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import type { StripeBillingApi, StripeBillingApiInput } from '@hive/stripe-billing'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; import { OrganizationBilling } from '../../../shared/entities'; +import { Session } from '../../auth/lib/authz'; +import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; import { Storage } from '../../shared/providers/storage'; import type { BillingConfig } from './tokens'; @@ -9,7 +11,7 @@ import { BILLING_CONFIG } from './tokens'; @Injectable({ global: true, - scope: Scope.Singleton, + scope: Scope.Operation, }) export class BillingProvider { private logger: Logger; @@ -20,6 +22,8 @@ export class BillingProvider { constructor( logger: Logger, private storage: Storage, + private idTranslator: IdTranslator, + private session: Session, @Inject(BILLING_CONFIG) billingConfig: BillingConfig, ) { this.logger = logger.child({ source: 'BillingProvider' }); @@ -106,15 +110,27 @@ export class BillingProvider { return await this.billingService.cancelSubscriptionForOrganization.mutate(input); } - async generateStripePortalLink(orgId: string) { - this.logger.debug('Generating Stripe portal link for id:' + orgId); + public async generateStripePortalLink(args: { organizationSlug: string }) { + this.logger.debug('Generating Stripe portal link for id:' + args.organizationSlug); if (!this.billingService) { throw new Error(`Billing service is not configured!`); } + const organizationId = await this.idTranslator.translateOrganizationId({ + organizationSlug: args.organizationSlug, + }); + + await this.session.assertPerformAction({ + action: 'billing:describe', + organizationId, + params: { + organizationId, + }, + }); + return await this.billingService.generateStripePortalLink.mutate({ - organizationId: orgId, + organizationId, }); } } diff --git a/packages/services/api/src/modules/billing/resolvers/Mutation/downgradeToHobby.ts b/packages/services/api/src/modules/billing/resolvers/Mutation/downgradeToHobby.ts index c2e01d3ea5..0d4e77da12 100644 --- a/packages/services/api/src/modules/billing/resolvers/Mutation/downgradeToHobby.ts +++ b/packages/services/api/src/modules/billing/resolvers/Mutation/downgradeToHobby.ts @@ -1,6 +1,5 @@ import { GraphQLError } from 'graphql'; -import { AuthManager } from '../../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../../auth/providers/organization-access'; +import { Session } from '../../../auth/lib/authz'; import { OrganizationManager } from '../../../organization/providers/organization-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { USAGE_DEFAULT_LIMITATIONS } from '../../constants'; @@ -15,9 +14,13 @@ export const downgradeToHobby: NonNullable = async (_, args, { injector }) => { - const organizationId = await injector.get(IdTranslator).translateOrganizationId({ + return injector.get(BillingProvider).generateStripePortalLink({ organizationSlug: args.selector.organizationSlug, }); - const organization = await injector.get(OrganizationManager).getOrganization( - { - organizationId: organizationId, - }, - OrganizationAccessScope.SETTINGS, - ); - - return injector.get(BillingProvider).generateStripePortalLink(organization.id); }; diff --git a/packages/services/api/src/modules/billing/resolvers/Mutation/upgradeToPro.ts b/packages/services/api/src/modules/billing/resolvers/Mutation/upgradeToPro.ts index baf91ff9f2..846adf8f59 100644 --- a/packages/services/api/src/modules/billing/resolvers/Mutation/upgradeToPro.ts +++ b/packages/services/api/src/modules/billing/resolvers/Mutation/upgradeToPro.ts @@ -1,7 +1,6 @@ import { GraphQLError } from 'graphql'; import { TRPCClientError } from '@trpc/client'; -import { AuthManager } from '../../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../../auth/providers/organization-access'; +import { Session } from '../../../auth/lib/authz'; import { OrganizationManager } from '../../../organization/providers/organization-manager'; import { IdTranslator } from '../../../shared/providers/id-translator'; import { USAGE_DEFAULT_LIMITATIONS } from '../../constants'; @@ -16,9 +15,12 @@ export const upgradeToPro: NonNullable = asyn const organizationId = await injector.get(IdTranslator).translateOrganizationId({ organizationSlug: args.input.organization.organizationSlug, }); - await injector.get(AuthManager).ensureOrganizationAccess({ + await injector.get(Session).assertPerformAction({ + action: 'billing:update', organizationId: organizationId, - scope: OrganizationAccessScope.SETTINGS, + params: { + organizationId: organizationId, + }, }); let organization = await injector.get(OrganizationManager).getOrganization({ diff --git a/packages/services/api/src/modules/cdn/providers/cdn.provider.ts b/packages/services/api/src/modules/cdn/providers/cdn.provider.ts index 57cff29b9d..e5568a31a5 100644 --- a/packages/services/api/src/modules/cdn/providers/cdn.provider.ts +++ b/packages/services/api/src/modules/cdn/providers/cdn.provider.ts @@ -4,8 +4,7 @@ import { z } from 'zod'; import { encodeCdnToken, generatePrivateKey } from '@hive/cdn-script/cdn-token'; import { HiveError } from '../../../shared/errors'; import { isUUID } from '../../../shared/is-uuid'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { TargetAccessScope } from '../../auth/providers/scopes'; +import { Session } from '../../auth/lib/authz'; import type { Contract } from '../../schema/providers/contracts'; import { Logger } from '../../shared/providers/logger'; import { S3_CONFIG, type S3Config } from '../../shared/providers/s3-config'; @@ -23,7 +22,7 @@ export class CdnProvider { constructor( logger: Logger, - @Inject(AuthManager) private authManager: AuthManager, + private session: Session, @Inject(CDN_CONFIG) private config: CDNConfig, @Inject(S3_CONFIG) private s3Config: S3Config, @Inject(Storage) private storage: Storage, @@ -86,11 +85,14 @@ export class CdnProvider { } as const; } - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'cdnAccessToken:create', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.READ, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + targetId: args.targetId, + }, }); // generate all things upfront so we do net get surprised by encoding issues after writing to the destination. @@ -241,11 +243,14 @@ export class CdnProvider { args.cdnAccessTokenId, ); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'cdnAccessToken:delete', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.SETTINGS, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + targetId: args.targetId, + }, }); if (isUUID(args.cdnAccessTokenId) === false) { @@ -326,11 +331,14 @@ export class CdnProvider { first: number | null; cursor: string | null; }) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'cdnAccessToken:describe', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.SETTINGS, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + targetId: args.targetId, + }, }); const paginatedResult = await this.storage.getPaginatedCDNAccessTokensForTarget({ diff --git a/packages/services/api/src/modules/collection/providers/collection.provider.ts b/packages/services/api/src/modules/collection/providers/collection.provider.ts index d0e3ba409a..364c005d0c 100644 --- a/packages/services/api/src/modules/collection/providers/collection.provider.ts +++ b/packages/services/api/src/modules/collection/providers/collection.provider.ts @@ -1,8 +1,7 @@ import { Injectable, Scope } from 'graphql-modules'; import * as zod from 'zod'; import { isUUID } from '../../../shared/is-uuid'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { TargetAccessScope } from '../../auth/providers/scopes'; +import { Session } from '../../auth/lib/authz'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; import { Storage } from '../../shared/providers/storage'; @@ -17,7 +16,7 @@ export class CollectionProvider { constructor( logger: Logger, private storage: Storage, - private authManager: AuthManager, + private session: Session, private idTranslator: IdTranslator, ) { this.logger = logger.child({ source: 'CollectionProvider' }); @@ -64,11 +63,14 @@ export class CollectionProvider { this.idTranslator.translateTargetId(selector), ]); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'laboratory:createCollection', organizationId, - projectId, - targetId, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + organizationId, + projectId, + targetId, + }, }); const target = await this.storage.getTarget({ @@ -76,7 +78,7 @@ export class CollectionProvider { projectId, targetId, }); - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const collection = await this.storage.createDocumentCollection({ createdByUserId: currentUser.id, @@ -109,11 +111,14 @@ export class CollectionProvider { this.idTranslator.translateTargetId(selector), ]); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'laboratory:modifyCollection', organizationId, - projectId, - targetId, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + organizationId, + projectId, + targetId, + }, }); let collection = await this.storage.getDocumentCollection({ id: args.collectionId }); @@ -160,11 +165,14 @@ export class CollectionProvider { this.idTranslator.translateTargetId(selector), ]); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'laboratory:deleteCollection', organizationId, - projectId, - targetId, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + organizationId, + projectId, + targetId, + }, }); const target = await this.storage.getTarget({ @@ -213,11 +221,14 @@ export class CollectionProvider { this.idTranslator.translateTargetId(selector), ]); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'laboratory:modifyCollection', organizationId, - projectId, - targetId, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + organizationId, + projectId, + targetId, + }, }); if (!isUUID(args.collectionId)) { @@ -244,7 +255,7 @@ export class CollectionProvider { }; } - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const validationResult = OperationCreateModel.safeParse({ name: args.operation.name, @@ -297,11 +308,14 @@ export class CollectionProvider { this.idTranslator.translateTargetId(selector), ]); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'laboratory:modifyCollection', organizationId, - projectId, - targetId, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + organizationId, + projectId, + targetId, + }, }); if (!isUUID(args.collectionDocumentId)) { @@ -390,11 +404,14 @@ export class CollectionProvider { this.idTranslator.translateTargetId(selector), ]); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'laboratory:modifyCollection', organizationId, - projectId, - targetId, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + organizationId, + projectId, + targetId, + }, }); if (!isUUID(args.collectionDocumentId)) { diff --git a/packages/services/api/src/modules/collection/validation.ts b/packages/services/api/src/modules/collection/validation.ts deleted file mode 100644 index f295b20dd7..0000000000 --- a/packages/services/api/src/modules/collection/validation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Injector } from 'graphql-modules'; -import * as zod from 'zod'; -import type { TargetSelectorInput } from '../../__generated__/types'; -import { AuthManager } from '../auth/providers/auth-manager'; -import { TargetAccessScope } from '../auth/providers/scopes'; -import { IdTranslator } from '../shared/providers/id-translator'; -import { Storage } from '../shared/providers/storage'; - -const MAX_INPUT_LENGTH = 10_000; - -// The following validates the length and the validity of the JSON object incoming as string. -const inputObjectSchema = zod - .string() - .max(MAX_INPUT_LENGTH) - .optional() - .nullable() - .refine(v => { - if (!v) { - return true; - } - - try { - JSON.parse(v); - return true; - } catch { - return false; - } - }); - -export const OperationValidationInputModel = zod - .object({ - collectionId: zod.string(), - name: zod.string().min(1).max(100), - query: zod.string().min(1).max(MAX_INPUT_LENGTH), - variables: inputObjectSchema, - headers: inputObjectSchema, - }) - .partial() - .passthrough(); - -export async function validateTargetAccess( - injector: Injector, - selector: TargetSelectorInput, - scope: TargetAccessScope = TargetAccessScope.REGISTRY_READ, -) { - const translator = injector.get(IdTranslator); - const [organization, project, target] = await Promise.all([ - translator.translateOrganizationId(selector), - translator.translateProjectId(selector), - translator.translateTargetId(selector), - ]); - - await injector.get(AuthManager).ensureTargetAccess({ - organizationId: organization, - projectId: project, - targetId: target, - scope, - }); - - return await injector - .get(Storage) - .getTarget({ targetId: target, organizationId: organization, projectId: project }); -} diff --git a/packages/services/api/src/modules/feedback/index.ts b/packages/services/api/src/modules/feedback/index.ts deleted file mode 100644 index b641a2165d..0000000000 --- a/packages/services/api/src/modules/feedback/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createModule } from 'graphql-modules'; -import { resolvers } from './resolvers.generated'; -import typeDefs from './module.graphql'; - -export const feedbackModule = createModule({ - id: 'feedback', - dirname: __dirname, - typeDefs, - resolvers, -}); diff --git a/packages/services/api/src/modules/feedback/module.graphql.ts b/packages/services/api/src/modules/feedback/module.graphql.ts deleted file mode 100644 index cdddd9eb9a..0000000000 --- a/packages/services/api/src/modules/feedback/module.graphql.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from 'graphql-modules'; - -export default gql` - extend type Mutation { - sendFeedback(feedback: String!): Boolean! - } -`; diff --git a/packages/services/api/src/modules/feedback/providers/tokens.ts b/packages/services/api/src/modules/feedback/providers/tokens.ts deleted file mode 100644 index 92920b9a65..0000000000 --- a/packages/services/api/src/modules/feedback/providers/tokens.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { InjectionToken } from 'graphql-modules'; - -export const FEEDBACK_SLACK_TOKEN = new InjectionToken('FEEDBACK_SLACK_TOKEN'); -export const FEEDBACK_SLACK_CHANNEL = new InjectionToken('FEEDBACK_SLACK_CHANNEL'); diff --git a/packages/services/api/src/modules/feedback/resolvers/Mutation/sendFeedback.ts b/packages/services/api/src/modules/feedback/resolvers/Mutation/sendFeedback.ts deleted file mode 100644 index c18d6d1d28..0000000000 --- a/packages/services/api/src/modules/feedback/resolvers/Mutation/sendFeedback.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { WebClient } from '@slack/web-api'; -import { AuthManager } from '../../../auth/providers/auth-manager'; -import { FEEDBACK_SLACK_CHANNEL, FEEDBACK_SLACK_TOKEN } from '../../providers/tokens'; -import type { MutationResolvers } from './../../../../__generated__/types'; - -export const sendFeedback: NonNullable = async ( - _, - { feedback }, - { injector }, -) => { - const auth = injector.get(AuthManager); - const user = await auth.getCurrentUser(); - const slack = new WebClient(injector.get(FEEDBACK_SLACK_TOKEN)); - - await slack.chat - .postMessage({ - channel: injector.get(FEEDBACK_SLACK_CHANNEL), - mrkdwn: true, - text: [`Got a feedback from \`${user.email}\``, `> ${feedback}`].join('\n'), - }) - .catch(error => { - Sentry.captureException(error, { - extra: { - feedback, - }, - user: { - email: user.email, - }, - }); - }); - - return true; -}; diff --git a/packages/services/api/src/modules/integrations/providers/github-integration-manager.ts b/packages/services/api/src/modules/integrations/providers/github-integration-manager.ts index 0d56eb924f..61caf6a815 100644 --- a/packages/services/api/src/modules/integrations/providers/github-integration-manager.ts +++ b/packages/services/api/src/modules/integrations/providers/github-integration-manager.ts @@ -4,9 +4,7 @@ import { Octokit } from '@octokit/core'; import { RequestError } from '@octokit/request-error'; import type { GitHubIntegration } from '../../../__generated__/types'; import { HiveError } from '../../../shared/errors'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../auth/providers/organization-access'; -import { ProjectAccessScope } from '../../auth/providers/scopes'; +import { Session } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; import { OrganizationSelector, ProjectSelector, Storage } from '../../shared/providers/storage'; @@ -29,7 +27,7 @@ export class GitHubIntegrationManager { constructor( logger: Logger, - private authManager: AuthManager, + private session: Session, private storage: Storage, @Inject(GITHUB_APP_CONFIG) private config: GitHubApplicationConfig | null, ) { @@ -65,9 +63,12 @@ export class GitHubIntegrationManager { input.organizationId, input.installationId, ); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.INTEGRATIONS, + await this.session.assertPerformAction({ + action: 'gitHubIntegration:modify', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); this.logger.debug('Updating organization'); await this.storage.addGitHubIntegration({ @@ -78,9 +79,13 @@ export class GitHubIntegrationManager { async unregister(input: OrganizationSelector): Promise { this.logger.debug('Removing GitHub integration (organization=%s)', input.organizationId); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.INTEGRATIONS, + + await this.session.assertPerformAction({ + action: 'gitHubIntegration:modify', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); this.logger.debug('Updating organization'); await this.storage.deleteGitHubIntegration({ @@ -193,9 +198,12 @@ export class GitHubIntegrationManager { return null; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'gitHubIntegration:modify', organizationId: organization.id, - scope: OrganizationAccessScope.INTEGRATIONS, + params: { + organizationId: organization.id, + }, }); return organization; @@ -419,9 +427,12 @@ export class GitHubIntegrationManager { } async enableProjectNameInGithubCheck(input: ProjectSelector) { - await this.authManager.ensureProjectAccess({ - ...input, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'gitHubIntegration:modify', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); const project = await this.storage.getProject(input); diff --git a/packages/services/api/src/modules/integrations/providers/slack-integration-manager.ts b/packages/services/api/src/modules/integrations/providers/slack-integration-manager.ts index 983c8590ef..7660fb0d96 100644 --- a/packages/services/api/src/modules/integrations/providers/slack-integration-manager.ts +++ b/packages/services/api/src/modules/integrations/providers/slack-integration-manager.ts @@ -1,9 +1,6 @@ import { Injectable, Scope } from 'graphql-modules'; import { AccessError } from '../../../shared/errors'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../auth/providers/organization-access'; -import { ProjectAccessScope } from '../../auth/providers/project-access'; -import { TargetAccessScope } from '../../auth/providers/target-access'; +import { Session } from '../../auth/lib/authz'; import { CryptoProvider } from '../../shared/providers/crypto'; import { Logger } from '../../shared/providers/logger'; import { @@ -23,7 +20,7 @@ export class SlackIntegrationManager { constructor( logger: Logger, - private authManager: AuthManager, + private session: Session, private storage: Storage, private crypto: CryptoProvider, ) { @@ -38,9 +35,12 @@ export class SlackIntegrationManager { }, ): Promise { this.logger.debug('Registering Slack integration (organization=%s)', input.organizationId); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.INTEGRATIONS, + await this.session.assertPerformAction({ + action: 'slackIntegration:modify', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); this.logger.debug('Updating organization'); await this.storage.addSlackIntegration({ @@ -51,9 +51,12 @@ export class SlackIntegrationManager { async unregister(input: OrganizationSelector): Promise { this.logger.debug('Removing Slack integration (organization=%s)', input.organizationId); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.INTEGRATIONS, + await this.session.assertPerformAction({ + action: 'slackIntegration:modify', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); this.logger.debug('Updating organization'); await this.storage.deleteSlackIntegration({ @@ -105,9 +108,12 @@ export class SlackIntegrationManager { selector.organizationId, selector.context, ); - await this.authManager.ensureOrganizationAccess({ - ...selector, - scope: OrganizationAccessScope.INTEGRATIONS, + await this.session.assertPerformAction({ + action: 'slackIntegration:modify', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + }, }); break; } @@ -118,9 +124,13 @@ export class SlackIntegrationManager { selector.projectId, selector.context, ); - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.ALERTS, + await this.session.assertPerformAction({ + action: 'alert:modify', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); break; } @@ -132,9 +142,15 @@ export class SlackIntegrationManager { selector.targetId, selector.context, ); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_WRITE, + await this.session.assertPerformAction({ + action: 'schemaVersion:publish', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + targetId: selector.targetId, + serviceName: null, + }, }); break; } diff --git a/packages/services/api/src/modules/lab/resolvers/Query/lab.ts b/packages/services/api/src/modules/lab/resolvers/Query/lab.ts index f1bc853211..1139628ee8 100644 --- a/packages/services/api/src/modules/lab/resolvers/Query/lab.ts +++ b/packages/services/api/src/modules/lab/resolvers/Query/lab.ts @@ -1,32 +1,33 @@ -import { AuthManager } from '../../../auth/providers/auth-manager'; -import { TargetAccessScope } from '../../../auth/providers/target-access'; +import { Session } from '../../../auth/lib/authz'; import { SchemaManager } from '../../../schema/providers/schema-manager'; import { SchemaVersionHelper } from '../../../schema/providers/schema-version-helper'; import { IdTranslator } from '../../../shared/providers/id-translator'; +import { TargetManager } from '../../../target/providers/target-manager'; import type { QueryResolvers } from './../../../../__generated__/types'; export const lab: NonNullable = async (_, { selector }, { injector }) => { const translator = injector.get(IdTranslator); - const [organization, project, target] = await Promise.all([ + const [organization, project, targetId] = await Promise.all([ translator.translateOrganizationId(selector), translator.translateProjectId(selector), translator.translateTargetId(selector), ]); - await injector.get(AuthManager).ensureTargetAccess({ + await injector.get(Session).assertPerformAction({ + action: 'laboratory:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + targetId, + }, }); + const target = await injector.get(TargetManager).getTargetById({ targetId }); + const schemaManager = injector.get(SchemaManager); - const latestSchema = await schemaManager.getMaybeLatestValidVersion({ - organizationId: organization, - projectId: project, - targetId: target, - }); + const latestSchema = await schemaManager.getMaybeLatestValidVersion(target); if (!latestSchema) { return null; diff --git a/packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts b/packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts index 030ee34c31..a480e17931 100644 --- a/packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts +++ b/packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts @@ -2,8 +2,7 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import zod from 'zod'; import { OIDCIntegration } from '../../../shared/entities'; import { HiveError } from '../../../shared/errors'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../auth/providers/organization-access'; +import { Session } from '../../auth/lib/authz'; import { CryptoProvider } from '../../shared/providers/crypto'; import { Logger } from '../../shared/providers/logger'; import { PUB_SUB_CONFIG, type HivePubSub } from '../../shared/providers/pub-sub'; @@ -20,10 +19,10 @@ export class OIDCIntegrationsProvider { constructor( logger: Logger, private storage: Storage, - private authManager: AuthManager, private crypto: CryptoProvider, @Inject(PUB_SUB_CONFIG) private pubSub: HivePubSub, @Inject(OIDC_INTEGRATIONS_ENABLED) private enabled: boolean, + private session: Session, ) { this.logger = logger.child({ source: 'OIDCIntegrationsProvider' }); } @@ -37,15 +36,13 @@ export class OIDCIntegrationsProvider { return false; } - try { - await this.authManager.ensureOrganizationAccess({ - organizationId: organizationId, - scope: OrganizationAccessScope.INTEGRATIONS, - }); - return true; - } catch { - return false; - } + return await this.session.canPerformAction({ + organizationId, + action: 'oidc:modify', + params: { + organizationId, + }, + }); } async getOIDCIntegrationForOrganization(args: { @@ -60,9 +57,12 @@ export class OIDCIntegrationsProvider { return null; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ organizationId: args.organizationId, - scope: OrganizationAccessScope.INTEGRATIONS, + action: 'oidc:modify', + params: { + organizationId: args.organizationId, + }, }); return await this.storage.getOIDCIntegrationForOrganization({ @@ -90,9 +90,12 @@ export class OIDCIntegrationsProvider { } as const; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ organizationId: args.organizationId, - scope: OrganizationAccessScope.INTEGRATIONS, + action: 'oidc:modify', + params: { + organizationId: args.organizationId, + }, }); const organization = await this.storage.getOrganization({ @@ -194,9 +197,12 @@ export class OIDCIntegrationsProvider { } as const; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'oidc:modify', organizationId: integration.linkedOrganizationId, - scope: OrganizationAccessScope.INTEGRATIONS, + params: { + organizationId: integration.linkedOrganizationId, + }, }); const clientIdResult = maybe(OIDCIntegrationClientIdModel).safeParse(args.clientId); @@ -271,9 +277,12 @@ export class OIDCIntegrationsProvider { } as const; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ organizationId: integration.linkedOrganizationId, - scope: OrganizationAccessScope.INTEGRATIONS, + action: 'oidc:modify', + params: { + organizationId: integration.linkedOrganizationId, + }, }); await this.storage.deleteOIDCIntegration(args); @@ -303,9 +312,12 @@ export class OIDCIntegrationsProvider { } as const; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ organizationId: oidcIntegration.linkedOrganizationId, - scope: OrganizationAccessScope.INTEGRATIONS, + action: 'oidc:modify', + params: { + organizationId: oidcIntegration.linkedOrganizationId, + }, }); return { @@ -348,9 +360,12 @@ export class OIDCIntegrationsProvider { throw new HiveError('Integration not found.'); } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ organizationId: integration.linkedOrganizationId, - scope: OrganizationAccessScope.INTEGRATIONS, + action: 'oidc:modify', + params: { + organizationId: integration.linkedOrganizationId, + }, }); return this.pubSub.subscribe('oidcIntegrationLogs', integration.id); diff --git a/packages/services/api/src/modules/operations/providers/operations-manager.ts b/packages/services/api/src/modules/operations/providers/operations-manager.ts index 51e8d2e4d3..b66438541a 100644 --- a/packages/services/api/src/modules/operations/providers/operations-manager.ts +++ b/packages/services/api/src/modules/operations/providers/operations-manager.ts @@ -4,9 +4,7 @@ import LRU from 'lru-cache'; import type { DateRange } from '../../../shared/entities'; import type { Listify, Optional } from '../../../shared/helpers'; import { cache } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../auth/providers/organization-access'; -import { TargetAccessScope } from '../../auth/providers/target-access'; +import { Session } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; import type { OrganizationSelector, @@ -80,7 +78,7 @@ export class OperationsManager { constructor( logger: Logger, - private authManager: AuthManager, + private session: Session, private reader: OperationsReader, private storage: Storage, ) { @@ -104,33 +102,37 @@ export class OperationsManager { } async getOperation({ - organizationId: organization, - projectId: project, - targetId: target, + organizationId, + projectId, + targetId, hash, }: { hash: string } & TargetSelector) { - await this.authManager.ensureTargetAccess({ - organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + await this.session.assertPerformAction({ + action: 'project:describe', + organizationId: organizationId, + params: { + organizationId: organizationId, + projectId: projectId, + }, }); return await this.reader.readOperation({ - target, + target: targetId, hash, }); } - async readMonthlyUsage({ organizationId: organization }: OrganizationSelector) { - this.logger.info('Reading monthly usage (organization=%s)', organization); - await this.authManager.ensureOrganizationAccess({ - organizationId: organization, - // Why? Monthly usage is specific to organization settings (subscription page) - scope: OrganizationAccessScope.SETTINGS, + async readMonthlyUsage({ organizationId }: OrganizationSelector) { + this.logger.info('Reading monthly usage (organization=%s)', organizationId); + await this.session.assertPerformAction({ + action: 'billing:describe', + organizationId: organizationId, + params: { + organizationId: organizationId, + }, }); - return this.reader.readMonthlyUsage({ organization }); + return this.reader.readMonthlyUsage({ organization: organizationId }); } async countUniqueOperations({ @@ -146,11 +148,13 @@ export class OperationsManager { clients?: readonly string[]; } & TargetSelector) { this.logger.info('Counting unique operations (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return await this.reader.countUniqueDocuments({ @@ -167,11 +171,13 @@ export class OperationsManager { targetId: target, }: TargetSelector) { this.logger.info('Checking existence of collected operations (target=%s)', target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return hasCollectedOperationsCached(target, () => @@ -187,11 +193,13 @@ export class OperationsManager { targetId: target, }: TargetSelector) { this.logger.info('Checking existence of collected subscription operations (target=%s)', target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.getHasCollectedSubscriptionOperations({ @@ -200,9 +208,9 @@ export class OperationsManager { } async countRequestsWithSchemaCoordinate({ - organizationId: organization, - projectId: project, - targetId: target, + organizationId, + projectId, + targetId, period, schemaCoordinate, }: { @@ -212,19 +220,21 @@ export class OperationsManager { this.logger.info( 'Counting requests with schema coordinate (period=%o, target=%s, coordinate=%s)', period, - target, + targetId, schemaCoordinate, ); - await this.authManager.ensureTargetAccess({ - organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + await this.session.assertPerformAction({ + action: 'project:describe', + organizationId, + params: { + organizationId, + projectId, + }, }); return this.reader .countRequests({ - target, + target: targetId, period, schemaCoordinate, }) @@ -244,11 +254,13 @@ export class OperationsManager { clients?: readonly string[]; } & Listify) { this.logger.info('Counting requests and failures (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader @@ -270,11 +282,13 @@ export class OperationsManager { period: DateRange; } & Listify): Promise { this.logger.info('Counting requests (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.countOperationsWithoutDetails({ @@ -316,11 +330,13 @@ export class OperationsManager { clients?: readonly string[]; } & TargetSelector) { this.logger.info('Counting failures (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.countFailures({ @@ -346,11 +362,13 @@ export class OperationsManager { targetId: target, } = input; this.logger.info('Counting a field (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); const [totalField, total] = await Promise.all([ @@ -397,11 +415,13 @@ export class OperationsManager { excludedClients?.join(', ') ?? 'none', ); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.readFieldListStats({ @@ -427,11 +447,13 @@ export class OperationsManager { schemaCoordinate?: string; } & TargetSelector) { this.logger.info('Reading operations stats (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); // Maybe it needs less data @@ -468,16 +490,15 @@ export class OperationsManager { organizationId: organization, projectId: project, }); - await Promise.all( - targets.map(target => - this.authManager.ensureTargetAccess({ - organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, - }), - ), - ); + + await this.session.assertPerformAction({ + action: 'project:describe', + organizationId: organization, + params: { + organizationId: organization, + projectId: project, + }, + }); const groups = await this.requestsOverTimeOfTargetsLoader.load({ targets, @@ -529,16 +550,15 @@ export class OperationsManager { resolution, targets.join(';'), ); - await Promise.all( - targets.map(target => - this.authManager.ensureTargetAccess({ - organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, - }), - ), - ); + + await this.session.assertPerformAction({ + action: 'project:describe', + organizationId: organization, + params: { + organizationId: organization, + projectId: project, + }, + }); return this.requestsOverTimeOfTargetsLoader.load({ targets, @@ -569,11 +589,13 @@ export class OperationsManager { resolution, target, ); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.requestsOverTime({ @@ -606,11 +628,13 @@ export class OperationsManager { resolution, target, ); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.failuresOverTime({ @@ -642,11 +666,13 @@ export class OperationsManager { resolution, target, ); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.durationOverTime({ @@ -671,11 +697,13 @@ export class OperationsManager { clients?: readonly string[]; } & TargetSelector) { this.logger.info('Reading overall duration percentiles (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.generalDurationPercentiles({ @@ -707,11 +735,13 @@ export class OperationsManager { target, clients, ); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.durationPercentiles({ @@ -738,11 +768,13 @@ export class OperationsManager { schemaCoordinate?: string; } & TargetSelector) { this.logger.info('Counting unique clients (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.countUniqueClients({ @@ -762,11 +794,13 @@ export class OperationsManager { operations, }: { period: DateRange; operations?: readonly string[] } & Listify) { this.logger.info('Read unique client names (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.readUniqueClientNames({ @@ -789,11 +823,13 @@ export class OperationsManager { limit: number; } & TargetSelector) { this.logger.info('Read client versions (period=%o, target=%s)', period, target); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.readClientVersions({ @@ -820,11 +856,13 @@ export class OperationsManager { target, clientName, ); - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); return this.reader.countClientVersions({ @@ -874,11 +912,13 @@ export class OperationsManager { typename: string; } & TargetSelector, ) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + }, }); const loader = this.getClientNamesPerCoordinateOfTypeLoader({ @@ -953,11 +993,13 @@ export class OperationsManager { limit: number; coordinate: string; }) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + }, }); const loader = this.getTopOperationForTypeLoader({ @@ -975,11 +1017,13 @@ export class OperationsManager { organizationId: string; period: DateRange; }): Promise> { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + }, }); return this.reader.getReportedSchemaCoordinates({ @@ -1031,11 +1075,13 @@ export class OperationsManager { period: DateRange; typename: string; } & TargetSelector) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); const rows = await this.reader.countCoordinatesOfType({ @@ -1077,11 +1123,13 @@ export class OperationsManager { }: { period: DateRange; } & TargetSelector) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.REGISTRY_READ, + params: { + organizationId: organization, + projectId: project, + }, }); const rows = await this.reader.countCoordinatesOfTarget({ 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 68d6cfd8f9..5e2c3d9bc5 100644 --- a/packages/services/api/src/modules/organization/providers/organization-manager.ts +++ b/packages/services/api/src/modules/organization/providers/organization-manager.ts @@ -3,6 +3,7 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import { Organization, OrganizationMemberRole } from '../../../shared/entities'; import { HiveError } from '../../../shared/errors'; import { cache, diffArrays, share } from '../../../shared/helpers'; +import { Session } from '../../auth/lib/authz'; import { AuthManager } from '../../auth/providers/auth-manager'; import { OrganizationAccessScope } from '../../auth/providers/organization-access'; import { ProjectAccessScope } from '../../auth/providers/project-access'; @@ -60,6 +61,7 @@ export class OrganizationManager { logger: Logger, private storage: Storage, private authManager: AuthManager, + private session: Session, private tokenStorage: TokenStorage, private activityManager: ActivityManager, private billingProvider: BillingProvider, @@ -71,43 +73,42 @@ export class OrganizationManager { } getOrganizationFromToken: () => Promise = share(async () => { - const token = this.authManager.ensureApiToken(); - const result = await this.tokenStorage.getToken({ token }); + const { organizationId } = this.session.getLegacySelector(); - await this.authManager.ensureOrganizationAccess({ - organizationId: result.organization, - scope: OrganizationAccessScope.READ, + await this.session.assertPerformAction({ + action: 'organization:describe', + organizationId, + params: { + organizationId, + }, }); return this.storage.getOrganization({ - organizationId: result.organization, + organizationId, }); }); getOrganizationIdByToken: () => Promise = share(async () => { - const token = this.authManager.ensureApiToken(); - const { organization } = await this.tokenStorage.getToken({ - token, - }); - - return organization; + const { organizationId } = this.session.getLegacySelector(); + return organizationId; }); - async getOrganization( - selector: OrganizationSelector, - scope = OrganizationAccessScope.READ, - ): Promise { + async getOrganization(selector: OrganizationSelector): Promise { this.logger.debug('Fetching organization (selector=%o)', selector); - await this.authManager.ensureOrganizationAccess({ - ...selector, - scope, + await this.session.assertPerformAction({ + action: 'organization:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + }, }); + return this.storage.getOrganization(selector); } async getOrganizations(): Promise { this.logger.debug('Fetching organizations'); - const user = await this.authManager.getCurrentUser(); + const user = await this.session.getViewer(); return this.storage.getOrganizations({ userId: user.id }); } @@ -175,7 +176,7 @@ export class OrganizationManager { } > { this.logger.debug('Leaving organization (organization=%s)', organizationId); - const user = await this.authManager.getCurrentUser(); + const user = await this.session.getViewer(); const canLeave = await this.canLeaveOrganization({ organizationId, @@ -207,6 +208,7 @@ export class OrganizationManager { // Because we checked the access before, it's stale by now this.authManager.resetAccessCache(); + this.session.reset(); return { ok: true, @@ -229,9 +231,12 @@ export class OrganizationManager { }; } - const hasAccess = await this.authManager.checkOrganizationAccess({ + const hasAccess = await this.session.canPerformAction({ + action: 'organization:describe', organizationId: organization.id, - scope: OrganizationAccessScope.READ, + params: { + organizationId: organization.id, + }, }); if (hasAccess) { @@ -264,10 +269,14 @@ export class OrganizationManager { @cache((selector: OrganizationSelector) => selector.organizationId) async getInvitations(selector: OrganizationSelector) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'member:manageInvites', organizationId: selector.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: selector.organizationId, + }, }); + return this.storage.getOrganizationInvitations(selector); } @@ -317,12 +326,15 @@ export class OrganizationManager { async deleteOrganization(selector: OrganizationSelector): Promise { this.logger.info('Deleting an organization (organization=%s)', selector.organizationId); - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'organization:delete', organizationId: selector.organizationId, - scope: OrganizationAccessScope.DELETE, + params: { + organizationId: selector.organizationId, + }, }); - const organization = await this.getOrganization({ + const organization = await this.storage.getOrganization({ organizationId: selector.organizationId, }); @@ -334,6 +346,7 @@ export class OrganizationManager { // Because we checked the access before, it's stale by now this.authManager.resetAccessCache(); + this.session.reset(); return deletedOrganization; } @@ -345,11 +358,15 @@ export class OrganizationManager { ): Promise { const { plan } = input; this.logger.info('Updating an organization plan (input=%o)', input); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'billing:update', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); - const organization = await this.getOrganization({ + + const organization = await this.storage.getOrganization({ organizationId: input.organizationId, }); @@ -377,11 +394,15 @@ export class OrganizationManager { ): Promise { const { monthlyRateLimit } = input; this.logger.info('Updating an organization plan (input=%o)', input); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'billing:update', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); - const organization = await this.getOrganization({ + + const organization = await this.storage.getOrganization({ organizationId: input.organizationId, }); @@ -409,13 +430,17 @@ export class OrganizationManager { ) { const { slug } = input; this.logger.info('Updating an organization clean id (input=%o)', input); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'organization:modifySettings', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); + const [user, organization] = await Promise.all([ - this.authManager.getCurrentUser(), - this.getOrganization({ + this.session.getViewer(), + this.storage.getOrganization({ organizationId: input.organizationId, }), ]); @@ -450,17 +475,23 @@ export class OrganizationManager { } async deleteInvitation(input: { email: string; organizationId: string }) { - await this.authManager.ensureOrganizationAccess({ - scope: OrganizationAccessScope.MEMBERS, + await this.session.assertPerformAction({ + action: 'member:manageInvites', organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); return this.storage.deleteOrganizationInvitationByEmail(input); } async inviteByEmail(input: { email: string; organization: string; role?: string | null }) { - await this.authManager.ensureOrganizationAccess({ - scope: OrganizationAccessScope.MEMBERS, + await this.session.assertPerformAction({ + action: 'member:manageInvites', organizationId: input.organization, + params: { + organizationId: input.organization, + }, }); const { email } = input; @@ -574,7 +605,7 @@ export class OrganizationManager { async joinOrganization({ code }: { code: string }): Promise { this.logger.info('Joining an organization (code=%s)', code); - const user = await this.authManager.getCurrentUser(); + const user = await this.session.getViewer(); const isOIDCUser = user.oidcIntegrationId !== null; if (isOIDCUser) { @@ -613,6 +644,7 @@ export class OrganizationManager { // Because we checked the access before, it's stale by now this.authManager.resetAccessCache(); + this.session.reset(); await Promise.all([ this.storage.completeGetStartedStep({ @@ -636,7 +668,7 @@ export class OrganizationManager { userId: string; } & OrganizationSelector, ) { - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); if (currentUser.id === selector.userId) { return { @@ -706,11 +738,14 @@ export class OrganizationManager { code: string; } & OrganizationSelector, ) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'organization:describe', organizationId: selector.organizationId, - scope: OrganizationAccessScope.READ, + params: { + organizationId: selector.organizationId, + }, }); - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); return this.storage.getOrganizationTransferRequest({ organizationId: selector.organizationId, @@ -725,11 +760,14 @@ export class OrganizationManager { accept: boolean; } & OrganizationSelector, ) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'organization:describe', organizationId: input.organizationId, - scope: OrganizationAccessScope.READ, + params: { + organizationId: input.organizationId, + }, }); - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); await this.storage.answerOrganizationTransferRequest({ organizationId: input.organizationId, @@ -745,10 +783,14 @@ export class OrganizationManager { } & OrganizationSelector, ): Promise { this.logger.info('Deleting a member from an organization (selector=%o)', selector); - await this.authManager.ensureOrganizationAccess({ - ...selector, - scope: OrganizationAccessScope.MEMBERS, + await this.session.assertPerformAction({ + action: 'member:removeMember', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + }, }); + const owner = await this.getOrganizationOwner(selector); const { user, organizationId: organization } = selector; @@ -756,7 +798,7 @@ export class OrganizationManager { throw new HiveError(`Cannot remove the owner from the organization`); } - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const [currentUserAsMember, member] = await Promise.all([ this.storage.getOrganizationMember({ @@ -807,6 +849,7 @@ export class OrganizationManager { // Because we checked the access before, it's stale by now this.authManager.resetAccessCache(); + this.session.reset(); return this.storage.getOrganization({ organizationId: organization, @@ -822,12 +865,15 @@ export class OrganizationManager { } & OrganizationSelector, ) { this.logger.info('Updating a member access in an organization (input=%o)', input); - await this.authManager.ensureOrganizationAccess({ - ...input, - scope: OrganizationAccessScope.MEMBERS, + await this.session.assertPerformAction({ + action: 'member:assignRole', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + }, }); - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const [currentMember, member] = await Promise.all([ this.getOrganizationMember({ @@ -873,6 +919,7 @@ export class OrganizationManager { // Because we checked the access before, it's stale by now this.authManager.resetAccessCache(); + this.session.reset(); return this.storage.getOrganization({ organizationId: input.organizationId, @@ -887,9 +934,12 @@ export class OrganizationManager { projectAccessScopes: readonly ProjectAccessScope[]; targetAccessScopes: readonly TargetAccessScope[]; }) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'member:modifyRole', organizationId: input.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: input.organizationId, + }, }); const scopes = ensureReadAccess([ @@ -898,7 +948,7 @@ export class OrganizationManager { ...input.targetAccessScopes, ]); - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const currentUserAsMember = await this.getOrganizationMember({ organizationId: input.organizationId, userId: currentUser.id, @@ -958,9 +1008,12 @@ export class OrganizationManager { } async deleteMemberRole(input: { organizationId: string; roleId: string }) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'member:modifyRole', organizationId: input.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: input.organizationId, + }, }); const role = await this.storage.getOrganizationMemberRole({ @@ -976,7 +1029,7 @@ export class OrganizationManager { }; } - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const currentUserAsMember = await this.getOrganizationMember({ organizationId: input.organizationId, userId: currentUser.id, @@ -1008,9 +1061,12 @@ export class OrganizationManager { } async assignMemberRole(input: { organizationId: string; userId: string; roleId: string }) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'member:assignRole', organizationId: input.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: input.organizationId, + }, }); // Ensure selected member is part of the organization @@ -1023,7 +1079,7 @@ export class OrganizationManager { throw new Error(`Member is not part of the organization`); } - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const [currentUserAsMember, newRole] = await Promise.all([ this.getOrganizationMember({ organizationId: input.organizationId, @@ -1101,6 +1157,7 @@ export class OrganizationManager { // Access cache is stale by now this.authManager.resetAccessCache(); + this.session.reset(); return { ok: { @@ -1122,12 +1179,14 @@ export class OrganizationManager { projectAccessScopes: readonly ProjectAccessScope[]; targetAccessScopes: readonly TargetAccessScope[]; }) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'member:modifyRole', organizationId: input.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: input.organizationId, + }, }); - - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const [role, currentUserAsMember] = await Promise.all([ this.storage.getOrganizationMemberRole({ organizationId: input.organizationId, @@ -1232,6 +1291,7 @@ export class OrganizationManager { // Access cache is stale by now this.authManager.resetAccessCache(); + this.session.reset(); return { ok: { @@ -1242,9 +1302,12 @@ export class OrganizationManager { async getMembersWithoutRole(selector: { organizationId: string }) { if ( - await this.authManager.checkOrganizationAccess({ + await this.session.canPerformAction({ + action: 'member:describe', organizationId: selector.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: selector.organizationId, + }, }) ) { return this.storage.getMembersWithoutRole({ @@ -1257,9 +1320,12 @@ export class OrganizationManager { } async getMemberRoles(selector: { organizationId: string }) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'member:describe', organizationId: selector.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: selector.organizationId, + }, }); return this.storage.getOrganizationMemberRoles({ @@ -1268,9 +1334,12 @@ export class OrganizationManager { } async getMemberRole(selector: { organizationId: string; roleId: string }) { - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ + action: 'member:describe', organizationId: selector.organizationId, - scope: OrganizationAccessScope.MEMBERS, + params: { + organizationId: selector.organizationId, + }, }); return this.storage.getOrganizationMemberRole({ @@ -1442,7 +1511,7 @@ export class OrganizationManager { userIds: readonly string[]; } | null; }) { - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const currentUserAsMember = await this.getOrganizationMember({ organizationId: organizationId, userId: currentUser.id, diff --git a/packages/services/api/src/modules/organization/resolvers/MemberRole.ts b/packages/services/api/src/modules/organization/resolvers/MemberRole.ts index 45096e2968..f988985252 100644 --- a/packages/services/api/src/modules/organization/resolvers/MemberRole.ts +++ b/packages/services/api/src/modules/organization/resolvers/MemberRole.ts @@ -1,4 +1,4 @@ -import { AuthManager } from '../../auth/providers/auth-manager'; +import { Session } from '../../auth/lib/authz'; import { isOrganizationScope } from '../../auth/providers/organization-access'; import { isProjectScope } from '../../auth/providers/project-access'; import { isTargetScope } from '../../auth/providers/target-access'; @@ -33,7 +33,7 @@ export const MemberRole: MemberRoleResolvers = { return false; } - const currentUser = await injector.get(AuthManager).getCurrentUser(); + const currentUser = await injector.get(Session).getViewer(); const currentUserAsMember = await injector.get(OrganizationManager).getOrganizationMember({ organizationId: role.organizationId, userId: currentUser.id, @@ -49,7 +49,7 @@ export const MemberRole: MemberRoleResolvers = { if (role.locked) { return false; } - const currentUser = await injector.get(AuthManager).getCurrentUser(); + const currentUser = await injector.get(Session).getViewer(); const currentUserAsMember = await injector.get(OrganizationManager).getOrganizationMember({ organizationId: role.organizationId, userId: currentUser.id, @@ -62,7 +62,7 @@ export const MemberRole: MemberRoleResolvers = { return result.ok; }, canInvite: async (role, _, { injector }) => { - const currentUser = await injector.get(AuthManager).getCurrentUser(); + const currentUser = await injector.get(Session).getViewer(); const currentUserAsMember = await injector.get(OrganizationManager).getOrganizationMember({ organizationId: role.organizationId, userId: currentUser.id, diff --git a/packages/services/api/src/modules/organization/resolvers/Mutation/createOrganization.ts b/packages/services/api/src/modules/organization/resolvers/Mutation/createOrganization.ts index d185975a72..187fcfa062 100644 --- a/packages/services/api/src/modules/organization/resolvers/Mutation/createOrganization.ts +++ b/packages/services/api/src/modules/organization/resolvers/Mutation/createOrganization.ts @@ -1,4 +1,4 @@ -import { AuthManager } from '../../../auth/providers/auth-manager'; +import { Session } from '../../../auth/lib/authz'; import { OrganizationManager } from '../../providers/organization-manager'; import { OrganizationSlugModel } from '../../validation'; import type { MutationResolvers } from './../../../../__generated__/types'; @@ -20,7 +20,7 @@ export const createOrganization: NonNullable { - const me = await injector.get(AuthManager).getCurrentUser(); + const me = await injector.get(Session).getViewer(); const members = await injector .get(OrganizationManager) .getOrganizationMembers({ organizationId: organization.id }); 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 9b72f47320..169dea7ff5 100644 --- a/packages/services/api/src/modules/organization/resolvers/Query/myDefaultOrganization.ts +++ b/packages/services/api/src/modules/organization/resolvers/Query/myDefaultOrganization.ts @@ -1,4 +1,4 @@ -import { AuthManager } from '../../../auth/providers/auth-manager'; +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'; @@ -9,7 +9,7 @@ export const myDefaultOrganization: NonNullable { - const user = await injector.get(AuthManager).getCurrentUser(); + const user = await injector.get(Session).getViewer(); const organizationManager = injector.get(OrganizationManager); // For an OIDC Integration User we want to return the linked organization diff --git a/packages/services/api/src/modules/policy/module.graphql.ts b/packages/services/api/src/modules/policy/module.graphql.ts index b1def1f0a9..9c9fb3f6f2 100644 --- a/packages/services/api/src/modules/policy/module.graphql.ts +++ b/packages/services/api/src/modules/policy/module.graphql.ts @@ -83,17 +83,4 @@ export default gql` schemaPolicy: SchemaPolicy parentSchemaPolicy: SchemaPolicy } - - extend type Target { - """ - A merged representation of the schema policy, as inherited from the organization and project. - """ - schemaPolicy: TargetSchemaPolicy - } - - type TargetSchemaPolicy { - organizationPolicy: SchemaPolicy - projectPolicy: SchemaPolicy - mergedRules: [SchemaPolicyRuleInstance!]! - } `; diff --git a/packages/services/api/src/modules/policy/providers/schema-policy.provider.ts b/packages/services/api/src/modules/policy/providers/schema-policy.provider.ts index c285f72f16..57752c0e2d 100644 --- a/packages/services/api/src/modules/policy/providers/schema-policy.provider.ts +++ b/packages/services/api/src/modules/policy/providers/schema-policy.provider.ts @@ -1,12 +1,7 @@ import { Injectable, Scope } from 'graphql-modules'; import type { CheckPolicyResponse, PolicyConfigurationObject } from '@hive/policy'; import { SchemaPolicy } from '../../../shared/entities'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { - OrganizationAccessScope, - ProjectAccessScope, - TargetAccessScope, -} from '../../auth/providers/scopes'; +import { Session } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; import { OrganizationSelector, @@ -26,7 +21,7 @@ export class SchemaPolicyProvider { constructor( rootLogger: Logger, private storage: Storage, - private authManager: AuthManager, + private session: Session, private api: SchemaPolicyApiProvider, ) { this.logger = rootLogger.child({ service: 'SchemaPolicyProvider' }); @@ -41,19 +36,6 @@ export class SchemaPolicyProvider { }, {} as PolicyConfigurationObject); } - async getCalculatedTargetPolicyForApi(selector: TargetSelector): Promise<{ - orgLevel: SchemaPolicy | null; - projectLevel: SchemaPolicy | null; - mergedPolicy: PolicyConfigurationObject | null; - }> { - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.SETTINGS, - }); - - return this._getCalculatedPolicyForTarget(selector); - } - private async _getCalculatedPolicyForTarget(selector: TargetSelector): Promise<{ orgLevel: SchemaPolicy | null; projectLevel: SchemaPolicy | null; @@ -143,9 +125,12 @@ export class SchemaPolicyProvider { policy: any, allowOverrides: boolean, ) { - await this.authManager.ensureOrganizationAccess({ - ...selector, - scope: OrganizationAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'schemaLinting:modifyOrganizationRules', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + }, }); return await this.storage.setSchemaPolicyForOrganization({ @@ -156,9 +141,13 @@ export class SchemaPolicyProvider { } async setProjectPolicy(selector: ProjectSelector, policy: any) { - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'schemaLinting:modifyProjectRules', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); return await this.storage.setSchemaPolicyForProject({ @@ -168,27 +157,38 @@ export class SchemaPolicyProvider { } async getOrganizationPolicy(selector: OrganizationSelector) { - await this.authManager.ensureOrganizationAccess({ - ...selector, - scope: OrganizationAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'schemaLinting:modifyOrganizationRules', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + }, }); return this.storage.getSchemaPolicyForOrganization(selector.organizationId); } async getOrganizationPolicyForProject(selector: ProjectSelector) { - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'schemaLinting:modifyProjectRules', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); return this.storage.getSchemaPolicyForOrganization(selector.organizationId); } async getProjectPolicy(selector: ProjectSelector) { - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'schemaLinting:modifyProjectRules', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); return this.storage.getSchemaPolicyForProject(selector.projectId); diff --git a/packages/services/api/src/modules/policy/resolvers/Target.ts b/packages/services/api/src/modules/policy/resolvers/Target.ts deleted file mode 100644 index 7f636a1462..0000000000 --- a/packages/services/api/src/modules/policy/resolvers/Target.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { SchemaPolicyApiProvider } from '../providers/schema-policy-api.provider'; -import { SchemaPolicyProvider } from '../providers/schema-policy.provider'; -import { serializeSeverity } from '../utils'; -import type { TargetResolvers } from './../../../__generated__/types'; - -export const Target: Pick = { - schemaPolicy: async (target, _, { injector }) => { - const { mergedPolicy, orgLevel, projectLevel } = await injector - .get(SchemaPolicyProvider) - .getCalculatedTargetPolicyForApi({ - projectId: target.projectId, - organizationId: target.orgId, - targetId: target.id, - }); - - if (!mergedPolicy) { - return null; - } - - const availableRules = await injector.get(SchemaPolicyApiProvider).listAvailableRules(); - const rules = Object.entries(mergedPolicy).map(([ruleId, config]) => ({ - rule: availableRules.find(r => r.name === ruleId)!, - severity: serializeSeverity(config[0]), - configuration: config[1], - })); - - return { - mergedRules: rules, - organizationPolicy: orgLevel, - projectPolicy: projectLevel, - }; - }, -}; diff --git a/packages/services/api/src/modules/project/providers/project-manager.ts b/packages/services/api/src/modules/project/providers/project-manager.ts index 6787f799fa..0d06c63482 100644 --- a/packages/services/api/src/modules/project/providers/project-manager.ts +++ b/packages/services/api/src/modules/project/providers/project-manager.ts @@ -1,9 +1,7 @@ import { Injectable, Scope } from 'graphql-modules'; import type { Project, ProjectType } from '../../../shared/entities'; import { share } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../auth/providers/organization-access'; -import { ProjectAccessScope } from '../../auth/providers/project-access'; +import { Session } from '../../auth/lib/authz'; import { ActivityManager } from '../../shared/providers/activity-manager'; import { Logger } from '../../shared/providers/logger'; import { OrganizationSelector, ProjectSelector, Storage } from '../../shared/providers/storage'; @@ -25,7 +23,7 @@ export class ProjectManager { constructor( logger: Logger, private storage: Storage, - private authManager: AuthManager, + private session: Session, private tokenStorage: TokenStorage, private activityManager: ActivityManager, ) { @@ -41,9 +39,12 @@ export class ProjectManager { const { slug, type, organizationId: organization } = input; this.logger.info('Creating a project (input=%o)', input); - await this.authManager.ensureOrganizationAccess({ - organizationId: input.organizationId, - scope: OrganizationAccessScope.READ, + await this.session.assertPerformAction({ + action: 'project:create', + organizationId: organization, + params: { + organizationId: organization, + }, }); if (reservedSlugs.includes(slug)) { @@ -87,10 +88,13 @@ export class ProjectManager { projectId: project, }: ProjectSelector): Promise { this.logger.info('Deleting a project (project=%s, organization=%s)', project, organization); - await this.authManager.ensureProjectAccess({ - projectId: project, + await this.session.assertPerformAction({ + action: 'project:delete', organizationId: organization, - scope: ProjectAccessScope.DELETE, + params: { + organizationId: organization, + projectId: project, + }, }); const deletedProject = await this.storage.deleteProject({ @@ -115,28 +119,47 @@ export class ProjectManager { } getProjectIdByToken: () => Promise = share(async () => { - const token = this.authManager.ensureApiToken(); - const { project } = await this.tokenStorage.getToken({ token }); - - return project; + const token = this.session.getLegacySelector(); + return token.projectId; }); async getProject(selector: ProjectSelector): Promise { this.logger.debug('Fetching project (selector=%o)', selector); - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.READ, + await this.session.assertPerformAction({ + action: 'project:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); return this.storage.getProject(selector); } async getProjects(selector: OrganizationSelector): Promise { this.logger.debug('Fetching projects (selector=%o)', selector); - await this.authManager.ensureOrganizationAccess({ - ...selector, - scope: OrganizationAccessScope.READ, - }); - return this.storage.getProjects(selector); + const projects = await this.storage.getProjects(selector); + + const filteredProjects: Project[] = []; + + for (const project of projects) { + if ( + false === + (await this.session.canPerformAction({ + action: 'project:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: project.id, + }, + })) + ) { + continue; + } + filteredProjects.push(project); + } + + return filteredProjects; } async updateSlug( @@ -155,11 +178,16 @@ export class ProjectManager { > { const { slug, organizationId: organization, projectId: project } = input; this.logger.info('Updating a project slug (input=%o)', input); - await this.authManager.ensureProjectAccess({ - ...input, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'project:modifySettings', + organizationId: organization, + params: { + organizationId: organization, + projectId: project, + }, }); - const user = await this.authManager.getCurrentUser(); + + const user = await this.session.getViewer(); if (reservedSlugs.includes(slug)) { return { diff --git a/packages/services/api/src/modules/rate-limit/providers/in-memory-rate-limiter.ts b/packages/services/api/src/modules/rate-limit/providers/in-memory-rate-limiter.ts index a7e0bed649..29e5f8981a 100644 --- a/packages/services/api/src/modules/rate-limit/providers/in-memory-rate-limiter.ts +++ b/packages/services/api/src/modules/rate-limit/providers/in-memory-rate-limiter.ts @@ -1,7 +1,7 @@ import { Injectable, Scope } from 'graphql-modules'; import LRU from 'lru-cache'; import { HiveError } from '../../../shared/errors'; -import { AuthManager } from '../../auth/providers/auth-manager'; +import { Session } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; @Injectable({ @@ -48,7 +48,7 @@ export class InMemoryRateLimiter { constructor( private logger: Logger, private store: InMemoryRateLimitStore, - private authManager: AuthManager, + private session: Session, ) { this.logger = logger.child({ service: 'InMemoryRateLimiter' }); } @@ -60,11 +60,13 @@ export class InMemoryRateLimiter { windowSizeInMs, maxActions, ); - if (!this.authManager.isUser()) { + + if (!this.session.isViewer()) { throw new Error('Expected to be called for an authenticated user.'); } - const user = await this.authManager.getCurrentUser(); + const user = await this.session.getViewer(); + const limiter = this.store.ensureLimiter(action, windowSizeInMs, maxActions); if (!limiter.isAllowed(user.id)) { diff --git a/packages/services/api/src/modules/schema/providers/contracts-manager.ts b/packages/services/api/src/modules/schema/providers/contracts-manager.ts index d4f10614f3..b53c34fb17 100644 --- a/packages/services/api/src/modules/schema/providers/contracts-manager.ts +++ b/packages/services/api/src/modules/schema/providers/contracts-manager.ts @@ -2,8 +2,7 @@ import { Injectable, Scope } from 'graphql-modules'; import type { SchemaCheck, SchemaVersion } from '@hive/storage'; import type { Target } from '../../../shared/entities'; import { cache } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { TargetAccessScope } from '../../auth/providers/scopes'; +import { Session } from '../../auth/lib/authz'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; import { Storage } from '../../shared/providers/storage'; @@ -25,7 +24,7 @@ export class ContractsManager { logger: Logger, private contracts: Contracts, private storage: Storage, - private authManager: AuthManager, + private session: Session, private idTranslator: IdTranslator, private breakingSchemaChangeUsageHelper: BreakingSchemaChangeUsageHelper, ) { @@ -51,11 +50,14 @@ export class ContractsManager { this.idTranslator.translateTargetId(breadcrumb), ]); - await this.authManager.ensureTargetAccess({ - organizationId: organizationId, - projectId: projectId, - targetId: targetId, - scope: TargetAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'target:modifySettings', + organizationId, + params: { + organizationId, + projectId, + targetId, + }, }); return await this.contracts.createContract(args); @@ -86,11 +88,14 @@ export class ContractsManager { this.idTranslator.translateTargetId(breadcrumb), ]); - await this.authManager.ensureTargetAccess({ - organizationId: organizationId, - projectId: projectId, - targetId: targetId, - scope: TargetAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'target:modifySettings', + organizationId, + params: { + organizationId, + projectId, + targetId, + }, }); return await this.contracts.disableContract({ @@ -98,7 +103,7 @@ export class ContractsManager { }); } - async getViewerCanDisableContractForContract(contract: Contract) { + async getViewerCanDisableContractForContract(contract: Contract): Promise { if (contract.isDisabled) { return false; } @@ -106,6 +111,7 @@ export class ContractsManager { const breadcrumb = await this.storage.getTargetBreadcrumbForTargetId({ targetId: contract.targetId, }); + if (!breadcrumb) { return false; } @@ -116,15 +122,15 @@ export class ContractsManager { this.idTranslator.translateTargetId(breadcrumb), ]); - return await this.authManager - .ensureTargetAccess({ - organizationId: organizationId, - projectId: projectId, - targetId: targetId, - scope: TargetAccessScope.SETTINGS, - }) - .then(() => true) - .catch(() => false); + return await this.session.canPerformAction({ + action: 'target:modifySettings', + organizationId, + params: { + organizationId, + projectId, + targetId, + }, + }); } public async getPaginatedContractsForTarget(args: { @@ -132,11 +138,14 @@ export class ContractsManager { cursor: string | null; first: number | null; }) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'target:modifySettings', organizationId: args.target.orgId, - projectId: args.target.projectId, - targetId: args.target.id, - scope: TargetAccessScope.SETTINGS, + params: { + organizationId: args.target.orgId, + projectId: args.target.projectId, + targetId: args.target.id, + }, }); return this.contracts.getPaginatedContractsByTargetId({ @@ -152,11 +161,13 @@ export class ContractsManager { cursor: string | null; first: number | null; }) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'project:describe', organizationId: args.target.orgId, - projectId: args.target.projectId, - targetId: args.target.id, - scope: TargetAccessScope.READ, + params: { + organizationId: args.target.orgId, + projectId: args.target.projectId, + }, }); return this.contracts.getPaginatedContractsByTargetId({ diff --git a/packages/services/api/src/modules/schema/providers/schema-manager.ts b/packages/services/api/src/modules/schema/providers/schema-manager.ts index 0db420d630..b46eb9971c 100644 --- a/packages/services/api/src/modules/schema/providers/schema-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-manager.ts @@ -18,15 +18,13 @@ import { Organization, Project, ProjectType, + Target, } from '../../../shared/entities'; import { HiveError } from '../../../shared/errors'; import { atomic, cache, stringifySelector } from '../../../shared/helpers'; import { parseGraphQLSource } from '../../../shared/schema'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { ProjectAccessScope } from '../../auth/providers/project-access'; -import { TargetAccessScope } from '../../auth/providers/target-access'; +import { Session } from '../../auth/lib/authz'; import { GitHubIntegrationManager } from '../../integrations/providers/github-integration-manager'; -import { OrganizationManager } from '../../organization/providers/organization-manager'; import { ProjectManager } from '../../project/providers/project-manager'; import { CryptoProvider } from '../../shared/providers/crypto'; import { Logger } from '../../shared/providers/logger'; @@ -71,7 +69,7 @@ export class SchemaManager { constructor( logger: Logger, - private authManager: AuthManager, + private session: Session, private storage: Storage, private projectManager: ProjectManager, private singleOrchestrator: SingleOrchestrator, @@ -80,7 +78,6 @@ export class SchemaManager { private crypto: CryptoProvider, private githubIntegrationManager: GitHubIntegrationManager, private targetManager: TargetManager, - private organizationManager: OrganizationManager, private schemaHelper: SchemaHelper, private contracts: Contracts, private breakingSchemaChangeUsageHelper: BreakingSchemaChangeUsageHelper, @@ -89,13 +86,13 @@ export class SchemaManager { this.logger = logger.child({ source: 'SchemaManager' }); } - async hasSchema(selector: TargetSelector) { - this.logger.debug('Checking if schema is available (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, + async hasSchema(target: Target) { + this.logger.debug('Checking if schema is available (targetId=%s)', target.id); + return this.storage.hasSchema({ + organizationId: target.orgId, + projectId: target.projectId, + targetId: target.id, }); - return this.storage.hasSchema(selector); } async compose( @@ -109,20 +106,25 @@ export class SchemaManager { }, ) { this.logger.debug('Composing schemas (input=%o)', lodash.omit(input, 'services')); - await this.authManager.ensureTargetAccess({ - ...input, - scope: TargetAccessScope.REGISTRY_READ, + await this.session.canPerformAction({ + action: 'schema:compose', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }, }); const [organization, project, latestSchemas] = await Promise.all([ - this.organizationManager.getOrganization({ + this.storage.getOrganization({ organizationId: input.organizationId, }), - this.projectManager.getProject({ + this.storage.getProject({ organizationId: input.organizationId, projectId: input.projectId, }), - this.getLatestSchemas({ + this.storage.getLatestSchemas({ organizationId: input.organizationId, projectId: input.projectId, targetId: input.targetId, @@ -193,10 +195,6 @@ export class SchemaManager { } & TargetSelector, ) { this.logger.debug('Fetching non-empty list of schemas (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); const schemas = await this.storage.getSchemasOfVersion(selector); if (schemas.length === 0) { @@ -207,49 +205,9 @@ export class SchemaManager { } @atomic(stringifySelector) - async getMaybeSchemasOfVersion( - selector: { - versionId: string; - includeMetadata?: boolean; - } & TargetSelector, - ) { - this.logger.debug('Fetching schemas (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); - return this.storage.getSchemasOfVersion(selector); - } - - async getSchemasOfPreviousVersion( - selector: { - versionId: string; - onlyComposable: boolean; - } & TargetSelector, - ) { - this.logger.debug( - 'Fetching schemas from the previous version (onlyComposable=%s, selector=%o)', - selector.onlyComposable, - selector, - ); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); - return this.storage.getSchemasOfPreviousVersion(selector); - } - - async getLatestSchemas( - selector: TargetSelector & { - onlyComposable?: boolean; - }, - ) { - this.logger.debug('Fetching latest schemas (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); - return this.storage.getLatestSchemas(selector); + async getMaybeSchemasOfVersion(schemaVersion: SchemaVersion) { + this.logger.debug('Fetching schemas (schemaVersionId=%s)', schemaVersion.id); + return this.storage.getSchemasOfVersion({ versionId: schemaVersion.id }); } async getMatchingServiceSchemaOfVersions(versions: { before: string | null; after: string }) { @@ -257,33 +215,26 @@ export class SchemaManager { return this.storage.getMatchingServiceSchemaOfVersions(versions); } - async getMaybeLatestValidVersion(selector: TargetSelector) { - this.logger.debug('Fetching maybe latest valid version (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, + async getMaybeLatestValidVersion(target: Target) { + this.logger.debug('Fetching maybe latest valid version (targetId=%o)', target.id); + const version = await this.storage.getMaybeLatestValidVersion({ + targetId: target.id, }); - const version = await this.storage.getMaybeLatestValidVersion(selector); - if (!version) { return null; } return { ...version, - projectId: selector.projectId, - targetId: selector.targetId, - organizationId: selector.organizationId, + projectId: target.projectId, + targetId: target.id, + organizationId: target.orgId, }; } async getLatestValidVersion(selector: TargetSelector) { this.logger.debug('Fetching latest valid version (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); return { ...(await this.storage.getLatestValidVersion(selector)), projectId: selector.projectId, @@ -292,47 +243,28 @@ export class SchemaManager { }; } - async getLatestVersion(selector: TargetSelector) { - this.logger.debug('Fetching latest version (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); - return { - ...(await this.storage.getLatestVersion(selector)), - projectId: selector.projectId, - targetId: selector.targetId, - organizationId: selector.organizationId, - }; - } - - async getMaybeLatestVersion(selector: TargetSelector) { - this.logger.debug('Fetching maybe latest version (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, + async getMaybeLatestVersion(target: Target) { + this.logger.debug('Fetching maybe latest version (targetId=%o)', target.id); + const latest = await this.storage.getMaybeLatestVersion({ + targetId: target.id, + projectId: target.projectId, + organizationId: target.orgId, }); - const latest = await this.storage.getMaybeLatestVersion(selector); - if (!latest) { return null; } return { ...latest, - projectId: selector.projectId, - targetId: selector.targetId, - organizationId: selector.organizationId, + projectId: target.projectId, + targetId: target.id, + organizationId: target.orgId, }; } async getSchemaVersion(selector: TargetSelector & { versionId: string }) { this.logger.debug('Fetching single schema version (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); const result = await this.storage.getVersion(selector); return { @@ -343,16 +275,6 @@ export class SchemaManager { }; } - async getSchemaChangesForVersion(selector: TargetSelector & { version: string }) { - this.logger.debug('Fetching single schema version changes (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); - - return await this.storage.getSchemaChangesForVersion({ versionId: selector.version }); - } - async getPaginatedSchemaVersionsForTargetId(args: { targetId: string; organizationId: string; @@ -380,9 +302,14 @@ export class SchemaManager { input: TargetSelector & { versionId: string; valid: boolean }, ): Promise { this.logger.debug('Updating schema version status (input=%o)', input); - await this.authManager.ensureTargetAccess({ - ...input, - scope: TargetAccessScope.REGISTRY_WRITE, + await this.session.assertPerformAction({ + action: 'schemaVersion:approve', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }, }); const project = await this.storage.getProject({ @@ -404,10 +331,6 @@ export class SchemaManager { async getSchemaLog(selector: { commit: string } & TargetSelector) { this.logger.debug('Fetching schema log (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, - }); return this.storage.getSchemaLog({ commit: selector.commit, targetId: selector.targetId, @@ -477,13 +400,6 @@ export class SchemaManager { ]), ); - await this.authManager.ensureTargetAccess({ - projectId: input.projectId, - organizationId: input.organizationId, - targetId: input.targetId, - scope: TargetAccessScope.REGISTRY_WRITE, - }); - return this.storage.createVersion({ ...input, logIds: input.logIds, @@ -491,10 +407,13 @@ export class SchemaManager { } async testExternalSchemaComposition(selector: { projectId: string; organizationId: string }) { - await this.authManager.ensureProjectAccess({ - projectId: selector.projectId, + await this.session.assertPerformAction({ organizationId: selector.organizationId, - scope: ProjectAccessScope.SETTINGS, + action: 'project:modifySettings', + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); const [project, organization] = await Promise.all([ @@ -576,19 +495,25 @@ export class SchemaManager { } } - async getBaseSchema(selector: TargetSelector) { - this.logger.debug('Fetching base schema (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_READ, + async getBaseSchemaForTarget(target: Target) { + this.logger.debug('Fetching base schema (selector=%o)', target); + + return await this.storage.getBaseSchema({ + organizationId: target.orgId, + projectId: target.projectId, + targetId: target.id, }); - return await this.storage.getBaseSchema(selector); } async updateBaseSchema(selector: TargetSelector, newBaseSchema: string | null) { this.logger.debug('Updating base schema (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.REGISTRY_WRITE, + await this.session.assertPerformAction({ + action: 'target:modifySettings', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + targetId: selector.targetId, + }, }); await this.storage.updateBaseSchema(selector, newBaseSchema); } @@ -629,9 +554,13 @@ export class SchemaManager { async disableExternalSchemaComposition(input: ProjectSelector) { this.logger.debug('Disabling external composition (input=%o)', input); - await this.authManager.ensureProjectAccess({ - ...input, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + organizationId: input.organizationId, + action: 'project:modifySettings', + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); await this.storage.disableExternalSchemaComposition(input); @@ -651,11 +580,14 @@ export class SchemaManager { }, ) { this.logger.debug('Enabling external composition (input=%o)', lodash.omit(input, ['secret'])); - await this.authManager.ensureProjectAccess({ - ...input, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + organizationId: input.organizationId, + action: 'project:modifySettings', + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); - const parseResult = ENABLE_EXTERNAL_COMPOSITION_SCHEMA.safeParse({ endpoint: input.endpoint, secret: input.secret, @@ -696,9 +628,13 @@ export class SchemaManager { }, ) { this.logger.debug('Updating native schema composition (input=%o)', input); - await this.authManager.ensureProjectAccess({ - ...input, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + organizationId: input.organizationId, + action: 'project:modifySettings', + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); const project = await this.projectManager.getProject({ @@ -723,32 +659,29 @@ export class SchemaManager { }, ) { this.logger.debug('Updating registry model (input=%o)', input); - await this.authManager.ensureProjectAccess({ - ...input, - scope: ProjectAccessScope.SETTINGS, + await this.session.assertPerformAction({ + organizationId: input.organizationId, + action: 'project:modifySettings', + params: { + organizationId: input.organizationId, + projectId: input.projectId, + }, }); return this.storage.updateProjectRegistryModel(input); } - async getPaginatedSchemaChecksForTarget(args: { - organizationId: string; - projectId: string; - targetId: string; - first: number | null; - cursor: string | null; - transformNode: (check: SchemaCheck) => TransformedSchemaCheck; - filters: SchemaChecksFilter | null; - }) { - await this.authManager.ensureTargetAccess({ - organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.REGISTRY_READ, - }); - + async getPaginatedSchemaChecksForTarget( + target: Target, + args: { + first: number | null; + cursor: string | null; + transformNode: (check: SchemaCheck) => TransformedSchemaCheck; + filters: SchemaChecksFilter | null; + }, + ) { const paginatedResult = await this.storage.getPaginatedSchemaChecksForTarget({ - targetId: args.targetId, + targetId: target.id, first: args.first, cursor: args.cursor, transformNode: node => args.transformNode(node), @@ -758,26 +691,24 @@ export class SchemaManager { return paginatedResult; } - async findSchemaCheck(args: { - targetId: string; - projectId: string; - organizationId: string; - schemaCheckId: string; - }) { - this.logger.debug('Find schema check (args=%o)', args); - await this.authManager.ensureTargetAccess({ - targetId: args.targetId, - projectId: args.projectId, - organizationId: args.organizationId, - scope: TargetAccessScope.REGISTRY_READ, - }); + async findSchemaCheckForTarget(target: Target, schemaCheckId: string) { + this.logger.debug( + 'Find schema check (targetId=%s, schemaCheckId=%s)', + target.id, + schemaCheckId, + ); const schemaCheck = await this.storage.findSchemaCheck({ - schemaCheckId: args.schemaCheckId, + targetId: target.id, + schemaCheckId, }); if (schemaCheck == null) { - this.logger.debug('Schema check not found (args=%o)', args); + this.logger.debug( + 'Schema check not found (targetId=%s, schemaCheckId=%s)', + target.id, + schemaCheckId, + ); return null; } @@ -883,23 +814,34 @@ export class SchemaManager { schemaCheck: SchemaCheck & { selector: { organizationId: string; + projectId: string; }; }, ) { - if (!this.authManager.isUser()) { + if (!this.session.getViewer()) { // TODO: support approving a schema check via non web app user? return false; } - const user = await this.authManager.getCurrentUser(); + const isViewer = this.session.isViewer(); + + if (!isViewer) { + return false; + } - const scopes = await this.authManager.getMemberTargetScopes({ - userId: user.id, + const isAllowedToApproveFailedSchemaCheck = await this.session.canPerformAction({ + action: 'schemaCheck:approve', organizationId: schemaCheck.selector.organizationId, + params: { + organizationId: schemaCheck.selector.organizationId, + projectId: schemaCheck.selector.projectId, + targetId: schemaCheck.targetId, + serviceName: schemaCheck.serviceName ?? null, + }, }); - if (scopes.includes(TargetAccessScope.REGISTRY_WRITE)) { - return true; + if (isAllowedToApproveFailedSchemaCheck === false) { + return false; } return await this.getFailedSchemaCheckCanBeApproved(schemaCheck); @@ -918,18 +860,17 @@ export class SchemaManager { }) { this.logger.debug('Manually approve failed schema check (args=%o)', args); - await this.authManager.ensureTargetAccess({ - targetId: args.targetId, - projectId: args.projectId, - organizationId: args.organizationId, - scope: TargetAccessScope.REGISTRY_WRITE, - }); - - let [schemaCheck, viewer] = await Promise.all([ + let [schemaCheck, viewer, target] = await Promise.all([ this.storage.findSchemaCheck({ + targetId: args.targetId, schemaCheckId: args.schemaCheckId, }), - this.authManager.getCurrentUser(), + this.session.getViewer(), + this.storage.getTarget({ + organizationId: args.organizationId, + projectId: args.projectId, + targetId: args.targetId, + }), ]); if (schemaCheck == null || schemaCheck.targetId !== args.targetId) { @@ -940,6 +881,17 @@ export class SchemaManager { } as const; } + await this.session.assertPerformAction({ + action: 'schemaCheck:approve', + organizationId: args.organizationId, + params: { + organizationId: target.orgId, + projectId: target.projectId, + targetId: target.id, + serviceName: schemaCheck.serviceName ?? null, + }, + }); + if (schemaCheck.isSuccess) { this.logger.debug('Schema check is not failed (args=%o)', args); return { @@ -992,6 +944,7 @@ export class SchemaManager { } schemaCheck = await this.storage.approveFailedSchemaCheck({ + targetId: target.id, contracts: this.contracts, schemaCheckId: args.schemaCheckId, userId: viewer.id, @@ -1023,10 +976,7 @@ export class SchemaManager { } async getSchemaVersionByActionId(args: { actionId: string }) { - const [target, organization] = await Promise.all([ - this.targetManager.getTargetFromToken(), - this.organizationManager.getOrganizationFromToken(), - ]); + const target = await this.targetManager.getTargetFromToken(); this.logger.debug('Fetch schema version by action id. (args=%o)', { projectId: target.projectId, @@ -1034,11 +984,14 @@ export class SchemaManager { actionId: args.actionId, }); - await this.authManager.ensureTargetAccess({ - organizationId: organization.id, - projectId: target.projectId, - targetId: target.id, - scope: TargetAccessScope.REGISTRY_READ, + await this.session.assertPerformAction({ + action: 'schema:loadFromRegistry', + organizationId: target.orgId, + params: { + organizationId: target.orgId, + projectId: target.projectId, + targetId: target.id, + }, }); const record = await this.storage.getSchemaVersionByActionId({ @@ -1055,7 +1008,7 @@ export class SchemaManager { ...record, projectId: target.projectId, targetId: target.id, - organizationId: organization.id, + organizationId: target.orgId, }; } @@ -1069,10 +1022,10 @@ export class SchemaManager { this.logger.debug('Fetch version before version id. (args=%o)', args); const [organization, project] = await Promise.all([ - this.organizationManager.getOrganization({ + this.storage.getOrganization({ organizationId: args.organization, }), - this.projectManager.getProject({ + this.storage.getProject({ organizationId: args.organization, projectId: args.project, }), @@ -1179,13 +1132,7 @@ export class SchemaManager { }); const possibleVersions = await Promise.all( - targets.map(t => - this.getMaybeLatestValidVersion({ - organizationId: project.orgId, - projectId: project.id, - targetId: t.id, - }), - ), + targets.map(target => this.getMaybeLatestValidVersion(target)), ); const versions = possibleVersions.filter((v): v is SchemaVersion => !!v); diff --git a/packages/services/api/src/modules/schema/providers/schema-publisher.ts b/packages/services/api/src/modules/schema/providers/schema-publisher.ts index 9d3e5df394..9bcf531579 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -20,8 +20,7 @@ import { createPeriod } from '../../../shared/helpers'; import { isGitHubRepositoryString } from '../../../shared/is-github-repository-string'; import { bolderize } from '../../../shared/markdown'; import { AlertsManager } from '../../alerts/providers/alerts-manager'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { TargetAccessScope } from '../../auth/providers/target-access'; +import { Session } from '../../auth/lib/authz'; import { GitHubIntegrationManager, type GitHubCheckRun, @@ -142,7 +141,7 @@ export class SchemaPublisher { constructor( logger: Logger, - private authManager: AuthManager, + private session: Session, private storage: Storage, private schemaManager: SchemaManager, private targetManager: TargetManager, @@ -288,55 +287,47 @@ export class SchemaPublisher { async check(input: CheckInput) { this.logger.info('Checking schema (input=%o)', lodash.omit(input, ['sdl'])); - await this.authManager.ensureTargetAccess({ - targetId: input.targetId, - projectId: input.projectId, + await this.session.assertPerformAction({ + action: 'schemaCheck:create', organizationId: input.organizationId, - scope: TargetAccessScope.REGISTRY_READ, - }); - - const [ - target, - project, - organization, - latestVersion, - latestComposableVersion, - latestSchemaVersion, - latestComposableSchemaVersion, - ] = await Promise.all([ - this.targetManager.getTarget({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }), - this.projectManager.getProject({ - organizationId: input.organizationId, - projectId: input.projectId, - }), - this.organizationManager.getOrganization({ - organizationId: input.organizationId, - }), - this.schemaManager.getLatestSchemas({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }), - this.schemaManager.getLatestSchemas({ + params: { organizationId: input.organizationId, projectId: input.projectId, targetId: input.targetId, - onlyComposable: true, - }), - this.schemaManager.getMaybeLatestVersion({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }), - this.schemaManager.getMaybeLatestValidVersion({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }), + serviceName: input.service ?? null, + }, + }); + + const [target, project, organization, latestVersion, latestComposableVersion] = + await Promise.all([ + this.storage.getTarget({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }), + this.storage.getProject({ + organizationId: input.organizationId, + projectId: input.projectId, + }), + this.storage.getOrganization({ + organizationId: input.organizationId, + }), + this.storage.getLatestSchemas({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }), + this.storage.getLatestSchemas({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + onlyComposable: true, + }), + ]); + + const [latestSchemaVersion, latestComposableSchemaVersion] = await Promise.all([ + this.schemaManager.getMaybeLatestVersion(target), + this.schemaManager.getMaybeLatestValidVersion(target), ]); const projectModelVersion = project.legacyRegistryModel ? 'legacy' : 'modern'; @@ -463,11 +454,7 @@ export class SchemaPublisher { step: 'checkingSchema', }); - const baseSchema = await this.schemaManager.getBaseSchema({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }); + const baseSchema = await this.schemaManager.getBaseSchemaForTarget(target); const selector = { organizationId: input.organizationId, @@ -1019,14 +1006,17 @@ export class SchemaPublisher { input.targetId, ); - const token = this.authManager.ensureApiToken(); + const selector = this.session.getLegacySelector(); + + const target = await this.storage.getTarget({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }); + const [contracts, latestVersion] = await Promise.all([ this.contracts.getActiveContractsByTargetId({ targetId: input.targetId }), - this.schemaManager.getMaybeLatestVersion({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }), + this.schemaManager.getMaybeLatestVersion(target), ]); const checksum = createHash('md5') @@ -1047,7 +1037,7 @@ export class SchemaPublisher { latestVersionId: latestVersion?.id, }), ) - .update(token) + .update(selector.token) .digest('base64'); this.logger.debug( @@ -1074,11 +1064,15 @@ export class SchemaPublisher { signal, }, async () => { - await this.authManager.ensureTargetAccess({ - targetId: input.targetId, - projectId: input.projectId, + await this.session.assertPerformAction({ + action: 'schemaVersion:publish', organizationId: input.organizationId, - scope: TargetAccessScope.REGISTRY_WRITE, + params: { + targetId: input.targetId, + projectId: input.projectId, + organizationId: input.organizationId, + serviceName: input.service ?? null, + }, }); return this.distributedCache.wrap({ key: `schema:publish:${checksum}`, @@ -1189,54 +1183,52 @@ export class SchemaPublisher { signal, }, async () => { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'schemaVersion:deleteService', organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.target.id, - scope: TargetAccessScope.REGISTRY_WRITE, - }); - const [ - project, - organization, - latestVersion, - latestComposableVersion, - baseSchema, - latestSchemaVersion, - latestComposableSchemaVersion, - ] = await Promise.all([ - this.projectManager.getProject({ - organizationId: input.organizationId, - projectId: input.projectId, - }), - this.organizationManager.getOrganization({ - organizationId: input.organizationId, - }), - this.schemaManager.getLatestSchemas({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.target.id, - }), - this.schemaManager.getLatestSchemas({ + params: { organizationId: input.organizationId, projectId: input.projectId, targetId: input.target.id, - onlyComposable: true, - }), - this.schemaManager.getBaseSchema({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.target.id, - }), - this.schemaManager.getMaybeLatestVersion({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.target.id, - }), - this.schemaManager.getMaybeLatestValidVersion({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.target.id, - }), + serviceName: input.serviceName, + }, + }); + + const [organization, project, target, latestVersion, latestComposableVersion, baseSchema] = + await Promise.all([ + this.storage.getOrganization({ + organizationId: input.organizationId, + }), + this.storage.getProject({ + organizationId: input.organizationId, + projectId: input.projectId, + }), + this.storage.getTarget({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.target.id, + }), + this.storage.getLatestSchemas({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.target.id, + }), + this.storage.getLatestSchemas({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.target.id, + onlyComposable: true, + }), + this.storage.getBaseSchema({ + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.target.id, + }), + ]); + + const [latestSchemaVersion, latestComposableSchemaVersion] = await Promise.all([ + this.schemaManager.getMaybeLatestVersion(target), + this.schemaManager.getMaybeLatestValidVersion(target), ]); const compareToPreviousComposableVersion = shouldUseLatestComposableVersion( @@ -1498,54 +1490,41 @@ export class SchemaPublisher { metadata: !!input.metadata, }); - const [ - organization, - project, - target, - latestVersion, - latestComposable, - baseSchema, - latestSchemaVersion, - latestComposableSchemaVersion, - ] = await Promise.all([ - this.organizationManager.getOrganization({ - organizationId: organizationId, - }), - this.projectManager.getProject({ - organizationId: organizationId, - projectId: projectId, - }), - this.targetManager.getTarget({ - organizationId: organizationId, - projectId: projectId, - targetId: targetId, - }), - this.schemaManager.getLatestSchemas({ - organizationId: organizationId, - projectId: projectId, - targetId: targetId, - }), - this.schemaManager.getLatestSchemas({ - organizationId: organizationId, - projectId: projectId, - targetId: targetId, - onlyComposable: true, - }), - this.schemaManager.getBaseSchema({ - organizationId: organizationId, - projectId: projectId, - targetId: targetId, - }), - this.schemaManager.getMaybeLatestVersion({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }), - this.schemaManager.getMaybeLatestValidVersion({ - organizationId: input.organizationId, - projectId: input.projectId, - targetId: input.targetId, - }), + const [organization, project, target, latestVersion, latestComposable, baseSchema] = + await Promise.all([ + this.storage.getOrganization({ + organizationId: organizationId, + }), + this.storage.getProject({ + organizationId: organizationId, + projectId: projectId, + }), + this.storage.getTarget({ + organizationId: organizationId, + projectId: projectId, + targetId: targetId, + }), + this.storage.getLatestSchemas({ + organizationId: organizationId, + projectId: projectId, + targetId: targetId, + }), + this.storage.getLatestSchemas({ + organizationId: organizationId, + projectId: projectId, + targetId: targetId, + onlyComposable: true, + }), + this.storage.getBaseSchema({ + organizationId: organizationId, + projectId: projectId, + targetId: targetId, + }), + ]); + + const [latestSchemaVersion, latestComposableSchemaVersion] = await Promise.all([ + this.schemaManager.getMaybeLatestVersion(target), + this.schemaManager.getMaybeLatestValidVersion(target), ]); const modelVersion = project.legacyRegistryModel ? 'legacy' : 'modern'; diff --git a/packages/services/api/src/modules/schema/providers/schema-version-helper.ts b/packages/services/api/src/modules/schema/providers/schema-version-helper.ts index cfcbf330d1..e628ab893f 100644 --- a/packages/services/api/src/modules/schema/providers/schema-version-helper.ts +++ b/packages/services/api/src/modules/schema/providers/schema-version-helper.ts @@ -41,11 +41,8 @@ export class SchemaVersionHelper { @cache(version => version.id) private async composeSchemaVersion(schemaVersion: SchemaVersion) { const [schemas, project, organization] = await Promise.all([ - this.schemaManager.getMaybeSchemasOfVersion({ + this.storage.getSchemasOfVersion({ versionId: schemaVersion.id, - organizationId: schemaVersion.organizationId, - projectId: schemaVersion.projectId, - targetId: schemaVersion.targetId, }), this.projectManager.getProject({ organizationId: schemaVersion.organizationId, @@ -155,11 +152,8 @@ export class SchemaVersionHelper { } if (schemaVersion.hasPersistedSchemaChanges) { - const changes = await this.schemaManager.getSchemaChangesForVersion({ - organizationId: schemaVersion.organizationId, - projectId: schemaVersion.projectId, - targetId: schemaVersion.targetId, - version: schemaVersion.id, + const changes = await this.storage.getSchemaChangesForVersion({ + versionId: schemaVersion.id, }); const safeChanges: Array = []; @@ -189,16 +183,10 @@ export class SchemaVersionHelper { const incomingSdl = await this.getCompositeSchemaSdl(schemaVersion); const [schemaBefore, schemasAfter] = await Promise.all([ - this.schemaManager.getMaybeSchemasOfVersion({ - organizationId: schemaVersion.organizationId, - projectId: schemaVersion.projectId, - targetId: schemaVersion.targetId, + this.storage.getSchemasOfVersion({ versionId: schemaVersion.id, }), - this.schemaManager.getMaybeSchemasOfVersion({ - organizationId: schemaVersion.organizationId, - projectId: schemaVersion.projectId, - targetId: schemaVersion.targetId, + this.storage.getSchemasOfVersion({ versionId: previousVersion.id, }), ]); diff --git a/packages/services/api/src/modules/schema/resolvers/Mutation/schemaDelete.ts b/packages/services/api/src/modules/schema/resolvers/Mutation/schemaDelete.ts index 75cb2a6608..3bc5afee1c 100644 --- a/packages/services/api/src/modules/schema/resolvers/Mutation/schemaDelete.ts +++ b/packages/services/api/src/modules/schema/resolvers/Mutation/schemaDelete.ts @@ -1,6 +1,6 @@ import { createHash } from 'node:crypto'; import stringify from 'fast-json-stable-stringify'; -import { AuthManager } from '../../../auth/providers/auth-manager'; +import { Session } from '../../../auth/lib/authz'; import { OrganizationManager } from '../../../organization/providers/organization-manager'; import { ProjectManager } from '../../../project/providers/project-manager'; import { TargetManager } from '../../../target/providers/target-manager'; @@ -18,7 +18,7 @@ export const schemaDelete: NonNullable = asyn injector.get(TargetManager).getTargetFromToken(), ]); - const token = injector.get(AuthManager).ensureApiToken(); + const token = injector.get(Session).getLegacySelector(); const checksum = createHash('md5') .update( @@ -27,7 +27,7 @@ export const schemaDelete: NonNullable = asyn serviceName: input.serviceName.toLowerCase(), }), ) - .update(token) + .update(token.token) .digest('base64'); const result = await injector.get(SchemaPublisher).delete( diff --git a/packages/services/api/src/modules/schema/resolvers/Query/latestValidVersion.ts b/packages/services/api/src/modules/schema/resolvers/Query/latestValidVersion.ts index ae242d966e..033c8d34d4 100644 --- a/packages/services/api/src/modules/schema/resolvers/Query/latestValidVersion.ts +++ b/packages/services/api/src/modules/schema/resolvers/Query/latestValidVersion.ts @@ -9,9 +9,5 @@ export const latestValidVersion: NonNullable { const target = await injector.get(TargetManager).getTargetFromToken(); - return injector.get(SchemaManager).getMaybeLatestValidVersion({ - organizationId: target.orgId, - projectId: target.projectId, - targetId: target.id, - }); + return injector.get(SchemaManager).getMaybeLatestValidVersion(target); }; diff --git a/packages/services/api/src/modules/schema/resolvers/Query/latestVersion.ts b/packages/services/api/src/modules/schema/resolvers/Query/latestVersion.ts index 5b590bae9d..04f05d845e 100644 --- a/packages/services/api/src/modules/schema/resolvers/Query/latestVersion.ts +++ b/packages/services/api/src/modules/schema/resolvers/Query/latestVersion.ts @@ -8,10 +8,5 @@ export const latestVersion: NonNullable = async { injector }, ) => { const target = await injector.get(TargetManager).getTargetFromToken(); - - return injector.get(SchemaManager).getMaybeLatestVersion({ - organizationId: target.orgId, - projectId: target.projectId, - targetId: target.id, - }); + return injector.get(SchemaManager).getMaybeLatestVersion(target); }; diff --git a/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts b/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts index 52de291860..113f43de9a 100644 --- a/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts +++ b/packages/services/api/src/modules/schema/resolvers/SchemaVersion.ts @@ -64,12 +64,7 @@ export const SchemaVersion: SchemaVersionResolvers = { }; }, schemas: (version, _, { injector }) => { - return injector.get(SchemaManager).getMaybeSchemasOfVersion({ - versionId: version.id, - organizationId: version.organizationId, - projectId: version.projectId, - targetId: version.targetId, - }); + return injector.get(SchemaManager).getMaybeSchemasOfVersion(version); }, schemaCompositionErrors: async (version, _, { injector }) => { return injector.get(SchemaVersionHelper).getSchemaCompositionErrors(version); diff --git a/packages/services/api/src/modules/schema/resolvers/Target.ts b/packages/services/api/src/modules/schema/resolvers/Target.ts index fee4aae61d..40c2d1bc38 100644 --- a/packages/services/api/src/modules/schema/resolvers/Target.ts +++ b/packages/services/api/src/modules/schema/resolvers/Target.ts @@ -50,40 +50,19 @@ export const Target: Pick< }; }, latestSchemaVersion: (target, _, { injector }) => { - return injector.get(SchemaManager).getMaybeLatestVersion({ - targetId: target.id, - projectId: target.projectId, - organizationId: target.orgId, - }); + return injector.get(SchemaManager).getMaybeLatestVersion(target); }, latestValidSchemaVersion: async (target, __, { injector }) => { - return injector.get(SchemaManager).getMaybeLatestValidVersion({ - organizationId: target.orgId, - projectId: target.projectId, - targetId: target.id, - }); + return injector.get(SchemaManager).getMaybeLatestValidVersion(target); }, baseSchema: (target, _, { injector }) => { - return injector.get(SchemaManager).getBaseSchema({ - targetId: target.id, - projectId: target.projectId, - organizationId: target.orgId, - }); + return injector.get(SchemaManager).getBaseSchemaForTarget(target); }, hasSchema: (target, _, { injector }) => { - return injector.get(SchemaManager).hasSchema({ - targetId: target.id, - projectId: target.projectId, - organizationId: target.orgId, - }); + return injector.get(SchemaManager).hasSchema(target); }, schemaCheck: async (target, args, { injector }) => { - const schemaCheck = await injector.get(SchemaManager).findSchemaCheck({ - targetId: target.id, - projectId: target.projectId, - organizationId: target.orgId, - schemaCheckId: args.id, - }); + const schemaCheck = await injector.get(SchemaManager).findSchemaCheckForTarget(target, args.id); if (schemaCheck == null) { return null; @@ -98,10 +77,7 @@ export const Target: Pick< ); }, schemaChecks: async (target, args, { injector }) => { - const result = await injector.get(SchemaManager).getPaginatedSchemaChecksForTarget({ - targetId: target.id, - projectId: target.projectId, - organizationId: target.orgId, + const result = await injector.get(SchemaManager).getPaginatedSchemaChecksForTarget(target, { first: args.first ?? null, cursor: args.after ?? null, filters: args.filters ?? null, diff --git a/packages/services/api/src/modules/shared/providers/activity-manager.ts b/packages/services/api/src/modules/shared/providers/activity-manager.ts index 92329e17c9..20b4478a28 100644 --- a/packages/services/api/src/modules/shared/providers/activity-manager.ts +++ b/packages/services/api/src/modules/shared/providers/activity-manager.ts @@ -1,5 +1,5 @@ import { Injectable, Scope } from 'graphql-modules'; -import { AuthManager } from '../../auth/providers/auth-manager'; +import { Session } from '../../auth/lib/authz'; import { Logger } from '../../shared/providers/logger'; import { Storage } from '../../shared/providers/storage'; @@ -12,7 +12,7 @@ export class ActivityManager { constructor( logger: Logger, - private authManager: AuthManager, + private session: Session, private storage: Storage, ) { this.logger = logger.child({ @@ -24,7 +24,7 @@ export class ActivityManager { try { this.logger.debug('Creating an activity'); - const user = activity.user ? activity.user.id : (await this.authManager.getCurrentUser()).id; + const user = activity.user ? activity.user.id : (await this.session.getViewer()).id; await this.storage.createActivity({ organizationId: activity.selector.organizationId, diff --git a/packages/services/api/src/modules/shared/providers/logger.ts b/packages/services/api/src/modules/shared/providers/logger.ts index b7c526b8dc..65bb8254f8 100644 --- a/packages/services/api/src/modules/shared/providers/logger.ts +++ b/packages/services/api/src/modules/shared/providers/logger.ts @@ -18,3 +18,15 @@ export class Logger { debug: LogFn = notImplemented('debug'); child: (bindings: Record) => Logger = notImplemented('child'); } + +function noop() {} + +export class NoopLogger extends Logger { + info = noop; + warn = noop; + error = noop; + fatal = noop; + trace = noop; + debug = noop; + child = () => this; +} diff --git a/packages/services/api/src/modules/shared/providers/storage.ts b/packages/services/api/src/modules/shared/providers/storage.ts index fbc6e0da43..9786c4eb57 100644 --- a/packages/services/api/src/modules/shared/providers/storage.ts +++ b/packages/services/api/src/modules/shared/providers/storage.ts @@ -447,18 +447,6 @@ export interface Storage { }>; getSchemasOfVersion(_: { versionId: string; includeMetadata?: boolean }): Promise; getSchemaByNameOfVersion(_: { versionId: string; serviceName: string }): Promise; - getSchemasOfPreviousVersion( - _: { - versionId: string; - onlyComposable: boolean; - } & TargetSelector, - ): Promise< - | { - schemas: readonly Schema[]; - id?: string; - } - | never - >; getServiceSchemaOfVersion(args: { schemaVersionId: string; serviceName: string; @@ -816,7 +804,7 @@ export interface Storage { /** * Find schema check for a given ID and target. */ - findSchemaCheck(input: { schemaCheckId: string }): Promise; + findSchemaCheck(input: { targetId: string; schemaCheckId: string }): Promise; /** * Retrieve paginated schema checks for a given target. */ @@ -850,6 +838,7 @@ export interface Storage { * Overwrite and approve a schema check. */ approveFailedSchemaCheck(input: { + targetId: string; /** We inject this here as a dirty way to avoid chicken egg issues :) */ contracts: Contracts; schemaCheckId: string; diff --git a/packages/services/api/src/modules/support/providers/support-manager.ts b/packages/services/api/src/modules/support/providers/support-manager.ts index d1ca7acf37..bbe44ece34 100644 --- a/packages/services/api/src/modules/support/providers/support-manager.ts +++ b/packages/services/api/src/modules/support/providers/support-manager.ts @@ -3,8 +3,7 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import { z } from 'zod'; import { Organization, SupportTicketPriority, SupportTicketStatus } from '../../../shared/entities'; import { atomic } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { OrganizationAccessScope } from '../../auth/providers/scopes'; +import { Session } from '../../auth/lib/authz'; import { HttpClient } from '../../shared/providers/http-client'; import { Logger } from '../../shared/providers/logger'; import { Storage } from '../../shared/providers/storage'; @@ -134,9 +133,9 @@ export class SupportManager { @Inject(SUPPORT_MODULE_CONFIG) private config: SupportConfig, logger: Logger, private httpClient: HttpClient, - private authManager: AuthManager, private organizationManager: OrganizationManager, private storage: Storage, + private session: Session, ) { this.logger = logger.child({ service: 'SupportManager' }); } @@ -376,9 +375,12 @@ export class SupportManager { async getTickets(organizationId: string) { this.logger.info('Fetching support tickets (id: %s)', organizationId); - await this.authManager.ensureOrganizationAccess({ - organizationId: organizationId, - scope: OrganizationAccessScope.READ, + await this.session.assertPerformAction({ + organizationId, + action: 'support:manageTickets', + params: { + organizationId, + }, }); const internalOrganizationId = await this.ensureZendeskOrganizationId(organizationId); @@ -417,9 +419,12 @@ export class SupportManager { organizationId, ticketId, ); - await this.authManager.ensureOrganizationAccess({ - organizationId: organizationId, - scope: OrganizationAccessScope.READ, + await this.session.assertPerformAction({ + organizationId, + action: 'support:manageTickets', + params: { + organizationId, + }, }); const zendeskOrganizationId = await this.ensureZendeskOrganizationId(organizationId); @@ -515,12 +520,14 @@ export class SupportManager { }; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ organizationId: input.organizationId, - scope: OrganizationAccessScope.READ, + action: 'support:manageTickets', + params: { + organizationId: input.organizationId, + }, }); - const currentUser = await this.authManager.getCurrentUser(); - + const currentUser = await this.session.getViewer(); const internalOrganizationId = await this.ensureZendeskOrganizationId(input.organizationId); const internalUserId = await this.ensureZendeskUserId({ userId: currentUser.id, @@ -603,11 +610,14 @@ export class SupportManager { }; } - await this.authManager.ensureOrganizationAccess({ + await this.session.assertPerformAction({ organizationId: input.organizationId, - scope: OrganizationAccessScope.READ, + action: 'support:manageTickets', + params: { + organizationId: input.organizationId, + }, }); - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const internalUserId = await this.ensureZendeskUserId({ userId: currentUser.id, organizationId: input.organizationId, diff --git a/packages/services/api/src/modules/target/providers/target-manager.ts b/packages/services/api/src/modules/target/providers/target-manager.ts index e348631d98..edfa3037d4 100644 --- a/packages/services/api/src/modules/target/providers/target-manager.ts +++ b/packages/services/api/src/modules/target/providers/target-manager.ts @@ -2,9 +2,7 @@ import { Injectable, Scope } from 'graphql-modules'; import * as zod from 'zod'; import type { Target, TargetSettings } from '../../../shared/entities'; import { share } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; -import { ProjectAccessScope } from '../../auth/providers/project-access'; -import { TargetAccessScope } from '../../auth/providers/target-access'; +import { Session } from '../../auth/lib/authz'; import { ActivityManager } from '../../shared/providers/activity-manager'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; @@ -29,7 +27,7 @@ export class TargetManager { logger: Logger, private storage: Storage, private tokenStorage: TokenStorage, - private authManager: AuthManager, + private session: Session, private activityManager: ActivityManager, private idTranslator: IdTranslator, ) { @@ -58,10 +56,13 @@ export class TargetManager { project, organization, ); - await this.authManager.ensureProjectAccess({ - projectId: project, + await this.session.assertPerformAction({ + action: 'target:create', organizationId: organization, - scope: ProjectAccessScope.READ, + params: { + organizationId: organization, + projectId: project, + }, }); if (reservedSlugs.includes(slug)) { @@ -103,11 +104,14 @@ export class TargetManager { project, organization, ); - await this.authManager.ensureTargetAccess({ - projectId: project, + await this.session.assertPerformAction({ + action: 'target:delete', organizationId: organization, - targetId: target, - scope: TargetAccessScope.DELETE, + params: { + organizationId: organization, + projectId: project, + targetId: target, + }, }); const deletedTarget = await this.storage.deleteTarget({ @@ -134,40 +138,42 @@ export class TargetManager { async getTargets(selector: ProjectSelector): Promise { this.logger.debug('Fetching targets (selector=%o)', selector); - await this.authManager.ensureProjectAccess({ - ...selector, - scope: ProjectAccessScope.READ, + await this.session.assertPerformAction({ + action: 'project:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); + return this.storage.getTargets(selector); } - async getTarget(selector: TargetSelector, scope = TargetAccessScope.READ): Promise { + async getTarget(selector: TargetSelector): Promise { this.logger.debug('Fetching target (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope, + await this.session.assertPerformAction({ + action: 'project:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + }, }); return this.storage.getTarget(selector); } getTargetIdByToken: () => Promise = share(async () => { - const token = this.authManager.ensureApiToken(); - const { target } = await this.tokenStorage.getToken({ token }); + const selector = this.session.getLegacySelector(); + const { target } = await this.tokenStorage.getToken({ token: selector.token }); return target; }); getTargetFromToken: () => Promise = share(async () => { - const token = this.authManager.ensureApiToken(); + const selector = this.session.getLegacySelector(); const { target, project, organization } = await this.tokenStorage.getToken({ - token, - }); - - await this.authManager.ensureTargetAccess({ - organizationId: organization, - projectId: project, - targetId: target, - scope: TargetAccessScope.READ, + token: selector.token, }); return this.storage.getTarget({ @@ -179,9 +185,14 @@ export class TargetManager { async getTargetSettings(selector: TargetSelector): Promise { this.logger.debug('Fetching target settings (selector=%o)', selector); - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'target:modifySettings', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + targetId: selector.targetId, + }, }); return this.storage.getTargetSettings(selector); @@ -193,9 +204,14 @@ export class TargetManager { } & TargetSelector, ): Promise { this.logger.debug('Setting target validation (input=%o)', input); - await this.authManager.ensureTargetAccess({ - ...input, - scope: TargetAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'target:modifySettings', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }, }); await this.storage.completeGetStartedStep({ @@ -210,9 +226,14 @@ export class TargetManager { input: Omit & TargetSelector, ): Promise { this.logger.debug('Updating target validation settings (input=%o)', input); - await this.authManager.ensureTargetAccess({ - ...input, - scope: TargetAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'target:modifySettings', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }, }); if (input.targets.length === 0) { @@ -238,11 +259,17 @@ export class TargetManager { > { const { slug, organizationId: organization, projectId: project, targetId: target } = input; this.logger.info('Updating a target slug (input=%o)', input); - await this.authManager.ensureTargetAccess({ - ...input, - scope: TargetAccessScope.SETTINGS, + await this.session.assertPerformAction({ + action: 'target:modifySettings', + organizationId: input.organizationId, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }, }); - const user = await this.authManager.getCurrentUser(); + + const user = await this.session.getViewer(); if (reservedSlugs.includes(slug)) { return { @@ -282,11 +309,14 @@ export class TargetManager { targetId: string; graphqlEndpointUrl: string | null; }) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'target:modifySettings', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.SETTINGS, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + targetId: args.targetId, + }, }); const graphqlEndpointUrl = TargetGraphQLEndpointUrlModel.safeParse(args.graphqlEndpointUrl); @@ -347,11 +377,14 @@ export class TargetManager { targetId: string; nativeComposition: boolean; }) { - await this.authManager.ensureTargetAccess({ + await this.session.assertPerformAction({ + action: 'target:modifySettings', organizationId: args.organizationId, - projectId: args.projectId, - targetId: args.targetId, - scope: TargetAccessScope.SETTINGS, + params: { + organizationId: args.organizationId, + projectId: args.projectId, + targetId: args.targetId, + }, }); this.logger.info( diff --git a/packages/services/api/src/modules/token/providers/token-manager.ts b/packages/services/api/src/modules/token/providers/token-manager.ts index e4cf0a5cde..5ffb6210c6 100644 --- a/packages/services/api/src/modules/token/providers/token-manager.ts +++ b/packages/services/api/src/modules/token/providers/token-manager.ts @@ -2,7 +2,7 @@ import { Injectable, Scope } from 'graphql-modules'; import type { Token } from '../../../shared/entities'; import { HiveError } from '../../../shared/errors'; import { diffArrays, pushIfMissing } from '../../../shared/helpers'; -import { AuthManager } from '../../auth/providers/auth-manager'; +import { Session } from '../../auth/lib/authz'; import { OrganizationAccessScope } from '../../auth/providers/organization-access'; import { ProjectAccessScope } from '../../auth/providers/project-access'; import { TargetAccessScope } from '../../auth/providers/target-access'; @@ -30,7 +30,7 @@ export class TokenManager { private logger: Logger; constructor( - private authManager: AuthManager, + private session: Session, private tokenStorage: TokenStorage, private storage: Storage, logger: Logger, @@ -41,16 +41,19 @@ export class TokenManager { } async createToken(input: CreateTokenInput): Promise { - await this.authManager.ensureTargetAccess({ - projectId: input.projectId, + await this.session.assertPerformAction({ + action: 'targetAccessToken:create', organizationId: input.organizationId, - targetId: input.targetId, - scope: TargetAccessScope.TOKENS_WRITE, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }, }); const scopes = [...input.organizationScopes, ...input.projectScopes, ...input.targetScopes]; - const currentUser = await this.authManager.getCurrentUser(); + const currentUser = await this.session.getViewer(); const currentMember = await this.storage.getOrganizationMember({ organizationId: input.organizationId, userId: currentUser.id, @@ -94,26 +97,34 @@ export class TokenManager { projectId: string; targetId: string; }): Promise { - await this.authManager.ensureTargetAccess({ - projectId: input.projectId, + await this.session.assertPerformAction({ + action: 'targetAccessToken:delete', organizationId: input.organizationId, - targetId: input.targetId, - scope: TargetAccessScope.TOKENS_WRITE, + params: { + organizationId: input.organizationId, + projectId: input.projectId, + targetId: input.targetId, + }, }); return this.tokenStorage.deleteTokens(input); } async getTokens(selector: TargetSelector): Promise { - await this.authManager.ensureTargetAccess({ - ...selector, - scope: TargetAccessScope.TOKENS_READ, + await this.session.assertPerformAction({ + action: 'targetAccessToken:describe', + organizationId: selector.organizationId, + params: { + organizationId: selector.organizationId, + projectId: selector.projectId, + targetId: selector.targetId, + }, }); return this.tokenStorage.getTokens(selector); } async getCurrentToken(): Promise { - const token = this.authManager.ensureApiToken(); - return this.tokenStorage.getToken({ token }); + const token = this.session.getLegacySelector(); + return this.tokenStorage.getToken({ token: token.token }); } } diff --git a/packages/services/api/src/modules/token/resolvers/Query/tokenInfo.ts b/packages/services/api/src/modules/token/resolvers/Query/tokenInfo.ts index 4390841277..eab781b282 100644 --- a/packages/services/api/src/modules/token/resolvers/Query/tokenInfo.ts +++ b/packages/services/api/src/modules/token/resolvers/Query/tokenInfo.ts @@ -1,4 +1,4 @@ -import { AuthManager } from '../../../auth/providers/auth-manager'; +import { Session } from '../../../auth/lib/authz'; import { TokenManager } from '../../providers/token-manager'; import type { QueryResolvers } from './../../../../__generated__/types'; @@ -8,7 +8,7 @@ export const tokenInfo: NonNullable = async ( { injector }, ) => { try { - injector.get(AuthManager).ensureApiToken(); + injector.get(Session).getLegacySelector(); } catch (error) { return { __typename: 'TokenNotFoundError', diff --git a/packages/services/api/src/modules/usage-estimation/providers/usage-estimation.provider.ts b/packages/services/api/src/modules/usage-estimation/providers/usage-estimation.provider.ts index 5226ef3ef0..73c66859e9 100644 --- a/packages/services/api/src/modules/usage-estimation/providers/usage-estimation.provider.ts +++ b/packages/services/api/src/modules/usage-estimation/providers/usage-estimation.provider.ts @@ -1,13 +1,16 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import { traceFn } from '@hive/service-common'; -import type { UsageEstimatorApi, UsageEstimatorApiInput } from '@hive/usage-estimator'; +import type { UsageEstimatorApi } from '@hive/usage-estimator'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; +import { Session } from '../../auth/lib/authz'; +import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; import type { UsageEstimationServiceConfig } from './tokens'; import { USAGE_ESTIMATION_SERVICE_CONFIG } from './tokens'; @Injectable({ - scope: Scope.Singleton, + scope: Scope.Operation, + global: true, }) export class UsageEstimationProvider { private logger: Logger; @@ -17,6 +20,8 @@ export class UsageEstimationProvider { logger: Logger, @Inject(USAGE_ESTIMATION_SERVICE_CONFIG) usageEstimationConfig: UsageEstimationServiceConfig, + private idTranslator: IdTranslator, + private session: Session, ) { this.logger = logger.child({ service: 'UsageEstimationProvider' }); this.usageEstimator = usageEstimationConfig.endpoint @@ -39,9 +44,11 @@ export class UsageEstimationProvider { 'hive.usageEstimation.operations.estimated': result ?? 0, }), }) - async estimateOperationsForOrganization( - input: UsageEstimatorApiInput['estimateOperationsForOrganization'], - ): Promise { + async _estimateOperationsForOrganization(input: { + organizationId: string; + month: number; + year: number; + }): Promise { this.logger.debug('Estimation operations, input: %o', input); if (!this.usageEstimator) { @@ -50,8 +57,36 @@ export class UsageEstimationProvider { return null; } - const result = await this.usageEstimator.estimateOperationsForOrganization.query(input); + const result = await this.usageEstimator.estimateOperationsForOrganization.query({ + organizationId: input.organizationId, + year: input.year, + month: input.month, + }); return result.totalOperations; } + + async estimateOperationsForOrganization(input: { + organizationSlug: string; + month: number; + year: number; + }): Promise { + const organizationId = await this.idTranslator.translateOrganizationId({ + organizationSlug: input.organizationSlug, + }); + + await this.session.assertPerformAction({ + action: 'billing:describe', + organizationId, + params: { + organizationId, + }, + }); + + return await this._estimateOperationsForOrganization({ + organizationId, + year: input.year, + month: input.month, + }); + } } diff --git a/packages/services/api/src/modules/usage-estimation/resolvers/Query/usageEstimation.ts b/packages/services/api/src/modules/usage-estimation/resolvers/Query/usageEstimation.ts index 8540205aaa..8345c18da4 100644 --- a/packages/services/api/src/modules/usage-estimation/resolvers/Query/usageEstimation.ts +++ b/packages/services/api/src/modules/usage-estimation/resolvers/Query/usageEstimation.ts @@ -1,25 +1,14 @@ import { GraphQLError } from 'graphql'; -import { AuthManager } from '../../../auth/providers/auth-manager'; -import { IdTranslator } from '../../../shared/providers/id-translator'; import { UsageEstimationProvider } from '../../providers/usage-estimation.provider'; -import { OrganizationAccessScope, type QueryResolvers } from './../../../../__generated__/types'; +import { type QueryResolvers } from './../../../../__generated__/types'; export const usageEstimation: NonNullable = async ( _parent, args, { injector }, ) => { - const organizationId = await injector.get(IdTranslator).translateOrganizationId({ - organizationSlug: args.input.organizationSlug, - }); - - await injector.get(AuthManager).ensureOrganizationAccess({ - organizationId: organizationId, - scope: OrganizationAccessScope.SETTINGS, - }); - const result = await injector.get(UsageEstimationProvider).estimateOperationsForOrganization({ - organizationId: organizationId, + organizationSlug: args.input.organizationSlug, month: args.input.month, year: args.input.year, }); diff --git a/packages/services/server/package.json b/packages/services/server/package.json index 1f533a54fc..edc9269cea 100644 --- a/packages/services/server/package.json +++ b/packages/services/server/package.json @@ -10,7 +10,6 @@ }, "devDependencies": { "@envelop/core": "5.0.2", - "@envelop/generic-auth": "7.0.0", "@envelop/graphql-jit": "8.0.3", "@envelop/graphql-modules": "6.0.0", "@envelop/opentelemetry": "6.3.1", diff --git a/packages/services/server/src/graphql-handler.ts b/packages/services/server/src/graphql-handler.ts index 842ab57d82..270c395710 100644 --- a/packages/services/server/src/graphql-handler.ts +++ b/packages/services/server/src/graphql-handler.ts @@ -9,10 +9,15 @@ import { type DefinitionNode, type OperationDefinitionNode, } from 'graphql'; -import { createYoga, Plugin, useErrorHandler, useExecutionCancellation } from 'graphql-yoga'; +import { + createYoga, + Plugin, + useErrorHandler, + useExecutionCancellation, + useExtendContext, +} from 'graphql-yoga'; import hyperid from 'hyperid'; import { isGraphQLError } from '@envelop/core'; -import { useGenericAuth } from '@envelop/generic-auth'; import { useGraphQlJit } from '@envelop/graphql-jit'; import { useGraphQLModules } from '@envelop/graphql-modules'; import { useOpenTelemetry } from '@envelop/opentelemetry'; @@ -22,9 +27,9 @@ import { useResponseCache } from '@graphql-yoga/plugin-response-cache'; import { Registry, RegistryContext } from '@hive/api'; import { cleanRequestId, type TracingInstance } from '@hive/service-common'; import { runWithAsyncContext } from '@sentry/node'; +import { AuthN, Session } from '../../api/src/modules/auth/lib/authz'; import { asyncStorage } from './async-storage'; import type { HiveConfig, HivePersistedDocumentsConfig } from './environment'; -import { resolveUser, type SupertokensSession } from './supertokens'; import { useArmor } from './use-armor'; import { extractUserId, useSentryUser } from './use-sentry-user'; @@ -48,12 +53,13 @@ export interface GraphQLHandlerOptions { hivePersistedDocumentsConfig: HivePersistedDocumentsConfig; release: string; logger: FastifyBaseLogger; + authN: AuthN; } interface Context extends RegistryContext { req: FastifyRequest; reply: FastifyReply; - session: SupertokensSession | null; + session: Session; } const NoIntrospection: ValidationRule = (context: ValidationContext) => ({ @@ -156,13 +162,9 @@ export const graphqlHandler = (options: GraphQLHandlerOptions): RouteHandlerMeth } } }), - useGenericAuth({ - mode: 'resolve-only', - contextFieldName: 'session', - async resolveUserFn(ctx: Context) { - return resolveUser(ctx); - }, - }), + useExtendContext(async context => ({ + session: await options.authN.authenticate(context), + })), useHive({ debug: true, enabled: !!options.hiveConfig, @@ -270,7 +272,6 @@ export const graphqlHandler = (options: GraphQLHandlerOptions): RouteHandlerMeth reply, headers: req.headers, requestId, - session: null, }); }); diff --git a/packages/services/server/src/index.ts b/packages/services/server/src/index.ts index 9898e15137..8fa615b281 100644 --- a/packages/services/server/src/index.ts +++ b/packages/services/server/src/index.ts @@ -46,6 +46,9 @@ import { SeverityLevel, } from '@sentry/node'; import { createServerAdapter } from '@whatwg-node/server'; +import { AuthN } from '../../api/src/modules/auth/lib/authz'; +import { SuperTokensUserAuthNStrategy } from '../../api/src/modules/auth/lib/supertokens-strategy'; +import { TargetAccessTokenStrategy } from '../../api/src/modules/auth/lib/target-access-token-strategy'; import { createContext, internalApiRouter } from './api'; import { asyncStorage } from './async-storage'; import { env } from './environment'; @@ -362,10 +365,6 @@ export async function main() { } : null, encryptionSecret: env.encryptionSecret, - feedback: { - token: 'noop', - channel: 'noop', - }, schemaConfig: env.hiveServices.webApp ? { schemaPublishLink(input) { @@ -388,6 +387,21 @@ export async function main() { appDeploymentsEnabled: env.featureFlags.appDeploymentsEnabled, }); + const authN = new AuthN({ + strategies: [ + new SuperTokensUserAuthNStrategy({ + logger: server.log, + storage, + }), + new TargetAccessTokenStrategy({ + logger: server.log, + tokensConfig: { + endpoint: env.hiveServices.tokens.endpoint, + }, + }), + ], + }); + const graphqlPath = '/graphql'; const port = env.http.port; const signature = Math.random().toString(16).substr(2); @@ -405,6 +419,7 @@ export async function main() { hivePersistedDocumentsConfig: env.hivePersistedDocuments, tracing, logger: logger as any, + authN, }); server.route({ diff --git a/packages/services/server/src/supertokens.ts b/packages/services/server/src/supertokens.ts index 6d68818ac9..3b99ebe74d 100644 --- a/packages/services/server/src/supertokens.ts +++ b/packages/services/server/src/supertokens.ts @@ -1,4 +1,4 @@ -import type { FastifyBaseLogger, FastifyReply, FastifyRequest } from 'fastify'; +import type { FastifyBaseLogger } from 'fastify'; import { CryptoProvider } from 'packages/services/api/src/modules/shared/providers/crypto'; import { OverrideableBuilder } from 'supertokens-js-override/lib/build/index.js'; import supertokens from 'supertokens-node'; @@ -9,9 +9,8 @@ import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemail import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types'; import type { TypeInput } from 'supertokens-node/types'; import zod from 'zod'; -import { HiveError, type Storage } from '@hive/api'; +import { type Storage } from '@hive/api'; import type { EmailsApi } from '@hive/emails'; -import { captureException } from '@sentry/node'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; import { createInternalApiCaller } from './api'; import { env } from './environment'; @@ -367,74 +366,6 @@ export function initSupertokens(requirements: { supertokens.init(backendConfig(requirements)); } -export async function resolveUser(ctx: { req: FastifyRequest; reply: FastifyReply }) { - ctx.req.log.debug('Resolving user'); - let session: SessionNode.SessionContainer | undefined; - - try { - session = await SessionNode.getSession(ctx.req, ctx.reply, { - sessionRequired: false, - antiCsrfCheck: false, - checkDatabase: true, - }); - ctx.req.log.debug('Session resolution ended successfully'); - } catch (error) { - if (SessionNode.Error.isErrorFromSuperTokens(error)) { - // Check whether the email is already verified. - // If it is not then we need to redirect to the email verification page - which will trigger the email sending. - if (error.type === SessionNode.Error.INVALID_CLAIMS) { - throw new HiveError('Your account is not verified. Please verify your email address.', { - extensions: { - code: 'VERIFY_EMAIL', - }, - }); - } else if ( - error.type === SessionNode.Error.TRY_REFRESH_TOKEN || - error.type === SessionNode.Error.UNAUTHORISED - ) { - throw new HiveError('Invalid session', { - extensions: { - code: 'NEEDS_REFRESH', - }, - }); - } - } - - ctx.req.log.error(error, 'Error while resolving user'); - captureException(error); - - throw error; - } - - if (!session) { - ctx.req.log.debug('No session found'); - return null; - } - - const payload = session.getAccessTokenPayload(); - - if (!payload) { - ctx.req.log.error('No access token payload found'); - return null; - } - - const result = SuperTokenAccessTokenModel.safeParse(payload); - - if (result.success === false) { - ctx.req.log.error('SuperTokens session payload is invalid'); - ctx.req.log.debug('SuperTokens session payload: %s', JSON.stringify(payload)); - ctx.req.log.debug( - 'SuperTokens session parsing errors: %s', - JSON.stringify(result.error.flatten().fieldErrors), - ); - throw new HiveError(`Invalid access token provided`); - } - - ctx.req.log.debug('User resolved successfully'); - - return result.data; -} - type OidcIdLookupResponse = | { ok: true; diff --git a/packages/services/service-common/src/fastify.ts b/packages/services/service-common/src/fastify.ts index 6c25b5a998..49e4cc0305 100644 --- a/packages/services/service-common/src/fastify.ts +++ b/packages/services/service-common/src/fastify.ts @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/node'; import { useRequestLogging } from './request-logs'; import { useSentryErrorHandler } from './sentry'; -export type { FastifyBaseLogger, FastifyRequest } from 'fastify'; +export type { FastifyBaseLogger, FastifyRequest, FastifyReply } from 'fastify'; export async function createServer(options: { sentryErrorHandler: boolean; diff --git a/packages/services/service-common/src/index.ts b/packages/services/service-common/src/index.ts index 1151f8eb66..be8c9f0470 100644 --- a/packages/services/service-common/src/index.ts +++ b/packages/services/service-common/src/index.ts @@ -1,5 +1,5 @@ export { createServer } from './fastify'; -export type { FastifyBaseLogger as ServiceLogger, FastifyRequest } from './fastify'; +export type { FastifyBaseLogger as ServiceLogger, FastifyRequest, FastifyReply } from './fastify'; export * from './errors'; export * from './metrics'; export * from './heartbeats'; diff --git a/packages/services/storage/src/index.ts b/packages/services/storage/src/index.ts index 12f9de6e71..b3dbc613a2 100644 --- a/packages/services/storage/src/index.ts +++ b/packages/services/storage/src/index.ts @@ -49,7 +49,6 @@ import { projects, schema_log as schema_log_in_db, schema_policy_config, - schema_version_to_log, schema_versions, target_validation, targets, @@ -1899,7 +1898,7 @@ export async function createStorage( const uniqueSelectors = Array.from(uniqueSelectorsMap.values()); const rows = await pool - .many( + .any( sql`/* getTarget */ SELECT ${targetSQLFields} @@ -2169,13 +2168,13 @@ export async function createStorage( `, ); }, - async getMaybeLatestValidVersion({ targetId: target }) { + async getMaybeLatestValidVersion(args) { const version = await pool.maybeOne( sql`/* getMaybeLatestValidVersion */ SELECT ${schemaVersionSQLFields(sql`sv.`)} FROM schema_versions as sv - WHERE sv.target_id = ${target} AND sv.is_composable IS TRUE + WHERE sv.target_id = ${args.targetId} AND sv.is_composable IS TRUE ORDER BY sv.created_at DESC LIMIT 1 `, @@ -2201,14 +2200,14 @@ export async function createStorage( return SchemaVersionModel.parse(version); }, - async getLatestVersion({ projectId: project, targetId: target }) { + async getLatestVersion(args) { const version = await pool.maybeOne( sql`/* getLatestVersion */ SELECT ${schemaVersionSQLFields(sql`sv.`)} FROM schema_versions as sv LEFT JOIN targets as t ON (t.id = sv.target_id) - WHERE sv.target_id = ${target} AND t.project_id = ${project} + WHERE sv.target_id = ${args.targetId} AND t.project_id = ${args.projectId} ORDER BY sv.created_at DESC LIMIT 1 `, @@ -2217,14 +2216,14 @@ export async function createStorage( return SchemaVersionModel.parse(version); }, - async getMaybeLatestVersion({ projectId: project, targetId: target }) { + async getMaybeLatestVersion(args) { const version = await pool.maybeOne( sql`/* getMaybeLatestVersion */ SELECT ${schemaVersionSQLFields(sql`sv.`)} FROM schema_versions as sv LEFT JOIN targets as t ON (t.id = sv.target_id) - WHERE sv.target_id = ${target} AND t.project_id = ${project} + WHERE sv.target_id = ${args.targetId} AND t.project_id = ${args.projectId} ORDER BY sv.created_at DESC LIMIT 1 `, @@ -2390,40 +2389,6 @@ export async function createStorage( return result.rows.map(transformSchema); }, - async getSchemasOfPreviousVersion({ versionId: version, targetId: target, onlyComposable }) { - const results = await pool.query< - OverrideProp & - Pick & - Pick - >( - sql`/* getSchemasOfPreviousVersion */ - SELECT sl.*, lower(sl.service_name) as service_name, p.type, svl.version_id as version_id - FROM schema_version_to_log as svl - LEFT JOIN schema_log as sl ON (sl.id = svl.action_id) - LEFT JOIN projects as p ON (p.id = sl.project_id) - WHERE svl.version_id = ( - SELECT sv.id FROM schema_versions as sv WHERE sv.created_at < ( - SELECT svi.created_at FROM schema_versions as svi WHERE svi.id = ${version} - ) AND sv.target_id = ${target} AND ${ - onlyComposable ? sql`sv.is_composable IS TRUE` : true - } ORDER BY sv.created_at DESC LIMIT 1 - ) AND sl.action = 'PUSH' - ORDER BY sl.created_at DESC - `, - ); - - if (results.rowCount === 0) { - return { - schemas: [], - }; - } - - return { - schemas: results.rows.map(transformSchema), - id: results.rows[0].version_id, - }; - }, - async getServiceSchemaOfVersion(args) { const result = await pool.maybeOne< Pick< @@ -4162,6 +4127,7 @@ export async function createStorage( }); const check = await this.findSchemaCheck({ + targetId: args.targetId, schemaCheckId: result.id, }); @@ -4182,6 +4148,7 @@ export async function createStorage( LEFT JOIN "sdl_store" as s_supergraph ON s_supergraph."id" = c."supergraph_sdl_store_id" WHERE c."id" = ${args.schemaCheckId} + AND c."target_id" = ${args.targetId} `); if (result == null) { @@ -4192,6 +4159,7 @@ export async function createStorage( }, async approveFailedSchemaCheck(args) { const schemaCheck = await this.findSchemaCheck({ + targetId: args.targetId, schemaCheckId: args.schemaCheckId, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dff92167f8..e251bd82ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1135,9 +1135,6 @@ importers: '@envelop/core': specifier: 5.0.2 version: 5.0.2 - '@envelop/generic-auth': - specifier: 7.0.0 - version: 7.0.0(@envelop/core@5.0.2)(graphql@16.9.0) '@envelop/graphql-jit': specifier: 8.0.3 version: 8.0.3(@envelop/core@5.0.2)(graphql@16.9.0) @@ -3111,20 +3108,6 @@ packages: resolution: {integrity: sha512-tVL6OrMe6UjqLosiE+EH9uxh2TQC0469GwF4tE014ugRaDDKKVWwFwZe0TBMlcyHKh5MD4ZxktWo/1hqUxIuhw==} engines: {node: '>=18.0.0'} - '@envelop/extended-validation@4.0.0': - resolution: {integrity: sha512-pvJ/OL+C+lpNiiCXezHT+vP3PTq37MQicoOB1l5MdgOOZZWRAp0NDOgvEKcXUY7AWNpvNHgSE0QFSRfGwsfwFQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@envelop/core': ^5.0.0 - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@envelop/generic-auth@7.0.0': - resolution: {integrity: sha512-FxGzoSjIXJlv+aiVyhQ8oHoz41ol4gJe8KAzNX7FP3qrhldbrqcC5Q+J/VtNlo5jXYX0YuLHH6ehF80tQDZJ4Q==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@envelop/core': ^5.0.0 - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - '@envelop/graphql-jit@8.0.3': resolution: {integrity: sha512-IZnKc7dVOQV9jEi5s5RkG8fVKqc6Ss/mBN9PRt2iYFa9o6XkL/haPLJRfWFsS/CSJfFOQuzLyxYuALA8DaoOYw==} engines: {node: '>=18.0.0'} @@ -3854,6 +3837,7 @@ packages: '@fastify/vite@6.0.7': resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} + bundledDependencies: [] '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} @@ -16257,8 +16241,8 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0 - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16365,11 +16349,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.596.0': + '@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16408,6 +16392,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.0 transitivePeerDependencies: + - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.682.0(@aws-sdk/client-sts@3.682.0)': @@ -16541,11 +16526,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)': + '@aws-sdk/client-sts@3.596.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0 + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16584,7 +16569,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.0 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.682.0': @@ -16698,7 +16682,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/credential-provider-env': 3.587.0 '@aws-sdk/credential-provider-http': 3.596.0 '@aws-sdk/credential-provider-process': 3.587.0 @@ -16817,7 +16801,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.8 '@smithy/types': 3.6.0 @@ -16990,7 +16974,7 @@ snapshots: '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.596.0 + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.8 '@smithy/shared-ini-file-loader': 3.1.9 @@ -17846,21 +17830,6 @@ snapshots: '@envelop/types': 5.0.0 tslib: 2.8.0 - '@envelop/extended-validation@4.0.0(@envelop/core@5.0.2)(graphql@16.9.0)': - dependencies: - '@envelop/core': 5.0.2 - '@graphql-tools/utils': 10.5.5(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.8.0 - - '@envelop/generic-auth@7.0.0(@envelop/core@5.0.2)(graphql@16.9.0)': - dependencies: - '@envelop/core': 5.0.2 - '@envelop/extended-validation': 4.0.0(@envelop/core@5.0.2)(graphql@16.9.0) - '@graphql-tools/utils': 10.5.5(graphql@16.9.0) - graphql: 16.9.0 - tslib: 2.8.0 - '@envelop/graphql-jit@8.0.3(@envelop/core@5.0.2)(graphql@16.9.0)': dependencies: '@envelop/core': 5.0.2