diff --git a/doc/api.md b/doc/api.md
index d4251ed..b3836b2 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -539,6 +539,7 @@ Azure account credentials. Must have the permission to create containers.
| storageAccount | string
| name of azure storage account |
| storageAccessKey | string
| access key for azure storage account |
| containerName | string
| name of container to store files. Another `${containerName}-public` will also be used for public files. |
+| [hostName] | string
| custom domain for returned URLs |
diff --git a/lib/FilesError.js b/lib/FilesError.js
index b5781e3..6f87470 100644
--- a/lib/FilesError.js
+++ b/lib/FilesError.js
@@ -45,6 +45,7 @@ E('ERROR_FILE_NOT_EXISTS', 'file `%s` does not exist')
E('ERROR_BAD_FILE_TYPE', '%s')
E('ERROR_OUT_OF_RANGE', '%s')
E('ERROR_MISSING_OPTION', '%s')
+E('ERROR_UNSUPPORTED_OPERATION', '%s')
module.exports = {
codes,
diff --git a/lib/impl/AzureBlobFiles.js b/lib/impl/AzureBlobFiles.js
index f4f2d24..b8d9267 100644
--- a/lib/impl/AzureBlobFiles.js
+++ b/lib/impl/AzureBlobFiles.js
@@ -70,9 +70,12 @@ class AzureBlobFiles extends Files {
* @memberof AzureBlobFiles
* @private
*/
- constructor (credentials, tvm) {
+ constructor (credentials, tvm = null) {
super()
+ /** @private */
this.tvm = tvm
+ /** @private */
+ this.hasOwnCredentials = (tvm === null)
const cloned = utils.withHiddenFields(credentials, ['storageAccessKey', 'sasURLPrivate', 'sasURLPublic'])
logger.debug(`init AzureBlobFiles with config ${JSON.stringify(cloned, null, 2)}`)
@@ -95,6 +98,8 @@ class AzureBlobFiles extends Files {
/** @private */
this._azure = {}
this._azure.aborter = azure.Aborter.none
+ /** @private */
+ this.credentials = utils.clone(credentials)
if (credentials.sasURLPrivate) {
const azureCreds = new azure.AnonymousCredential()
const pipeline = azure.StorageURL.newPipeline(azureCreds)
@@ -335,9 +340,17 @@ class AzureBlobFiles extends Files {
* @private
*/
_getUrl (filePath) {
+ let hostName = DEFAULT_CDN_STORAGE_HOST
const azureURL = this._propsForPath(filePath).blockBlobURL.url.split('?')[0]
+ if (this.hasOwnCredentials) {
+ if (this.credentials.hostName) {
+ hostName = this.credentials.hostName
+ } else {
+ return azureURL
+ }
+ }
const index = azureURL.indexOf(AZURE_STORAGE_DOMAIN)
- return 'https://' + DEFAULT_CDN_STORAGE_HOST + azureURL.substring(index + AZURE_STORAGE_DOMAIN.length)
+ return 'https://' + hostName + azureURL.substring(index + AZURE_STORAGE_DOMAIN.length)
}
/**
@@ -355,10 +368,46 @@ class AzureBlobFiles extends Files {
}
const presignOptions = Object.assign({ blobName: filePath }, options)
- const cred = await utils.wrapTVMRequest(this.tvm.getAzureBlobPresignCredentials(presignOptions))
+
+ let cred
+ if (this.hasOwnCredentials) {
+ // generate signature based on options and using own credentials
+ cred = await this._getAzureBlobPresignCredentials(presignOptions)
+ } else {
+ cred = await utils.wrapTVMRequest(this.tvm.getAzureBlobPresignCredentials(presignOptions))
+ }
+
return this._getUrl(filePath) + '?' + cred.signature
}
+ /**
+ * @memberof AzureBlobFiles
+ * @private
+ */
+ async _getAzureBlobPresignCredentials (params) {
+ if (this._azure.sasCreds) {
+ const msg = 'generatePresignURL is not supported with Azure Container SAS credentials, please initialize the SDK with Azure storage account credentials instead'
+ logAndThrow(new codes.ERROR_UNSUPPORTED_OPERATION({ messageValues: [msg], sdkDetails: 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 permissions = azure.BlobSASPermissions.parse(params.permissions)
+ const commonSasParams = {
+ permissions: permissions.toString(),
+ expiryTime: expiryTime,
+ blobName: params.blobName
+ }
+
+ const sasQueryParamsPrivate = azure.generateBlobSASQueryParameters({ ...commonSasParams, containerName: containerName }, sharedKeyCredential)
+ return {
+ signature: sasQueryParamsPrivate.toString()
+ }
+ }
+
/**
* @memberof AzureBlobFiles
* @override
diff --git a/lib/init.js b/lib/init.js
index 6079e56..76ec362 100644
--- a/lib/init.js
+++ b/lib/init.js
@@ -57,24 +57,20 @@ async function init (config = {}) {
// 1. set provider
const provider = 'azure' // only azure is supported for now
- // 2. instantiate tvm
- let tvm
- /* 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 }
- tvm = await TvmClient.init(tvmArgs)
- }
-
- // 3. return state store based on provider
+ // 2. return state store based on provider
switch (provider) {
case 'azure':
if (config.azure) {
logger.debug('init with azure blob credentials.')
- return AzureBlobFiles.init(config.azure, tvm)
+ return AzureBlobFiles.init(config.azure, null)
+ } else {
+ 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 tvm = await TvmClient.init(tvmArgs)
+ return AzureBlobFiles.init(await utils.wrapTVMRequest(tvm.getAzureBlobCredentials()), tvm)
}
- return AzureBlobFiles.init(await utils.wrapTVMRequest(tvm.getAzureBlobCredentials()), tvm)
+
// default:
// throw new FilesError(`provider '${provider}' is not supported.`, FilesError.codes.BadArgument)
}
diff --git a/lib/types.jsdoc.js b/lib/types.jsdoc.js
index 9c0d677..cae66ef 100644
--- a/lib/types.jsdoc.js
+++ b/lib/types.jsdoc.js
@@ -43,6 +43,7 @@ governing permissions and limitations under the License.
* @property {string} storageAccessKey access key for azure storage account
* @property {string} containerName name of container to store files.
* Another `${containerName}-public` will also be used for public files.
+ * @property {string} [hostName] custom domain for returned URLs
*/
/**
* @typedef RemotePathString
diff --git a/lib/utils.js b/lib/utils.js
index 8dfdbfb..baac8e7 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -30,7 +30,13 @@ function withHiddenFields (toCopy, fields) {
return copyConfig
}
+// eslint-disable-next-line jsdoc/require-jsdoc
+function clone (toCopy) {
+ return cloneDeep(toCopy)
+}
+
module.exports = {
withHiddenFields,
- wrapTVMRequest
+ wrapTVMRequest,
+ clone
}
diff --git a/test/impl/AzureBlobFiles.test.js b/test/impl/AzureBlobFiles.test.js
index 3e08424..1896bd0 100644
--- a/test/impl/AzureBlobFiles.test.js
+++ b/test/impl/AzureBlobFiles.test.js
@@ -21,6 +21,11 @@ const fakeSASCredentials = {
sasURLPrivate: 'https://fake.com/private?secret=abcd',
sasURLPublic: 'https://fake.com/public?secret=abcd'
}
+const fakeUserCredentials = {
+ containerName: 'fake',
+ storageAccessKey: 'fakeKey',
+ storageAccount: 'fakeAccount'
+}
const fakeAborter = 'fakeAborter'
const DEFAULT_CDN_STORAGE_HOST = 'https://firefly.azureedge.net'
@@ -52,11 +57,6 @@ const testWithProviderError = async (boundFunc, providerMock, errorDetails, file
describe('init', () => {
const fakeAzureAborter = 'fakeAborter'
const mockContainerCreate = jest.fn()
- const fakeUserCredentials = {
- containerName: 'fake',
- storageAccessKey: 'fakeKey',
- storageAccount: 'fakeAccount'
- }
beforeEach(async () => {
mockContainerCreate.mockReset()
@@ -495,18 +495,35 @@ describe('_copyRemoteToRemoteFile', () => {
})
describe('_getUrl', () => {
+ const fakeAzureAborter = 'fakeAborter'
+ const mockContainerCreate = jest.fn()
+
const mockBlockBlob = jest.fn()
const setMockBlobUrl = url => {
azure.BlockBlobURL.fromContainerURL = mockBlockBlob.mockReturnValue({ url })
}
+
+ const tvm = jest.fn()
+
/** @type {AzureBlobFiles} */
let files
beforeEach(async () => {
mockBlockBlob.mockReset()
+
+ tvm.getAzureBlobPresignCredentials = jest.fn()
+ tvm.getAzureBlobPresignCredentials.mockResolvedValue({
+ signature: 'fakesign'
+ })
+
azure.ContainerURL = jest.fn()
- files = await AzureBlobFiles.init(fakeSASCredentials)
+ files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
files._azure.aborter = fakeAborter
+
+ mockContainerCreate.mockReset()
+ azure.ContainerURL = { fromServiceURL: jest.fn() }
+ azure.Aborter = { none: fakeAzureAborter }
+ azure.ContainerURL.fromServiceURL.mockReturnValue({ create: mockContainerCreate })
})
test('url with no query args', async () => {
@@ -524,9 +541,21 @@ describe('_getUrl', () => {
const url = files._getUrl('fakesub/afile')
expect(url).toEqual(expectedUrl)
})
+
+ test('test _getUrl custom host', async () => {
+ files = new AzureBlobFiles({ ...fakeUserCredentials, hostName: 'fakeHost' }, null)
+ const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
+ setMockBlobUrl(cleanUrl)
+ const expectedUrl = 'https://fakeHost/fake/fakesub/afile'
+ const url = files._getUrl('fakesub/afile')
+ expect(url).toEqual(expectedUrl)
+ })
})
describe('_getPresignUrl', () => {
+ const fakeAzureAborter = 'fakeAborter'
+ const mockContainerCreate = jest.fn()
+
const mockBlockBlob = jest.fn()
const setMockBlobUrl = url => {
azure.BlockBlobURL.fromContainerURL = mockBlockBlob.mockReturnValue({ url })
@@ -534,18 +563,32 @@ describe('_getPresignUrl', () => {
const tvm = jest.fn()
/** @type {AzureBlobFiles} */
let files
+ azure.generateBlobSASQueryParameters = jest.fn()
+ azure.BlobSASPermissions.parse = jest.fn()
+
beforeEach(async () => {
tvm.mockReset()
+ azure.generateBlobSASQueryParameters.mockReset()
+ azure.BlobSASPermissions.parse.mockReset()
+
+ mockContainerCreate.mockReset()
+ mockBlockBlob.mockReset()
+ azure.ContainerURL = jest.fn()
+ azure.ContainerURL.fromServiceURL = jest.fn()
+ azure.ContainerURL.fromServiceURL.mockReturnValue({ create: mockContainerCreate })
+ azure.Aborter = { none: fakeAzureAborter }
+
+ // defaults that work
+ azure.generateBlobSASQueryParameters.mockReturnValue({ toString: () => 'fakeSAS' })
+ azure.BlobSASPermissions.parse.mockReturnValue({ toString: () => 'fakePermissionStr' })
+
tvm.getAzureBlobPresignCredentials = jest.fn()
tvm.getAzureBlobPresignCredentials.mockResolvedValue({
signature: 'fakesign'
})
- mockBlockBlob.mockReset()
- azure.ContainerURL = jest.fn()
- files = await AzureBlobFiles.init(fakeSASCredentials)
+ files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
files._azure.aborter = fakeAborter
- files.tvm = tvm
})
test('_getPresignUrl with no options', async () => {
@@ -571,6 +614,29 @@ describe('_getPresignUrl', () => {
expect(url).toEqual(expectedUrl)
expect(tvm.getAzureBlobPresignCredentials).toHaveBeenCalledWith({ blobName: 'fakesub/afile', expiryInSeconds: 60, permissions: 'fake' })
})
+
+ test('_getPresignUrl with correct options default permission own credentials', async () => {
+ files = await AzureBlobFiles.init(fakeUserCredentials)
+ const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
+ setMockBlobUrl(cleanUrl)
+ const expectedUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile?fakeSAS'
+ const url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60 })
+ expect(url).toEqual(expectedUrl)
+ })
+
+ test('_getPresignUrl with correct options explicit permission own credentials', async () => {
+ files = await AzureBlobFiles.init(fakeUserCredentials)
+ const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
+ setMockBlobUrl(cleanUrl)
+ const expectedUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile?fakeSAS'
+ const url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60, permissions: 'fake' })
+ expect(url).toEqual(expectedUrl)
+ })
+
+ test('_getPresignUrl with correct options explicit permission own sas credentials', async () => {
+ files = await AzureBlobFiles.init(fakeSASCredentials)
+ await expect(files._getAzureBlobPresignCredentials('fakesub/afile', { expiryInSeconds: 60 })).rejects.toThrow('[FilesLib:ERROR_UNSUPPORTED_OPERATION] generatePresignURL is not supported with Azure Container SAS credentials, please initialize the SDK with Azure storage account credentials instead')
+ })
})
describe('_statusFromProviderError', () => {
diff --git a/test/init.test.js b/test/init.test.js
index ff3f0dd..4d38e7c 100644
--- a/test/init.test.js
+++ b/test/init.test.js
@@ -63,10 +63,8 @@ describe('init', () => {
test('with azure config', async () => {
await filesLib.init({ azure: fakeAzureBlobConfig })
expect(AzureBlobFiles.init).toHaveBeenCalledTimes(1)
- expect(AzureBlobFiles.init).toHaveBeenCalledWith(fakeAzureBlobConfig, {
- getAzureBlobCredentials: azureBlobTvmMock
- })
- expect(TvmClient.init).toHaveBeenCalledTimes(1)
+ expect(AzureBlobFiles.init).toHaveBeenCalledWith(fakeAzureBlobConfig, null)
+ expect(TvmClient.init).toHaveBeenCalledTimes(0)
expect(global.mockLogDebug).toHaveBeenCalledWith(expect.stringContaining('azure'))
checkInitDebugLogNoSecrets(fakeAzureBlobConfig.storageAccessKey)
checkInitDebugLogNoSecrets(fakeAzureBlobConfig.sasURLPrivate)
diff --git a/types.d.ts b/types.d.ts
index 926d544..42a9c79 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -311,11 +311,13 @@ export type AzureCredentialsSAS = {
* @property storageAccessKey - access key for azure storage account
* @property containerName - name of container to store files.
* Another `${containerName}-public` will also be used for public files.
+ * @property [hostName] - custom domain for returned URLs
*/
export type AzureCredentialsAccount = {
storageAccount: string;
storageAccessKey: string;
containerName: string;
+ hostName?: string;
};
/**