diff --git a/src/datastore/AppsDataStore.ts b/src/datastore/AppsDataStore.ts index 81520a9..48349c1 100644 --- a/src/datastore/AppsDataStore.ts +++ b/src/datastore/AppsDataStore.ts @@ -5,6 +5,7 @@ import Authenticator from '../user/Authenticator' import ApacheMd5 from '../utils/ApacheMd5' import CaptainConstants from '../utils/CaptainConstants' import CaptainEncryptor from '../utils/Encryptor' +import Logger from '../utils/Logger' import Utils from '../utils/Utils' import configstore = require('configstore') @@ -617,7 +618,8 @@ class AppsDataStore { customNginxConfig: string, preDeployFunction: string, serviceUpdateOverride: string, - websocketSupport: boolean + websocketSupport: boolean, + appDeployTokenConfig: AppDeployTokenConfig ) { const self = this let appObj: IAppDef @@ -692,6 +694,33 @@ class AppsDataStore { appObj.serviceUpdateOverride = serviceUpdateOverride appObj.description = description + appObj.appDeployTokenConfig = { + enabled: !!appDeployTokenConfig.enabled, + appDeployToken: `${ + appDeployTokenConfig.appDeployToken + ? appDeployTokenConfig.appDeployToken + : '' + }`, + } + + if ( + appObj.appDeployTokenConfig.appDeployToken === + 'undefined' || + appObj.appDeployTokenConfig.appDeployToken === 'null' + ) { + appObj.appDeployTokenConfig = { enabled: false } + Logger.e('Bad values in the token') + } + + if (!appObj.appDeployTokenConfig.enabled) { + appObj.appDeployTokenConfig.appDeployToken = undefined + } else if (!appObj.appDeployTokenConfig.appDeployToken) { + // App is supposed to have a token, but it doesn't have one yet. The first time use case. + appObj.appDeployTokenConfig.appDeployToken = Utils.generateRandomString( + 32 + ) + } + if (httpAuth && httpAuth.user) { const newAuth: IHttpAuth = { user: httpAuth.user + '', diff --git a/src/datastore/DataStoreProvider.ts b/src/datastore/DataStoreProvider.ts index be25153..38004bf 100644 --- a/src/datastore/DataStoreProvider.ts +++ b/src/datastore/DataStoreProvider.ts @@ -2,6 +2,8 @@ * Created by kasra on 27/06/17. */ +import ApiStatusCodes from '../api/ApiStatusCodes' +import CaptainConstants from '../utils/CaptainConstants' import DataStore from './DataStore' const dataStoreCache: IHashMapGeneric = {} @@ -12,6 +14,13 @@ export default { throw new Error('NameSpace is empty') } + if (namespace !== CaptainConstants.rootNameSpace) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + 'Namespace unknown' + ) + } + if (!dataStoreCache[namespace]) { dataStoreCache[namespace] = new DataStore(namespace) } diff --git a/src/injection/Injector.ts b/src/injection/Injector.ts index cc0bb69..4abaa25 100644 --- a/src/injection/Injector.ts +++ b/src/injection/Injector.ts @@ -90,6 +90,77 @@ export function injectUser() { } } +/** + * A pseudo user injection. Only used for build triggers. Can only trigger certain actions. + */ +export function injectUserForBuildTrigger() { + return function (req: Request, res: Response, next: NextFunction) { + const locals = res.locals + + const token = req.header(CaptainConstants.headerAppToken) as string + const namespace = locals.namespace + const appName = req.params.appName as string + + if (!token || !namespace || !appName) { + Logger.e( + 'Trigger app build is called with no token/namespace/appName' + ) + next() + return + } + + const dataStore = DataStoreProvider.getDataStore(namespace) + let app: IAppDef | undefined = undefined + + Promise.resolve() + .then(function () { + return dataStore.getAppsDataStore().getAppDefinition(appName) + }) + .then(function (appFound) { + app = appFound + + const tokenMatches = + app?.appDeployTokenConfig?.enabled && + app.appDeployTokenConfig.appDeployToken === token + + if (!tokenMatches) { + Logger.e('Token mismatch for app build') + next() + return + } + + const datastore = DataStoreProvider.getDataStore(namespace) + + const serviceManager = ServiceManager.get( + namespace, + Authenticator.getAuthenticator(namespace), + datastore, + dockerApi, + CaptainManager.get().getLoadBalanceManager(), + CaptainManager.get().getDomainResolveChecker() + ) + + const user: UserModel.UserInjected = { + namespace: namespace, + dataStore: datastore, + serviceManager: serviceManager, + initialized: serviceManager.isInited(), + } + + res.locals.user = user + res.locals.app = app + res.locals.appName = appName + + next() + }) + .catch(function (error) { + Logger.e(error) + res.locals.app = undefined + next() + }) + } +} + /** * A pseudo user injection. Only used for webhooks. Can only trigger certain actions. */ diff --git a/src/models/AppDefinition.ts b/src/models/AppDefinition.ts index e60dc00..cb84984 100644 --- a/src/models/AppDefinition.ts +++ b/src/models/AppDefinition.ts @@ -68,8 +68,8 @@ interface IAppDefinitionBase { ports: IAppPort[] volumes: IAppVolume[] envVars: IAppEnvVar[] - versions: IAppVersion[] + appDeployTokenConfig?: AppDeployTokenConfig } interface IHttpAuth { @@ -78,6 +78,11 @@ interface IHttpAuth { passwordHashed?: string } +interface AppDeployTokenConfig { + enabled: boolean + appDeployToken?: string +} + interface IAppDef extends IAppDefinitionBase { appPushWebhook?: { tokenVersion: string diff --git a/src/routes/user/UserRouter.ts b/src/routes/user/UserRouter.ts index 08d4318..8cedbae 100644 --- a/src/routes/user/UserRouter.ts +++ b/src/routes/user/UserRouter.ts @@ -18,6 +18,10 @@ const threadLockNamespace = {} as IHashMapGeneric router.use('/apps/webhooks/', Injector.injectUserForWebhook()) +// Only for POST request to build the image +// Ensure that it doesn't allow for GET requests etc. +router.post('/apps/appData/:appName/', Injector.injectUserForBuildTrigger()) + router.use(Injector.injectUser()) router.use(function (req, res, next) { diff --git a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts index 370b0e4..980698f 100644 --- a/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts +++ b/src/routes/user/apps/appdefinition/AppDefinitionRouter.ts @@ -328,8 +328,24 @@ router.post('/update/', function (req, res, next) { let serviceUpdateOverride = req.body.serviceUpdateOverride || '' let containerHttpPort = Number(req.body.containerHttpPort) || 80 let httpAuth = req.body.httpAuth + let appDeployTokenConfig = req.body.appDeployTokenConfig as + | AppDeployTokenConfig + | undefined let description = req.body.description || '' + if (!appDeployTokenConfig) { + appDeployTokenConfig = { enabled: false } + } else { + appDeployTokenConfig = { + enabled: !!appDeployTokenConfig.enabled, + appDeployToken: `${ + appDeployTokenConfig.appDeployToken + ? appDeployTokenConfig.appDeployToken + : '' + }`.trim(), + } + } + if (repoInfo.user) { repoInfo.user = repoInfo.user.trim() } @@ -381,7 +397,8 @@ router.post('/update/', function (req, res, next) { customNginxConfig, preDeployFunction, serviceUpdateOverride, - websocketSupport + websocketSupport, + appDeployTokenConfig ) .then(function () { Logger.d(`AppName is updated: ${appName}`) diff --git a/src/user/ServiceManager.ts b/src/user/ServiceManager.ts index 0d983c7..3752ad1 100644 --- a/src/user/ServiceManager.ts +++ b/src/user/ServiceManager.ts @@ -664,7 +664,8 @@ class ServiceManager { customNginxConfig: string, preDeployFunction: string, serviceUpdateOverride: string, - websocketSupport: boolean + websocketSupport: boolean, + appDeployTokenConfig: AppDeployTokenConfig ) { const self = this const dataStore = this.dataStore @@ -783,7 +784,8 @@ class ServiceManager { customNginxConfig, preDeployFunction, serviceUpdateOverride, - websocketSupport + websocketSupport, + appDeployTokenConfig ) }) .then(function () { diff --git a/src/utils/CaptainConstants.ts b/src/utils/CaptainConstants.ts index 578a350..7c9f42e 100644 --- a/src/utils/CaptainConstants.ts +++ b/src/utils/CaptainConstants.ts @@ -157,6 +157,8 @@ let data = { headerAuth: 'x-captain-auth', + headerAppToken: 'x-captain-app-token', + headerNamespace: 'x-namespace', // ********************* ETC ************************ diff --git a/src/utils/MigrateCaptainDuckDuck.ts b/src/utils/MigrateCaptainDuckDuck.ts index 27a05ec..bc94395 100644 --- a/src/utils/MigrateCaptainDuckDuck.ts +++ b/src/utils/MigrateCaptainDuckDuck.ts @@ -335,7 +335,10 @@ export default class MigrateCaptainDuckDuck { app.customNginxConfig, app.preDeployFunction, '', - false + false, + { + enabled: false, + } ) }) }) diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 7a23735..924780b 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -1,3 +1,4 @@ +import * as crypto from 'crypto' import { remove } from 'fs-extra' import * as yaml from 'yaml' import Logger from './Logger' @@ -10,6 +11,13 @@ export default class Utils { return input } + static generateRandomString(byteLength?: number) { + if (!byteLength) { + byteLength = 12 + } + return crypto.randomBytes(byteLength).toString('hex') + } + static isValidIp(ip: string) { return /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( ip