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

Commit

Permalink
Merge pull request #94 from elenasolomon/azure-blob-access
Browse files Browse the repository at this point in the history
Azure blob access
  • Loading branch information
jonasfj committed Feb 28, 2017
2 parents ce1f509 + e647e30 commit e4d5660
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 45 deletions.
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"
description: |
Response to a request for an Shared-Access-Signature to access and Azure
Table Storage table.
Expand Down
87 changes: 78 additions & 9 deletions src/azure.js
Expand Up @@ -73,12 +73,6 @@ api.declare({
var tableName = req.params.table;
var level = req.params.level;

if (!['read-write', 'read-only'].includes(level)) {
return res.status(404).json({
message: "Level '" + level + "' is not valid. Must be one of ['read-write', 'read-only']."
});
}

// We have a complicated scope situation for read-only since we want
// read-write to grant read-only permissions as well
if (!(level === 'read-only' && req.satisfies({account, table: tableName, level: 'read-write'}, true)) &&
Expand All @@ -88,9 +82,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('ResourceNotFound',
`Account '${account}' not found, can't delegate access`);
}

// Construct client
Expand Down Expand Up @@ -131,3 +124,79 @@ 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;

// 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('ResourceNotFound',
`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
167 changes: 133 additions & 34 deletions test/azure_test.js
@@ -1,5 +1,4 @@
suite('azure table (sas)', function() {
var Promise = require('promise');
suite.only('azure table and blob (sas)', function() {
var assert = require('assert');
var debug = require('debug')('auth:test:azure');
var helper = require('./helper');
Expand Down Expand Up @@ -92,7 +91,7 @@ suite('azure table (sas)', function() {
).then(function(result) {
assert(false, "This should have thrown an error");
}).catch(function(err) {
assert.equal(err.message, "Level 'foo-bar-baz' is not valid. Must be one of ['read-write', 'read-only'].");
assert.equal(err.code, "InvalidRequestArguments");
});
});

Expand All @@ -101,16 +100,11 @@ 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,
credentials: rootCredentials,
authorizedScopes: [
'auth:azure-table:read-write:' + helper.testaccount + '/allowedTable'
]
});
let auth = helper.scopes([
'auth:azure-table:read-write:' + helper.testaccount + '/allowedTable'
]);
return auth.azureTableSAS(
helper.testaccount,
'allowedTable',
Expand All @@ -124,13 +118,9 @@ suite('azure table (sas)', function() {

test('azureTableSAS (allowed table rw -> ro)', function() {
// Restrict access a bit
var auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: rootCredentials,
authorizedScopes: [
'auth:azure-table:read-write:' + helper.testaccount + '/allowedTable'
]
});
let auth = helper.scopes([
'auth:azure-table:read-write:' + helper.testaccount + '/allowedTable'
]);
return auth.azureTableSAS(
helper.testaccount,
'allowedTable',
Expand All @@ -144,13 +134,9 @@ suite('azure table (sas)', function() {

test('azureTableSAS (too high permission)', function() {
// Restrict access a bit
var auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: rootCredentials,
authorizedScopes: [
'auth:azure-table:read-only:' + helper.testaccount + '/allowedTable'
]
});
let auth = helper.scopes([
'auth:azure-table:read-only:' + helper.testaccount + '/allowedTable'
]);
return auth.azureTableSAS(
helper.testaccount,
'allowedTable',
Expand All @@ -164,13 +150,9 @@ suite('azure table (sas)', function() {

test('azureTableSAS (unauthorized table)', function() {
// Restrict access a bit
var auth = new helper.Auth({
baseUrl: helper.baseUrl,
credentials: rootCredentials,
authorizedScopes: [
'auth:azure-table:read-write:' + helper.testaccount + '/allowedTable'
]
});
let auth = helper.scopes([
'auth:azure-table:read-write:' + helper.testaccount + '/allowedTable'
]);
return auth.azureTableSAS(
helper.testaccount,
'unauthorizedTable',
Expand All @@ -181,4 +163,121 @@ 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 = helper.scopes([
'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 () => {
let auth = helper.scopes([
'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 = helper.scopes([
'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!");
});
});
11 changes: 11 additions & 0 deletions test/helper.js
Expand Up @@ -81,6 +81,17 @@ mocha.before(async () => {
}
});

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

// Create test server
let {
server: testServer_,
Expand Down

0 comments on commit e4d5660

Please sign in to comment.