diff --git a/.changeset/rotten-scissors-matter.md b/.changeset/rotten-scissors-matter.md new file mode 100644 index 0000000000..748a2040de --- /dev/null +++ b/.changeset/rotten-scissors-matter.md @@ -0,0 +1,6 @@ +--- +'hive': patch +'@graphql-hive/cli': patch +--- + +Restrict new service names to 64 characters, alphanumberic, \_, -, and / diff --git a/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap b/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap index e54376b50e..dc6a4230ec 100644 --- a/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap +++ b/integration-tests/tests/cli/__snapshots__/schema.spec.ts.snap @@ -91,6 +91,85 @@ stdout--------------------------------------------: ℹ Available at http://__URL__ `; +exports[`FEDERATION > check validates the service name > onlyNumbers 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +1 +stderr--------------------------------------------: +__NONE__ +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`FEDERATION > check validates the service name > specialCharacters 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +1 +stderr--------------------------------------------: +__NONE__ +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`FEDERATION > check validates the service name > success 1`] = ` +:::::::::::::::: CLI SUCCESS OUTPUT ::::::::::::::::: + +stdout--------------------------------------------: +✔ Schema registry is empty, nothing to compare your schema with. +View full report: +http://__URL__ +`; + +exports[`FEDERATION > publish validates the service name > onlyNumbers 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +2 +stderr--------------------------------------------: + › Error: Schema publish failed. [300] + › > See https://__URL__ for + › a complete list of error codes and recommended fixes. + › To disable this message set HIVE_NO_ERROR_TIP=1 +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`FEDERATION > publish validates the service name > specialCharacters 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +2 +stderr--------------------------------------------: + › Error: Schema publish failed. [300] + › > See https://__URL__ for + › a complete list of error codes and recommended fixes. + › To disable this message set HIVE_NO_ERROR_TIP=1 +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`FEDERATION > publish validates the service name > success 1`] = ` +:::::::::::::::: CLI SUCCESS OUTPUT ::::::::::::::::: + +stdout--------------------------------------------: +✔ Published initial schema. +ℹ Available at http://__URL__ +`; + exports[`FEDERATION > publishing invalid schema SDL provides meaningful feedback for the user. > schemaPublish 1`] = ` :::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: exitCode------------------------------------------: @@ -256,6 +335,85 @@ stdout--------------------------------------------: ℹ Available at http://__URL__ `; +exports[`SINGLE > check validates the service name > onlyNumbers 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +1 +stderr--------------------------------------------: +__NONE__ +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), underscore (_), or forward slash (/). + + +`; + +exports[`SINGLE > check validates the service name > specialCharacters 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +1 +stderr--------------------------------------------: +__NONE__ +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), underscore (_), or forward slash (/). + + +`; + +exports[`SINGLE > check validates the service name > success 1`] = ` +:::::::::::::::: CLI SUCCESS OUTPUT ::::::::::::::::: + +stdout--------------------------------------------: +✔ Schema registry is empty, nothing to compare your schema with. +View full report: +http://__URL__ +`; + +exports[`SINGLE > publish validates the service name > onlyNumbers 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +2 +stderr--------------------------------------------: + › Error: Schema publish failed. [300] + › > See https://__URL__ for + › a complete list of error codes and recommended fixes. + › To disable this message set HIVE_NO_ERROR_TIP=1 +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), underscore (_), or forward slash (/). + + +`; + +exports[`SINGLE > publish validates the service name > specialCharacters 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +2 +stderr--------------------------------------------: + › Error: Schema publish failed. [300] + › > See https://__URL__ for + › a complete list of error codes and recommended fixes. + › To disable this message set HIVE_NO_ERROR_TIP=1 +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), underscore (_), or forward slash (/). + + +`; + +exports[`SINGLE > publish validates the service name > success 1`] = ` +:::::::::::::::: CLI SUCCESS OUTPUT ::::::::::::::::: + +stdout--------------------------------------------: +✔ Published initial schema. +ℹ Available at http://__URL__ +`; + exports[`SINGLE > publishing invalid schema SDL provides meaningful feedback for the user. > schemaPublish 1`] = ` :::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: exitCode------------------------------------------: @@ -442,6 +600,85 @@ stdout--------------------------------------------: ℹ Available at http://__URL__ `; +exports[`STITCHING > check validates the service name > onlyNumbers 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +1 +stderr--------------------------------------------: +__NONE__ +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`STITCHING > check validates the service name > specialCharacters 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +1 +stderr--------------------------------------------: +__NONE__ +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`STITCHING > check validates the service name > success 1`] = ` +:::::::::::::::: CLI SUCCESS OUTPUT ::::::::::::::::: + +stdout--------------------------------------------: +✔ Schema registry is empty, nothing to compare your schema with. +View full report: +http://__URL__ +`; + +exports[`STITCHING > publish validates the service name > onlyNumbers 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +2 +stderr--------------------------------------------: + › Error: Schema publish failed. [300] + › > See https://__URL__ for + › a complete list of error codes and recommended fixes. + › To disable this message set HIVE_NO_ERROR_TIP=1 +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`STITCHING > publish validates the service name > specialCharacters 1`] = ` +:::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: +exitCode------------------------------------------: +2 +stderr--------------------------------------------: + › Error: Schema publish failed. [300] + › > See https://__URL__ for + › a complete list of error codes and recommended fixes. + › To disable this message set HIVE_NO_ERROR_TIP=1 +stdout--------------------------------------------: +✖ Detected 1 error + + - Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_). + + +`; + +exports[`STITCHING > publish validates the service name > success 1`] = ` +:::::::::::::::: CLI SUCCESS OUTPUT ::::::::::::::::: + +stdout--------------------------------------------: +✔ Published initial schema. +ℹ Available at http://__URL__ +`; + exports[`STITCHING > publishing invalid schema SDL provides meaningful feedback for the user. > schemaPublish 1`] = ` :::::::::::::::: CLI FAILURE OUTPUT ::::::::::::::: exitCode------------------------------------------: diff --git a/integration-tests/tests/cli/schema.spec.ts b/integration-tests/tests/cli/schema.spec.ts index 10d45dafd7..b49921149f 100644 --- a/integration-tests/tests/cli/schema.spec.ts +++ b/integration-tests/tests/cli/schema.spec.ts @@ -95,6 +95,113 @@ describe.each([ProjectType.Stitching, ProjectType.Federation, ProjectType.Single }, ); + test + .skipIf(projectType === ProjectType.Single) + .concurrent('publish validates the service name', async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { inviteAndJoinMember, createProject } = await createOrg(); + await inviteAndJoinMember(); + const { createTargetAccessToken } = await createProject(projectType); + const { secret } = await createTargetAccessToken({}); + + await expect( + schemaPublish([ + '--registry.accessToken', + secret, + '--author', + 'Kamil', + '--commit', + 'abc123', + '--service', + '900', + ...serviceUrlArgs, + 'fixtures/init-schema.graphql', + ]), + ).rejects.toMatchSnapshot('onlyNumbers'); + + await expect( + schemaPublish([ + '--registry.accessToken', + secret, + '--author', + 'Kamil', + '--commit', + 'abc123', + '--service', + 'asdf$#%^#@!#', + ...serviceUrlArgs, + 'fixtures/init-schema.graphql', + ]), + ).rejects.toMatchSnapshot('specialCharacters'); + + await expect( + schemaPublish([ + '--registry.accessToken', + secret, + '--author', + 'Kamil', + '--commit', + 'abc123', + '--service', + 'valid-name0', + ...serviceUrlArgs, + 'fixtures/init-schema.graphql', + ]), + ).resolves.toMatchSnapshot('success'); + }); + + test + .skipIf(projectType === ProjectType.Single) + .concurrent('check validates the service name', async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { inviteAndJoinMember, createProject } = await createOrg(); + await inviteAndJoinMember(); + const { createTargetAccessToken } = await createProject(projectType); + const { secret } = await createTargetAccessToken({}); + + await expect( + schemaCheck([ + '--registry.accessToken', + secret, + '--author', + 'Kamil', + '--commit', + 'abc123', + '--service', + '900', + 'fixtures/init-schema.graphql', + ]), + ).rejects.toMatchSnapshot('onlyNumbers'); + + await expect( + schemaCheck([ + '--registry.accessToken', + secret, + '--author', + 'Kamil', + '--commit', + 'abc123', + '--service', + 'asdf$#%^#@!#', + 'fixtures/init-schema.graphql', + ]), + ).rejects.toMatchSnapshot('specialCharacters'); + + await expect( + schemaCheck([ + '--registry.accessToken', + secret, + '--author', + 'Kamil', + '--commit', + 'abc123', + '--service', + 'valid-name0', + 'fixtures/init-schema.graphql', + ]), + ).resolves.toMatchSnapshot('success'); + }); + test.concurrent( 'publishing invalid schema SDL provides meaningful feedback for the user.', async ({ expect }) => { diff --git a/packages/services/api/src/modules/schema/providers/schema-helper.ts b/packages/services/api/src/modules/schema/providers/schema-helper.ts index 1ef4bba3e1..c4d01b8e0c 100644 --- a/packages/services/api/src/modules/schema/providers/schema-helper.ts +++ b/packages/services/api/src/modules/schema/providers/schema-helper.ts @@ -37,7 +37,7 @@ export function ensureSingleSchema(schema: Schema | Schema[]): SingleSchema { throw new Error('Expected a single schema'); } -export function ensureCompositeSchemas(schemas: readonly Schema[]): CompositeSchema[] | never { +export function ensureCompositeSchemas(schemas: readonly Schema[]): CompositeSchema[] { return schemas.filter(isCompositeSchema); } 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 02bd6194f2..2b9264422b 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -328,6 +328,30 @@ export class SchemaPublisher { }), ]); + if (input.service) { + let serviceExists = false; + if (latestVersion?.schemas) { + serviceExists = !!ensureCompositeSchemas(latestVersion.schemas).find( + ({ service_name }) => service_name === input.service, + ); + } + // this is a new service. Validate the service name. + if (!serviceExists && !isValidServiceName(input.service)) { + return { + __typename: 'SchemaCheckError', + valid: false, + changes: [], + warnings: [], + errors: [ + { + message: + 'Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_).', + }, + ], + } as const; + } + } + const [latestSchemaVersion, latestComposableSchemaVersion] = await Promise.all([ this.schemaManager.getMaybeLatestVersion(target), this.schemaManager.getMaybeLatestValidVersion(target), @@ -1059,11 +1083,42 @@ export class SchemaPublisher { targetId: selector.targetId, }); - const [contracts, latestVersion] = await Promise.all([ + const [contracts, latestVersion, latestSchemas] = await Promise.all([ this.contracts.getActiveContractsByTargetId({ targetId: selector.targetId }), this.schemaManager.getMaybeLatestVersion(target), + input.service + ? this.storage.getLatestSchemas({ + organizationId: selector.organizationId, + projectId: selector.projectId, + targetId: selector.targetId, + }) + : Promise.resolve(), ]); + // If trying to push with a service name and there are existing services + if (input.service) { + let serviceExists = false; + if (latestSchemas?.schemas) { + serviceExists = !!ensureCompositeSchemas(latestSchemas.schemas).find( + ({ service_name }) => service_name === input.service, + ); + } + // this is a new service. Validate the service name. + if (!serviceExists && !isValidServiceName(input.service)) { + return { + __typename: 'SchemaPublishError', + valid: false, + changes: [], + errors: [ + { + message: + 'Invalid service name. Service name must be 64 characters or less, must start with a letter, and can only contain alphanumeric characters, dash (-), or underscore (_).', + }, + ], + }; + } + } + const checksum = createHash('md5') .update( stringify({ @@ -2461,3 +2516,7 @@ const SchemaCheckContextIdModel = z .max(200, { message: 'Context ID cannot exceed length of 200 characters.', }); + +function isValidServiceName(service: string): boolean { + return service.length <= 64 && /^[a-zA-Z][\w_-]*$/g.test(service); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 564085c8dd..1d94d5ef1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16306,8 +16306,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-sts': 3.596.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/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 @@ -16414,11 +16414,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)': + '@aws-sdk/client-sso-oidc@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-sts': 3.596.0(@aws-sdk/client-sso-oidc@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 @@ -16457,7 +16457,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.723.0(@aws-sdk/client-sts@3.723.0)': @@ -16591,11 +16590,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.596.0': + '@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@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-sts@3.596.0) + '@aws-sdk/client-sso-oidc': 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 @@ -16634,6 +16633,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.723.0': @@ -16747,7 +16747,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-sts': 3.596.0(@aws-sdk/client-sso-oidc@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 @@ -16866,7 +16866,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-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/types': 3.7.2 @@ -17041,7 +17041,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-sts@3.596.0) + '@aws-sdk/client-sso-oidc': 3.596.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.11 '@smithy/shared-ini-file-loader': 3.1.12