Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
davlgd committed Feb 4, 2025
1 parent 1995abc commit d45f8f3
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 1 deletion.
49 changes: 48 additions & 1 deletion bin/clever.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as accesslogsModule from '../src/commands/accesslogs.js';
import * as activity from '../src/commands/activity.js';
import * as addon from '../src/commands/addon.js';
import * as applications from '../src/commands/applications.js';
import * as biscuits from '../src/commands/biscuits.js';
import * as cancelDeploy from '../src/commands/cancel-deploy.js';
import * as config from '../src/commands/config.js';
import * as create from '../src/commands/create.js';
Expand Down Expand Up @@ -62,6 +63,7 @@ import * as stop from '../src/commands/stop.js';
import * as tcpRedirs from '../src/commands/tcp-redirs.js';
import * as unlink from '../src/commands/unlink.js';
import * as version from '../src/commands/version.js';
import * as waf from '../src/commands/waf.js';
import * as webhooks from '../src/commands/webhooks.js';
import * as database from '../src/commands/database.js';
import { curl } from '../src/commands/curl.js';
Expand Down Expand Up @@ -94,11 +96,14 @@ async function run () {

// ARGUMENTS
const args = {
otoroshiKeypairId: cliparse.argument('keypair-id', {
description: 'A Biscuit keypair ID from an Otoroshi operator',
}),
otoroshiDestination: cliparse.argument('destination', {
description: 'A destination path for a protected route (e.g. /api)',
}),
otoroshiRoute: cliparse.argument('route', {
description: 'A route to manage with Otoroshi add-on',
description: 'A route to manage with Otoroshi operator',
}),
kvRawCommand: cliparse.argument('command', {
description: 'The raw command to send to the Materia KV or Redis® add-on',
Expand Down Expand Up @@ -627,6 +632,28 @@ async function run () {
commands: [applicationsListRemoteCommand, applicationsExposeCommand, applicationsUnexposeCommand],
}, applications.list);

// BISCUITS COMMANDS
const biscuitsGenKeypairCommand = cliparse.command('gen-keypair', {
description: 'Generate a new Biscuit keypair',
args: [args.addonIdOrName],
options: [opts.humanJsonOutputFormat],
}, biscuits.keygen);
const biscuitsListCommand = cliparse.command('get', {
description: 'Get a Biscuit keypairs from an Otoroshi add-on',
args: [args.otoroshiKeypairId, args.addonIdOrName],
options: [opts.humanJsonOutputFormat],
}, biscuits.get);
const biscuitsProtectCommand = cliparse.command('protect', {
description: 'Protect an application route with a Biscuit-based token',
args: [args.addonIdOrName, args.otoroshiRoute, args.otoroshiDestination],
}, biscuits.protect);
const biscuitsCommand = cliparse.command('biscuits', {
description: 'List all biscuits',
args: [args.addonIdOrName],
privateOptions: [opts.humanJsonOutputFormat],
commands: [biscuitsGenKeypairCommand, biscuitsListCommand, biscuitsProtectCommand],
}, biscuits.list);

// CANCEL DEPLOY COMMAND
const cancelDeployCommand = cliparse.command('cancel-deploy', {
description: 'Cancel an ongoing deployment',
Expand Down Expand Up @@ -1180,6 +1207,24 @@ async function run () {
args: [],
}, version.version);

const wafEnableCommand = cliparse.command('enable', {
description: 'Enable WAF services for a route on an Otoroshi add-on',
args: [args.addonIdOrName, args.otoroshiRoute, args.otoroshiDestination],
}, waf.enable);
const wafDisableCommand = cliparse.command('disable', {
description: 'Disable WAF services for a route on an Otoroshi add-on',
args: [args.ngIdOrLabel],
}, waf.disable);
const wafGetCommand = cliparse.command('get', {
description: 'Get information about the WAF services for an Otoroshi add-on',
args: [args.addonIdOrName],
options: [opts.humanJsonOutputFormat],
}, waf.get);
const wafCommand = cliparse.command('waf', {
description: 'Manage the Web Application Service for Clever Cloud applications',
commands: [wafEnableCommand, wafDisableCommand, wafGetCommand],
}, waf.get);

// WEBHOOKS COMMAND
const addWebhookCommand = cliparse.command('add', {
description: 'Register webhook to be called when events happen',
Expand Down Expand Up @@ -1267,9 +1312,11 @@ async function run () {
const featuresFromConf = await getFeatures();

if (featuresFromConf.operators) {
commands.push(colorizeExperimentalCommand(biscuitsCommand, 'operators'));
commands.push(colorizeExperimentalCommand(keycloakCommand, 'operators'));
commands.push(colorizeExperimentalCommand(metabaseCommand, 'operators'));
commands.push(colorizeExperimentalCommand(otoroshiCommand, 'operators'));
commands.push(colorizeExperimentalCommand(wafCommand, 'operators'));
}

if (featuresFromConf.kv) {
Expand Down
54 changes: 54 additions & 0 deletions src/commands/biscuits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import colors from 'colors/safe.js';

import { Logger } from '../logger.js';
import { findAddonsByNameOrId } from '../models/ids-resolver.js';
import { genBiscuitsKeypair, getBiscuitsKeypair, getBiscuitsKeyPairTemplate, protectRoute } from '../models/otoroshi-biscuits.js';
import { getBiscuitKeypairs } from '../models/otoroshi-instances-api.js';
import { getOtoroshiApiParams, sendToOtoroshi } from '../models/otoroshi.js';

export async function get (params) {
const [keypairId, addonIdOrName] = params.args;
const { format } = params.options;
const keypair = await getBiscuitsKeypair(addonIdOrName, keypairId);

switch (format) {
case 'json':
Logger.printJson(keypair);
break;
case 'human':
default:
console.table(keypair);
break;
}
}

export async function protect (params) {
const [addonIdOrName, route, destination] = params.args;
await protectRoute(addonIdOrName, route, destination);
}

export async function keypairsList (params) {
const [addonIdOrName] = params.args;
const [otoroshi] = await findAddonsByNameOrId(addonIdOrName.addon_name);

const otoroshiConfig = await getOtoroshiApiParams(otoroshi.realId);
const keypairs = await getBiscuitKeypairs(otoroshiConfig).then(sendToOtoroshi);

keypairs.forEach((k) => {
console.table(k);
});
}

export async function keygen (params) {
const [addonIdOrName] = params.args;

const template = await getBiscuitsKeyPairTemplate(addonIdOrName);

template.name = 'Biscuit CT Key Pair';
template.description = 'A new Biscuit key pair created from Clever Tools';

const keypair = await genBiscuitsKeypair(addonIdOrName, template);

Logger.println(`🔑 New key pair successfully generated for Otoroshi add-on ${colors.blue(addonIdOrName.addon_name)}`);
Logger.println(` └─ Public key: ${colors.green(keypair.pubKey)}`);
}
17 changes: 17 additions & 0 deletions src/commands/waf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { enableWaf } from '../models/waf.js';

export async function enable (params) {
const [addonIdOrName, route, destination] = params.args;

await enableWaf(addonIdOrName, route, destination);
}

export async function disable (params) {
const [addonIdOrName, route, destination] = params.args;

await enableWaf(addonIdOrName, route, destination);
}

export async function get (params) {

}
76 changes: 76 additions & 0 deletions src/models/otoroshi-biscuits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import colors from 'colors/safe.js';

import { Logger } from '../logger.js';
import { select } from '@inquirer/prompts';
import { getOtoroshiApiParams, sendToOtoroshi } from './otoroshi.js';
import { createBiscuitVerifier, createRoute, genBiscuitKeypair, getBiscuitKeypair, getBiscuitKeypairs, getBiscuitKeypairsTemplate, getBiscuitVerifierTemplate, getRouteTemplate } from './otoroshi-instances-api.js';

export async function protectRoute (addonIdOrName, pathToProtect, destination) {
const otoroshi = await getOtoroshiApiParams(addonIdOrName);

const keypairs = await getBiscuitKeypairs(otoroshi).then(sendToOtoroshi);

if (!keypairs.length) {
throw new Error(`No biscuit keypair found, please generate one first with ${colors.blue('clever biscuits gen-keypair')} command`);
}

let keypair = keypairs[0].id;
if (keypairs.length > 0) {
keypair = await select({
type: 'select',
name: 'keypairId',
message: 'Select a keypair to use:',
choices: keypairs.map((keypair) => ({
name: `${keypair.name} (${keypair.pubKey})`,
value: keypair.id,
})),
});
}

const responseTemplate = await getBiscuitVerifierTemplate(otoroshi).then(sendToOtoroshi);

const verifier = await responseTemplate.json();
verifier.keypair_ref = keypair;
verifier.config.checks = ['check if time($time), $time <= 2025-03-30T20:00:00Z;'];
verifier.config.policies = ['allow if user("davlgd");'];

const createdVerifier = await createBiscuitVerifier(otoroshi, verifier).then(sendToOtoroshi);

const verifierPlugin = {
enabled: true,
debug: false,
plugin: 'cp:otoroshi_plugins.com.cloud.apim.otoroshi.extensions.biscuit.plugins.BiscuitTokenValidator',
config: {
verifier_ref: createdVerifier.id,
extractor_type: 'header',
extractor_name: 'Authorization',
},
};

const [hostname, ...path] = destination.split('/');
const route = await getRouteTemplate(otoroshi).then(sendToOtoroshi);
route.frontend.domains = [`${otoroshi.routeBaseDomain}${pathToProtect}`];
route.backend.targets[0].hostname = hostname;
route.backend.root = `/${path.join('/')}`;
route.plugins.push(verifierPlugin);

await createRoute(otoroshi, route).then(sendToOtoroshi);

Logger.println(`${colors.green('✔')} Route protection enabled successfully`);
Logger.println(` └─ URL: ${colors.blue(`http://${otoroshi.routeBaseDomain}${pathToProtect}`)}`);
}

export async function getBiscuitsKeyPairTemplate (addonIdOrName) {
const auth = await getOtoroshiApiParams(addonIdOrName);
return getBiscuitKeypairsTemplate(auth).then(sendToOtoroshi);
}

export async function genBiscuitsKeypair (addonIdOrName, body) {
const auth = await getOtoroshiApiParams(addonIdOrName);
return genBiscuitKeypair(auth, body).then(sendToOtoroshi);
}

export async function getBiscuitsKeypair (addonIdOrName, keypairId) {
const auth = await getOtoroshiApiParams(addonIdOrName);
return getBiscuitKeypair(auth, keypairId).then(sendToOtoroshi);
}
20 changes: 20 additions & 0 deletions src/models/otoroshi-instances-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ export function getBiscuitKeypairs (auth) {
// no body
});
}

/**
* GET /apis/biscuit.extensions.cloud-apim.com/v1/biscuit-keypairs
* @param {string} auth.apiId
* @param {string} auth.apiSecret
* @param {string} keypairId
*/
export function getBiscuitKeypair (auth, keypairId) {
return Promise.resolve({
method: 'get',
url: `${auth.apiUrl}/apis/biscuit.extensions.cloud-apim.com/v1/biscuit-keypairs/${keypairId}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${Buffer.from(`${auth.apiId}:${auth.apiSecret}`).toString('base64')}`,
},
// no queryParams
// no body
});
}

/**
* GET /apis/biscuit.extensions.cloud-apim.com/v1/biscuit-keypairs/_template
* @param {string} auth.apiId
Expand Down
39 changes: 39 additions & 0 deletions src/models/waf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import colors from 'colors/safe.js';

import { Logger } from '../logger.js';
import { getOtoroshiApiParams, sendToOtoroshi } from './otoroshi.js';
import { createRoute, createWaf, getRouteTemplate, getWafTemplate } from './otoroshi-instances-api.js';

export async function enableWaf (addonIdOrName, pathToProtect, destination) {
const otoroshi = await getOtoroshiApiParams(addonIdOrName);

const responseTemplate = await getWafTemplate(otoroshi).then(sendToOtoroshi);
const waf = await responseTemplate.json();
waf.name = pathToProtect;
waf.description = `WAF for ${pathToProtect}`;

const responseWafConfig = await createWaf(otoroshi, waf).then(sendToOtoroshi);
const createdWafConfig = await responseWafConfig.json();

const wafPlugin = {
enabled: true,
debug: false,
plugin: 'cp:otoroshi.wasm.proxywasm.NgCorazaWAF',
config: {
ref: createdWafConfig.id,
},
};

const [hostname, ...path] = destination.split('/');
const responseRoute = await getRouteTemplate(otoroshi).then(sendToOtoroshi);
const route = await responseRoute.json();
route.frontend.domains = [`${otoroshi.routeBaseDomain}${pathToProtect}`];
route.backend.targets[0].hostname = hostname;
route.backend.root = `/${path.join('/')}`;
route.plugins.push(wafPlugin);

await createRoute(otoroshi, route).then(sendToOtoroshi);

Logger.println(`${colors.green('✔')} WAF enabled successfully`);
Logger.println(` └─ URL: ${colors.blue(`http://${otoroshi.routeBaseDomain}${pathToProtect}`)}`);
}

0 comments on commit d45f8f3

Please sign in to comment.