Skip to content
Open
70 changes: 70 additions & 0 deletions src/casl/abilities/users.ability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
AbilityBuilder,
ExtractSubjectType,
MongoAbility,
createMongoAbility,
} from "@casl/ability";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AccessGroupsType } from "src/config/configuration";
import { Action } from "../action.enum";
import {
Subjects,
PossibleAbilities,
Conditions,
} from "../types/casl-subjects";
import { JWTUser } from "src/auth/interfaces/jwt-user.interface";
import { User } from "src/users/schemas/user.schema";

@Injectable()
export class UserAbility {
constructor(private configService: ConfigService) {
this.accessGroups =
this.configService.get<AccessGroupsType>("accessGroups");
}
private accessGroups;

buildAbility(user: JWTUser): MongoAbility<PossibleAbilities, Conditions> {
const { can, build } = new AbilityBuilder(
createMongoAbility<PossibleAbilities, Conditions>,
);

/**
* Unauthenticated user
*/
if (!user) {
return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}

const matchUid = { _id: user._id };

/**
* Authenticated user
*/
can(Action.UserCreate, User, matchUid);
can(Action.UserRead, User, matchUid);
can(Action.UserUpdate, User, matchUid);
can(Action.UserDelete, User, matchUid);

if (user.currentGroups.some((g) => this.accessGroups?.admin.includes(g))) {
/**
* User belonging to ADMIN_GROUPS
*/
can(Action.AccessAny, User);

can(Action.UserCreate, User);
can(Action.UserRead, User);
can(Action.UserUpdate, User);
can(Action.UserDelete, User);
can(Action.UserCreateJwt, User);
}

return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
}
20 changes: 7 additions & 13 deletions src/casl/action.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export enum Action {
// Currently used by addAccessBasedFilters for admin/special group users
AccessAny = "access_any",

// Users
UserCreate = "user_create",
UserRead = "user_read",
UserUpdate = "user_update",
UserDelete = "user_delete",
UserCreateJwt = "user_create_jwt",

// ---------------
// Datasets
// endpoint authorization actions
Expand Down Expand Up @@ -217,19 +224,6 @@ export enum Action {
JobUpdateAny = "job_update_any",
//JobDeleteAny = "job_delete_any",

// -------------
// Users actions
UserReadOwn = "user_read_own",
UserReadAny = "user_read_any",
UserCreateOwn = "user_create_own",
UserCreateAny = "user_create_any",
UserUpdateOwn = "user_update_own",
UserUpdateAny = "user_update_any",
UserDeleteOwn = "user_delete_own",
UserDeleteAny = "user_delete_any",
UserCreateJwt = "user_create_jwt",
UserListAll = "user_list_all",
UserListOwn = "user_list_own",
// Instrument actions
InstrumentRead = "instrument_read",
InstrumentUpdate = "instrument_update",
Expand Down
71 changes: 7 additions & 64 deletions src/casl/casl-ability.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import { ProposalClass } from "src/proposals/schemas/proposal.schema";
import { PublishedData } from "src/published-data/schemas/published-data.schema";
import { RuntimeConfig } from "src/config/runtime-config/schemas/runtime-config.schema";
import { SampleClass } from "src/samples/schemas/sample.schema";
import { User } from "src/users/schemas/user.schema";
import { Action } from "./action.enum";
import { Subjects, PossibleAbilities, Conditions } from "./types/casl-subjects";
import { UserAbility } from "./abilities/users.ability";

export type AppAbility = MongoAbility<PossibleAbilities, Conditions>;

Expand All @@ -37,6 +37,7 @@ export class CaslAbilityFactory {
constructor(
private configService: ConfigService,
private jobConfigService: JobConfigService,
private userAbility: UserAbility,
) {
this.accessGroups =
this.configService.get<AccessGroupsType>("accessGroups");
Expand All @@ -61,7 +62,7 @@ export class CaslAbilityFactory {
publisheddata: this.publishedDataEndpointAccess,
runtimeconfig: this.runtimeConfigEndpointAccess,
samples: this.samplesEndpointAccess,
users: this.userEndpointAccess,
users: this.userAccess,
};

endpointAccess(endpoint: string, user: JWTUser) {
Expand All @@ -74,6 +75,10 @@ export class CaslAbilityFactory {
return accessFunction.call(this, user);
}

userAccess(user: JWTUser) {
return this.userAbility.buildAbility(user);
}

datasetEndpointAccess(user: JWTUser) {
const { can, cannot, build } = new AbilityBuilder(
createMongoAbility<PossibleAbilities, Conditions>,
Expand Down Expand Up @@ -1202,68 +1207,6 @@ export class CaslAbilityFactory {
});
}

userEndpointAccess(user: JWTUser) {
const { can, cannot, build } = new AbilityBuilder(
createMongoAbility<PossibleAbilities, Conditions>,
);

if (!user) {
/**
/* unauthenticated users
**/

cannot(Action.UserReadOwn, User);
cannot(Action.UserCreateOwn, User);
cannot(Action.UserUpdateOwn, User);
cannot(Action.UserDeleteOwn, User);
cannot(Action.UserReadAny, User);
cannot(Action.UserCreateAny, User);
cannot(Action.UserUpdateAny, User);
cannot(Action.UserDeleteAny, User);
} else {
if (
user.currentGroups.some((g) => this.accessGroups?.admin.includes(g))
) {
/*
/ user that belongs to any of the group listed in ADMIN_GROUPS
*/

// can(Action.ReadAll, UserIdentity); NOT used?

// -------------------------------------
// user endpoint, including useridentity
can(Action.UserReadAny, User);
can(Action.UserReadOwn, User);
can(Action.UserCreateAny, User);
can(Action.UserUpdateAny, User);
can(Action.UserDeleteAny, User);
can(Action.UserCreateJwt, User);
can(Action.UserListAll, User);

// -------------------------------------
} else if (user) {
/**
/* authenticated users
**/
cannot(Action.UserReadAny, User);
cannot(Action.UserCreateAny, User);
cannot(Action.UserUpdateAny, User);
cannot(Action.UserDeleteAny, User);
cannot(Action.UserCreateJwt, User);
cannot(Action.UserListAll, User);
}
can(Action.UserReadOwn, User, { _id: user._id });
can(Action.UserCreateOwn, User, { _id: user._id });
can(Action.UserUpdateOwn, User, { _id: user._id });
can(Action.UserDeleteOwn, User, { _id: user._id });
can(Action.UserListOwn, User);
}
return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}

datasetInstanceAccess(user: JWTUser) {
const { can, build } = new AbilityBuilder(
createMongoAbility<PossibleAbilities, Conditions>,
Expand Down
4 changes: 3 additions & 1 deletion src/casl/casl.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { CaslAbilityFactory } from "./casl-ability.factory";
import { JobConfigModule } from "src/config/job-config/jobconfig.module";
import { UserAbility } from "./abilities/users.ability";

@Module({
imports: [JobConfigModule, ConfigModule],
providers: [CaslAbilityFactory],
providers: [CaslAbilityFactory, UserAbility],
exports: [CaslAbilityFactory],
})
export class CaslModule {}
20 changes: 8 additions & 12 deletions src/users/user-identities.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,8 @@ export class UserIdentitiesController {
) {}

@UseGuards(AuthenticatedPoliciesGuard)
@CheckPolicies(
"users",
(ability: AppAbility) =>
ability.can(Action.UserReadOwn, User) ||
ability.can(Action.UserReadAny, User),
@CheckPolicies("users", (ability: AppAbility) =>
ability.can(Action.UserRead, User),
)
@Get("/findOne")
@ApiQuery({
Expand Down Expand Up @@ -83,12 +80,11 @@ export class UserIdentitiesController {
}

const authenticatedUser: JWTUser = request.user as JWTUser;
const ability =
await this.caslAbilityFactory.userEndpointAccess(authenticatedUser);
const ability = this.caslAbilityFactory.userAccess(authenticatedUser);

if (
!ability.can(Action.UserReadAny, User) &&
ability.can(Action.UserReadOwn, User)
!ability.can(Action.AccessAny, User) &&
ability.can(Action.UserRead, User)
) {
// this user can only see his/her user identity
filter = { userId: authenticatedUser._id, ...filter };
Expand All @@ -106,8 +102,8 @@ export class UserIdentitiesController {
user._id = identity.userId;
user.id = identity.userId;
if (
!ability.can(Action.UserReadOwn, user) &&
!ability.can(Action.UserReadAny, User)
!ability.can(Action.UserRead, user) &&
!ability.can(Action.AccessAny, User)
) {
throw new ForbiddenException("Access Forbidden or Unauthorized");
}
Expand All @@ -117,7 +113,7 @@ export class UserIdentitiesController {

@UseGuards(AuthenticatedPoliciesGuard)
@CheckPolicies("users", (ability: AppAbility) =>
ability.can(Action.UserReadAny, User),
ability.can(Action.AccessAny, User),
)
@Get("/isValidEmail")
@ApiOperation({
Expand Down
Loading
Loading