feat(auth): Add external authentication (OAuth/OIDC & LDAP) with admin management, UI, and encryption support
This commit is contained in:
252
ts/models/auth.provider.ts
Normal file
252
ts/models/auth.provider.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Authentication Provider model for Stack.Gallery Registry
|
||||
* Stores OAuth/OIDC and LDAP provider configurations
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import type {
|
||||
IAuthProvider,
|
||||
TAuthProviderType,
|
||||
TAuthProviderStatus,
|
||||
IOAuthConfig,
|
||||
ILdapConfig,
|
||||
IAttributeMapping,
|
||||
IProvisioningSettings,
|
||||
} from '../interfaces/auth.interfaces.ts';
|
||||
import { db } from './db.ts';
|
||||
|
||||
const DEFAULT_ATTRIBUTE_MAPPING: IAttributeMapping = {
|
||||
email: 'email',
|
||||
username: 'preferred_username',
|
||||
displayName: 'name',
|
||||
};
|
||||
|
||||
const DEFAULT_PROVISIONING: IProvisioningSettings = {
|
||||
jitEnabled: true,
|
||||
autoLinkByEmail: true,
|
||||
};
|
||||
|
||||
@plugins.smartdata.Collection(() => db)
|
||||
export class AuthProvider
|
||||
extends plugins.smartdata.SmartDataDbDoc<AuthProvider, AuthProvider>
|
||||
implements IAuthProvider
|
||||
{
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index({ unique: true })
|
||||
public name: string = ''; // URL-safe slug identifier
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.searchable()
|
||||
public displayName: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public type: TAuthProviderType = 'oidc';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public status: TAuthProviderStatus = 'disabled';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public priority: number = 100; // Lower = shown first in UI
|
||||
|
||||
// Type-specific config (only one should be populated based on type)
|
||||
@plugins.smartdata.svDb()
|
||||
public oauthConfig?: IOAuthConfig;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public ldapConfig?: ILdapConfig;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public attributeMapping: IAttributeMapping = DEFAULT_ATTRIBUTE_MAPPING;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public provisioning: IProvisioningSettings = DEFAULT_PROVISIONING;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public updatedAt: Date = new Date();
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public createdById: string = '';
|
||||
|
||||
// Connection test tracking
|
||||
@plugins.smartdata.svDb()
|
||||
public lastTestedAt?: Date;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastTestResult?: 'success' | 'failure';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastTestError?: string;
|
||||
|
||||
/**
|
||||
* Find provider by ID
|
||||
*/
|
||||
public static async findById(id: string): Promise<AuthProvider | null> {
|
||||
return await AuthProvider.getInstance({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find provider by name (slug)
|
||||
*/
|
||||
public static async findByName(name: string): Promise<AuthProvider | null> {
|
||||
return await AuthProvider.getInstance({ name: name.toLowerCase() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active providers (for login page)
|
||||
*/
|
||||
public static async getActiveProviders(): Promise<AuthProvider[]> {
|
||||
const providers = await AuthProvider.getInstances({ status: 'active' });
|
||||
return providers.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all providers (for admin)
|
||||
*/
|
||||
public static async getAllProviders(): Promise<AuthProvider[]> {
|
||||
const providers = await AuthProvider.getInstances({});
|
||||
return providers.sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new OAuth/OIDC provider
|
||||
*/
|
||||
public static async createOAuthProvider(data: {
|
||||
name: string;
|
||||
displayName: string;
|
||||
oauthConfig: IOAuthConfig;
|
||||
attributeMapping?: IAttributeMapping;
|
||||
provisioning?: IProvisioningSettings;
|
||||
createdById: string;
|
||||
}): Promise<AuthProvider> {
|
||||
const provider = new AuthProvider();
|
||||
provider.id = await AuthProvider.getNewId();
|
||||
provider.name = data.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||
provider.displayName = data.displayName;
|
||||
provider.type = 'oidc';
|
||||
provider.status = 'disabled';
|
||||
provider.oauthConfig = data.oauthConfig;
|
||||
provider.attributeMapping = data.attributeMapping || DEFAULT_ATTRIBUTE_MAPPING;
|
||||
provider.provisioning = data.provisioning || DEFAULT_PROVISIONING;
|
||||
provider.createdById = data.createdById;
|
||||
provider.createdAt = new Date();
|
||||
provider.updatedAt = new Date();
|
||||
await provider.save();
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new LDAP provider
|
||||
*/
|
||||
public static async createLdapProvider(data: {
|
||||
name: string;
|
||||
displayName: string;
|
||||
ldapConfig: ILdapConfig;
|
||||
attributeMapping?: IAttributeMapping;
|
||||
provisioning?: IProvisioningSettings;
|
||||
createdById: string;
|
||||
}): Promise<AuthProvider> {
|
||||
const provider = new AuthProvider();
|
||||
provider.id = await AuthProvider.getNewId();
|
||||
provider.name = data.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||
provider.displayName = data.displayName;
|
||||
provider.type = 'ldap';
|
||||
provider.status = 'disabled';
|
||||
provider.ldapConfig = data.ldapConfig;
|
||||
provider.attributeMapping = data.attributeMapping || {
|
||||
email: 'mail',
|
||||
username: 'uid',
|
||||
displayName: 'displayName',
|
||||
};
|
||||
provider.provisioning = data.provisioning || DEFAULT_PROVISIONING;
|
||||
provider.createdById = data.createdById;
|
||||
provider.createdAt = new Date();
|
||||
provider.updatedAt = new Date();
|
||||
await provider.save();
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update connection test result
|
||||
*/
|
||||
public async updateTestResult(success: boolean, error?: string): Promise<void> {
|
||||
this.lastTestedAt = new Date();
|
||||
this.lastTestResult = success ? 'success' : 'failure';
|
||||
this.lastTestError = error;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook: Update timestamps before save
|
||||
*/
|
||||
public async beforeSave(): Promise<void> {
|
||||
this.updatedAt = new Date();
|
||||
if (!this.id) {
|
||||
this.id = await AuthProvider.getNewId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public info (for login page - no secrets)
|
||||
*/
|
||||
public toPublicInfo(): {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: TAuthProviderType;
|
||||
} {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
displayName: this.displayName,
|
||||
type: this.type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get admin info (secrets masked)
|
||||
*/
|
||||
public toAdminInfo(): Record<string, unknown> {
|
||||
const info: Record<string, unknown> = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
displayName: this.displayName,
|
||||
type: this.type,
|
||||
status: this.status,
|
||||
priority: this.priority,
|
||||
attributeMapping: this.attributeMapping,
|
||||
provisioning: this.provisioning,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
createdById: this.createdById,
|
||||
lastTestedAt: this.lastTestedAt,
|
||||
lastTestResult: this.lastTestResult,
|
||||
lastTestError: this.lastTestError,
|
||||
};
|
||||
|
||||
// Mask secrets in config
|
||||
if (this.oauthConfig) {
|
||||
info.oauthConfig = {
|
||||
...this.oauthConfig,
|
||||
clientSecretEncrypted: this.oauthConfig.clientSecretEncrypted ? '********' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.ldapConfig) {
|
||||
info.ldapConfig = {
|
||||
...this.ldapConfig,
|
||||
bindPasswordEncrypted: this.ldapConfig.bindPasswordEncrypted ? '********' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user