mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
chore: cleanup authorization code (#6736)
This commit is contained in:
parent
d1b8d4ce1d
commit
c6badf5fe7
19 changed files with 146 additions and 719 deletions
|
|
@ -36,10 +36,9 @@ const config: CodegenConfig = {
|
|||
ProjectType: '../shared/entities#ProjectType',
|
||||
NativeFederationCompatibilityStatus:
|
||||
'../shared/entities#NativeFederationCompatibilityStatus',
|
||||
TargetAccessScope: '../modules/auth/providers/target-access#TargetAccessScope',
|
||||
ProjectAccessScope: '../modules/auth/providers/project-access#ProjectAccessScope',
|
||||
OrganizationAccessScope:
|
||||
'../modules/auth/providers/organization-access#OrganizationAccessScope',
|
||||
TargetAccessScope: '../modules/auth/providers/scopes#TargetAccessScope',
|
||||
ProjectAccessScope: '../modules/auth/providers/scopes#ProjectAccessScope',
|
||||
OrganizationAccessScope: '../modules/auth/providers/scopes#OrganizationAccessScope',
|
||||
SupportTicketPriority: '../shared/entities#SupportTicketPriority',
|
||||
SupportTicketStatus: '../shared/entities#SupportTicketStatus',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { createModule } from 'graphql-modules';
|
||||
import { AuditLogManager } from '../audit-logs/providers/audit-logs-manager';
|
||||
import { AuthManager } from './providers/auth-manager';
|
||||
import { OrganizationAccess } from './providers/organization-access';
|
||||
import { OrganizationAccessTokenValidationCache } from './providers/organization-access-token-validation-cache';
|
||||
import { ProjectAccess } from './providers/project-access';
|
||||
import { TargetAccess } from './providers/target-access';
|
||||
import { UserManager } from './providers/user-manager';
|
||||
import { resolvers } from './resolvers.generated';
|
||||
import typeDefs from './module.graphql';
|
||||
|
|
@ -14,13 +11,5 @@ export const authModule = createModule({
|
|||
dirname: __dirname,
|
||||
typeDefs,
|
||||
resolvers,
|
||||
providers: [
|
||||
AuthManager,
|
||||
UserManager,
|
||||
OrganizationAccess,
|
||||
ProjectAccess,
|
||||
TargetAccess,
|
||||
AuditLogManager,
|
||||
OrganizationAccessTokenValidationCache,
|
||||
],
|
||||
providers: [AuthManager, UserManager, AuditLogManager, OrganizationAccessTokenValidationCache],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ class TestSession extends Session {
|
|||
): Promise<Array<AuthorizationPolicyStatement>> | Array<AuthorizationPolicyStatement> {
|
||||
return this.policyStatements;
|
||||
}
|
||||
|
||||
getActor(): Promise<never> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Session.assertPerformAction', () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { User } from '../../../shared/entities';
|
|||
import { AccessError } from '../../../shared/errors';
|
||||
import { objectEntries, objectFromEntries } from '../../../shared/helpers';
|
||||
import { isUUID } from '../../../shared/is-uuid';
|
||||
import type { OrganizationAccessToken } from '../../organization/providers/organization-access-tokens';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
|
||||
export type AuthorizationPolicyStatement = {
|
||||
|
|
@ -47,6 +48,18 @@ function parseResourceIdentifier(resource: string) {
|
|||
return { organizationId, resourceId: parts[2] };
|
||||
}
|
||||
|
||||
export type UserActor = {
|
||||
type: 'user';
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type OrganizationAccessTokenActor = {
|
||||
type: 'organizationAccessToken';
|
||||
organizationAccessToken: OrganizationAccessToken;
|
||||
};
|
||||
|
||||
type Actor = UserActor | OrganizationAccessTokenActor;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
@ -75,8 +88,21 @@ export abstract class Session {
|
|||
abstract readonly id: string;
|
||||
|
||||
/** Retrieve the current viewer. Implementations of the session need to implement this function */
|
||||
public getViewer(): Promise<User> {
|
||||
throw new AccessError('Authorization token is missing', 'UNAUTHENTICATED');
|
||||
public abstract getActor(): Promise<Actor>;
|
||||
|
||||
/**
|
||||
* Retrieve the Viewer of the session.
|
||||
* A viewer can only be a {User} aka {SuperTokensSessions{}.
|
||||
* If the session does not have a user an exception is raised.
|
||||
*/
|
||||
public async getViewer(): Promise<User> {
|
||||
const actor = await this.getActor();
|
||||
|
||||
if (actor.type !== 'user') {
|
||||
throw new AccessError('Only authenticated users can perform this action.');
|
||||
}
|
||||
|
||||
return actor.user;
|
||||
}
|
||||
|
||||
public isViewer(): boolean {
|
||||
|
|
@ -498,6 +524,10 @@ class UnauthenticatedSession extends Session {
|
|||
return [];
|
||||
}
|
||||
id = 'noop';
|
||||
|
||||
public getActor(): Promise<Actor> {
|
||||
throw new AccessError('Authorization token is missing', 'UNAUTHENTICATED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import { type FastifyReply, type FastifyRequest } from '@hive/service-common';
|
||||
import * as OrganizationAccessKey from '../../organization/lib/organization-access-key';
|
||||
import type { OrganizationAccessToken } from '../../organization/providers/organization-access-tokens';
|
||||
import { OrganizationAccessTokensCache } from '../../organization/providers/organization-access-tokens-cache';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { OrganizationAccessTokenValidationCache } from '../providers/organization-access-token-validation-cache';
|
||||
import { AuthNStrategy, AuthorizationPolicyStatement, Session } from './authz';
|
||||
import {
|
||||
AuthNStrategy,
|
||||
AuthorizationPolicyStatement,
|
||||
OrganizationAccessTokenActor,
|
||||
Session,
|
||||
} from './authz';
|
||||
|
||||
function hashToken(token: string) {
|
||||
return crypto.createHash('sha256').update(token).digest('hex');
|
||||
|
|
@ -13,6 +19,7 @@ function hashToken(token: string) {
|
|||
export class OrganizationAccessTokenSession extends Session {
|
||||
public readonly organizationId: string;
|
||||
private policies: Array<AuthorizationPolicyStatement>;
|
||||
private organizationAccessToken: OrganizationAccessToken;
|
||||
readonly id: string;
|
||||
|
||||
constructor(
|
||||
|
|
@ -20,6 +27,7 @@ export class OrganizationAccessTokenSession extends Session {
|
|||
id: string;
|
||||
organizationId: string;
|
||||
policies: Array<AuthorizationPolicyStatement>;
|
||||
organizationAccessToken: OrganizationAccessToken;
|
||||
},
|
||||
deps: {
|
||||
logger: Logger;
|
||||
|
|
@ -29,6 +37,14 @@ export class OrganizationAccessTokenSession extends Session {
|
|||
this.id = args.id;
|
||||
this.organizationId = args.organizationId;
|
||||
this.policies = args.policies;
|
||||
this.organizationAccessToken = args.organizationAccessToken;
|
||||
}
|
||||
|
||||
public async getActor(): Promise<OrganizationAccessTokenActor> {
|
||||
return {
|
||||
type: 'organizationAccessToken',
|
||||
organizationAccessToken: this.organizationAccessToken,
|
||||
};
|
||||
}
|
||||
|
||||
protected loadPolicyStatementsForOrganization(
|
||||
|
|
@ -112,6 +128,7 @@ export class OrganizationAccessTokenStrategy extends AuthNStrategy<OrganizationA
|
|||
id: organizationAccessToken.id,
|
||||
organizationId: organizationAccessToken.organizationId,
|
||||
policies: organizationAccessToken.authorizationPolicyStatements,
|
||||
organizationAccessToken,
|
||||
},
|
||||
{
|
||||
logger: args.req.log,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import SessionNode from 'supertokens-node/recipe/session/index.js';
|
|||
import * as zod from 'zod';
|
||||
import type { FastifyReply, FastifyRequest } 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 { OrganizationMembers } from '../../organization/providers/organization-members';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import type { Storage } from '../../shared/providers/storage';
|
||||
import { AuthNStrategy, AuthorizationPolicyStatement, Session } from './authz';
|
||||
import { AuthNStrategy, AuthorizationPolicyStatement, Session, UserActor } from './authz';
|
||||
|
||||
export class SuperTokensCookieBasedSession extends Session {
|
||||
public superTokensUserId: string;
|
||||
|
|
@ -32,7 +31,7 @@ export class SuperTokensCookieBasedSession extends Session {
|
|||
protected async loadPolicyStatementsForOrganization(
|
||||
organizationId: string,
|
||||
): Promise<Array<AuthorizationPolicyStatement>> {
|
||||
const user = await this.getViewer();
|
||||
const { user } = await this.getActor();
|
||||
|
||||
this.logger.debug(
|
||||
'Loading policy statements for organization. (userId=%s, organizationId=%s)',
|
||||
|
|
@ -97,7 +96,7 @@ export class SuperTokensCookieBasedSession extends Session {
|
|||
return organizationMembership.assignedRole.authorizationPolicyStatements;
|
||||
}
|
||||
|
||||
public async getViewer(): Promise<User> {
|
||||
public async getActor(): Promise<UserActor> {
|
||||
const user = await this.storage.getUserBySuperTokenId({
|
||||
superTokensUserId: this.superTokensUserId,
|
||||
});
|
||||
|
|
@ -106,7 +105,10 @@ export class SuperTokensCookieBasedSession extends Session {
|
|||
throw new AccessError('User not found');
|
||||
}
|
||||
|
||||
return user;
|
||||
return {
|
||||
type: 'user',
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
public isViewer() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { maskToken, type FastifyReply, type FastifyRequest } from '@hive/service-common';
|
||||
import { AccessError } from '../../../shared/errors';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { TokenStorage } from '../../token/providers/token-storage';
|
||||
import { TokensConfig } from '../../token/providers/tokens';
|
||||
|
|
@ -55,6 +56,10 @@ export class TargetAccessTokenSession extends Session {
|
|||
targetId: this.targetId,
|
||||
};
|
||||
}
|
||||
|
||||
public async getActor(): Promise<never> {
|
||||
throw new AccessError('Authorization token is missing', 'UNAUTHENTICATED');
|
||||
}
|
||||
}
|
||||
|
||||
export class TargetAccessTokenStrategy extends AuthNStrategy<TargetAccessTokenSession> {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import type { User } from '../../shared/entities';
|
||||
import { PermissionGroup, PermissionRecord } from '../organization/lib/permissions';
|
||||
import type { OrganizationAccessScope } from './providers/organization-access';
|
||||
import type { ProjectAccessScope } from './providers/project-access';
|
||||
import type { TargetAccessScope } from './providers/target-access';
|
||||
import type {
|
||||
OrganizationAccessScope,
|
||||
ProjectAccessScope,
|
||||
TargetAccessScope,
|
||||
} from './providers/scopes';
|
||||
|
||||
export type OrganizationAccessScopeMapper = OrganizationAccessScope;
|
||||
export type ProjectAccessScopeMapper = ProjectAccessScope;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import DataLoader from 'dataloader';
|
||||
import { Injectable, Scope } from 'graphql-modules';
|
||||
import type { User } from '../../../shared/entities';
|
||||
import { AccessError } from '../../../shared/errors';
|
||||
import { Storage } from '../../shared/providers/storage';
|
||||
import { Session } from '../lib/authz';
|
||||
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 { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from './scopes';
|
||||
import { UserManager } from './user-manager';
|
||||
|
||||
export interface OrganizationAccessSelector {
|
||||
|
|
@ -39,53 +34,48 @@ export interface TargetAccessSelector {
|
|||
global: true,
|
||||
})
|
||||
export class AuthManager {
|
||||
ownership: DataLoader<
|
||||
{
|
||||
organizationId: string;
|
||||
},
|
||||
string | null,
|
||||
string
|
||||
>;
|
||||
|
||||
constructor(
|
||||
private organizationAccess: OrganizationAccess,
|
||||
private projectAccess: ProjectAccess,
|
||||
private targetAccess: TargetAccess,
|
||||
private userManager: UserManager,
|
||||
private session: Session,
|
||||
) {}
|
||||
private storage: Storage,
|
||||
) {
|
||||
this.ownership = new DataLoader(
|
||||
async selectors => {
|
||||
const ownerPerSelector = await Promise.all(
|
||||
selectors.map(selector => this.storage.getOrganizationOwnerId(selector)),
|
||||
);
|
||||
|
||||
async ensureOrganizationAccess(selector: OrganizationAccessSelector): Promise<void | never> {
|
||||
if (this.session instanceof TargetAccessTokenSession) {
|
||||
await this.organizationAccess.ensureAccessForToken({
|
||||
...selector,
|
||||
token: this.session.token,
|
||||
});
|
||||
} else {
|
||||
const user = await this.session.getViewer();
|
||||
|
||||
// If a user is an admin, we can allow access for all data
|
||||
if (user.isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.organizationAccess.ensureAccessForUser({
|
||||
...selector,
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async checkOrganizationAccess(selector: OrganizationAccessSelector): Promise<boolean> {
|
||||
if (this.session instanceof TargetAccessTokenSession) {
|
||||
throw new Error('checkOrganizationAccess for token is not implemented yet');
|
||||
}
|
||||
|
||||
const user = await this.session.getViewer();
|
||||
|
||||
return this.organizationAccess.checkAccessForUser({
|
||||
...selector,
|
||||
userId: user.id,
|
||||
});
|
||||
return selectors.map((_, i) => ownerPerSelector[i]);
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'OrganizationAccess:ownership',
|
||||
organization: selector.organizationId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async ensureOrganizationOwnership(selector: { organization: string }): Promise<void | never> {
|
||||
const user = await this.session.getViewer();
|
||||
const isOwner = await this.organizationAccess.checkOwnershipForUser({
|
||||
const actor = await this.session.getActor();
|
||||
|
||||
if (actor.type !== 'user') {
|
||||
throw new AccessError('Action can only be performed by user.');
|
||||
}
|
||||
|
||||
const isOwner = await this.checkOwnershipForUser({
|
||||
organizationId: selector.organization,
|
||||
userId: user.id,
|
||||
userId: actor.user.id,
|
||||
});
|
||||
|
||||
if (!isOwner) {
|
||||
|
|
@ -93,54 +83,30 @@ export class AuthManager {
|
|||
}
|
||||
}
|
||||
|
||||
async getCurrentUserAccessScopes(organizationId: string) {
|
||||
const user = await this.session.getViewer();
|
||||
private async checkOwnershipForUser(selector: OrganizationOwnershipSelector) {
|
||||
const owner = await this.ownership.load(selector);
|
||||
|
||||
if (!user) {
|
||||
throw new AccessError('User not found');
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [organizationScopes, projectScopes, targetScopes] = await Promise.all([
|
||||
this.getMemberOrganizationScopes({
|
||||
organizationId: organizationId,
|
||||
userId: user.id,
|
||||
}),
|
||||
this.getMemberProjectScopes({
|
||||
organizationId: organizationId,
|
||||
userId: user.id,
|
||||
}),
|
||||
this.getMemberTargetScopes({
|
||||
organizationId: organizationId,
|
||||
userId: user.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
return [...organizationScopes, ...projectScopes, ...targetScopes];
|
||||
return owner === selector.userId;
|
||||
}
|
||||
|
||||
async updateCurrentUser(input: { displayName: string; fullName: string }): Promise<User> {
|
||||
const user = await this.session.getViewer();
|
||||
const actor = await this.session.getActor();
|
||||
if (actor.type !== 'user') {
|
||||
throw new AccessError('Action can only be performed by user.');
|
||||
}
|
||||
|
||||
return this.userManager.updateUser({
|
||||
id: user.id,
|
||||
id: actor.user.id,
|
||||
...input,
|
||||
});
|
||||
}
|
||||
|
||||
getMemberOrganizationScopes(selector: OrganizationUserScopesSelector) {
|
||||
return this.organizationAccess.getMemberScopes(selector);
|
||||
}
|
||||
|
||||
getMemberProjectScopes(selector: ProjectUserScopesSelector) {
|
||||
return this.projectAccess.getMemberScopes(selector);
|
||||
}
|
||||
|
||||
getMemberTargetScopes(selector: TargetUserScopesSelector) {
|
||||
return this.targetAccess.getMemberScopes(selector);
|
||||
}
|
||||
|
||||
resetAccessCache() {
|
||||
this.organizationAccess.resetAccessCache();
|
||||
this.projectAccess.resetAccessCache();
|
||||
this.targetAccess.resetAccessCache();
|
||||
}
|
||||
}
|
||||
|
||||
export interface OrganizationOwnershipSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,249 +0,0 @@
|
|||
import DataLoader from 'dataloader';
|
||||
import { forwardRef, Inject, Injectable, Scope } from 'graphql-modules';
|
||||
import { Token } from '../../../shared/entities';
|
||||
import { AccessError } from '../../../shared/errors';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { Storage } from '../../shared/providers/storage';
|
||||
import { TokenSelector, TokenStorage } from '../../token/providers/token-storage';
|
||||
import type { ProjectAccessScope } from './project-access';
|
||||
import { OrganizationAccessScope } from './scopes';
|
||||
import type { TargetAccessScope } from './target-access';
|
||||
|
||||
export { OrganizationAccessScope } from './scopes';
|
||||
|
||||
export interface OrganizationOwnershipSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface OrganizationUserScopesSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface OrganizationUserAccessSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
scope: OrganizationAccessScope;
|
||||
}
|
||||
|
||||
interface OrganizationTokenAccessSelector {
|
||||
token: string;
|
||||
organizationId: string;
|
||||
scope: OrganizationAccessScope;
|
||||
}
|
||||
|
||||
const organizationAccessScopeValues = Object.values(OrganizationAccessScope);
|
||||
|
||||
export function isOrganizationScope(scope: any): scope is OrganizationAccessScope {
|
||||
return organizationAccessScopeValues.includes(scope);
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Operation,
|
||||
})
|
||||
export class OrganizationAccess {
|
||||
private logger: Logger;
|
||||
private userAccess: DataLoader<OrganizationUserAccessSelector, boolean, string>;
|
||||
private tokenAccess: DataLoader<OrganizationTokenAccessSelector, boolean, string>;
|
||||
private allScopes: DataLoader<
|
||||
OrganizationUserScopesSelector,
|
||||
ReadonlyArray<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>,
|
||||
string
|
||||
>;
|
||||
private scopes: DataLoader<
|
||||
OrganizationUserScopesSelector,
|
||||
readonly OrganizationAccessScope[],
|
||||
string
|
||||
>;
|
||||
tokenInfo: DataLoader<TokenSelector, Token | null, string>;
|
||||
ownership: DataLoader<
|
||||
{
|
||||
organizationId: string;
|
||||
},
|
||||
string | null,
|
||||
string
|
||||
>;
|
||||
|
||||
constructor(
|
||||
logger: Logger,
|
||||
private storage: Storage,
|
||||
@Inject(forwardRef(() => TokenStorage)) private tokenStorage: TokenStorage,
|
||||
) {
|
||||
this.logger = logger.child({
|
||||
source: 'OrganizationAccess',
|
||||
});
|
||||
this.userAccess = new DataLoader(
|
||||
async selectors => {
|
||||
const scopes = await this.scopes.loadMany(selectors);
|
||||
|
||||
return selectors.map((selector, i) => {
|
||||
const scopesForSelector = scopes[i];
|
||||
|
||||
if (scopesForSelector instanceof Error) {
|
||||
this.logger.warn(
|
||||
`OrganizationAccess:user (error=%s, selector=%o)`,
|
||||
scopesForSelector.message,
|
||||
selector,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return scopesForSelector.includes(selector.scope);
|
||||
});
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'OrganizationAccess:user',
|
||||
organization: selector.organizationId,
|
||||
user: selector.userId,
|
||||
scope: selector.scope,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
this.tokenAccess = new DataLoader(
|
||||
selectors =>
|
||||
Promise.all(
|
||||
selectors.map(async selector => {
|
||||
const tokenInfo = await this.tokenInfo.load(selector);
|
||||
|
||||
if (tokenInfo?.organization === selector.organizationId) {
|
||||
return tokenInfo.scopes.includes(selector.scope);
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
),
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'OrganizationAccess:token',
|
||||
organization: selector.organizationId,
|
||||
token: selector.token,
|
||||
scope: selector.scope,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
this.allScopes = new DataLoader(
|
||||
async selectors => {
|
||||
const scopesPerSelector = await this.storage.getOrganizationMemberAccessPairs(selectors);
|
||||
|
||||
return selectors.map((_, i) => scopesPerSelector[i]);
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'OrganizationAccess:allScopes',
|
||||
organization: selector.organizationId,
|
||||
user: selector.userId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
this.scopes = new DataLoader(
|
||||
async selectors => {
|
||||
const scopesPerSelector = await this.allScopes.loadMany(selectors);
|
||||
|
||||
return selectors.map((selector, i) => {
|
||||
const scopes = scopesPerSelector[i];
|
||||
|
||||
if (scopes instanceof Error) {
|
||||
this.logger.warn(
|
||||
`OrganizationAccess:scopes (error=%s, selector=%o)`,
|
||||
scopes.message,
|
||||
selector,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return scopes.filter(isOrganizationScope);
|
||||
});
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'OrganizationAccess:scopes',
|
||||
organization: selector.organizationId,
|
||||
user: selector.userId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.ownership = new DataLoader(
|
||||
async selectors => {
|
||||
const ownerPerSelector = await Promise.all(
|
||||
selectors.map(selector => this.storage.getOrganizationOwnerId(selector)),
|
||||
);
|
||||
|
||||
return selectors.map((_, i) => ownerPerSelector[i]);
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'OrganizationAccess:ownership',
|
||||
organization: selector.organizationId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.tokenInfo = new DataLoader(
|
||||
selectors => Promise.all(selectors.map(selector => this.tokenStorage.getToken(selector))),
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return selector.token;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async ensureAccessForToken(selector: OrganizationTokenAccessSelector): Promise<void | never> {
|
||||
const canAccess = await this.tokenAccess.load(selector);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new AccessError(`Missing ${selector.scope} permission`);
|
||||
}
|
||||
}
|
||||
|
||||
async ensureAccessForUser(selector: OrganizationUserAccessSelector): Promise<void | never> {
|
||||
const canAccess = await this.userAccess.load(selector);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new AccessError(`Missing ${selector.scope} permission`);
|
||||
}
|
||||
}
|
||||
|
||||
async checkAccessForUser(selector: OrganizationUserAccessSelector): Promise<boolean> {
|
||||
return this.userAccess.load(selector);
|
||||
}
|
||||
|
||||
async checkOwnershipForUser(selector: OrganizationOwnershipSelector) {
|
||||
const owner = await this.ownership.load(selector);
|
||||
|
||||
if (!owner) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return owner === selector.userId;
|
||||
}
|
||||
|
||||
async getMemberScopes(selector: OrganizationUserScopesSelector) {
|
||||
return this.scopes.load(selector);
|
||||
}
|
||||
|
||||
async getAllScopes(selectors: readonly OrganizationUserScopesSelector[]) {
|
||||
return this.allScopes.loadMany(selectors);
|
||||
}
|
||||
|
||||
resetAccessCache() {
|
||||
this.userAccess.clearAll();
|
||||
this.tokenAccess.clearAll();
|
||||
this.allScopes.clearAll();
|
||||
this.scopes.clearAll();
|
||||
this.tokenInfo.clearAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
import Dataloader from 'dataloader';
|
||||
import { Injectable, Scope } from 'graphql-modules';
|
||||
import { AccessError } from '../../../shared/errors';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { OrganizationAccess } from './organization-access';
|
||||
import { ProjectAccessScope } from './scopes';
|
||||
|
||||
export { ProjectAccessScope } from './scopes';
|
||||
|
||||
export interface ProjectUserAccessSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
scope: ProjectAccessScope;
|
||||
}
|
||||
|
||||
export interface ProjectUserScopesSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
interface ProjectTokenAccessSelector {
|
||||
token: string;
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
scope: ProjectAccessScope;
|
||||
}
|
||||
|
||||
const projectAccessScopeValues = Object.values(ProjectAccessScope);
|
||||
|
||||
export function isProjectScope(scope: any): scope is ProjectAccessScope {
|
||||
return projectAccessScopeValues.includes(scope);
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Operation,
|
||||
})
|
||||
export class ProjectAccess {
|
||||
private logger: Logger;
|
||||
private userAccess: Dataloader<ProjectUserAccessSelector, boolean, string>;
|
||||
private tokenAccess: Dataloader<ProjectTokenAccessSelector, boolean, string>;
|
||||
private scopes: Dataloader<ProjectUserScopesSelector, readonly ProjectAccessScope[], string>;
|
||||
|
||||
constructor(
|
||||
logger: Logger,
|
||||
private organizationAccess: OrganizationAccess,
|
||||
) {
|
||||
this.logger = logger.child({
|
||||
source: 'ProjectAccess',
|
||||
});
|
||||
this.userAccess = new Dataloader(
|
||||
async selectors => {
|
||||
const scopes = await this.scopes.loadMany(selectors);
|
||||
|
||||
return selectors.map((selector, i) => {
|
||||
const scopesForSelector = scopes[i];
|
||||
|
||||
if (scopesForSelector instanceof Error) {
|
||||
this.logger.warn(
|
||||
`ProjectAccess:user (error=%s, selector=%o)`,
|
||||
scopesForSelector.message,
|
||||
selector,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return scopesForSelector.includes(selector.scope);
|
||||
});
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'ProjectAccess:user',
|
||||
organization: selector.organizationId,
|
||||
project: selector.projectId,
|
||||
user: selector.userId,
|
||||
scope: selector.scope,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
this.tokenAccess = new Dataloader(
|
||||
selectors =>
|
||||
Promise.all(
|
||||
selectors.map(async selector => {
|
||||
const tokenInfo = await this.organizationAccess.tokenInfo.load(selector);
|
||||
|
||||
if (
|
||||
tokenInfo?.organization === selector.organizationId &&
|
||||
tokenInfo?.project === selector.projectId
|
||||
) {
|
||||
return tokenInfo.scopes.includes(selector.scope);
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
),
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'ProjectAccess:token',
|
||||
organization: selector.organizationId,
|
||||
project: selector.projectId,
|
||||
token: selector.token,
|
||||
scope: selector.scope,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
this.scopes = new Dataloader(
|
||||
async selectors => {
|
||||
const scopesPerSelector = await this.organizationAccess.getAllScopes(selectors);
|
||||
|
||||
return selectors.map((selector, i) => {
|
||||
const scopes = scopesPerSelector[i];
|
||||
|
||||
if (scopes instanceof Error) {
|
||||
this.logger.debug(
|
||||
`ProjectAccess:scopes (error=%s, selector=%o)`,
|
||||
scopes.message,
|
||||
selector,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return scopes.filter(isProjectScope);
|
||||
});
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'ProjectAccess:scopes',
|
||||
organization: selector.organizationId,
|
||||
user: selector.userId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async ensureAccessForToken(selector: ProjectTokenAccessSelector): Promise<void | never> {
|
||||
const canAccess = await this.tokenAccess.load(selector);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new AccessError(`Missing ${selector.scope} permission`);
|
||||
}
|
||||
}
|
||||
|
||||
async ensureAccessForUser(selector: ProjectUserAccessSelector): Promise<void | never> {
|
||||
const canAccess = await this.userAccess.load(selector);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new AccessError(`Missing ${selector.scope} permission`);
|
||||
}
|
||||
}
|
||||
|
||||
async getMemberScopes(selector: ProjectUserScopesSelector) {
|
||||
return this.scopes.load(selector);
|
||||
}
|
||||
|
||||
resetAccessCache() {
|
||||
this.userAccess.clearAll();
|
||||
this.tokenAccess.clearAll();
|
||||
this.scopes.clearAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
import Dataloader from 'dataloader';
|
||||
import { Injectable, Scope } from 'graphql-modules';
|
||||
import { AccessError } from '../../../shared/errors';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { OrganizationAccess } from './organization-access';
|
||||
import { TargetAccessScope } from './scopes';
|
||||
|
||||
export { TargetAccessScope } from './scopes';
|
||||
|
||||
export interface TargetUserAccessSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
targetId: string;
|
||||
scope: TargetAccessScope;
|
||||
}
|
||||
|
||||
export interface TargetUserScopesSelector {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
interface TargetTokenAccessSelector {
|
||||
token: string;
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
targetId: string;
|
||||
scope: TargetAccessScope;
|
||||
}
|
||||
|
||||
const targetAccessScopeValues = Object.values(TargetAccessScope);
|
||||
|
||||
export function isTargetScope(scope: any): scope is TargetAccessScope {
|
||||
return targetAccessScopeValues.includes(scope);
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Operation,
|
||||
})
|
||||
export class TargetAccess {
|
||||
private logger: Logger;
|
||||
private userAccess: Dataloader<TargetUserAccessSelector, boolean, string>;
|
||||
private tokenAccess: Dataloader<TargetTokenAccessSelector, boolean, string>;
|
||||
private scopes: Dataloader<TargetUserScopesSelector, readonly TargetAccessScope[], string>;
|
||||
|
||||
constructor(
|
||||
logger: Logger,
|
||||
private organizationAccess: OrganizationAccess,
|
||||
) {
|
||||
this.logger = logger.child({
|
||||
source: 'TargetAccess',
|
||||
});
|
||||
this.userAccess = new Dataloader(
|
||||
async selectors => {
|
||||
const scopes = await this.scopes.loadMany(selectors);
|
||||
|
||||
return selectors.map((selector, i) => {
|
||||
const scopesForSelector = scopes[i];
|
||||
|
||||
if (scopesForSelector instanceof Error) {
|
||||
this.logger.warn(
|
||||
`TargetAccess:user (error=%s, selector=%o)`,
|
||||
scopesForSelector.message,
|
||||
selector,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return scopesForSelector.includes(selector.scope);
|
||||
});
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'TargetAccess:user',
|
||||
organization: selector.organizationId,
|
||||
project: selector.projectId,
|
||||
target: selector.targetId,
|
||||
user: selector.userId,
|
||||
scope: selector.scope,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
this.tokenAccess = new Dataloader(
|
||||
selectors =>
|
||||
Promise.all(
|
||||
selectors.map(async selector => {
|
||||
const tokenInfo = await this.organizationAccess.tokenInfo.load(selector);
|
||||
|
||||
if (
|
||||
tokenInfo?.organization === selector.organizationId &&
|
||||
tokenInfo?.project === selector.projectId &&
|
||||
tokenInfo?.target === selector.targetId
|
||||
) {
|
||||
return tokenInfo.scopes.includes(selector.scope);
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
),
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'TargetAccess:token',
|
||||
organization: selector.organizationId,
|
||||
project: selector.projectId,
|
||||
target: selector.targetId,
|
||||
token: selector.token,
|
||||
scope: selector.scope,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.scopes = new Dataloader(
|
||||
async selectors => {
|
||||
const scopesPerSelector = await this.organizationAccess.getAllScopes(selectors);
|
||||
|
||||
return selectors.map((selector, i) => {
|
||||
const scopes = scopesPerSelector[i];
|
||||
|
||||
if (scopes instanceof Error) {
|
||||
this.logger.warn(
|
||||
`TargetAccess:scopes (error=%s, selector=%o)`,
|
||||
scopes.message,
|
||||
selector,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return scopes.filter(isTargetScope);
|
||||
});
|
||||
},
|
||||
{
|
||||
cacheKeyFn(selector) {
|
||||
return JSON.stringify({
|
||||
type: 'TargetAccess:scopes',
|
||||
organization: selector.organizationId,
|
||||
user: selector.userId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async ensureAccessForToken(selector: TargetTokenAccessSelector): Promise<void | never> {
|
||||
const canAccess = await this.tokenAccess.load(selector);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new AccessError(`Missing ${selector.scope} permission`);
|
||||
}
|
||||
}
|
||||
|
||||
async ensureAccessForUser(selector: TargetUserAccessSelector): Promise<void | never> {
|
||||
const canAccess = await this.userAccess.load(selector);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new AccessError(`Missing ${selector.scope} permission`);
|
||||
}
|
||||
}
|
||||
|
||||
async getMemberScopes(selector: TargetUserScopesSelector) {
|
||||
return this.scopes.load(selector);
|
||||
}
|
||||
|
||||
resetAccessCache() {
|
||||
this.userAccess.clearAll();
|
||||
this.tokenAccess.clearAll();
|
||||
this.scopes.clearAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { OrganizationAccessScope as OrganizationAccessScopeEnum } from '../providers/organization-access';
|
||||
import { OrganizationAccessScope as OrganizationAccessScopeEnum } from '../providers/scopes';
|
||||
import type { OrganizationAccessScopeResolvers } from './../../../__generated__/types';
|
||||
|
||||
export const OrganizationAccessScope: OrganizationAccessScopeResolvers = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ProjectAccessScope as ProjectAccessScopeEnum } from '../providers/project-access';
|
||||
import { ProjectAccessScope as ProjectAccessScopeEnum } from '../providers/scopes';
|
||||
import type { ProjectAccessScopeResolvers } from './../../../__generated__/types';
|
||||
|
||||
export const ProjectAccessScope: ProjectAccessScopeResolvers = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TargetAccessScope as TargetAccessScopeEnum } from '../providers/target-access';
|
||||
import { TargetAccessScope as TargetAccessScopeEnum } from '../providers/scopes';
|
||||
import type { TargetAccessScopeResolvers } from './../../../__generated__/types';
|
||||
|
||||
export const TargetAccessScope: TargetAccessScopeResolvers = {
|
||||
|
|
|
|||
|
|
@ -188,7 +188,6 @@ export class OrganizationManager {
|
|||
});
|
||||
|
||||
// Because we checked the access before, it's stale by now
|
||||
this.authManager.resetAccessCache();
|
||||
this.session.reset();
|
||||
|
||||
return {
|
||||
|
|
@ -329,7 +328,6 @@ export class OrganizationManager {
|
|||
await this.tokenStorage.invalidateTokens(deletedOrganization.tokens);
|
||||
|
||||
// Because we checked the access before, it's stale by now
|
||||
this.authManager.resetAccessCache();
|
||||
this.session.reset();
|
||||
|
||||
await this.auditLog.record({
|
||||
|
|
@ -609,7 +607,6 @@ export class OrganizationManager {
|
|||
});
|
||||
|
||||
// Because we checked the access before, it's stale by now
|
||||
this.authManager.resetAccessCache();
|
||||
this.session.reset();
|
||||
|
||||
await Promise.all([
|
||||
|
|
@ -791,7 +788,6 @@ export class OrganizationManager {
|
|||
});
|
||||
|
||||
// Because we checked the access before, it's stale by now
|
||||
this.authManager.resetAccessCache();
|
||||
this.session.reset();
|
||||
|
||||
await this.auditLog.record({
|
||||
|
|
@ -994,7 +990,6 @@ export class OrganizationManager {
|
|||
});
|
||||
|
||||
// Access cache is stale by now
|
||||
this.authManager.resetAccessCache();
|
||||
this.session.reset();
|
||||
|
||||
const previousMemberRole = previousMembership.assignedRole.role ?? null;
|
||||
|
|
@ -1109,7 +1104,6 @@ export class OrganizationManager {
|
|||
});
|
||||
|
||||
// Access cache is stale by now
|
||||
this.authManager.resetAccessCache();
|
||||
this.session.reset();
|
||||
|
||||
await this.auditLog.record({
|
||||
|
|
|
|||
|
|
@ -34,9 +34,11 @@ import type {
|
|||
TargetSettings,
|
||||
User,
|
||||
} from '../../../shared/entities';
|
||||
import type { OrganizationAccessScope } from '../../auth/providers/organization-access';
|
||||
import type { ProjectAccessScope } from '../../auth/providers/project-access';
|
||||
import type { TargetAccessScope } from '../../auth/providers/target-access';
|
||||
import type {
|
||||
OrganizationAccessScope,
|
||||
ProjectAccessScope,
|
||||
TargetAccessScope,
|
||||
} from '../../auth/providers/scopes';
|
||||
import type { Contracts } from '../../schema/providers/contracts';
|
||||
import type { SchemaCoordinatesDiffResult } from '../../schema/providers/inspector';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { HiveError } from '../../../shared/errors';
|
|||
import { pushIfMissing } from '../../../shared/helpers';
|
||||
import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder';
|
||||
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';
|
||||
import {
|
||||
OrganizationAccessScope,
|
||||
ProjectAccessScope,
|
||||
TargetAccessScope,
|
||||
} from '../../auth/providers/scopes';
|
||||
import { OrganizationMembers } from '../../organization/providers/organization-members';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { Storage, TargetSelector } from '../../shared/providers/storage';
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { createTRPCProxyClient, httpLink } from '@trpc/client';
|
|||
import type { Token } from '../../../shared/entities';
|
||||
import { HiveError } from '../../../shared/errors';
|
||||
import { atomic } from '../../../shared/helpers';
|
||||
import type { OrganizationAccessScope } from '../../auth/providers/organization-access';
|
||||
import type { ProjectAccessScope } from '../../auth/providers/project-access';
|
||||
import type { TargetAccessScope } from '../../auth/providers/target-access';
|
||||
import type {
|
||||
OrganizationAccessScope,
|
||||
ProjectAccessScope,
|
||||
TargetAccessScope,
|
||||
} from '../../auth/providers/scopes';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import type { TargetSelector } from '../../shared/providers/storage';
|
||||
import type { TokensConfig } from './tokens';
|
||||
|
|
|
|||
Loading…
Reference in a new issue