manu-sapiens's picture
copy of omnitool_latest - should be working
b39afbe
raw
history blame
8.37 kB
/**
* Copyright (c) 2023 MERCENARIES.AI PTE. LTD.
* All rights reserved.
*/
// ---------------------------------------------------------------------------------------------
// Authentication API integration
//
// Purpose: Provides the APIs for authentication
//
// Usage: This inherits from APIIntegration which can do the heavy lifting of registering routes
// and proxying APIS. See .mercs.(local.)yaml for how to do that
//
// ---------------------------------------------------------------------------------------------
import { type IntegrationsManager, EObjectAction, Group, Organisation, User, Workflow, omnilog } from 'omni-shared';
import { APIIntegration, type IAPIIntegrationConfig } from '../APIIntegration.js';
import {
createGetAuthenticatedUserHandler,
createLoginHandler,
createLogoutHandler,
createGenerateTokenHandler,
createAcceptTOSHandler
} from './handlers/user.js';
import { type DBService } from '../../services/DBService.js';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import {
validateCredit,
validateEmail,
validateName,
validatePassword,
validateStatus,
validateTier
} from '../../helper/validation.js';
import { generateId, hashPassword } from '../../helper/utils.js';
import { oauth2CallbackHandler, oauth2Handler } from './handlers/oauth2.js';
import { type FastifyRequest } from 'fastify';
import { getGroupByMemberId, loadUserPermission } from '../../helper/permission.js';
import { type IKVStorageConfig } from '../../core/KVStorage.js';
interface IAuthIntegrationConfig extends IAPIIntegrationConfig {
jwt: {
secret: string;
};
kvStorage: IKVStorageConfig;
}
class AuthIntegration extends APIIntegration {
db: DBService;
constructor(id: string, manager: IntegrationsManager, config: IAuthIntegrationConfig) {
super(id, manager, config || {});
this.db = manager.app.services.get('db') as DBService;
}
get serviceConfig(): IAuthIntegrationConfig {
return this.config as IAuthIntegrationConfig;
}
async load() {
this.handlers.set('login', createLoginHandler);
this.handlers.set('logout', createLogoutHandler);
this.handlers.set('getAuthenticatedUser', createGetAuthenticatedUserHandler);
this.handlers.set('generateToken', createGenerateTokenHandler);
this.handlers.set('oauth2', oauth2Handler);
this.handlers.set('oauth2Callback', oauth2CallbackHandler);
this.handlers.set('acceptTos', createAcceptTOSHandler);
return await super.load();
}
/**
* When a user logs in the system would:
* - load the user permissions
* - load the user settings
*
* @param user
*/
async login(request: FastifyRequest) {
const user = request.user as User;
const ability = await loadUserPermission(this.db, user);
// @ts-ignore
request.session.set('permission', ability);
omnilog.debug('Login user', user.id, request.session.sessionId, ability);
}
async isAdmin(user: User): Promise<boolean> {
const groups = await getGroupByMemberId(this.db, user.id);
for (const group of groups) {
if (group.name.toLowerCase() === 'admin') {
return true;
}
}
return false;
}
validateRequestParameters(params: any) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { username, password, email, status, credit, groups } = params;
const error = [];
if (username && !validateName(username)) {
this.error('Invalid username');
error.push('Invalid username');
}
if (password && !validatePassword(password)) {
this.error('Invalid password');
error.push('Invalid password');
}
if (email && !validateEmail(email)) {
this.error('Invalid email');
error.push('Invalid email');
}
if (status && !validateStatus(status)) {
this.error('Invalid status');
error.push('Invalid status');
}
if (credit && !validateCredit(credit)) {
this.error('Invalid credit');
error.push('Invalid credit');
}
return error;
}
async getUserByUsername(username: string): Promise<User | null> {
try {
const query = {
username: username.toLowerCase()
};
const result = await this.db.find(query);
if (result && result.length > 0) {
return result[0];
} else {
return null;
}
} catch (err) {
this.error(err);
throw err;
}
}
async handleRegister(username: string, password: string, tier?: any): Promise<User> {
const validationErrors = this.validateRequestParameters({ username, password });
if (!username || !password || validationErrors.length > 0) {
throw new Error('Invalid request parameters ' + validationErrors.join(', '));
}
if (tier && !(await validateTier(this.db, tier))) {
throw new Error('Invalid tier');
}
try {
const user = await this.getUserByUsername(username);
if (user == null) {
return await this.createUser(username, password, tier);
} else {
throw new Error('Unauthorized access');
}
} catch (err) {
this.error(err);
throw err;
}
}
async generateJwtToken(scopes: any, issuer: User, expiresIn: number = 3600) {
this.debug('Generating token with scopes: ', scopes);
try {
const jwtSecret = this.app.settings.get<string>('omni:auth.jwt.secret')?.value;
if (jwtSecret) {
// Generate JWT token
const token = jwt.sign(
{
scopes,
issuerId: issuer?.id || '',
tokenId: generateId()
},
jwtSecret,
{ expiresIn }
);
return token;
} else {
throw new Error('JWT secret not configured');
}
} catch (err) {
this.error(err);
throw err;
}
}
private async createUser(username: string, password: string, tier?: any): Promise<User> {
const salt = crypto.randomBytes(16);
const hashedPassword = hashPassword(password, salt);
// TODO : We automatically create an org and admin group for users on free tier
// For enterprise tier user, Org and group should be created first before members are provisioned
// Create organization
const newOrg = new Organisation(generateId(), `Org-${generateId()}`);
newOrg.createdAt = Math.floor(Date.now() / 1000);
newOrg.lastUpdated = Math.floor(Date.now() / 1000);
// Create admin group
const newGroup = new Group(generateId(), 'Admin');
newGroup.createdAt = Math.floor(Date.now() / 1000);
newGroup.lastUpdated = Math.floor(Date.now() / 1000);
newGroup.organisation = { id: newOrg.id, name: newOrg.name };
newGroup.permission = [
// Admin rights: r/w users from the same org
{
subject: User.modelName,
action: [EObjectAction.CREATE, EObjectAction.READ, EObjectAction.UPDATE, EObjectAction.DELETE],
conditions: [{ organisation: { id: newOrg.id } }]
},
// Admin rights: r/w groups from the same org
{
subject: Group.modelName,
action: [EObjectAction.CREATE, EObjectAction.READ, EObjectAction.UPDATE, EObjectAction.DELETE],
conditions: [{ organisation: { id: newOrg.id } }]
},
// Admin rights: r/w/x workflows of the same org
{
subject: Workflow.modelName,
action: [
EObjectAction.CREATE,
EObjectAction.READ,
EObjectAction.UPDATE,
EObjectAction.DELETE,
EObjectAction.EXECUTE
],
conditions: [{ org: { id: newOrg.id } }]
}
];
const newUser = new User(generateId(), username.toLowerCase());
newUser.password = hashedPassword.toString('hex');
newUser.salt = salt.toString('hex');
newUser.tier = tier;
newUser.organisation = { id: newOrg.id, name: newOrg.name };
newUser.createdAt = Math.floor(Date.now() / 1000);
newUser.lastUpdated = Math.floor(Date.now() / 1000);
// TODO: Add more info on sign up
await this.db.put(newUser);
newOrg.members = [{ id: newUser.id, name: newUser.username }];
newOrg.groups = [{ id: newGroup.id, name: newGroup.name }];
await this.db.put(newOrg);
newGroup.members = [{ id: newUser.id, name: newUser.username }];
await this.db.put(newGroup);
return newUser;
}
}
export { AuthIntegration, type IAuthIntegrationConfig };