Skip to content

Commit

Permalink
Migrate X-API-Token to Authorization header (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela authored Jul 1, 2022
1 parent 8035861 commit 25d6b01
Show file tree
Hide file tree
Showing 36 changed files with 260 additions and 3,381 deletions.
6 changes: 6 additions & 0 deletions .changeset/dry-ladybugs-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-hive/cli': minor
'@graphql-hive/client': minor
---

Migrate to Authorization header (previously X-API-Token)
6 changes: 6 additions & 0 deletions deployment/services/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export function deployProxy({
customRewrite: '/graphql',
service: graphql.service,
},
{
name: 'graphql-api',
path: '/graphql',
customRewrite: '/graphql',
service: graphql.service,
},
{
name: 'usage',
path: '/usage',
Expand Down
3 changes: 2 additions & 1 deletion integration-tests/testkit/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export function updateMemberAccess(input: OrganizationMemberAccessInput, authTok
});
}

export function publishSchema(input: SchemaPublishInput, token: string) {
export function publishSchema(input: SchemaPublishInput, token: string, authHeader?: 'x-api-token' | 'authorization') {
return execute({
document: gql(/* GraphQL */ `
mutation schemaPublish($input: SchemaPublishInput!) {
Expand Down Expand Up @@ -229,6 +229,7 @@ export function publishSchema(input: SchemaPublishInput, token: string) {
variables: {
input,
},
legacyAuthorizationMode: authHeader === 'x-api-token',
});
}

Expand Down
11 changes: 8 additions & 3 deletions integration-tests/testkit/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export async function execute<R, V>(params: {
variables?: V;
authToken?: string;
token?: string;
legacyAuthorizationMode?: boolean;
}) {
const res = await axios.post<ExecutionResult<R>>(
`http://${registryAddress}/graphql`,
Expand All @@ -28,9 +29,13 @@ export async function execute<R, V>(params: {
}
: {}),
...(params.token
? {
'X-API-Token': params.token,
}
? params.legacyAuthorizationMode
? {
'X-API-Token': params.token,
}
: {
Authorization: `Bearer ${params.token}`,
}
: {}),
},
responseType: 'json',
Expand Down
14 changes: 12 additions & 2 deletions integration-tests/testkit/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,21 @@ export interface CollectedOperation {
};
}

export async function collect(params: { operations: CollectedOperation[]; token: string }) {
export async function collect(params: {
operations: CollectedOperation[];
token: string;
authorizationHeader?: 'x-api-token' | 'authorization';
}) {
const res = await axios.post(`http://${usageAddress}`, params.operations, {
headers: {
'Content-Type': 'application/json',
'X-API-Token': params.token,
...(params.authorizationHeader === 'x-api-token'
? {
'X-API-Token': params.token,
}
: {
Authorization: `Bearer ${params.token}`,
}),
},
});

Expand Down
122 changes: 122 additions & 0 deletions integration-tests/tests/api/legacy-auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { TargetAccessScope, ProjectType } from '@app/gql/graphql';
import formatISO from 'date-fns/formatISO';
import subHours from 'date-fns/subHours';
import { authenticate } from '../../testkit/auth';
import {
publishSchema,
createOrganization,
createProject,
createToken,
readOperationsStats,
waitFor,
} from '../../testkit/flow';
import { collect } from '../../testkit/usage';

test('X-API-Token header should work when calling GraphQL API and collecting usage', async () => {
const { access_token: owner_access_token } = await authenticate('main');
const orgResult = await createOrganization(
{
name: 'foo',
},
owner_access_token
);

const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization;

const projectResult = await createProject(
{
organization: org.cleanId,
type: ProjectType.Single,
name: 'foo',
},
owner_access_token
);

const project = projectResult.body.data!.createProject.ok!.createdProject;
const target = projectResult.body.data!.createProject.ok!.createdTargets[0];

const tokenResult = await createToken(
{
name: 'test',
organization: org.cleanId,
project: project.cleanId,
target: target.cleanId,
organizationScopes: [],
projectScopes: [],
targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite],
},
owner_access_token
);

expect(tokenResult.body.errors).not.toBeDefined();

const token = tokenResult.body.data!.createToken.ok!.secret;

const result = await publishSchema(
{
author: 'Kamil',
commit: 'abc123',
sdl: `type Query { ping: String }`,
},
token,
'x-api-token'
);

expect(result.body.errors).not.toBeDefined();
expect(result.body.data!.schemaPublish.__typename).toBe('SchemaPublishSuccess');

const collectResult = await collect({
operations: [
{
operation: 'query ping { ping }',
operationName: 'ping',
fields: ['Query', 'Query.ping'],
execution: {
ok: true,
duration: 200000000,
errorsTotal: 0,
},
},
],
token,
authorizationHeader: 'x-api-token',
});

expect(collectResult.status).toEqual(200);

await waitFor(5_000);

const from = formatISO(subHours(Date.now(), 6));
const to = formatISO(Date.now());
const operationStatsResult = await readOperationsStats(
{
organization: org.cleanId,
project: project.cleanId,
target: target.cleanId,
period: {
from,
to,
},
},
token
);

expect(operationStatsResult.body.errors).not.toBeDefined();

const operationsStats = operationStatsResult.body.data!.operationsStats;

expect(operationsStats.operations.nodes).toHaveLength(1);

const op = operationsStats.operations.nodes[0];

expect(op.count).toEqual(1);
expect(op.document).toMatch('ping');
expect(op.operationHash).toBeDefined();
expect(op.duration.p75).toEqual(200);
expect(op.duration.p90).toEqual(200);
expect(op.duration.p95).toEqual(200);
expect(op.duration.p99).toEqual(200);
expect(op.kind).toEqual('query');
expect(op.name).toMatch('ping');
expect(op.percentage).toBeGreaterThan(99);
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"pre-commit": "lint-staged",
"prerelease": "yarn build:libraries",
"release": "changeset publish",
"upload-sourcemaps": "./scripts/upload-sourcmaps.sh",
"upload-sourcemaps": "./scripts/upload-sourcemaps.sh",
"test": "jest",
"lint": "eslint --cache --ignore-path .gitignore \"packages/**/*.{ts,tsx}\"",
"prettier": "prettier --cache --write --list-different .",
Expand Down
33 changes: 32 additions & 1 deletion packages/libraries/cli/src/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command, Config as OclifConfig, Errors } from '@oclif/core';
import colors from 'colors';
import symbols from 'log-symbols';
import { GraphQLClient } from 'graphql-request';
import { GraphQLClient, ClientError } from 'graphql-request';
import { Config } from './helpers/config';
import { getSdk } from './sdk';

Expand Down Expand Up @@ -125,6 +125,7 @@ export default abstract class extends Command {
new GraphQLClient(registry, {
headers: {
'User-Agent': `HiveCLI@${this.config.version}`,
// Authorization: `Bearer ${token}`,
'X-API-Token': token,
'graphql-client-name': 'Hive CLI',
'graphql-client-version': this.config.version,
Expand All @@ -133,6 +134,32 @@ export default abstract class extends Command {
);
}

handleFetchError(error: unknown): never {
if (typeof error === 'string') {
return this.error(error);
}

if (error instanceof Error) {
if (isClientError(error)) {
const errors = error.response?.errors;

if (Array.isArray(errors) && errors.length > 0) {
return this.error(errors[0].message, {
ref: this.cleanRequestId(error.response?.headers?.get('x-request-id')),
});
}

return this.error(error.message, {
ref: this.cleanRequestId(error.response?.headers?.get('x-request-id')),
});
}

return this.error(error);
}

return this.error(JSON.stringify(error));
}

async require<
TFlags extends {
require: string[];
Expand All @@ -144,3 +171,7 @@ export default abstract class extends Command {
}
}
}

function isClientError(error: Error): error is ClientError {
return 'response' in error;
}
13 changes: 3 additions & 10 deletions packages/libraries/cli/src/commands/operations/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { buildSchema, Source, GraphQLError } from 'graphql';
import { validate, InvalidDocument } from '@graphql-inspector/core';
import Command from '../../base-command';
import { loadOperations } from '../../helpers/operations';
import { graphqlEndpoint } from '../../helpers/config';

export default class OperationsCheck extends Command {
static description = 'checks operations against a published schema';
Expand Down Expand Up @@ -38,7 +39,7 @@ export default class OperationsCheck extends Command {
const registry = this.ensure({
key: 'registry',
args: flags,
defaultValue: 'https://app.graphql-hive.com/registry',
defaultValue: graphqlEndpoint,
env: 'HIVE_REGISTRY',
});
const file: string = args.file;
Expand Down Expand Up @@ -91,16 +92,8 @@ export default class OperationsCheck extends Command {
if (error instanceof Errors.ExitError) {
throw error;
} else {
const parsedError: Error & { response?: any } = error instanceof Error ? error : new Error(error as string);
this.fail('Failed to validate operations');

if ('response' in parsedError) {
this.error(parsedError.response.errors[0].message, {
ref: this.cleanRequestId(parsedError.response?.headers?.get('x-request-id')),
});
} else {
this.error(parsedError);
}
this.handleFetchError(error);
}
}
}
Expand Down
13 changes: 3 additions & 10 deletions packages/libraries/cli/src/commands/operations/publish.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Flags, Errors } from '@oclif/core';
import Command from '../../base-command';
import { loadOperations } from '../../helpers/operations';
import { graphqlEndpoint } from '../../helpers/config';

export default class OperationsPublish extends Command {
static description = 'saves operations to the store';
Expand Down Expand Up @@ -36,7 +37,7 @@ export default class OperationsPublish extends Command {
const registry = this.ensure({
key: 'registry',
args: flags,
defaultValue: 'https://app.graphql-hive.com/registry',
defaultValue: graphqlEndpoint,
env: 'HIVE_REGISTRY',
});
const file: string = args.file;
Expand Down Expand Up @@ -94,16 +95,8 @@ export default class OperationsPublish extends Command {
if (error instanceof Errors.ExitError) {
throw error;
} else {
const parsedError: Error & { response?: any } = error instanceof Error ? error : new Error(error as string);
this.fail('Failed to publish operations');

if ('response' in parsedError) {
this.error(parsedError.response.errors[0].message, {
ref: this.cleanRequestId(parsedError.response?.headers?.get('x-request-id')),
});
} else {
this.error(parsedError);
}
this.handleFetchError(error);
}
}
}
Expand Down
13 changes: 3 additions & 10 deletions packages/libraries/cli/src/commands/schema/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Flags, Errors } from '@oclif/core';
import { loadSchema, renderChanges, renderErrors, minifySchema } from '../../helpers/schema';
import { invariant } from '../../helpers/validation';
import { gitInfo } from '../../helpers/git';
import { graphqlEndpoint } from '../../helpers/config';
import Command from '../../base-command';

export default class SchemaCheck extends Command {
Expand Down Expand Up @@ -51,7 +52,7 @@ export default class SchemaCheck extends Command {
const registry = this.ensure({
key: 'registry',
args: flags,
defaultValue: 'https://app.graphql-hive.com/registry',
defaultValue: graphqlEndpoint,
env: 'HIVE_REGISTRY',
});
const file = args.file;
Expand Down Expand Up @@ -120,16 +121,8 @@ export default class SchemaCheck extends Command {
if (error instanceof Errors.ExitError) {
throw error;
} else {
const parsedError: Error & { response?: any } = error instanceof Error ? error : new Error(error as string);

this.fail('Failed to check schema');
if ('response' in parsedError) {
this.error(parsedError.response.errors[0].message, {
ref: this.cleanRequestId(parsedError.response?.headers?.get('x-request-id')),
});
} else {
this.error(parsedError);
}
this.handleFetchError(error);
}
}
}
Expand Down
Loading

0 comments on commit 25d6b01

Please sign in to comment.