/** * IAuthProvider implementation for smartregistry * Integrates Stack.Gallery's auth system with smartregistry's protocol handlers */ import * as plugins from '../plugins.ts'; import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts'; import { User } from '../models/user.ts'; import { TokenService } from '../services/token.service.ts'; import { PermissionService, type TAction } from '../services/permission.service.ts'; import { AuditService } from '../services/audit.service.ts'; import { AuthService } from '../services/auth.service.ts'; /** * Request actor representing the authenticated entity making a request */ export interface IStackGalleryActor { type: 'user' | 'api_token' | 'anonymous'; userId?: string; user?: User; tokenId?: string; ip?: string; userAgent?: string; protocols: TRegistryProtocol[]; permissions: { organizationId?: string; repositoryId?: string; canRead: boolean; canWrite: boolean; canDelete: boolean; }; } /** * Auth provider that implements smartregistry's IAuthProvider interface */ export class StackGalleryAuthProvider implements plugins.smartregistry.IAuthProvider { private tokenService: TokenService; private permissionService: PermissionService; private authService: AuthService; constructor() { this.tokenService = new TokenService(); this.permissionService = new PermissionService(); this.authService = new AuthService(); } /** * Authenticate with username/password credentials * Returns userId on success, null on failure */ public async authenticate( credentials: plugins.smartregistry.ICredentials ): Promise { const result = await this.authService.login(credentials.username, credentials.password); if (!result.success || !result.user) return null; return result.user.id; } /** * Validate a token and return auth token info */ public async validateToken( token: string, protocol?: plugins.smartregistry.TRegistryProtocol ): Promise { // Try API token (srg_ prefix) if (token.startsWith('srg_')) { const result = await this.tokenService.validateToken(token); if (!result.valid || !result.token || !result.user) return null; return { type: (protocol || result.token.protocols[0] || 'npm') as plugins.smartregistry.TRegistryProtocol, userId: result.user.id, scopes: result.token.scopes.map((s) => `${s.protocol}:${s.actions.join(',')}` ), readonly: !result.token.scopes.some((s) => s.actions.includes('write') || s.actions.includes('*') ), }; } // Try JWT access token const validated = await this.authService.validateAccessToken(token); if (!validated) return null; return { type: (protocol || 'npm') as plugins.smartregistry.TRegistryProtocol, userId: validated.user.id, scopes: ['*'], }; } /** * Create a new token for a user and protocol */ public async createToken( userId: string, protocol: plugins.smartregistry.TRegistryProtocol, options?: plugins.smartregistry.ITokenOptions ): Promise { const result = await this.tokenService.createToken({ userId, name: `${protocol}-token`, protocols: [protocol as TRegistryProtocol], scopes: [ { protocol: protocol as TRegistryProtocol, actions: options?.readonly ? ['read'] : ['read', 'write', 'delete'], }, ], }); return result.rawToken; } /** * Revoke a token */ public async revokeToken(token: string): Promise { if (token.startsWith('srg_')) { // Hash and find the token const result = await this.tokenService.validateToken(token); if (result.valid && result.token) { await this.tokenService.revokeToken(result.token.id, 'provider_revoked'); } } } /** * Check if a token holder is authorized for a resource and action */ public async authorize( token: plugins.smartregistry.IAuthToken | null, resource: string, action: string ): Promise { // Anonymous access: only public reads if (!token) return false; // Parse resource string (format: "protocol:type:name" or "org/repo") const userId = token.userId; if (!userId) return false; // Map action const mappedAction = this.mapAction(action); // For simple authorization without specific resource context, // check if user is active const user = await User.findById(userId); if (!user || !user.isActive) return false; // System admins bypass all checks if (user.isSystemAdmin) return true; return mappedAction === 'read'; // Default: authenticated users can read } /** * Map smartregistry action to our TAction type */ private mapAction(action: string): TAction { switch (action) { case 'read': case 'pull': case 'download': case 'fetch': return 'read'; case 'write': case 'push': case 'publish': case 'upload': return 'write'; case 'delete': case 'unpublish': case 'remove': return 'delete'; case 'admin': case 'manage': return 'admin'; default: return 'read'; } } }