feat(gaurds): use better smartguards to verify action authorization

This commit is contained in:
Philipp Kunz 2024-05-30 22:49:39 +02:00
parent 6b3fd2ce31
commit 929f250006
18 changed files with 1116 additions and 145 deletions

View File

@ -26,10 +26,10 @@
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^1.0.90",
"@git.zone/tswatch": "^2.0.23", "@git.zone/tswatch": "^2.0.23",
"@push.rocks/tapbundle": "^5.0.23", "@push.rocks/tapbundle": "^5.0.23",
"@types/node": "^20.12.12" "@types/node": "^20.12.13"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "3.0.25", "@api.global/typedrequest": "3.0.28",
"@api.global/typedserver": "^3.0.50", "@api.global/typedserver": "^3.0.50",
"@api.global/typedsocket": "^3.0.1", "@api.global/typedsocket": "^3.0.1",
"@apiclient.xyz/cloudflare": "^6.0.1", "@apiclient.xyz/cloudflare": "^6.0.1",
@ -51,7 +51,7 @@
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartexit": "^1.0.23", "@push.rocks/smartexit": "^1.0.23",
"@push.rocks/smartfile": "^11.0.15", "@push.rocks/smartfile": "^11.0.15",
"@push.rocks/smartguard": "^2.0.1", "@push.rocks/smartguard": "^3.0.2",
"@push.rocks/smartjson": "^5.0.19", "@push.rocks/smartjson": "^5.0.19",
"@push.rocks/smartjwt": "^2.0.4", "@push.rocks/smartjwt": "^2.0.4",
"@push.rocks/smartlog": "^3.0.6", "@push.rocks/smartlog": "^3.0.6",

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '1.0.216', version: '1.1.0',
description: 'A cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.' description: 'A cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.'
} }

View File

@ -21,7 +21,8 @@ import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'
import { CloudlyServerManager } from './manager.server/servermanager.js'; import { CloudlyServerManager } from './manager.server/servermanager.js';
import { ExternalApiManager } from './manager.status/statusmanager.js'; import { ExternalApiManager } from './manager.status/statusmanager.js';
import { ImageManager } from './manager.image/classes.imagemanager.js'; import { ImageManager } from './manager.image/classes.imagemanager.js';
import { logger } from './cloudly.logging.js'; import { logger } from './logger.js';
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
/** /**
* Cloudly class can be used to instantiate a cloudly server. * Cloudly class can be used to instantiate a cloudly server.
@ -48,6 +49,7 @@ export class Cloudly {
public mongodbConnector: MongodbConnector; public mongodbConnector: MongodbConnector;
// managers // managers
public authManager: CloudlyAuthManager;
public secretManager: CloudlySecretManager; public secretManager: CloudlySecretManager;
public clusterManager: ClusterManager; public clusterManager: ClusterManager;
public coreflowManager: CloudlyCoreflowManager; public coreflowManager: CloudlyCoreflowManager;
@ -71,7 +73,8 @@ export class Cloudly {
this.cloudflareConnector = new CloudflareConnector(this); this.cloudflareConnector = new CloudflareConnector(this);
this.letsencryptConnector = new LetsencryptConnector(this); this.letsencryptConnector = new LetsencryptConnector(this);
// processes // managers
this.authManager = new CloudlyAuthManager(this);
this.clusterManager = new ClusterManager(this); this.clusterManager = new ClusterManager(this);
this.coreflowManager = new CloudlyCoreflowManager(this); this.coreflowManager = new CloudlyCoreflowManager(this);
this.externalApiManager = new ExternalApiManager(this); this.externalApiManager = new ExternalApiManager(this);
@ -90,6 +93,7 @@ export class Cloudly {
await this.config.init(); await this.config.init();
// manageers // manageers
await this.authManager.start();
await this.secretManager.start(); await this.secretManager.start();
await this.serverManager.start(); await this.serverManager.start();

View File

@ -1,6 +1,6 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import * as paths from './paths.js'; import * as paths from './paths.js';
import { logger } from './cloudly.logging.js'; import { logger } from './logger.js';
import type { Cloudly } from './classes.cloudly.js'; import type { Cloudly } from './classes.cloudly.js';
/** /**
@ -56,19 +56,8 @@ export class CloudlyConfig {
], ],
}); });
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
const kvStore = await this.appData.getKvStore(); const kvStore = await this.appData.getKvStore();
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = await kvStore.readKey('jwtKeys');
if (!existingJwtKeys) {
await this.smartjwtInstance.createNewKeyPair();
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
await kvStore.writeKey('jwtKeys', newJwtKeys);
} else {
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
}
this.data = await kvStore.readAll(); this.data = await kvStore.readAll();
const missingKeys = await this.appData.logMissingKeys(); const missingKeys = await this.appData.logMissingKeys();
if (missingKeys.length > 0) { if (missingKeys.length > 0) {

View File

@ -1,7 +1,7 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import * as paths from './paths.js'; import * as paths from './paths.js';
import { Cloudly } from './classes.cloudly.js'; import { Cloudly } from './classes.cloudly.js';
import { logger } from './cloudly.logging.js'; import { logger } from './logger.js';
/** /**
* handles incoming requests from CI to deploy new versions of apps * handles incoming requests from CI to deploy new versions of apps

View File

@ -0,0 +1,9 @@
export const users = [
{
id: 'user1',
data: {
username: 'admin',
password: 'password',
}
}
]

View File

@ -35,4 +35,17 @@ export const installDemoData = async (cloudlyRef: Cloudly) => {
await cluster.delete(); await cluster.delete();
} }
// ================================================================================
// USERS
const users = await cloudlyRef.authManager.CUser.getInstances({});
for (const user of users) {
await user.delete();
}
const demoDataUsers = await import('./demo.data.users.js');
for (const user of demoDataUsers.users) {
const userInstance = new cloudlyRef.authManager.CUser();
Object.assign(userInstance, user);
await userInstance.save();
}
} }

View File

@ -3,7 +3,7 @@ early.start('cloudly');
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import * as paths from './paths.js'; import * as paths from './paths.js';
import { Cloudly } from './classes.cloudly.js'; import { Cloudly } from './classes.cloudly.js';
import { logger } from './cloudly.logging.js'; import { logger } from './logger.js';
const cloudlyQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir, true); const cloudlyQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir, true);
early.stop(); early.stop();

View File

@ -1,9 +1,17 @@
import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import type { Cloudly } from '../classes.cloudly.js';
import { logger } from '../logger.js';
import { Authorization } from './classes.authorization.js'; import { Authorization } from './classes.authorization.js';
import { User } from './classes.user.js'; import { User } from './classes.user.js';
export class AuthManager {
export interface IJwtData {
userId: string;
status: 'loggedIn' | 'loggedOut';
}
export class CloudlyAuthManager {
cloudlyRef: Cloudly cloudlyRef: Cloudly
public get db() { public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb; return this.cloudlyRef.mongodbConnector.smartdataDb;
@ -11,7 +19,58 @@ export class AuthManager {
public CUser = plugins.smartdata.setDefaultManagerForDoc(this, User); public CUser = plugins.smartdata.setDefaultManagerForDoc(this, User);
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization); public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
public typedrouter = new plugins.typedrequest.TypedRouter();
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
constructor(cloudlyRef: Cloudly) { constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef; this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
} }
public async start() {
// lets setup the smartjwtInstance
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
const kvStore = await this.cloudlyRef.config.appData.getKvStore();
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = await kvStore.readKey('jwtKeys');
if (!existingJwtKeys) {
await this.smartjwtInstance.createNewKeyPair();
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
await kvStore.writeKey('jwtKeys', newJwtKeys);
} else {
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
}
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_LoginWithUsernameAndPassword>(
'adminLoginWithUsernameAndPassword',
async (dataArg) => {
let jwt: string;
const user = await User.findUserByUsernameAndPassword(dataArg.username, dataArg.password);
if (!user) {
logger.log('warn', 'login failed');
} else {
jwt = await this.cloudlyRef.config.smartjwtInstance.createJWT({
userId: user.id,
status: 'loggedIn',
});
logger.log('success', 'login successful');
}
return {
jwt,
};
}
)
);
}
public async stop () {}
public adminJwtGuard = new plugins.smartguard.Guard<{jwt: string}>(async (dataArg) => {
const jwt = dataArg.jwt;
const jwtData: IJwtData = await this.cloudlyRef.config.smartjwtInstance.verifyJWTAndGetData(jwt);
const user = await this.CUser.getInstance({id: jwtData.userId});
return user.data.role === 'admin';
})
} }

View File

@ -2,5 +2,23 @@ import * as plugins from '../plugins.js';
@plugins.smartdata.managed() @plugins.smartdata.managed()
export class User extends plugins.smartdata.SmartDataDbDoc<User, User> { export class User extends plugins.smartdata.SmartDataDbDoc<User, User> {
public static async findUserByUsernameAndPassword(usernameArg: string, passwordArg: string) {
return await User.getInstance({
data: {
username: usernameArg,
password: passwordArg,
}
});
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: {
role: 'admin' | 'user';
username: string;
password: string;
}
} }

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import { Cloudly } from '../classes.cloudly.js'; import { Cloudly } from '../classes.cloudly.js';
import { logger } from '../cloudly.logging.js'; import { logger } from '../logger.js';
import { Cluster } from './cluster.js'; import { Cluster } from './cluster.js';

View File

@ -21,7 +21,8 @@ export class ImageManager {
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>( new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
'getAllImages', 'getAllImages',
async (requestArg) => { async (requestArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminJwtGuard], requestArg);
const images = await this.CImage.getInstances({}); const images = await this.CImage.getInstances({});
return { return {
images: await Promise.all( images: await Promise.all(

View File

@ -2,7 +2,7 @@ import * as plugins from '../plugins.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import { SecretBundle } from './classes.secretbundle.js'; import { SecretBundle } from './classes.secretbundle.js';
import { SecretGroup } from './classes.secretgroup.js'; import { SecretGroup } from './classes.secretgroup.js';
import { logger } from '../cloudly.logging.js'; import { logger } from '../logger.js';
import type { Cloudly } from '../classes.cloudly.js'; import type { Cloudly } from '../classes.cloudly.js';
/** /**
@ -34,27 +34,6 @@ export class CloudlySecretManager {
// lets set up a typedrouter // lets set up a typedrouter
this.typedrouter = new plugins.typedrequest.TypedRouter(); this.typedrouter = new plugins.typedrequest.TypedRouter();
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter); this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_LoginWithUsernameAndPassword>(
'adminLoginWithUsernameAndPassword',
async (dataArg) => {
let jwt: string;
// console.log(dataArg);
if (dataArg.username !== 'admin' || dataArg.password !== 'password') {
logger.log('warn', 'login failed');
} else {
jwt = await this.cloudlyRef.config.smartjwtInstance.createJWT({
status: 'loggedIn',
});
logger.log('success', 'login successful');
}
return {
jwt,
};
}
)
);
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>( new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>(

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js'; import { Cloudly } from '../classes.cloudly.js';
import { logger } from '../cloudly.logging.js'; import { logger } from '../logger.js';
export class CloudlyTaskmanager { export class CloudlyTaskmanager {
public cloudlyRef: Cloudly; public cloudlyRef: Cloudly;

View File

@ -32,7 +32,6 @@ import * as smartcli from '@push.rocks/smartcli';
import * as smartdata from '@push.rocks/smartdata'; import * as smartdata from '@push.rocks/smartdata';
import * as smartdelay from '@push.rocks/smartdelay'; import * as smartdelay from '@push.rocks/smartdelay';
import * as smartexit from '@push.rocks/smartexit'; import * as smartexit from '@push.rocks/smartexit';
import * as typedserver from '@api.global/typedserver';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartguard from '@push.rocks/smartguard'; import * as smartguard from '@push.rocks/smartguard';
import * as smartjson from '@push.rocks/smartjson'; import * as smartjson from '@push.rocks/smartjson';
@ -45,6 +44,7 @@ import * as smartssh from '@push.rocks/smartssh';
import * as smartstring from '@push.rocks/smartstring'; import * as smartstring from '@push.rocks/smartstring';
import * as smartunique from '@push.rocks/smartunique'; import * as smartunique from '@push.rocks/smartunique';
import * as taskbuffer from '@push.rocks/taskbuffer'; import * as taskbuffer from '@push.rocks/taskbuffer';
import * as typedserver from '@api.global/typedserver';
export { export {
npmextra, npmextra,
@ -55,7 +55,6 @@ export {
smartcli, smartcli,
smartdata, smartdata,
smartexit, smartexit,
typedserver,
smartdelay, smartdelay,
smartfile, smartfile,
smartguard, smartguard,
@ -69,6 +68,7 @@ export {
smartstring, smartstring,
smartunique, smartunique,
taskbuffer, taskbuffer,
typedserver,
}; };
// @servezone scope // @servezone scope

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '1.0.216', version: '1.1.0',
description: 'A cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.' description: 'A cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.'
} }