Skip to content

Commit

Permalink
Revoke presign URLs (#38)
Browse files Browse the repository at this point in the history
* TVM changes to support presign revoke
* moved common logic to AzureUtil js to get container URL
  • Loading branch information
sandeep-paliwal authored Nov 10, 2020
1 parent 19f890a commit 743dd4f
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 11 deletions.
24 changes: 24 additions & 0 deletions actions/azure-revoke/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
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 { AzureRevokePresignTvm } = require('../../lib/impl/AzureRevokePresignTvm')
const azureRevokePresignTvm = new AzureRevokePresignTvm()

/**
* @param {object} params the input params
* @returns {Promise<object>} tvm response
*/
async function main (params) {
return azureRevokePresignTvm.processRequest(params)
}

exports.main = main
11 changes: 7 additions & 4 deletions lib/impl/AzureBlobTvm.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ governing permissions and limitations under the License.
*/

const { Tvm } = require('../Tvm')
const azureUtil = require('./AzureUtil')
const azure = require('@azure/storage-blob')

// eslint-disable-next-line jsdoc/require-jsdoc
Expand Down Expand Up @@ -53,10 +54,12 @@ class AzureBlobTvm extends Tvm {
const publicContainerName = containerName + '-public'

// create containers - we need to do it here as the sas creds do not allow it
const pipeline = azure.StorageURL.newPipeline(sharedKeyCredential)
const serviceURL = new azure.ServiceURL(accountURL, pipeline)
await _createContainerIfNotExists(azure.ContainerURL.fromServiceURL(serviceURL, publicContainerName), azure.Aborter.none, { access: 'blob', metadata: { namespace: params.owNamespace } })
await _createContainerIfNotExists(azure.ContainerURL.fromServiceURL(serviceURL, privateContainerName), azure.Aborter.none, { metadata: { namespace: params.owNamespace } })
const privateContainerURL = azureUtil.getContainerURL(accountURL, sharedKeyCredential, privateContainerName)
await _createContainerIfNotExists(azureUtil.getContainerURL(accountURL, sharedKeyCredential, publicContainerName), azure.Aborter.none, { access: 'blob', metadata: { namespace: params.owNamespace } })
await _createContainerIfNotExists(privateContainerURL, azure.Aborter.none, { metadata: { namespace: params.owNamespace } })

// set default access policy if it does not exists
await azureUtil.addAccessPolicyIfNotExists(privateContainerURL, params.azureStorageAccount, params.azureStorageAccessKey)

// generate SAS token
const expiryTime = new Date()
Expand Down
10 changes: 9 additions & 1 deletion lib/impl/AzurePresignTvm.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ governing permissions and limitations under the License.

const joi = require('@hapi/joi')
const { Tvm } = require('../Tvm')
const azureUtil = require('./AzureUtil')
const azure = require('@azure/storage-blob')

/**
Expand Down Expand Up @@ -43,9 +44,15 @@ class AzurePresignTvm extends Tvm {
* @private
*/
async _generateCredentials (params) {
const accountURL = `https://${params.azureStorageAccount}.blob.core.windows.net`
const sharedKeyCredential = new azure.SharedKeyCredential(params.azureStorageAccount, params.azureStorageAccessKey)
const containerName = Tvm._hash(params.owNamespace)

const privateContainerURL = azureUtil.getContainerURL(accountURL, sharedKeyCredential, containerName)
const identifier = await azureUtil.getAccessPolicy(privateContainerURL, params.azureStorageAccount, params.azureStorageAccessKey)

if (identifier === undefined) { throw new Error(`No Access Policy set for container ${containerName}`) }

// generate SAS token
const expiryTime = new Date(Date.now() + (1000 * params.expiryInSeconds))
const perm = (params.permissions === undefined) ? 'r' : params.permissions
Expand All @@ -54,7 +61,8 @@ class AzurePresignTvm extends Tvm {
const commonSasParams = {
permissions: permissions.toString(),
expiryTime: expiryTime,
blobName: params.blobName
blobName: params.blobName,
identifier: identifier
}

const sasQueryParamsPrivate = azure.generateBlobSASQueryParameters({ ...commonSasParams, containerName: containerName }, sharedKeyCredential)
Expand Down
49 changes: 49 additions & 0 deletions lib/impl/AzureRevokePresignTvm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
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 { Tvm } = require('../Tvm')
const azureUtil = require('./AzureUtil')
const azure = require('@azure/storage-blob')

/**
* @class AzureRevokePresignTvm
* @classdesc Tvm implementation for Azure Revoke Presign URLs
* @augments {Tvm}
*/
class AzureRevokePresignTvm extends Tvm {
/**
* @memberof AzureRevokePresignTvm
* @override
*/
constructor () {
super()
this._addToValidationSchema('azureStorageAccount')
this._addToValidationSchema('azureStorageAccessKey')
}

/**
* @memberof AzureRevokePresignTvm
* @override
* @private
*/
async _generateCredentials (params) {
const containerName = Tvm._hash(params.owNamespace)
const accountURL = `https://${params.azureStorageAccount}.blob.core.windows.net`

const sharedKeyCredential = new azure.SharedKeyCredential(params.azureStorageAccount, params.azureStorageAccessKey)
const containerURL = azureUtil.getContainerURL(accountURL, sharedKeyCredential, containerName)

await azureUtil.setAccessPolicy(containerURL, params.azureStorageAccount)
}
}

module.exports = { AzureRevokePresignTvm }
124 changes: 124 additions & 0 deletions lib/impl/AzureUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
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 fetch = require('node-fetch')
const Crypto = require('crypto')
const { v4: uuidv4 } = require('uuid')
const xmlJS = require('xml-js')

const azure = require('@azure/storage-blob')

/**
* Sign Request
*
* @param {string} method http method
* @param {string} resource azure resource to be used
* @param {string }date Date string
* @param {string} storageAccessKey access Key
* @returns {string} signed request
*/
function _signRequest (method, resource, date, storageAccessKey) {
const canonicalHeaders = 'x-ms-date:' + date + '\n' + 'x-ms-version:2019-02-02'
var stringToSign = method + '\n\n\n\n\n\n\n\n\n\n\n\n' + canonicalHeaders + '\n' + resource
return Crypto.createHmac('sha256', Buffer.from(storageAccessKey, 'base64')).update(stringToSign, 'utf8').digest('base64')
}

/**
* Get Access policy
*
* @param {object} containerURL azure container URL
* @param {string} storageAccount azure account
* @param {string} storageAccessKey azure access key
* @returns {string} Id for access policy
*/
async function getAccessPolicy (containerURL, storageAccount, storageAccessKey) {
// use API call as this._azure.containerURLPrivate.getAccessPolicy calls fails for policy with empty permissions
var index = containerURL.url.lastIndexOf('/')
var containerName = containerURL.url.substring(index + 1, containerURL.url.length)

const resource = '/' + storageAccount + '/' + containerName + '\ncomp:acl\nrestype:container'
const date = new Date().toUTCString()
const sign = _signRequest('GET', resource, date, storageAccessKey)

const reqHeaders = {
'x-ms-date': date,
'x-ms-version': '2019-02-02',
authorization: 'SharedKey ' + storageAccount + ':' + sign
}
const url = containerURL.url + '?restype=container&comp=acl'
const res = await fetch(url, { method: 'GET', headers: reqHeaders })

const acl = await res.text()
const aclObj = xmlJS.xml2js(acl)
let id
if (aclObj.elements) {
const signedIdentifiers = aclObj.elements[0]
if (signedIdentifiers.elements) {
const signedIdentifier = signedIdentifiers.elements[0].elements
signedIdentifier.forEach(function (val, index, arr) {
if (val.name === 'Id') {
id = val.elements[0].text
return id
}
})
}
}
return id
}

/**
* Set new acceess policy
*
* @param {object} containerURL azure container URL
* @returns {void}
*/
async function setAccessPolicy (containerURL) {
const id = uuidv4()
// set access policy with new id and without any permissions
await containerURL.setAccessPolicy(azure.Aborter.none, undefined, [{ id: id, accessPolicy: { permission: '' } }])
}

/**
* Add new access policy if it doest not exists
*
* @param {object} containerURL azure container URL
* @param {string} storageAccount azure account
* @param {string} storageAccessKey azure access key
* @returns {void}
*/
async function addAccessPolicyIfNotExists (containerURL, storageAccount, storageAccessKey) {
const identifier = await getAccessPolicy(containerURL, storageAccount, storageAccessKey)
if (!identifier) {
await setAccessPolicy(containerURL)
}
}

/**
* Get Container URL
*
* @param {string} accountURL account URL
* @param {object} sharedKeyCredential azure sharedKeyCredential object
* @param {string} containerName azure container name
* @returns {object} container URL object
*/
function getContainerURL (accountURL, sharedKeyCredential, containerName) {
const pipeline = azure.StorageURL.newPipeline(sharedKeyCredential)
const serviceURL = new azure.ServiceURL(accountURL, pipeline)
return azure.ContainerURL.fromServiceURL(serviceURL, containerName)
}

module.exports = {
getAccessPolicy: getAccessPolicy,
setAccessPolicy: setAccessPolicy,
addAccessPolicyIfNotExists: addAccessPolicyIfNotExists,
getContainerURL: getContainerURL
}
21 changes: 21 additions & 0 deletions manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ packages:
annotations:
# this is important security wise
final: true
azure-revoke:
function: actions/azure-revoke/index.js
web: yes
runtime: 'nodejs:10'
inputs:
azureStorageAccount: $AZURE_STORAGE_ACCOUNT
azureStorageAccessKey: $AZURE_STORAGE_ACCESS_KEY
expirationDuration: $EXPIRATION_DURATION
owApihost: $AIO_RUNTIME_APIHOST
approvedList: $APPROVED_LIST
imsEnv: $IMS_ENV
disableAdobeIOApiGwTokenValidation: $DISABLE_ADOBE_IO_API_GW_TOKEN_VALIDATION
annotations:
# this is important security wise
final: true
azure-cosmos:
function: actions/azure-cosmos/index.js
web: yes
Expand Down Expand Up @@ -77,6 +92,12 @@ packages:
azure-presign:
method: GET
response: http
tvm-azure-revoke:
tvm:
azure/revoke/{owNamespace}:
azure-revoke:
method: GET
response: http
tvm-azure-cosmos:
tvm:
azure/cosmos/{owNamespace}:
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
"aws-sdk": "^2.539.0",
"crypto": "^1.0.1",
"lru-cache": "^6.0.0",
"openwhisk": "^3.20.0"
"openwhisk": "^3.20.0",
"node-fetch": "^2.6.0",
"uuid": "^8.3.1",
"xml-js": "^1.6.11"
}
}
24 changes: 24 additions & 0 deletions test/unit/actions/azure-revoke.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
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 azureRevokePresignAction = require('../../../actions/azure-revoke')

const { AzureRevokePresignTvm } = require('../../../lib/impl/AzureRevokePresignTvm')
jest.mock('../../../lib/impl/AzureRevokePresignTvm')

beforeEach(() => {
AzureRevokePresignTvm.prototype.processRequest.mockReset()
})
test('azure-revoke action has a main function and calls AzureRevokePresignTvm.processRequest', async () => {
const fakeParams = { a: { nested: 'param' }, another: 'param' }
await azureRevokePresignAction.main(fakeParams)
expect(AzureRevokePresignTvm.prototype.processRequest).toHaveBeenCalledWith(fakeParams)
})
14 changes: 11 additions & 3 deletions test/unit/lib/impl/AzureBlobTvm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ governing permissions and limitations under the License.

const { AzureBlobTvm } = require('../../../../lib/impl/AzureBlobTvm')

const azureUtil = require('../../../../lib/impl/AzureUtil')
jest.mock('../../../../lib/impl/AzureUtil')
azureUtil.addAccessPolicyIfNotExists = jest.fn()

const azure = require('@azure/storage-blob')
jest.mock('@azure/storage-blob')

Expand All @@ -21,13 +25,15 @@ const azureContainerCreateMock = jest.fn()
azure.SharedKeyCredential = jest.fn()
azure.StorageURL.newPipeline = jest.fn()
azure.ServiceURL = jest.fn()
azure.ContainerURL.fromServiceURL = jest.fn().mockReturnValue({
create: azureContainerCreateMock
})

azure.ContainerURL.prototype.create = jest.fn()
azure.generateBlobSASQueryParameters = jest.fn()
azure.Aborter.none = {}

azureUtil.getContainerURL.mockReturnValue({
create: azureContainerCreateMock
})

class FakePermission {
toString () {
return (this.add && this.read && this.create && this.delete && this.write && this.list && 'ok') || 'not ok'
Expand Down Expand Up @@ -56,6 +62,7 @@ describe('processRequest (Azure Cosmos)', () => {
tvm = new AzureBlobTvm()
azureContainerCreateMock.mockReset()
azure.generateBlobSASQueryParameters.mockReset()
azureUtil.addAccessPolicyIfNotExists.mockClear()

// defaults that work
azure.generateBlobSASQueryParameters.mockReturnValue({ toString: () => fakeSas })
Expand All @@ -74,6 +81,7 @@ describe('processRequest (Azure Cosmos)', () => {
const containerName = global.nsHash

expect(response.statusCode).toEqual(200)
expect(azureUtil.addAccessPolicyIfNotExists).toHaveBeenCalledTimes(1)
expect(response.body).toEqual({
sasURLPrivate: expect.stringContaining(containerName),
sasURLPublic: expect.stringContaining('public'),
Expand Down
Loading

0 comments on commit 743dd4f

Please sign in to comment.