Skip to content

Commit

Permalink
feat: target access token strategy and session
Browse files Browse the repository at this point in the history
  • Loading branch information
n1ru4l committed Oct 15, 2024
1 parent aabb0ef commit f8e838b
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 80 deletions.
2 changes: 2 additions & 0 deletions packages/services/api/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { FastifyRequest } from '@hive/service-common';
import { Session } from './modules/auth/lib/authz';

export interface RegistryContext {
req: FastifyRequest;
requestId: string;
user: any;
headers: Record<string, string | string[] | undefined>;
request: Request;
session: Session;
}

declare global {
Expand Down
10 changes: 1 addition & 9 deletions packages/services/api/src/modules/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { FastifyReply, FastifyRequest, ServiceLogger } from '@hive/service-common';
import { TokenStorage } from '../../token/providers/token-storage';
import { TokensConfig } from '../../token/providers/tokens';
import {
OrganizationAccessScope,
ProjectAccessScope,
TargetAccessScope,
} from '../providers/scopes';
import { AuthNStrategy, AuthorizationPolicyStatement, Session } from './authz';
import { transformLegacyPolicies } from './legacy-permissions';

export class TargetAccessTokenSession extends Session {
public readonly organizationId: string;
public readonly projectId: string;
public readonly targetId: string;
public readonly token: string;

private policies: Array<AuthorizationPolicyStatement>;

constructor(args: {
organizationId: string;
projectId: string;
targetId: string;
token: string;
policies: Array<AuthorizationPolicyStatement>;
}) {
super();
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<AuthorizationPolicyStatement>> | Array<AuthorizationPolicyStatement> {
return this.policies;
}
}

export class TargetAccessTokenStrategy extends AuthNStrategy<TargetAccessTokenSession> {
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<TargetAccessTokenSession | null> {
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: transformLegacyPolicies(
result.organization,
result.project,
result.target,
result.scopes as Array<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>,
),
});
}
}

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);
}
26 changes: 13 additions & 13 deletions packages/services/api/src/modules/auth/providers/auth-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CONTEXT, Inject, Injectable, Scope } from 'graphql-modules';
import type { RegistryContext } from '../../../context';
import type { User } from '../../../shared/entities';
import { AccessError } from '../../../shared/errors';
import type { Listify, MapToArray } from '../../../shared/helpers';
Expand All @@ -7,14 +8,14 @@ import { Storage } from '../../shared/providers/storage';
import { TokenStorage } from '../../token/providers/token-storage';
import { Session } from '../lib/authz';
import { SuperTokensCookieBasedSession } from '../lib/supertokens-strategy';
import { TargetAccessTokenSession } from '../lib/target-access-token-strategy';
import {
OrganizationAccess,
OrganizationAccessScope,
OrganizationUserScopesSelector,
} 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 {
Expand Down Expand Up @@ -47,8 +48,7 @@ export class AuthManager {
private session: Session;

constructor(
@Inject(ApiToken) private apiToken: string,
@Inject(CONTEXT) context: any,
@Inject(CONTEXT) context: RegistryContext,
private organizationAccess: OrganizationAccess,
private projectAccess: ProjectAccess,
private targetAccess: TargetAccess,
Expand All @@ -62,7 +62,7 @@ export class AuthManager {
async ensureTargetAccess(
selector: Listify<TargetAccessSelector, 'target'>,
): Promise<void | never> {
if (this.apiToken) {
if (this.session instanceof TargetAccessTokenSession) {
if (hasManyTargets(selector)) {
await Promise.all(
selector.target.map(target =>
Expand All @@ -75,7 +75,7 @@ export class AuthManager {
} else {
await this.targetAccess.ensureAccessForToken({
...(selector as TargetAccessSelector),
token: this.apiToken,
token: this.session.token,
});
}
} else if (hasManyTargets(selector)) {
Expand All @@ -97,10 +97,10 @@ export class AuthManager {
}

async ensureProjectAccess(selector: ProjectAccessSelector): Promise<void | never> {
if (this.apiToken) {
if (this.session instanceof TargetAccessTokenSession) {
await this.projectAccess.ensureAccessForToken({
...selector,
token: this.apiToken,
token: this.session.token,
});
} else {
const user = await this.getCurrentUser();
Expand All @@ -112,10 +112,10 @@ export class AuthManager {
}

async ensureOrganizationAccess(selector: OrganizationAccessSelector): Promise<void | never> {
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();
Expand All @@ -133,7 +133,7 @@ export class AuthManager {
}

async checkOrganizationAccess(selector: OrganizationAccessSelector): Promise<boolean> {
if (this.apiToken) {
if (this.session instanceof TargetAccessTokenSession) {
throw new Error('checkOrganizationAccess for token is not implemented yet');
}

Expand All @@ -158,11 +158,11 @@ export class AuthManager {
}

ensureApiToken(): string | never {
if (this.apiToken) {
return this.apiToken;
if (!(this.session instanceof TargetAccessTokenSession)) {
throw new AccessError('Authorization header is missing');
}

throw new AccessError('Authorization header is missing');
return this.session.token;
}

getOrganizationOwnerByToken: () => Promise<User | never> = share(async () => {
Expand Down
58 changes: 0 additions & 58 deletions packages/services/api/src/modules/auth/providers/tokens.ts

This file was deleted.

7 changes: 7 additions & 0 deletions packages/services/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createRedisEventTarget } from '@graphql-yoga/redis-event-target';
import 'reflect-metadata';
import { hostname } from 'os';
import { createPubSub } from 'graphql-yoga';
import { TargetAccessTokenStrategy } from 'packages/services/api/src/modules/auth/lib/target-access-token-strategy';
import { z } from 'zod';
import formDataPlugin from '@fastify/formbody';
import { createRegistry, createTaskRunner, CryptoProvider, LogFn, Logger } from '@hive/api';
Expand Down Expand Up @@ -396,6 +397,12 @@ export async function main() {
logger: server.log,
storage,
}),
new TargetAccessTokenStrategy({
logger: server.log,
tokensConfig: {
endpoint: env.hiveServices.tokens.endpoint,
},
}),
],
});

Expand Down

0 comments on commit f8e838b

Please sign in to comment.