IAM policies

This commit is contained in:
Wayne
2025-08-06 01:12:33 +03:00
parent 842f8092d6
commit 23ebe942b2
6 changed files with 68 additions and 70 deletions

View File

@@ -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. |
---

View File

@@ -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<Response> => {
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);

View File

@@ -32,6 +32,5 @@ export const createIamRouter = (iamController: IamController): Router => {
* @access Private
*/
router.delete('/roles/:id', requireAuth, iamController.deleteRole);
return router;
};

View File

@@ -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<string> = 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;
* }
*/

View File

@@ -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 ---

View File

@@ -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<User, 'email' | 'first_name' | 'last_name'> & { password?: string; }): Promise<(typeof schema.users.$inferSelect)> {
public async createAdminUser(userDetails: Pick<User, 'email' | 'first_name' | 'last_name'> & { password?: string; }): Promise<(typeof schema.users.$inferSelect)> {
const { email, first_name, last_name, password } = userDetails;
const userCountResult = await db.select({ count: sql<number>`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];
}
}