Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BYO creds related issue #66

Merged
merged 10 commits into from
Oct 5, 2020
88 changes: 88 additions & 0 deletions lib/AuthManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const TvmClient = require('@adobe/aio-lib-core-tvm')
const { AzureTVMWrapper } = require('./impl/AzureTVMWrapper')
const utils = require('./utils')
const logger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-files', { provider: 'debug' })

/**
* Provides creds either from remote or local TVM
* @class AuthManager
* @classdesc proxy to local or remote TVM
*/
class AuthManager {

/**
* [INTERNAL] Creates an instance of AuthManager. Use static init instead.
*
* @param {TVMClient} remoteTVM tvm client instance
* @param {TVMWrapper} localTVM TVMWrapper instance
* @memberof AuthManager
* @private
*/
constructor(remoteTVM, localTVM) {
this.remoteTVM = remoteTVM
this.localTVM = localTVM
}

/**
* Creates and return an instance of AuthManager
* @static
* @param {object} credentials abstract credentials
* @returns {Promise<Files>} a new Files instance
* @memberof Files
* @abstract
* @private
*/
static async init (config, provider) {
/* istanbul ignore else */
if (provider === 'azure') {
logger.debug('init with openwhisk credentials.')
// remember config.ow can be empty if env vars are set
const tvmArgs = { ow: config.ow, ...config.tvm }
const remoteTVM = await TvmClient.init(tvmArgs)

let localTVM
if (config.azure) {
logger.debug('init with azure blob credentials.')
localTVM = await AzureTVMWrapper.init(config.azure)
}

const authMgr = new AuthManager(remoteTVM, localTVM)
return authMgr
}
}

async getAzureBlobCredentials() {
let creds
if(this.localTVM !== undefined) {
creds = await this.localTVM.getAzureBlobCredentials()
creds.sasCreds = false
} else {
creds = await utils.wrapTVMRequest(this.remoteTVM.getAzureBlobCredentials())
creds.sasCreds = true
}
return creds
}

async getAzureBlobPresignCredentials(presignOptions) {
if(this.localTVM !== undefined) {
return await this.localTVM.getAzureBlobPresignCredentials(presignOptions)
} else {
return await utils.wrapTVMRequest(this.remoteTVM.getAzureBlobPresignCredentials(presignOptions))
}
}

}

module.exports = { AuthManager }
64 changes: 64 additions & 0 deletions lib/TVMWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
const joi = require('@hapi/joi')
const { Files, FilePermissions } = require('./Files')
const { codes, logAndThrow } = require('./FilesError')
const logger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-files', { provider: 'debug' })

/**
* Local TVM proxy to emulate functionality of TVM
* @abstract
* @class TVMWrapper
* @classdesc Emulates TVM
*/
class TVMWrapper {

/**
* Initializes and returns a new TVMWrapper instance
*
* @param {boolean} _isTest set this to true to allow construction
* @memberof TVMWrapper
* @abstract
* @private
*/
constructor (_isTest) {
if (new.target === TVMWrapper && !_isTest) throwNotImplemented('TVMWrapper')
}

/**
* @static
* @param {object} credentials abstract credentials
* @returns {Promise<TVMWrapper>} a new TVMWrapper instance
* @memberof TVMWrapper
* @abstract
* @private
*/
static async init (credentials) {
throwNotImplemented('init')
}

async getAzureBlobCredentials() {
throwNotImplemented('getAzureBlobCredentials')
}

async getAzureBlobPresignCredentials(presignOptions) {
throwNotImplemented('getAzureBlobPresignCredentials')
}

}

// eslint-disable-next-line jsdoc/require-jsdoc
function throwNotImplemented (funcName) {
logAndThrow(new codes.ERROR_NOT_IMPLEMENTED({ messageValues: [funcName] }))
}

module.exports = { TVMWrapper }
90 changes: 25 additions & 65 deletions lib/impl/AzureBlobFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,6 @@ const STREAM_MAX_BUFFERS = 20
const AZURE_STORAGE_DOMAIN = 'blob.core.windows.net'
const DEFAULT_CDN_STORAGE_HOST = 'firefly.azureedge.net'

/**
* Creates a container if it does not exist
*
* @param {azure.ContainerURL} containerURL azure ContainerUrl
* @param {azure.Aborter} aborter azure Aborter
* @param {boolean} [isPublic=false] set to true to create a public container
* @private
*/
async function createContainerIfNotExists (containerURL, aborter, isPublic = false) {
try {
logger.debug(`creating ${isPublic ? 'public' : 'private'} azure blob container`)
await containerURL.create(aborter, isPublic ? { access: 'blob' } : {})
} catch (e) {
// bug in the past where randomly switch from Code to code.. weird
if (!(typeof e.body === 'object' && (e.body.Code === 'ContainerAlreadyExists' || e.body.code === 'ContainerAlreadyExists'))) throw e
logger.debug(`${isPublic ? 'public' : 'private'} azure blob container already exists`)
}
}

// todo move somewhere else
// eslint-disable-next-line jsdoc/require-jsdoc
function lookupMimeType (filePath) {
Expand All @@ -70,44 +51,34 @@ class AzureBlobFiles extends Files {
* @memberof AzureBlobFiles
* @private
*/
constructor (credentials, tvm) {
constructor (credentials, authMgr) {
super()
this.tvm = tvm
const cloned = utils.withHiddenFields(credentials, ['storageAccessKey', 'sasURLPrivate', 'sasURLPublic'])
logger.debug(`init AzureBlobFiles with config ${JSON.stringify(cloned, null, 2)}`)

const res = joi.object().label('credentials').keys({
// either
sasURLPrivate: joi.string().uri(),
sasURLPublic: joi.string().uri(),
// or
storageAccessKey: joi.string(),
storageAccount: joi.string(),
containerName: joi.string()
}).required().unknown().and('storageAccount', 'storageAccessKey', 'containerName').and('sasURLPrivate', 'sasURLPublic').xor('sasURLPrivate', 'storageAccount')
.validate(credentials)
if (res.error) {
logAndThrow(new codes.ERROR_BAD_ARGUMENT({ messageValues: [res.error.message], sdkDetails: cloned }))
}

// todo parse containerName for invalid chars

/** @private */
this.authMgr = authMgr
this._azure = {}
this._azure.aborter = azure.Aborter.none
if (credentials.sasURLPrivate) {
if(credentials.sasCreds) {
const cloned = utils.withHiddenFields(credentials, ['storageAccessKey', 'sasURLPrivate', 'sasURLPublic'])
logger.debug(`init AzureBlobFiles with config ${JSON.stringify(cloned, null, 2)}`)

const res = joi.object().label('credentials').keys({
sasURLPrivate: joi.string().uri(),
sasURLPublic: joi.string().uri(),

}).required().unknown().and('sasURLPrivate', 'sasURLPublic')
.validate(credentials)
if (res.error) {
logAndThrow(new codes.ERROR_BAD_ARGUMENT({ messageValues: [res.error.message], sdkDetails: credentials }))
}

/** @private */
const azureCreds = new azure.AnonymousCredential()
const pipeline = azure.StorageURL.newPipeline(azureCreds)
this._azure.containerURLPrivate = new azure.ContainerURL(credentials.sasURLPrivate, pipeline)
this._azure.containerURLPublic = new azure.ContainerURL(credentials.sasURLPublic, pipeline)
this._azure.sasCreds = true

} else {
const azureCreds = new azure.SharedKeyCredential(credentials.storageAccount, credentials.storageAccessKey)
const pipeline = azure.StorageURL.newPipeline(azureCreds)
const serviceURL = new azure.ServiceURL(`https://${credentials.storageAccount}.blob.core.windows.net/`, pipeline)
this._azure.containerURLPrivate = azure.ContainerURL.fromServiceURL(serviceURL, credentials.containerName + '')
this._azure.containerURLPublic = azure.ContainerURL.fromServiceURL(serviceURL, credentials.containerName + '-public')
this._azure.sasCreds = false
this._azure.containerURLPrivate = credentials.sasURLPrivate
this._azure.containerURLPublic = credentials.sasURLPublic
}
}

Expand All @@ -121,21 +92,10 @@ class AzureBlobFiles extends Files {
* @returns {Promise<AzureBlobFiles>} new instance
* @memberof AzureBlobFiles
*/
static async init (credentials, tvm) {
const azureFiles = new AzureBlobFiles(credentials, tvm)

// todo don't do those requests for perf reasons?
// container sas creds are not allowed to create containers and so those
// credentials must point to already existing containers
if (!azureFiles._azure.sasCreds) {
logger.debug('using azure storage account credentials')
// for the non sasCreds case we can make sure those containers exists
const errorDetails = { containerName: credentials.containerName, storageAccount: credentials.storageAccount }
await azureFiles._wrapProviderRequest(createContainerIfNotExists(azureFiles._azure.containerURLPrivate, azureFiles._azure.aborter), errorDetails)
await azureFiles._wrapProviderRequest(createContainerIfNotExists(azureFiles._azure.containerURLPublic, azureFiles._azure.aborter, true), errorDetails)
return azureFiles
}
logger.debug('using azure SAS credentials')
static async init (authMgr) {
//get credentails from AuthManager
const credentials = await authMgr.getAzureBlobCredentials()
const azureFiles = new AzureBlobFiles(credentials, authMgr)
return azureFiles
}

Expand Down Expand Up @@ -355,7 +315,7 @@ class AzureBlobFiles extends Files {
}

const presignOptions = Object.assign({ blobName: filePath }, options)
const cred = await utils.wrapTVMRequest(this.tvm.getAzureBlobPresignCredentials(presignOptions))
const cred = await utils.wrapTVMRequest(this.authMgr.getAzureBlobPresignCredentials(presignOptions))
return this._getUrl(filePath) + '?' + cred.signature
}

Expand Down
119 changes: 119 additions & 0 deletions lib/impl/AzureTVMWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const azure = require('@azure/storage-blob')
const joi = require('@hapi/joi')
const { TVMWrapper } = require('../TVMWrapper')
const { codes, logAndThrow } = require('./FilesError')
const logger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-files', { provider: 'debug' })

/**
* Creates a container if it does not exist
*
* @param {azure.ContainerURL} containerURL azure ContainerUrl
* @param {azure.Aborter} aborter azure Aborter
* @param {boolean} [isPublic=false] set to true to create a public container
* @private
*/
async function createContainerIfNotExists (containerURL, aborter, isPublic = false) {
try {
logger.debug(`creating ${isPublic ? 'public' : 'private'} azure blob container`)
await containerURL.create(aborter, isPublic ? { access: 'blob' } : {})
} catch (e) {
// bug in the past where randomly switch from Code to code.. weird
if (!(typeof e.body === 'object' && (e.body.Code === 'ContainerAlreadyExists' || e.body.code === 'ContainerAlreadyExists'))) throw e
logger.debug(`${isPublic ? 'public' : 'private'} azure blob container already exists`)
}
}

/**
* Local TVM proxy to emulate Azure based functionality of TVM
* @class AzureTVMWrapper
* @classdesc Local TVM Implementation on top of Azure Blob
* @augments LocalTVM
* @hideconstructor
* @private
*/
class AzureTVMWrapper extends TVMWrapper {

/**
* [INTERNAL] Creates an instance of AzureTVMWrapper. Use static init instead.
*
* @param {AzureCredentials} credentials {@link AzureCredentials}
* @memberof AzureTVMWrapper
* @private
*/
constructor (credentials) {
super()
this.credentials = credentials
const res = joi.object().label('credentials').keys({
storageAccessKey: joi.string(),
storageAccount: joi.string(),
containerName: joi.string()
}).required().unknown().and('storageAccount', 'storageAccessKey', 'containerName')
.validate(credentials)
if (res.error) {
logAndThrow(new codes.ERROR_BAD_ARGUMENT({ messageValues: [res.error.message], sdkDetails: cloned }))
}
}

/**
* Creates and return an instance of AzureTVMWrapper
*
* @static
* @param {AzureCredentials} credentials {@link AzureCredentials}
* @returns {Promise<AzureTVMWrapper>} new instance
* @memberof AzureTVMWrapper
*/
static async init (credentials) {
return new AzureTVMWrapper(credentials)
}

async getAzureBlobCredentials() {
const azureCreds = new azure.SharedKeyCredential(this.credentials.storageAccount, this.credentials.storageAccessKey)
const pipeline = azure.StorageURL.newPipeline(azureCreds)
const serviceURL = new azure.ServiceURL(`https://${this.credentials.storageAccount}.blob.core.windows.net/`, pipeline)
const privateSASURL = azure.ContainerURL.fromServiceURL(serviceURL, this.credentials.containerName + '')
const publicSASURL = azure.ContainerURL.fromServiceURL(serviceURL, this.credentials.containerName + '-public')

const errorDetails = { containerName: this.credentials.containerName, storageAccount: this.credentials.storageAccount }
await createContainerIfNotExists(privateSASURL, azure.Aborter.none)
await createContainerIfNotExists(publicSASURL, azure.Aborter.none, true)

return {
sasURLPrivate: privateSASURL,
sasURLPublic: publicSASURL
}
}

async getAzureBlobPresignCredentials(params) {
const sharedKeyCredential = new azure.SharedKeyCredential(this.credentials.storageAccount, this.credentials.storageAccessKey)
const containerName = this.credentials.containerName
// generate SAS token
const expiryTime = new Date(Date.now() + (1000 * params.expiryInSeconds))
const perm = (params.permissions === undefined) ? 'r' : params.permissions
const permissions = azure.BlobSASPermissions.parse(perm)
const commonSasParams = {
permissions: permissions.toString(),
expiryTime: expiryTime,
blobName: params.blobName
}

const sasQueryParamsPrivate = azure.generateBlobSASQueryParameters({ ...commonSasParams, containerName: containerName }, sharedKeyCredential)
return {
signature: sasQueryParamsPrivate.toString()
}
}

}

module.exports = { AzureTVMWrapper }
Loading