651 lines
18 KiB
Markdown
651 lines
18 KiB
Markdown
# Pattern Comparison: Template vs IAM Service
|
|
|
|
## Overview
|
|
|
|
This document compares implementation patterns between `_template` (baseline microservice) and `iam-service` (production implementation) to guide documentation updates.
|
|
|
|
## Module Structure Pattern
|
|
|
|
### Template Pattern (Simple)
|
|
```
|
|
src/modules/
|
|
├── common/ # Base repository, shared types
|
|
├── feature/ # Example CRUD module
|
|
├── health/ # Health checks
|
|
└── metrics/ # Prometheus metrics
|
|
```
|
|
|
|
### IAM Pattern (Production)
|
|
```
|
|
src/modules/
|
|
├── common/ # Base repository, shared types (same)
|
|
├── feature/ # Example CRUD module (inherited)
|
|
├── health/ # Health checks (same)
|
|
├── metrics/ # Prometheus metrics (same)
|
|
├── auth/ # + Core authentication
|
|
├── rbac/ # + Authorization
|
|
├── token/ # + Token management
|
|
├── session/ # + Session management
|
|
├── mfa/ # + Multi-factor auth
|
|
├── social/ # + Social OAuth
|
|
├── oidc/ # + OpenID Connect
|
|
├── identity/ # + Identity management
|
|
├── access/ # + Access workflows
|
|
└── governance/ # + Compliance & reporting
|
|
```
|
|
|
|
**Documentation Impact**:
|
|
- Template docs should focus on **foundational patterns**
|
|
- IAM docs should show **advanced patterns** and **real-world implementation**
|
|
|
|
---
|
|
|
|
## Controller Pattern
|
|
|
|
### Template (Basic)
|
|
```typescript
|
|
// src/modules/feature/feature.controller.ts
|
|
export class FeatureController {
|
|
constructor(private service: FeatureService) {}
|
|
|
|
async create(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const dto = CreateFeatureDto.parse(req.body);
|
|
const result = await this.service.create(dto);
|
|
res.status(201).json({ success: true, data: result });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### IAM (Production with Auth)
|
|
```typescript
|
|
// src/modules/identity/identity.controller.ts
|
|
export class IdentityController {
|
|
constructor(
|
|
private service: IdentityService,
|
|
private auditService: AuditService
|
|
) {}
|
|
|
|
@RequireAuth()
|
|
@RequirePermission('users', 'create')
|
|
@RateLimit('strict')
|
|
async create(req: Request, res: Response, next: NextFunction) {
|
|
try {
|
|
const dto = CreateUserDto.parse(req.body);
|
|
const result = await this.service.create(dto);
|
|
|
|
// Audit logging
|
|
await this.auditService.log('USER_CREATED', req.user.id, {
|
|
userId: result.id,
|
|
correlationId: req.correlationId,
|
|
});
|
|
|
|
res.status(201).json({ success: true, data: result });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Pattern Differences**:
|
|
1. **Decorators**: IAM uses decorators for auth, permissions, rate limiting
|
|
2. **Audit Logging**: IAM logs all operations
|
|
3. **Dependency Injection**: IAM injects multiple services (Service + AuditService)
|
|
|
|
**Documentation Impact**:
|
|
- Template docs: Show basic controller pattern
|
|
- IAM docs: Show auth decorators, audit logging, multi-service injection
|
|
|
|
---
|
|
|
|
## Service Pattern
|
|
|
|
### Template (Simple Business Logic)
|
|
```typescript
|
|
// src/modules/feature/feature.service.ts
|
|
export class FeatureService {
|
|
constructor(private repository: FeatureRepository) {}
|
|
|
|
async create(data: CreateFeatureDto) {
|
|
// Business validation
|
|
const existing = await this.repository.findByName(data.name);
|
|
if (existing) {
|
|
throw new ConflictError('Feature already exists');
|
|
}
|
|
|
|
return await this.repository.create(data);
|
|
}
|
|
}
|
|
```
|
|
|
|
### IAM (with Caching & Complex Logic)
|
|
```typescript
|
|
// src/modules/rbac/rbac.service.ts
|
|
export class RBACService {
|
|
constructor(
|
|
private repository: RBACRepository,
|
|
private cacheService: CacheService,
|
|
private auditService: AuditService
|
|
) {}
|
|
|
|
async checkPermission(
|
|
userId: string,
|
|
resource: string,
|
|
action: string
|
|
): Promise<boolean> {
|
|
// Try cache first (L1 → L2)
|
|
const cacheKey = `user:${userId}:permissions`;
|
|
const cached = await this.cacheService.get<string[]>(cacheKey);
|
|
|
|
let permissions: string[];
|
|
if (cached) {
|
|
permissions = cached;
|
|
} else {
|
|
// Cache miss - fetch from DB
|
|
const userRoles = await this.repository.getUserRoles(userId);
|
|
const rolePermissions = await this.repository.getRolePermissions(userRoles);
|
|
const directPermissions = await this.repository.getUserPermissions(userId);
|
|
|
|
permissions = [...rolePermissions, ...directPermissions];
|
|
|
|
// Cache for 5 minutes
|
|
await this.cacheService.set(cacheKey, permissions, 300);
|
|
}
|
|
|
|
// Check permission
|
|
const required = `${resource}:${action}`;
|
|
const hasPermission = permissions.some(p => this.matchesPermission(p, required));
|
|
|
|
// Audit log
|
|
await this.auditService.log(
|
|
hasPermission ? 'ACCESS_GRANTED' : 'ACCESS_DENIED',
|
|
userId,
|
|
{ resource, action }
|
|
);
|
|
|
|
return hasPermission;
|
|
}
|
|
|
|
private matchesPermission(granted: string, required: string): boolean {
|
|
// Handle wildcards: users:*:* matches users:create:org
|
|
const grantedParts = granted.split(':');
|
|
const requiredParts = required.split(':');
|
|
|
|
return grantedParts.every((part, i) =>
|
|
part === '*' || part === requiredParts[i]
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Pattern Differences**:
|
|
1. **Caching**: IAM uses multi-layer caching
|
|
2. **Complex Logic**: Permission matching with wildcards
|
|
3. **Audit Logging**: Every operation logged
|
|
4. **Performance**: Cache-first approach for frequently accessed data
|
|
|
|
**Documentation Impact**:
|
|
- Template docs: Simple business logic
|
|
- IAM docs: Caching strategies, complex permission logic, audit logging
|
|
|
|
---
|
|
|
|
## Repository Pattern
|
|
|
|
### Template (Basic CRUD)
|
|
```typescript
|
|
// src/modules/feature/feature.repository.ts
|
|
export class FeatureRepository extends BaseRepository<Feature> {
|
|
constructor(prisma: PrismaClient) {
|
|
super(prisma, 'feature');
|
|
}
|
|
|
|
async findByName(name: string): Promise<Feature | null> {
|
|
return this.prisma.feature.findUnique({ where: { name } });
|
|
}
|
|
}
|
|
```
|
|
|
|
### IAM (with Joins & Complex Queries)
|
|
```typescript
|
|
// src/modules/rbac/rbac.repository.ts
|
|
export class RBACRepository extends BaseRepository<Role> {
|
|
constructor(prisma: PrismaClient) {
|
|
super(prisma, 'role');
|
|
}
|
|
|
|
async getUserRoles(userId: string): Promise<string[]> {
|
|
const userRoles = await this.prisma.userRole.findMany({
|
|
where: {
|
|
userId,
|
|
OR: [
|
|
{ expiresAt: null },
|
|
{ expiresAt: { gt: new Date() } }
|
|
]
|
|
},
|
|
include: { role: true }
|
|
});
|
|
|
|
return userRoles.map(ur => ur.role.name);
|
|
}
|
|
|
|
async getRolePermissions(roleNames: string[]): Promise<string[]> {
|
|
const rolePermissions = await this.prisma.rolePermission.findMany({
|
|
where: { role: { name: { in: roleNames } } },
|
|
include: { permission: true }
|
|
});
|
|
|
|
return rolePermissions.map(rp => rp.permission.code);
|
|
}
|
|
|
|
async getUserPermissions(userId: string): Promise<string[]> {
|
|
const userPermissions = await this.prisma.userPermission.findMany({
|
|
where: { userId },
|
|
include: { permission: true }
|
|
});
|
|
|
|
return userPermissions.map(up => up.permission.code);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Pattern Differences**:
|
|
1. **Complex Joins**: IAM uses nested includes
|
|
2. **Conditional Logic**: Expiration checking
|
|
3. **Data Aggregation**: Combining data from multiple sources
|
|
|
|
**Documentation Impact**:
|
|
- Template docs: Basic CRUD with unique lookups
|
|
- IAM docs: Complex joins, conditional queries, data aggregation
|
|
|
|
---
|
|
|
|
## Middleware Pattern
|
|
|
|
### Template (Basic)
|
|
```typescript
|
|
// src/middlewares/logger.middleware.ts
|
|
export const loggerMiddleware = (
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
const start = Date.now();
|
|
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - start;
|
|
logger.info('Request completed', {
|
|
method: req.method,
|
|
url: req.url,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
});
|
|
});
|
|
|
|
next();
|
|
};
|
|
```
|
|
|
|
### IAM (with Correlation IDs & Audit)
|
|
```typescript
|
|
// src/middlewares/logger.middleware.ts
|
|
export const loggerMiddleware = (
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
const start = Date.now();
|
|
const correlationId = req.correlationId || generateCorrelationId();
|
|
|
|
// Attach to request for downstream use
|
|
req.correlationId = correlationId;
|
|
res.setHeader('x-correlation-id', correlationId);
|
|
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - start;
|
|
logger.info('Request completed', {
|
|
correlationId,
|
|
method: req.method,
|
|
url: req.url,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
userId: req.user?.id,
|
|
ipAddress: req.ip,
|
|
userAgent: req.headers['user-agent'],
|
|
});
|
|
|
|
// Audit logging for sensitive endpoints
|
|
if (req.url.startsWith('/api/v1/auth') || req.url.startsWith('/api/v1/rbac')) {
|
|
auditService.log('API_REQUEST', req.user?.id, {
|
|
method: req.method,
|
|
url: req.url,
|
|
statusCode: res.statusCode,
|
|
correlationId,
|
|
});
|
|
}
|
|
});
|
|
|
|
next();
|
|
};
|
|
```
|
|
|
|
**Pattern Differences**:
|
|
1. **Correlation IDs**: IAM propagates correlation IDs
|
|
2. **Enhanced Context**: User ID, IP, user agent
|
|
3. **Audit Integration**: Sensitive endpoints get audit logs
|
|
|
|
**Documentation Impact**:
|
|
- Template docs: Basic request logging
|
|
- IAM docs: Distributed tracing, audit integration, correlation IDs
|
|
|
|
---
|
|
|
|
## Configuration Pattern
|
|
|
|
### Template (Simple Zod Validation)
|
|
```typescript
|
|
// src/config/app.config.ts
|
|
import { z } from 'zod';
|
|
|
|
const envSchema = z.object({
|
|
PORT: z.coerce.number().default(5000),
|
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
DATABASE_URL: z.string().url(),
|
|
REDIS_URL: z.string().url().optional(),
|
|
});
|
|
|
|
const env = envSchema.parse(process.env);
|
|
|
|
export const appConfig = {
|
|
port: env.PORT,
|
|
env: env.NODE_ENV,
|
|
database: { url: env.DATABASE_URL },
|
|
redis: { url: env.REDIS_URL },
|
|
};
|
|
```
|
|
|
|
### IAM (with Nested Configs & Secrets)
|
|
```typescript
|
|
// src/config/app.config.ts
|
|
import { z } from 'zod';
|
|
|
|
const envSchema = z.object({
|
|
// Server
|
|
PORT: z.coerce.number().default(3001),
|
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
|
|
// Database
|
|
DATABASE_URL: z.string().url(),
|
|
DATABASE_POOL_SIZE: z.coerce.number().default(10),
|
|
|
|
// Redis
|
|
REDIS_HOST: z.string().default('localhost'),
|
|
REDIS_PORT: z.coerce.number().default(6379),
|
|
REDIS_PASSWORD: z.string().optional(),
|
|
REDIS_DB: z.coerce.number().default(0),
|
|
|
|
// JWT
|
|
JWT_SECRET: z.string().min(32),
|
|
JWT_REFRESH_SECRET: z.string().min(32),
|
|
JWT_ACCESS_EXPIRY: z.string().default('15m'),
|
|
JWT_REFRESH_EXPIRY: z.string().default('7d'),
|
|
|
|
// Encryption
|
|
ENCRYPTION_KEY: z.string().min(32).optional(),
|
|
|
|
// OAuth
|
|
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
|
FACEBOOK_APP_ID: z.string().optional(),
|
|
FACEBOOK_APP_SECRET: z.string().optional(),
|
|
GITHUB_CLIENT_ID: z.string().optional(),
|
|
GITHUB_CLIENT_SECRET: z.string().optional(),
|
|
|
|
// Feature Flags
|
|
MFA_ENABLED: z.coerce.boolean().default(true),
|
|
SOCIAL_AUTH_ENABLED: z.coerce.boolean().default(true),
|
|
TRACING_ENABLED: z.coerce.boolean().default(false),
|
|
});
|
|
|
|
const env = envSchema.parse(process.env);
|
|
|
|
export const appConfig = {
|
|
server: {
|
|
port: env.PORT,
|
|
env: env.NODE_ENV,
|
|
},
|
|
database: {
|
|
url: env.DATABASE_URL,
|
|
poolSize: env.DATABASE_POOL_SIZE,
|
|
},
|
|
redis: {
|
|
host: env.REDIS_HOST,
|
|
port: env.REDIS_PORT,
|
|
password: env.REDIS_PASSWORD,
|
|
db: env.REDIS_DB,
|
|
},
|
|
jwt: {
|
|
secret: env.JWT_SECRET,
|
|
refreshSecret: env.JWT_REFRESH_SECRET,
|
|
accessExpiry: env.JWT_ACCESS_EXPIRY,
|
|
refreshExpiry: env.JWT_REFRESH_EXPIRY,
|
|
},
|
|
encryption: {
|
|
key: env.ENCRYPTION_KEY,
|
|
},
|
|
oauth: {
|
|
google: {
|
|
clientId: env.GOOGLE_CLIENT_ID,
|
|
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
},
|
|
facebook: {
|
|
appId: env.FACEBOOK_APP_ID,
|
|
appSecret: env.FACEBOOK_APP_SECRET,
|
|
},
|
|
github: {
|
|
clientId: env.GITHUB_CLIENT_ID,
|
|
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
},
|
|
},
|
|
features: {
|
|
mfa: env.MFA_ENABLED,
|
|
socialAuth: env.SOCIAL_AUTH_ENABLED,
|
|
tracing: env.TRACING_ENABLED,
|
|
},
|
|
};
|
|
```
|
|
|
|
**Pattern Differences**:
|
|
1. **Nested Configuration**: IAM groups related configs
|
|
2. **Secrets Management**: JWT, encryption, OAuth secrets
|
|
3. **Feature Flags**: Runtime feature toggles
|
|
4. **Complex Validation**: Min length requirements, optional fields
|
|
|
|
**Documentation Impact**:
|
|
-Template docs: Show basic Zod validation
|
|
- IAM docs: Show nested configs, secrets management, feature flags
|
|
|
|
---
|
|
|
|
## Testing Pattern
|
|
|
|
### Template (Simple Unit Test)
|
|
```typescript
|
|
// src/modules/feature/__tests__/feature.service.test.ts
|
|
describe('FeatureService', () => {
|
|
let service: FeatureService;
|
|
let mockRepository: any;
|
|
|
|
beforeEach(() => {
|
|
mockRepository = {
|
|
findByName: jest.fn(),
|
|
create: jest.fn(),
|
|
};
|
|
service = new FeatureService(mockRepository);
|
|
});
|
|
|
|
it('should create feature', async () => {
|
|
mockRepository.findByName.mockResolvedValue(null);
|
|
mockRepository.create.mockResolvedValue({ id: '1', name: 'test' });
|
|
|
|
const result = await service.create({ name: 'test' });
|
|
|
|
expect(result).toEqual({ id: '1', name: 'test' });
|
|
expect(mockRepository.create).toHaveBeenCalledWith({ name: 'test' });
|
|
});
|
|
|
|
it('should throw conflict error if feature exists', async () => {
|
|
mockRepository.findByName.mockResolvedValue({ id: '1', name: 'test' });
|
|
|
|
await expect(service.create({ name: 'test' }))
|
|
.rejects
|
|
.toThrow(ConflictError);
|
|
});
|
|
});
|
|
```
|
|
|
|
### IAM (with Mocking Complete Dependencies)
|
|
```typescript
|
|
// src/modules/rbac/__tests__/rbac.service.test.ts
|
|
describe('RBACService', () => {
|
|
let service: RBACService;
|
|
let mockRepository: any;
|
|
let mockCacheService: any;
|
|
let mockAuditService: any;
|
|
|
|
beforeEach(() => {
|
|
mockRepository = {
|
|
getUserRoles: jest.fn(),
|
|
getRolePermissions: jest.fn(),
|
|
getUserPermissions: jest.fn(),
|
|
};
|
|
mockCacheService = {
|
|
get: jest.fn(),
|
|
set: jest.fn(),
|
|
};
|
|
mockAuditService = {
|
|
log: jest.fn(),
|
|
};
|
|
|
|
service = new RBACService(
|
|
mockRepository,
|
|
mockCacheService,
|
|
mockAuditService
|
|
);
|
|
});
|
|
|
|
describe('checkPermission', () => {
|
|
it('should return true if user has permission (cache hit)', async () => {
|
|
mockCacheService.get.mockResolvedValue(['users:create:*', 'posts:read:*']);
|
|
|
|
const result = await service.checkPermission('user1', 'users', 'create');
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockCacheService.get).toHaveBeenCalledWith('user:user1:permissions');
|
|
expect(mockRepository.getUserRoles).not.toHaveBeenCalled(); // Cache hit
|
|
expect(mockAuditService.log).toHaveBeenCalledWith(
|
|
'ACCESS_GRANTED',
|
|
'user1',
|
|
{ resource: 'users', action: 'create' }
|
|
);
|
|
});
|
|
|
|
it('should return false if user lacks permission (cache miss)', async () => {
|
|
mockCacheService.get.mockResolvedValue(null); // Cache miss
|
|
mockRepository.getUserRoles.mockResolvedValue(['user']);
|
|
mockRepository.getRolePermissions.mockResolvedValue(['posts:read:*']);
|
|
mockRepository.getUserPermissions.mockResolvedValue([]);
|
|
|
|
const result = await service.checkPermission('user1', 'users', 'delete');
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockCacheService.set).toHaveBeenCalledWith(
|
|
'user:user1:permissions',
|
|
['posts:read:*'],
|
|
300
|
|
);
|
|
expect(mockAuditService.log).toHaveBeenCalledWith(
|
|
'ACCESS_DENIED',
|
|
'user1',
|
|
{ resource: 'users', action: 'delete' }
|
|
);
|
|
});
|
|
|
|
it('should handle wildcard permissions', async () => {
|
|
mockCacheService.get.mockResolvedValue(['users:*:*']);
|
|
|
|
const result = await service.checkPermission('user1', 'users', 'create');
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
**Pattern Differences**:
|
|
1. **Multiple Dependencies**: IAM mocks repository, cache, audit
|
|
2. **Cache Behavior**: Tests cache hit and cache miss scenarios
|
|
3. **Audit Verification**: Ensures audit logs are created
|
|
4. **Complex Logic**: Tests wildcard permission matching
|
|
|
|
**Documentation Impact**:
|
|
- Template docs: Simple mocking, basic test cases
|
|
- IAM docs: Complex mocking, cache behavior, audit verification
|
|
|
|
---
|
|
|
|
## Summary Table
|
|
|
|
| Pattern | Template | IAM | Docs to Update |
|
|
|---------|----------|-----|----------------|
|
|
| **Controller** | Basic try-catch | + Auth decorators, audit | `api-design.md`, `middleware-patterns.md` |
|
|
| **Service** | Simple logic | + Caching, complex logic | `service-layer-patterns.md`, `caching-patterns.md` |
|
|
| **Repository** | Basic CRUD | + Complex joins, aggregation | `repository-pattern.md`, `database-prisma.md` |
|
|
| **Middleware** | Basic logging | + Correlation IDs, audit | `middleware-patterns.md`, `observability-monitoring.md` |
|
|
| **Configuration** | Simple Zod | + Nested configs, secrets | `configuration-management.md` |
|
|
| **Testing** | Simple mocks | + Multi-dependency mocks | `testing-patterns.md` |
|
|
| **Caching** | None | Multi-layer (L1/L2/L3) | `caching-patterns.md` (NEW examples) |
|
|
| **Security** | Helmet only | + Zero-trust, encryption | `security.md` (NEW examples) |
|
|
| **Authorization** | None | RBAC + ABAC | NEW skill docs needed |
|
|
| **Audit Logging** | None | Event sourcing | `observability-monitoring.md` |
|
|
|
|
---
|
|
|
|
## Recommended Documentation Updates
|
|
|
|
### High Priority
|
|
1. **Update `caching-patterns.md`** with IAM multi-layer caching
|
|
2. **Update `security.md`** with zero-trust, encryption, JWT patterns
|
|
3. **Update `service-layer-patterns.md`** with caching integration
|
|
4. **Update `middleware-patterns.md`** with correlation IDs, audit
|
|
5. **Update `testing-patterns.md`** with complex mocking examples
|
|
|
|
### Medium Priority
|
|
6. **Update `repository-pattern.md`** with complex joins
|
|
7. **Update `api-design.md`** with auth middleware patterns
|
|
8. **Update `configuration-management.md`** with secrets management
|
|
9. **Update `observability-monitoring.md`** with audit logging
|
|
|
|
### New Content Needed
|
|
10. **RBAC patterns** (currently basic in user rules)
|
|
11. **Event sourcing** (audit logging implementation)
|
|
12. **Zero-trust architecture** (security validation)
|
|
|
|
---
|
|
|
|
## Code Example Sources
|
|
|
|
All code examples in docs should reference:
|
|
- **Template**: For foundational patterns
|
|
- **IAM Service**: For production patterns
|
|
|
|
**Example Mapping**:
|
|
- `_template/src/modules/feature/feature.service.ts` → Basic service pattern
|
|
- `iam-service/src/modules/rbac/rbac.service.ts` → Advanced service with caching
|
|
- `iam-service/src/core/cache/multi-layer-cache.ts` → Caching implementation
|
|
- `iam-service/src/core/security/zero-trust-validator.ts` → Security patterns
|