Skip to content

Commit

Permalink
refactor(auth): type ability templates
Browse files Browse the repository at this point in the history
refs #247
  • Loading branch information
ygrishajev committed Aug 6, 2024
1 parent 45656d7 commit 2cdfafd
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 33 deletions.
48 changes: 25 additions & 23 deletions apps/api/src/auth/services/ability/ability.service.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
import { Ability } from "@casl/ability";
import { Ability, RawRule } from "@casl/ability";
import type { TemplateExecutor } from "lodash";
import template from "lodash/template";
import { singleton } from "tsyringe";

type Role = "REGULAR_USER" | "REGULAR_ANONYMOUS_USER" | "SUPER_USER";

@singleton()
export class AbilityService {
private readonly createRegularUserRules = template(
JSON.stringify([
readonly EMPTY_ABILITY = new Ability([]);

private readonly RULES: Record<Role, RawRule[]> = {
REGULAR_USER: [
{ action: ["create", "read", "sign"], subject: "UserWallet", conditions: { userId: "${user.id}" } },
{ action: "read", subject: "User", conditions: { id: "${user.id}" } }
])
);
private readonly createRegularAnonymousUserRules = template(
JSON.stringify([
],
REGULAR_ANONYMOUS_USER: [
{ action: ["create", "read", "sign"], subject: "UserWallet", conditions: { userId: "${user.id}" } },
{ action: "read", subject: "User", conditions: { id: "${user.id}", userId: null } }
])
);

getAbilityForUser(user: { userId: string }) {
const rules = this.createRegularUserRules({ user });
return new Ability(JSON.parse(rules));
}
{ action: "read", subject: "User", conditions: { id: "${user.id}" } }
],
SUPER_USER: [{ action: "manage", subject: "all" }]
};

getAbilityForAnonymousUser(user: { id: string }) {
const rules = this.createRegularAnonymousUserRules({ user });
return new Ability(JSON.parse(rules));
}
private readonly templates = (Object.keys(this.RULES) as Role[]).reduce(
(acc, role) => {
acc[role] = template(JSON.stringify(this.RULES[role]));
return acc;
},
{} as Record<Role, TemplateExecutor>
);

getEmptyAbility() {
return new Ability([]);
getAbilityFor(role: Role, user: { userId: string }) {
return this.toAbility(this.templates[role]({ user }));
}

getSuperUserAbility() {
return new Ability([{ action: "manage", subject: "all" }]);
private toAbility(raw: string) {
return new Ability(JSON.parse(raw));
}
}
8 changes: 3 additions & 5 deletions apps/api/src/auth/services/auth.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { singleton } from "tsyringe";

import { AbilityService } from "@src/auth/services/ability/ability.service";
import { AuthService } from "@src/auth/services/auth.service";
import { ExecutionContextService } from "@src/core/services/execution-context/execution-context.service";
import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type";
import { getCurrentUserId } from "@src/middlewares/userMiddleware";
import { UserRepository } from "@src/user/repositories";
Expand All @@ -13,7 +12,6 @@ export class AuthInterceptor implements HonoInterceptor {
constructor(
private readonly abilityService: AbilityService,
private readonly userRepository: UserRepository,
private readonly executionContextService: ExecutionContextService,
private readonly authService: AuthService
) {}

Expand All @@ -25,7 +23,7 @@ export class AuthInterceptor implements HonoInterceptor {
const currentUser = await this.userRepository.findByUserId(userId);

this.authService.currentUser = currentUser;
this.authService.ability = currentUser ? this.abilityService.getAbilityForUser(currentUser) : this.abilityService.getEmptyAbility();
this.authService.ability = currentUser ? this.abilityService.getAbilityFor("REGULAR_USER", currentUser) : this.abilityService.EMPTY_ABILITY;

return await next();
}
Expand All @@ -35,12 +33,12 @@ export class AuthInterceptor implements HonoInterceptor {
const currentUser = await this.userRepository.findAnonymousById(anonymousUserId);

this.authService.currentUser = currentUser;
this.authService.ability = currentUser ? this.abilityService.getAbilityForAnonymousUser(currentUser) : this.abilityService.getEmptyAbility();
this.authService.ability = currentUser ? this.abilityService.getAbilityFor("REGULAR_ANONYMOUS_USER", currentUser) : this.abilityService.EMPTY_ABILITY;

return await next();
}

this.authService.ability = this.abilityService.getEmptyAbility();
this.authService.ability = this.abilityService.EMPTY_ABILITY;

return await next();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class TxSignerService {

async signAndBroadcast(userId: UserWalletOutput["userId"], messages: StringifiedEncodeObject[]) {
const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findByUserId(userId);
assert(userWallet, 403);
assert(userWallet, 404, "UserWallet Not Found");

const decodedMessages = this.decodeMessages(messages);

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/lib/drizzle-ability/drizzle-ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class DrizzleAbility<T extends PgTableWithColumns<any>, A extends AnyAbil

const { $and = [], $or = [] } = rulesToQuery(this.ability, params[0], params[1], rule => {
if (!rule.ast) {
throw new Error("Unable to create knex query without AST");
throw new Error("Unable to create query without AST");
}

if (rule.inverted) {
Expand Down
6 changes: 3 additions & 3 deletions apps/api/test/functional/sign-and-broadcast-tx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe("Tx Sign", () => {
expect(await res.json()).toMatchObject({ error: "UnauthorizedError", message: "Unauthorized" });
});

it("should throw 403 provided a different user auth header", async () => {
it("should throw 404 provided a different user auth header", async () => {
const { user, wallet } = await walletService.createUserAndWallet();
const differentUserResponse = await app.request("/v1/anonymous-users", {
method: "POST",
Expand All @@ -60,8 +60,8 @@ describe("Tx Sign", () => {
headers: new Headers({ "Content-Type": "application/json", "x-anonymous-user-id": differentUser.id })
});

expect(res.status).toBe(403);
expect(await res.json()).toMatchObject({ error: "ForbiddenError", message: "Forbidden" });
expect(res.status).toBe(404);
expect(await res.json()).toMatchObject({ error: "NotFoundError", message: "UserWallet Not Found" });
});
});

Expand Down

0 comments on commit 2cdfafd

Please sign in to comment.