1043 timebox prepare zapier integration (#1967)

* Add create api-key route

* Import module

* Remove required mutation parameter

* Fix Authentication

* Generate random key

* Update Read ApiKeyAbility handler

* Add findMany apiKey route

* Remove useless attribute

* Use signed token for apiKeys

* Authenticate with api keys

* Fix typo

* Add a test for apiKey module

* Revoke token when api key does not exist

* Handler expiresAt parameter

* Fix user passport

* Code review returns: Add API_TOKEN_SECRET

* Code review returns: Rename variable

* Code review returns: Update code style

* Update apiKey schema

* Update create token route

* Update delete token route

* Filter revoked api keys from listApiKeys

* Rename endpoint

* Set default expiry to 2 years

* Code review returns: Update comment

* Generate token after create apiKey

* Code review returns: Update env variable

* Code review returns: Move method to proper service

---------

Co-authored-by: martmull <martmull@hotmail.com>
This commit is contained in:
martmull
2023-10-12 18:07:44 +02:00
committed by GitHub
parent 6b990c8501
commit 8fbad7d3ba
20 changed files with 430 additions and 42 deletions

View File

@ -6,6 +6,7 @@ import {
Activity,
ActivityTarget,
Attachment,
ApiKey,
Comment,
Company,
Favorite,
@ -30,6 +31,7 @@ type SubjectsAbility = Subjects<{
Activity: Activity;
ActivityTarget: ActivityTarget;
Attachment: Attachment;
ApiKey: ApiKey;
Comment: Comment;
Company: Company;
Favorite: Favorite;
@ -55,7 +57,7 @@ export type AppAbility = PureAbility<
@Injectable()
export class AbilityFactory {
defineAbility(user: User, workspace: Workspace) {
defineAbility(workspace: Workspace, user?: User) {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
createPrismaAbility,
);
@ -66,8 +68,18 @@ export class AbilityFactory {
workspaceId: workspace.id,
},
});
can(AbilityAction.Update, 'User', { id: user.id });
can(AbilityAction.Delete, 'User', { id: user.id });
if (user) {
can(AbilityAction.Update, 'User', { id: user.id });
can(AbilityAction.Delete, 'User', { id: user.id });
} else {
cannot(AbilityAction.Update, 'User');
cannot(AbilityAction.Delete, 'User');
}
// ApiKey
can(AbilityAction.Read, 'ApiKey', { workspaceId: workspace.id });
can(AbilityAction.Create, 'ApiKey');
can(AbilityAction.Update, 'ApiKey', { workspaceId: workspace.id });
// Workspace
can(AbilityAction.Read, 'Workspace');
@ -76,12 +88,19 @@ export class AbilityFactory {
// Workspace Member
can(AbilityAction.Read, 'WorkspaceMember', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'WorkspaceMember', { workspaceId: workspace.id });
cannot(AbilityAction.Delete, 'WorkspaceMember', { userId: user.id });
can(AbilityAction.Update, 'WorkspaceMember', {
userId: user.id,
workspaceId: workspace.id,
});
if (user) {
can(AbilityAction.Delete, 'WorkspaceMember', {
workspaceId: workspace.id,
});
cannot(AbilityAction.Delete, 'WorkspaceMember', { userId: user.id });
can(AbilityAction.Update, 'WorkspaceMember', {
userId: user.id,
workspaceId: workspace.id,
});
} else {
cannot(AbilityAction.Delete, 'WorkspaceMember');
cannot(AbilityAction.Update, 'WorkspaceMember');
}
// Company
can(AbilityAction.Read, 'Company', { workspaceId: workspace.id });
@ -107,14 +126,19 @@ export class AbilityFactory {
// Comment
can(AbilityAction.Read, 'Comment', { workspaceId: workspace.id });
can(AbilityAction.Create, 'Comment');
can(AbilityAction.Update, 'Comment', {
workspaceId: workspace.id,
authorId: user.id,
});
can(AbilityAction.Delete, 'Comment', {
workspaceId: workspace.id,
authorId: user.id,
});
if (user) {
can(AbilityAction.Update, 'Comment', {
workspaceId: workspace.id,
authorId: user.id,
});
can(AbilityAction.Delete, 'Comment', {
workspaceId: workspace.id,
authorId: user.id,
});
} else {
cannot(AbilityAction.Update, 'Comment');
cannot(AbilityAction.Delete, 'Comment');
}
// ActivityTarget
can(AbilityAction.Read, 'ActivityTarget');

View File

@ -122,6 +122,12 @@ import {
ReadViewFilterAbilityHandler,
UpdateViewFilterAbilityHandler,
} from './handlers/view-filter.ability-handler';
import {
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
ManageApiKeyAbilityHandler,
ReadApiKeyAbilityHandler,
} from './handlers/api-key.ability-handler';
@Global()
@Module({
@ -229,6 +235,11 @@ import {
CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
// ApiKey
ReadApiKeyAbilityHandler,
ManageApiKeyAbilityHandler,
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
],
exports: [
AbilityFactory,
@ -333,6 +344,11 @@ import {
CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
// ApiKey
ReadApiKeyAbilityHandler,
ManageApiKeyAbilityHandler,
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
],
})
export class AbilityModule {}

View File

@ -0,0 +1,85 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { AppAbility } from 'src/ability/ability.factory';
import { AbilityAction } from 'src/ability/ability.action';
import { PrismaService } from 'src/database/prisma.service';
import { ApiKeyWhereUniqueInput } from 'src/core/@generated/api-key/api-key-where-unique.input';
import { ApiKeyWhereInput } from 'src/core/@generated/api-key/api-key-where.input';
import { assert } from 'src/utils/assert';
import {
convertToWhereInput,
relationAbilityChecker,
} from 'src/ability/ability.util';
class ApiKeyArgs {
where?: ApiKeyWhereUniqueInput | ApiKeyWhereInput;
[key: string]: any;
}
@Injectable()
export class ManageApiKeyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Manage, 'ApiKey');
}
}
@Injectable()
export class ReadApiKeyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'ApiKey');
}
}
@Injectable()
export class CreateApiKeyAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'ApiKey',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'ApiKey');
}
}
@Injectable()
export class UpdateApiKeyAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ApiKeyArgs>();
const where = convertToWhereInput(args.where);
const apiKey = await this.prismaService.client.apiKey.findFirst({
where,
});
assert(apiKey, '', NotFoundException);
const allowed = await relationAbilityChecker(
'ApiKey',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('ApiKey', apiKey));
}
}