Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: role hierarchy #160

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
29 changes: 25 additions & 4 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,31 @@

intelli-mate was created with the standalone platform format in mind. In order to be easily customized and adopted by a multitude of companies, it needs a well-defined guide to deploy this in the most common cloud providers.

The product is cloud native ready by leveraging kubernetes, meaning that you won't have any issues with automatic scaling due to traffic spikes or any other motives.
The product is cloud native ready by leveraging kubernetes, meaning that you won't have any issues with automatic scaling due to traffic spikes or any other motives and that you won't have issues with which cloud provider to use as the solution for deployment is very cloud platform agnostic.

## Azure
## Dependencies

To deploy the solution there are certain pre-requisites in terms of infrastructure that you need to have setup in your favorite cloud provider:

- Helm
- Redis
- MongoDB
- File storage volume

## How to deploy

Most of the setup is done in the helm charts `values` files, e.g: `/helm/intelli-mate-api/values/production.yaml`.
The default values can always be checked in the `/helm/intelli-mate-api/values.yaml` as any other helm chart, but should be overriden in the specific environments like `production`, `staging`, etc.

### Secrets



### File storage persistence

To leverage a shared file storage between the API pods, we need a volume and to create a volume in your cloud provider, we need a storage class.

E.g:
> With a terminal that has access to your k8s cluster, you enter `kubectl get storageclasses.storage.k8s.io` and you should be able to replace that value in the `values.persistence.storageClass` property.

This is the cloud provider that xgeeks uses and thus it will serve as the starting point to guide other companies to deploy.

TODO:
24 changes: 21 additions & 3 deletions packages/api/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,27 @@
},
"authorization": {
"roles": [
"Admin",
"Collaborator"
{
"key": "C-Level",
"priority": 1
},
{
"key": "PX",
"priority": 1
},
{
"key": "Manager",
"priority": 1
},
{
"key": "Staff",
"priority": 2
},
{
"key": "Engineer",
"priority": 3
}
],
"defaultRole": "Collaborator"
"defaultRole": "Engineer"
}
}
5 changes: 3 additions & 2 deletions packages/api/src/app-config/app-config.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppConfigNotFoundException } from '@/app-config/exceptions/app-config-not-found.exception';
import { Role } from '@/common/types/user-roles';
import { Injectable } from '@nestjs/common';
import * as appConfig from 'config';

Expand All @@ -15,12 +16,12 @@ export class AppConfigService {

return appConfig.get<AiAppConfig>('ai');
}
async getAppRoles(): Promise<string[]> {
async getAppRoles(): Promise<Role[]> {
if (!appConfig.has('authorization.roles')) {
throw new AppConfigNotFoundException();
}

return Promise.resolve(appConfig.get<string[]>('authorization.roles'));
return Promise.resolve(appConfig.get<Role[]>('authorization.roles'));
}

getDefaultRole(): string {
Expand Down
14 changes: 12 additions & 2 deletions packages/api/src/auth/auth.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DB_USER_ROLES_MODEL_KEY } from '@/common/constants/models/user-roles';
import { UserRoles } from '@/common/types/user-roles';
import { Role, UserRoles } from '@/common/types/user-roles';
import { Inject, Injectable } from '@nestjs/common';
import { Model } from 'mongoose';

Expand All @@ -19,8 +19,18 @@ export class AuthRepository {
return this.userRolesModel.find({ userId: { $in: userIds } });
}

async assignRolesToUser(userId: string, roles: string[]): Promise<UserRoles> {
async assignRolesToUser(
userId: string,
rolesToAssign: string[],
allRoles: Role[]
): Promise<UserRoles> {
const userRoles: UserRoles = await this.userRolesModel.findOne({ userId });
const roles: Role[] = [];

for (const roleToAssign of rolesToAssign) {
roles.push(allRoles.find((role) => role.key === roleToAssign));
}

if (userRoles) {
userRoles.roles = roles;
await userRoles.save();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ export class ClerkAuthUserProvider implements AuthProvider {
}

async assignRolesToUser(id: string, roles: string[]): Promise<void> {
await this.authRepository.assignRolesToUser(id, roles);
const allRoles = await this.appConfigService.getAppRoles();
await this.authRepository.assignRolesToUser(id, roles, allRoles);
}

async assignDefaultRoleToUser(id: string): Promise<void> {
const allRoles = await this.appConfigService.getAppRoles();
const defaultRole = this.appConfigService.getDefaultRole();
await this.authRepository.assignRolesToUser(id, [defaultRole]);
await this.authRepository.assignRolesToUser(id, [defaultRole], allRoles);
}
}
16 changes: 15 additions & 1 deletion packages/api/src/auth/schemas/user-roles.mongoose.schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import * as mongoose from 'mongoose';

const RoleSchema = new mongoose.Schema(
{
key: {
type: String,
required: true,
},
priority: {
type: Number,
required: true,
},
},
{ _id: false, required: true }
);

export const UserRolesSchema = new mongoose.Schema({
userId: {
type: String,
index: true,
required: true,
},
roles: {
type: [String],
type: [RoleSchema],
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ export class AdminUpdateUserRolesUsecase implements Usecase {
superAdminUpdateUserRoleDto: SuperAdminUpdateUserRoleRequestDto
): Promise<User> {
const roles = await this.appConfigService.getAppRoles();
const roleKeys = roles.map((role) => role.key);

if (
!superAdminUpdateUserRoleDto.roles.every((element) =>
roles.includes(element)
roleKeys.includes(element)
)
) {
throw new RolesNotConfiguredException();
}

await this.authRepository.assignRolesToUser(
superAdminUpdateUserRoleDto.userId,
superAdminUpdateUserRoleDto.roles
superAdminUpdateUserRoleDto.roles,
roles
);

return this.clerkAuthUserProvider.findUser(
Expand Down
7 changes: 6 additions & 1 deletion packages/api/src/common/types/user-roles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Document } from 'mongoose';

export interface Role {
key: string;
priority: number;
}

export interface UserRoles extends Document {
userId: string;
roles: string[];
roles: Role[];
}
4 changes: 3 additions & 1 deletion packages/api/src/common/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Role } from '@/common/types/user-roles';

type EmailAddress = {
readonly id: string;
readonly emailAddress: string;
Expand All @@ -11,5 +13,5 @@ export type User = {
readonly firstName: string | null;
readonly lastName: string | null;
readonly emailAddresses: EmailAddress[];
readonly roles: string[];
readonly roles: Role[];
};
7 changes: 6 additions & 1 deletion packages/contract/auth/user.response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@ export const UserResponseSchema = z.object({
firstName: z.string().nullable(),
lastName: z.string().nullable(),
emailAddresses: z.array(EmailAddressResponseSchema),
roles: z.array(z.string()),
roles: z.array(
z.object({
key: z.string(),
priority: z.number(),
})
),
});
23 changes: 22 additions & 1 deletion packages/web-ui/app-config/roles.ts
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
export const roles = ['Admin', 'Collaborator'] as const;
export const roles = [
{
key: 'C-Level',
priority: 1,
},
{
key: 'PX',
priority: 1,
},
{
key: 'Manager',
priority: 1,
},
{
key: 'Staff',
priority: 2,
},
{
key: 'Engineer',
priority: 3,
},
] as const;
7 changes: 5 additions & 2 deletions packages/web-ui/app/admin/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,15 @@ export default async function AdminUsers() {
</TableCell>
<TableCell>{getUserEmail(user)}</TableCell>
<TableCell>
{user.roles.sort(alphaAscSortPredicate).join(', ')}
{user.roles
.map((role) => role.key)
.sort(alphaAscSortPredicate)
.join(', ')}
</TableCell>
<TableCell className="text-right">
<UpdateRolesForm
userId={user.id}
defaultRoles={user.roles}
defaultRoles={user.roles.map((role) => role.key)}
/>
</TableCell>
</TableRow>
Expand Down
2 changes: 1 addition & 1 deletion packages/web-ui/components/admin/update-roles-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface UpdateRolesFormProps {
defaultRoles: string[];
}

const roles = getAppRoles().map((role) => ({ id: role, label: role }));
const roles = getAppRoles().map((role) => ({ id: role.key, label: role.key }));

export function UpdateRolesForm({
userId,
Expand Down