Merge pull request #1154 from caprover/app-tokens

Add support for app tokens
This commit is contained in:
Kasra Bigdeli
2021-08-02 21:15:06 -07:00
committed by GitHub
10 changed files with 156 additions and 6 deletions
+30 -1
View File
@@ -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 + '',
+9
View File
@@ -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<DataStore> = {}
@@ -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)
}
+71
View File
@@ -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.
*/
+6 -1
View File
@@ -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
+4
View File
@@ -18,6 +18,10 @@ const threadLockNamespace = {} as IHashMapGeneric<boolean>
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) {
@@ -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}`)
+4 -2
View File
@@ -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 () {
+2
View File
@@ -157,6 +157,8 @@ let data = {
headerAuth: 'x-captain-auth',
headerAppToken: 'x-captain-app-token',
headerNamespace: 'x-namespace',
// ********************* ETC ************************
+4 -1
View File
@@ -335,7 +335,10 @@ export default class MigrateCaptainDuckDuck {
app.customNginxConfig,
app.preDeployFunction,
'',
false
false,
{
enabled: false,
}
)
})
})
+8
View File
@@ -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