diff --git a/docs/developer-guides/iam-policies.md b/docs/services/IAM-service/iam-policies.md similarity index 75% rename from docs/developer-guides/iam-policies.md rename to docs/services/IAM-service/iam-policies.md index edbcea8..08a1891 100644 --- a/docs/developer-guides/iam-policies.md +++ b/docs/services/IAM-service/iam-policies.md @@ -18,7 +18,37 @@ A policy is a JSON object that consists of one or more statements. Each statemen - **`Action`**: A list of operations that the policy grants or denies permission to perform. Actions are formatted as `service:operation`. - **`Resource`**: A list of resources to which the actions apply. Resources are specified in a hierarchical format. Wildcards (`*`) can be used. -## 2. Actions and Resources by Service +## 2. Wildcard Support + +Our IAM system supports wildcards (`*`) in both `Action` and `Resource` fields to provide flexible permission management, as defined in the `PolicyValidator`. + +### Action Wildcards + +You can use wildcards to grant broad permissions for actions: + +- **Global Wildcard (`*`)**: A standalone `*` in the `Action` field grants permission for all possible actions across all services. + ```json + "Action": ["*"] + ``` +- **Service-Level Wildcard (`service:*`)**: A wildcard at the end of an action string grants permission for all actions within that specific service. + ```json + "Action": ["archive:*"] + ``` + +### Resource Wildcards + +Wildcards can also be used to specify resources: + +- **Global Wildcard (`*`)**: A standalone `*` in the `Resource` field applies the policy to all resources in the system. + ```json + "Resource": ["*"] + ``` +- **Partial Wildcards**: Some services allow wildcards at specific points in the resource path to refer to all resources of a certain type. For example, to target all ingestion sources: + ```json + "Resource": ["ingestion-source/*"] + ``` + +## 3. Actions and Resources by Service The following sections define the available actions and resources, categorized by their respective services. @@ -36,12 +66,12 @@ The `archive` service pertains to all actions related to accessing and managing **Resources:** -| Resource | Description | -| :------------------------------------ | :------------------------------------------------------------- | -| `archive/all` | Represents the entire email archive. | -| `archive/ingestion-source/{sourceId}` | Scopes the action to emails from a specific ingestion source. | -| `archive/email/{emailId}` | Scopes the action to a single, specific email. | -| `archive/custodian/{custodianId}` | Scopes the action to emails belonging to a specific custodian. | +| Resource | Description | +| :------------------------------------ | :--------------------------------------------------------------------------------------- | +| `archive/all` | Represents the entire email archive. | +| `archive/ingestion-source/{sourceId}` | Scopes the action to emails from a specific ingestion source. | +| `archive/mailbox/{email}` | Scopes the action to a single, specific mailbox, usually identified by an email address. | +| `archive/custodian/{custodianId}` | Scopes the action to emails belonging to a specific custodian. | --- diff --git a/packages/backend/src/api/controllers/auth.controller.ts b/packages/backend/src/api/controllers/auth.controller.ts index 3ddd3ca..b7f7d98 100644 --- a/packages/backend/src/api/controllers/auth.controller.ts +++ b/packages/backend/src/api/controllers/auth.controller.ts @@ -13,7 +13,12 @@ export class AuthController { this.#authService = authService; this.#userService = userService; } - + /** + * Only used for setting up the instance, should only be displayed once upon instance set up. + * @param req + * @param res + * @returns + */ public setup = async (req: Request, res: Response): Promise => { const { email, password, first_name, last_name } = req.body; @@ -29,9 +34,8 @@ export class AuthController { return res.status(403).json({ message: 'Setup has already been completed.' }); } - const newUser = await this.#userService.createUser({ email, password, first_name, last_name }); + const newUser = await this.#userService.createAdminUser({ email, password, first_name, last_name }); const result = await this.#authService.login(email, password); - return res.status(201).json(result); } catch (error) { console.error('Setup error:', error); diff --git a/packages/backend/src/api/routes/iam.routes.ts b/packages/backend/src/api/routes/iam.routes.ts index 0c5cbd3..ad000a7 100644 --- a/packages/backend/src/api/routes/iam.routes.ts +++ b/packages/backend/src/api/routes/iam.routes.ts @@ -32,6 +32,5 @@ export const createIamRouter = (iamController: IamController): Router => { * @access Private */ router.delete('/roles/:id', requireAuth, iamController.deleteRole); - return router; }; diff --git a/packages/backend/src/iam-policy/iam-definitions.ts b/packages/backend/src/iam-policy/iam-definitions.ts index 2ba8bd7..bf16cfb 100644 --- a/packages/backend/src/iam-policy/iam-definitions.ts +++ b/packages/backend/src/iam-policy/iam-definitions.ts @@ -29,7 +29,7 @@ const ARCHIVE_ACTIONS = { const ARCHIVE_RESOURCES = { ALL: 'archive/all', INGESTION_SOURCE: 'archive/ingestion-source/*', - EMAIL: 'archive/email/*', + MAILBOX: 'archive/mailbox/*', CUSTODIAN: 'archive/custodian/*', } as const; @@ -113,43 +113,8 @@ export const ValidActions: Set = new Set([ * as is `archive/email/123-abc`. */ export const ValidResourcePatterns = { - archive: /^archive\/(all|ingestion-source\/[^\/]+|email\/[^\/]+|custodian\/[^\/]+)$/, + archive: /^archive\/(all|ingestion-source\/[^\/]+|mailbox\/[^\/]+|custodian\/[^\/]+)$/, ingestion: /^ingestion-source\/(\*|[^\/]+)$/, system: /^system\/(settings|users|user\/[^\/]+)$/, dashboard: /^dashboard\/\*$/, }; - - -/** - * --- How to Use These Definitions for Validation (Conceptual) --- - * - * A validator function would be created, likely in an `AuthorizationService`, - * that accepts a `PolicyStatement` object. - * - * export function isPolicyStatementValid(statement: PolicyStatement): boolean { - * // 1. Validate Actions - * for (const action of statement.Action) { - * if (action.endsWith('*')) { - * // For wildcards, check if the service prefix is valid - * const service = action.split(':')[0]; - * if (!Object.keys(ValidResourcePatterns).includes(service)) { - * return false; // Invalid service - * } - * } else if (!ValidActions.has(action)) { - * return false; // Action is not in the set of known actions - * } - * } - * - * // 2. Validate Resources - * for (const resource of statement.Resource) { - * const service = resource.split('/')[0]; - * const pattern = ValidResourcePatterns[service]; - * - * if (!pattern || !pattern.test(resource)) { - * return false; // Resource format is invalid for the specified service - * } - * } - * - * return true; - * } - */ diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f23b382..582917d 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -35,7 +35,7 @@ const { if (!PORT_BACKEND || !JWT_SECRET || !JWT_EXPIRES_IN) { - throw new Error('Missing required environment variables for the backend.'); + throw new Error('Missing required environment variables for the backend: PORT_BACKEND, JWT_SECRET, JWT_EXPIRES_IN.'); } // --- Dependency Injection Setup --- diff --git a/packages/backend/src/services/UserService.ts b/packages/backend/src/services/UserService.ts index 6f4ddc0..c8a2f6e 100644 --- a/packages/backend/src/services/UserService.ts +++ b/packages/backend/src/services/UserService.ts @@ -36,7 +36,7 @@ export class UserService { * @param userDetails The details of the user to create. * @returns The newly created user object. */ - public async createUser(userDetails: Pick & { password?: string; }): Promise<(typeof schema.users.$inferSelect)> { + public async createAdminUser(userDetails: Pick & { password?: string; }): Promise<(typeof schema.users.$inferSelect)> { const { email, first_name, last_name, password } = userDetails; const userCountResult = await db.select({ count: sql`count(*)` }).from(schema.users); @@ -51,29 +51,29 @@ export class UserService { password: hashedPassword, }).returning(); - if (isFirstUser) { - let superAdminRole = await db.query.roles.findFirst({ - where: eq(schema.roles.name, 'Super Admin') - }); + // find super admin role + let superAdminRole = await db.query.roles.findFirst({ + where: eq(schema.roles.name, 'Super Admin') + }); - if (!superAdminRole) { - const suerAdminPolicies: PolicyStatement[] = [{ - Effect: 'Allow', - Action: ['*'], - Resource: ['*'] - }]; - superAdminRole = (await db.insert(schema.roles).values({ - name: 'Super Admin', - policies: suerAdminPolicies - }).returning())[0]; - } - - await db.insert(schema.userRoles).values({ - userId: newUser[0].id, - roleId: superAdminRole.id - }); + if (!superAdminRole) { + const suerAdminPolicies: PolicyStatement[] = [{ + Effect: 'Allow', + Action: ['*'], + Resource: ['*'] + }]; + superAdminRole = (await db.insert(schema.roles).values({ + name: 'Super Admin', + policies: suerAdminPolicies + }).returning())[0]; } + await db.insert(schema.userRoles).values({ + userId: newUser[0].id, + roleId: superAdminRole.id + }); + + return newUser[0]; } }