2025-11-27 22:15:38 +00:00
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-20 14:14:39 +00:00
|
|
|
* Authenticate with username/password credentials
|
|
|
|
|
* Returns userId on success, null on failure
|
2025-11-27 22:15:38 +00:00
|
|
|
*/
|
2026-03-20 14:14:39 +00:00
|
|
|
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;
|
2025-11-27 22:15:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-20 14:14:39 +00:00
|
|
|
* Validate a token and return auth token info
|
2025-11-27 22:15:38 +00:00
|
|
|
*/
|
2026-03-20 14:14:39 +00:00
|
|
|
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;
|
2025-11-27 22:15:38 +00:00
|
|
|
|
|
|
|
|
return {
|
2026-03-20 14:14:39 +00:00
|
|
|
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('*')
|
|
|
|
|
),
|
2025-11-27 22:15:38 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
// Try JWT access token
|
|
|
|
|
const validated = await this.authService.validateAccessToken(token);
|
|
|
|
|
if (!validated) return null;
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
return {
|
|
|
|
|
type: (protocol || 'npm') as plugins.smartregistry.TRegistryProtocol,
|
|
|
|
|
userId: validated.user.id,
|
|
|
|
|
scopes: ['*'],
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
/**
|
|
|
|
|
* 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'],
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-11-27 22:15:38 +00:00
|
|
|
});
|
2026-03-20 14:14:39 +00:00
|
|
|
return result.rawToken;
|
2025-11-27 22:15:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-20 14:14:39 +00:00
|
|
|
* Revoke a token
|
2025-11-27 22:15:38 +00:00
|
|
|
*/
|
2026-03-20 14:14:39 +00:00
|
|
|
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');
|
|
|
|
|
}
|
2025-11-27 22:15:38 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-20 14:14:39 +00:00
|
|
|
* Check if a token holder is authorized for a resource and action
|
2025-11-27 22:15:38 +00:00
|
|
|
*/
|
2026-03-20 14:14:39 +00:00
|
|
|
public async authorize(
|
|
|
|
|
token: plugins.smartregistry.IAuthToken | null,
|
|
|
|
|
resource: string,
|
|
|
|
|
action: string
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
// Anonymous access: only public reads
|
|
|
|
|
if (!token) return false;
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
// Parse resource string (format: "protocol:type:name" or "org/repo")
|
|
|
|
|
const userId = token.userId;
|
|
|
|
|
if (!userId) return false;
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
// Map action
|
|
|
|
|
const mappedAction = this.mapAction(action);
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
// For simple authorization without specific resource context,
|
|
|
|
|
// check if user is active
|
|
|
|
|
const user = await User.findById(userId);
|
|
|
|
|
if (!user || !user.isActive) return false;
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
// System admins bypass all checks
|
|
|
|
|
if (user.isSystemAdmin) return true;
|
2025-11-27 22:15:38 +00:00
|
|
|
|
2026-03-20 14:14:39 +00:00
|
|
|
return mappedAction === 'read'; // Default: authenticated users can read
|
2025-11-27 22:15:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|