Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions libs/database/src/lib/constants/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Dictionary } from "lodash";

// NOTE: When making changes to the roles and permissions assignments, this command must be run
// in the REPL for the change to take effect:
// > await Permission.syncPermissions();

export const PERMISSIONS = {
"framework-ppc": "Framework PPC",
"framework-terrafund": "Framework Terrafund",
"framework-enterprises": "Framework Terrafund Enterprises",
"framework-terrafund-landscapes": "Framework Terrafund Landscapes",
"framework-hbf": "Framework Harit Bharat Fund",
"framework-epa-ghana-pilot": "Framework EPA Ghana Pilot",
"framework-fundo-flora": "Framework Fundo Flora",
"custom-forms-manage": "Manage custom forms",
"users-manage": "Manage users",
"monitoring-manage": "Manage monitoring",
"reports-manage": "Manage Reports",
"manage-own": "Manage own",
"projects-read": "Read all projects",
"polygons-manage": "Manage polygons",
"media-manage": "Manage media",
"view-dashboard": "View dashboard",
"projects-manage": "Manage projects"
} as const;

export type Permission = keyof typeof PERMISSIONS;

export const ROLES: Dictionary<Permission[]> = {
"admin-super": [
"framework-terrafund",
"framework-ppc",
"framework-enterprises",
"framework-terrafund-landscapes",
"framework-hbf",
"framework-epa-ghana-pilot",
"framework-fundo-flora",
"custom-forms-manage",
"users-manage",
"monitoring-manage",
"reports-manage"
],
"admin-ppc": ["framework-ppc", "custom-forms-manage", "users-manage", "monitoring-manage", "reports-manage"],
"admin-terrafund": [
"framework-terrafund",
"framework-enterprises",
"framework-terrafund-landscapes",
"custom-forms-manage",
"users-manage",
"monitoring-manage",
"reports-manage"
],
"admin-hbf": ["framework-hbf", "custom-forms-manage", "users-manage", "monitoring-manage", "reports-manage"],
"admin-epa-ghana-pilot": [
"framework-epa-ghana-pilot",
"custom-forms-manage",
"users-manage",
"monitoring-manage",
"reports-manage"
],
"admin-fundo-floral": [
"framework-fundo-flora",
"custom-forms-manage",
"users-manage",
"monitoring-manage",
"reports-manage"
],
"project-developer": ["manage-own"],
"project-manager": ["projects-manage"],
"greenhouse-service-account": ["projects-read", "polygons-manage", "media-manage"],
"research-service-account": ["projects-read", "polygons-manage"],
government: ["view-dashboard"],
funder: ["view-dashboard"]
};
1 change: 1 addition & 0 deletions libs/database/src/lib/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export * from "./project-polygon.entity";
export * from "./project-report.entity";
export * from "./project-user.entity";
export * from "./role.entity";
export * from "./role-has-permission.entity";
export * from "./scheduled-job.entity";
export * from "./seeding.entity";
export * from "./site.entity";
Expand Down
87 changes: 82 additions & 5 deletions libs/database/src/lib/entities/permission.entity.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript";
import { BIGINT, QueryTypes, STRING } from "sequelize";
import { BIGINT, CreationOptional, InferAttributes, InferCreationAttributes, Op, QueryTypes, STRING } from "sequelize";
import { User } from "./user.entity";
import { PERMISSIONS, ROLES, Permission as PermissionName } from "../constants/permissions";
import { Role } from "./role.entity";
import { RoleHasPermission } from "./role-has-permission.entity";
import { flatten, uniq } from "lodash";

@Table({ tableName: "permissions", underscored: true })
export class Permission extends Model<Permission> {
export class Permission extends Model<InferAttributes<Permission>, InferCreationAttributes<Permission>> {
@PrimaryKey
@AutoIncrement
@Column(BIGINT.UNSIGNED)
override id: number;
override id: CreationOptional<number>;

@Column(STRING)
name: string;

@Column(STRING)
guardName: string;
@Column({ type: STRING, defaultValue: "api" })
guardName: CreationOptional<string>;

/**
* Gets the list of permission names that the given user has access to through the roles that are
Expand Down Expand Up @@ -43,4 +47,77 @@ export class Permission extends Model<Permission> {

return permissions?.map(({ name }) => name) ?? [];
}

/**
* Syncs the Role / Permissions defined in permissions.ts with what's in the DB. For now, the DB
* record is the source of truth, and the configuration in permissions.ts is only referenced for
* this sync process.
*
* Once we have fully decommissioned the PHP codebase, it may make sense to drop the permissions
* table and simplify the system by only assigning roles to users and letting the configuration
* dictate which permissions they then have access to.
*/
public static async syncPermissions() {
// First, check that all the permissions specified in the ROLES constant are included in PERMISSIONS
const configRolePermissions = uniq(flatten(Object.values(ROLES)));
const missingConfigPermissions = configRolePermissions.filter(
permission => !Object.keys(PERMISSIONS).includes(permission)
);
if (missingConfigPermissions.length > 0) {
throw new Error(
`Some roles have permissions that do not exist in the permissions config [${missingConfigPermissions.join(
", "
)}]`
);
}

const dbPermissionNames = (await Permission.findAll({ attributes: ["name"] })).map(({ name }) => name);
const configPermissionNames = Object.keys(PERMISSIONS);
const permissionsToAdd = configPermissionNames.filter(permission => !dbPermissionNames.includes(permission));
if (permissionsToAdd.length > 0) {
await Permission.bulkCreate(permissionsToAdd.map(name => ({ name })));
}

const permissionsToRemove = dbPermissionNames.filter(permission => !configPermissionNames.includes(permission));
if (permissionsToRemove.length > 0) {
await Permission.destroy({ where: { name: permissionsToRemove } });
}

// these tables are all tiny, so let's just fetch all the data, figure it out in memory and then sync to the DB.
const dbRoles = await Role.findAll();
const dbPermissions = await Permission.findAll();
const rolePermissions = await RoleHasPermission.findAll();
const rolesSynced: string[] = [];
for (const [role, permissions] of Object.entries(ROLES)) {
rolesSynced.push(role);

let dbRole = dbRoles.find(({ name }) => name === role);
if (dbRole == null) {
dbRole = await Role.create({ name: role });
}

const currentPermissions = rolePermissions.filter(({ roleId }) => roleId === dbRole.id);
const requiredPermissions = dbPermissions.filter(({ name }) => permissions.includes(name as PermissionName));
const rolePermissionsToAdd = requiredPermissions.filter(
({ id }) => currentPermissions.find(({ permissionId }) => permissionId === id) == null
);
if (rolePermissionsToAdd.length > 0) {
await RoleHasPermission.bulkCreate(
rolePermissionsToAdd.map(({ id }) => ({ roleId: dbRole.id, permissionId: id }))
);
}

if (currentPermissions.length + rolePermissionsToAdd.length !== requiredPermissions.length) {
await RoleHasPermission.destroy({
where: { roleId: dbRole.id, permissionId: { [Op.notIn]: requiredPermissions.map(({ id }) => id) } }
});
}
}

const rolesToRemove = dbRoles.filter(({ name }) => !rolesSynced.includes(name)).map(({ id }) => id as number);
if (rolesToRemove.length > 0) {
await RoleHasPermission.destroy({ where: { roleId: rolesToRemove } });
await Role.destroy({ where: { id: rolesToRemove } });
}
}
}
16 changes: 16 additions & 0 deletions libs/database/src/lib/entities/role-has-permission.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Column, Model, PrimaryKey, Table } from "sequelize-typescript";
import { BIGINT, InferAttributes, InferCreationAttributes } from "sequelize";

@Table({ tableName: "role_has_permissions", underscored: true, timestamps: false })
export class RoleHasPermission extends Model<
InferAttributes<RoleHasPermission>,
InferCreationAttributes<RoleHasPermission>
> {
@PrimaryKey
@Column(BIGINT.UNSIGNED)
roleId!: number;

@PrimaryKey
@Column(BIGINT.UNSIGNED)
permissionId!: number;
}
10 changes: 5 additions & 5 deletions libs/database/src/lib/entities/role.entity.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript";
import { BIGINT, STRING } from "sequelize";
import { BIGINT, CreationOptional, InferAttributes, InferCreationAttributes, STRING } from "sequelize";

@Table({ tableName: "roles", underscored: true })
export class Role extends Model<Role> {
export class Role extends Model<InferAttributes<Role>, InferCreationAttributes<Role>> {
@PrimaryKey
@AutoIncrement
@Column(BIGINT.UNSIGNED)
override id: number;
override id: CreationOptional<number>;

@Column(STRING)
name: string;

@Column(STRING)
guardName: string;
@Column({ type: STRING, defaultValue: "api" })
guardName: CreationOptional<string>;
}