Skip to content
This repository has been archived by the owner on Jan 5, 2019. It is now read-only.

Azure blob access #94

Merged
merged 12 commits into from Feb 28, 2017
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -25,7 +25,7 @@
"babel-runtime": "^6.6.1",
"cryptiles": "^2.0.4",
"debug": "^2.2.0",
"fast-azure-storage": "^0.3.5",
"fast-azure-storage": "^1.0.0",
"hawk": "2.3.0",
"hoek": "^2.16.3",
"lodash": "^3.9.3",
Expand Down
22 changes: 22 additions & 0 deletions schemas/azure-blob-response.yml
@@ -0,0 +1,22 @@
$schema: http://json-schema.org/draft-04/schema#
title: "Azure Blob Shared-Access-Signature Response"
description: |
Response to a request for an Shared-Access-Signature to access an Azure
Blob Storage container.
type: object
properties:
sas:
description: |
Shared-Access-Signature string. This is the querystring parameters to
be appened after `?` or `&` depending on whether or not a querystring is
already present in the URL.
type: string
expiry:
description: |
Date and time of when the Shared-Access-Signature expires.
type: string
format: date-time
additionalProperties: false
required:
- sas
- expiry
2 changes: 1 addition & 1 deletion schemas/azure-table-access-response.yml
@@ -1,5 +1,5 @@
$schema: http://json-schema.org/draft-04/schema#
title: "Azure Shared-Access-Signature Response"
title: "Azure Table Shared-Access-Signature Response"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks :)

description: |
Response to a request for an Shared-Access-Signature to access and Azure
Table Storage table.
Expand Down
86 changes: 83 additions & 3 deletions src/azure.js
Expand Up @@ -88,9 +88,8 @@ api.declare({

// Check that the account exists
if (!this.azureAccounts[account]) {
return res.status(404).json({
message: "Account '" + account + "' not found, can't delegate access"
});
return res.reportError('InvalidRequestArguments',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceNotFound produces 404

`Account '${account}' not found, can't delegate access`);
}

// Construct client
Expand Down Expand Up @@ -131,3 +130,84 @@ api.declare({
expiry: expiry.toJSON()
});
});

api.declare({
method: 'get',
route: '/azure/:account/containers/:container/:level',
name: 'azureBlobSAS',
input: undefined,
output: 'azure-blob-response.json#',
deferAuth: true,
stability: 'stable',
scopes: [['auth:azure-blob:<level>:<account>/<container>']],
title: "Get Shared-Access-Signature for Azure Blob",
description: [
"Get a shared access signature (SAS) string for use with a specific Azure",
"Blob Storage container. If the level is read-write, the container will be created, " +
"if it doesn't already exists."
].join('\n')
}, async function(req, res){
// Get parameters
let account = req.params.account;
let container = req.params.container;
let level = req.params.level;

if (['read-write', 'read-only'].indexOf(level) < 0) {
return res.reportError('InvalidRequestArguments',
`Level '${level}' is not valid. Must be one of ['read-write', 'read-only'].`);
}

// Check that the client is authorized to access given account and container
if (!(level === 'read-only' &&
req.satisfies({account, container, level: 'read-write'}, true)) &&
!req.satisfies({account, container, level})) {
return;
}

// Check that the account exists
if (!this.azureAccounts[account]) {
return res.reportError('InvalidRequestArguments',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again this is probably a not found error

`Account '${level}' not found, can't delegate access.`);
}

// Construct client
let blob = new azure.Blob({
accountId: account,
accessKey: this.azureAccounts[account]
});

// Create container ignore error, if it already exists
if (level === 'read-write') {
try {
await blob.createContainer(container);
} catch (err) {
if (err.code !== 'ContainerAlreadyExists') {
throw err;
}
}
}

let perm = level === 'read-write';

// Construct SAS
let expiry = new Date(Date.now() + 25 * 60 * 1000);
let sas = blob.sas(container, null, {
start: new Date(Date.now() - 15 * 60 * 1000),
expiry: expiry,
resourceType: 'container',
permissions: {
read: true,
add: perm,
create: perm,
write: perm,
delete: perm,
list: true,
}
});

// Return the generated SAS
return res.reply({
sas: sas,
expiry: expiry.toJSON()
});
});
2 changes: 2 additions & 0 deletions src/v1.js
Expand Up @@ -55,6 +55,8 @@ var api = new API({
// Patterns for Azure
account: /^[a-z0-9]{3,24}$/,
table: /^[A-Za-z][A-Za-z0-9]{2,62}$/,
container: /^(?!.*[-]{2})[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/,
level: /^(read-write|read-only)$/,

// Patterns for AWS
bucket: /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/,
Expand Down
136 changes: 132 additions & 4 deletions test/azure_test.js
@@ -1,4 +1,4 @@
suite('azure table (sas)', function() {
suite.only('azure table (sas)', function() {
var Promise = require('promise');
var assert = require('assert');
var debug = require('debug')('auth:test:azure');
Expand Down Expand Up @@ -101,8 +101,7 @@ suite('azure table (sas)', function() {
accessToken: helper.rootAccessToken
};


test('azureTableSAS (allowed table)', function() {
test('azureTableSAS (allowed table)', () => {
// Restrict access a bit
var auth = new helper.Auth({
baseUrl: helper.baseUrl,
Expand Down Expand Up @@ -181,4 +180,133 @@ suite('azure table (sas)', function() {
assert(err.statusCode == 403, "Expected authorization error!");
});
});
});

test('azureBlobSAS', async () => {
let result = await helper.auth.azureBlobSAS(
helper.testaccount,
'container-test',
'read-write'
);

assert(typeof(result.sas) === 'string', "Expected some form of string");
assert(new Date(result.expiry).getTime() > new Date().getTime(),
"Expected expiry to be in the future");
});

test('azureBlobSAS (read-write)', async () => {
let result = await helper.auth.azureBlobSAS(
helper.testaccount,
'container-test',
'read-write',
);
assert(typeof(result.sas) === 'string', "Expected some form of string");
assert(new Date(result.expiry).getTime() > new Date().getTime(),
"Expected expiry to be in the future");

let blob = new azure.Blob({
accountId: helper.testaccount,
sas: result.sas
});

result = await blob.putBlob('container-test', 'blobTest', {type: 'BlockBlob'});
assert(result);
});

test('azureBlobSAS (read-only)', async () => {
let result = await helper.auth.azureBlobSAS(
helper.testaccount,
'container-test',
'read-only',
);
assert(typeof(result.sas) === 'string', "Expected some form of string");
assert(new Date(result.expiry).getTime() > new Date().getTime(),
"Expected expiry to be in the future");

let blob = new azure.Blob({
accountId: helper.testaccount,
sas: result.sas
});

try {
await blob.putBlob('container-test', 'blobTest', {type: 'BlockBlob'});
} catch (error) {
assert.equal(error.code, 'AuthorizationPermissionMismatch');
return;
}
assert(false, 'This should have thrown an error because the write is not allowed.');
});

test('azureBlobSAS (invalid level)', async () => {
try {
await helper.auth.azureBlobSAS(
helper.testaccount,
'container-test',
'foo-bar-baz',
);
} catch (error) {
assert.equal(error.code, 'InvalidRequestArguments');
return;
}
assert(false, "This should have thrown an error");
});

test('azureBlobSAS (allowed container)', async () => {
let auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: rootCredentials,
authorizedScopes: [
'auth:azure-blob:read-write:' + helper.testaccount + '/allowed-container'
]
});

let result = await auth.azureBlobSAS(
helper.testaccount,
'allowed-container',
'read-write'
);

assert(typeof(result.sas) === 'string', "Expected some form of string");
assert(new Date(result.expiry).getTime() > new Date().getTime(),
"Expected expiry to be in the future");
});

test('azureBlobSAS (allowed read-write -> read-only)', async () => {
var auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: rootCredentials,
authorizedScopes: [
'auth:azure-blob:read-write:' + helper.testaccount + '/allowed-container'
]
});

let result = await auth.azureBlobSAS(
helper.testaccount,
'allowed-container',
'read-only',
);
assert(typeof(result.sas) === 'string', "Expected some form of string");
assert(new Date(result.expiry).getTime() > new Date().getTime(),
"Expected expiry to be in the future");
});

test('azureBlobSAS (unauthorized container)', async () => {
let auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: rootCredentials,
authorizedScopes: [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helper has a quick method to set authorised scopes...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which method? Sorry, but I do not find a method that can set authorizedScopes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, we don't have it here...
In queue we do:

helper.auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: {
clientId: 'root',
accessToken: cfg.app.rootAccessToken,
}
});

We could do the same here:

helper.auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: {
clientId: 'root',
accessToken: cfg.app.rootAccessToken,
}
});

Ie. basically wrap with a helper.scopes method...

'auth:azure-blob:read-write:' + helper.testaccount + '/allowed-container'
]
});
try {
await auth.azureBlobSAS(
helper.testaccount,
'unauthorized-container',
'read-write'
);
} catch (error) {
assert(error.statusCode == 403, "Expected authorization error!");
return;
}
assert(false, "Expected an authentication error!");
});
});