Files
registry/ts/providers/auth.provider.ts

185 lines
5.2 KiB
TypeScript

/**
* 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<string | null> {
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<plugins.smartregistry.IAuthToken | null> {
// 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<string> {
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<void> {
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<boolean> {
// 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';
}
}
}