Merge branch 'release/1.7.1' into production

This commit is contained in:
Miroslav Bajtoš 2014-03-18 18:15:55 +01:00
commit 5a97ceafa8
11 changed files with 162 additions and 246 deletions

View File

@ -279,11 +279,16 @@ app.enableAuth = function() {
var modelId = modelInstance && modelInstance.id || req.param('id'); var modelId = modelInstance && modelInstance.id || req.param('id');
if(Model.checkAccess) { if(Model.checkAccess) {
// Pause the request before checking access
// See https://github.com/strongloop/loopback-storage-service/issues/7
req.pause();
Model.checkAccess( Model.checkAccess(
req.accessToken, req.accessToken,
modelId, modelId,
method.name, method.name,
function(err, allowed) { function(err, allowed) {
// Emit any cached data events that fired while checking access.
req.resume();
if(err) { if(err) {
console.log(err); console.log(err);
next(err); next(err);

View File

@ -57,7 +57,7 @@ loopback.mime = express.mime;
*/ */
loopback.compat = require('./compat'); loopback.compat = require('./compat');
/** /*!
* Create an loopback application. * Create an loopback application.
* *
* @return {Function} * @return {Function}
@ -305,7 +305,7 @@ loopback.autoAttachModel = function(ModelCtor) {
} }
}; };
/* /*!
* Built in models / services * Built in models / services
*/ */

View File

@ -194,8 +194,8 @@ Principal.SCOPE = 'SCOPE';
/** /**
* Compare if two principals are equal * Compare if two principals are equal
* @param p The other principal * Returns true if argument principal is equal to this principal.
* @returns {boolean} * @param {Object} principal The other principal
*/ */
Principal.prototype.equals = function (p) { Principal.prototype.equals = function (p) {
if (p instanceof Principal) { if (p instanceof Principal) {
@ -205,11 +205,11 @@ Principal.prototype.equals = function (p) {
}; };
/** /**
* A request to access protected resources * A request to access protected resources.
* @param {String} model The model name * @param {String} model The model name
* @param {String} property * @param {String} property
* @param {String} accessType The access type * @param {String} accessType The access type
* @param {String} permission The permission * @param {String} permission The requested permission
* @returns {AccessRequest} * @returns {AccessRequest}
* @class * @class
*/ */

View File

@ -50,7 +50,7 @@ var PushNotificationSettingSchema = {
* Data model for Application * Data model for Application
*/ */
var ApplicationSchema = { var ApplicationSchema = {
id: {type: String, id: true, generated: true}, id: {type: String, id: true},
// Basic information // Basic information
name: {type: String, required: true}, // The name name: {type: String, required: true}, // The name
description: String, // The description description: String, // The description
@ -90,7 +90,7 @@ var ApplicationSchema = {
modified: {type: Date, default: Date} modified: {type: Date, default: Date}
}; };
/** /*!
* Application management functions * Application management functions
*/ */
@ -189,8 +189,7 @@ Application.resetKeys = function (appId, cb) {
/** /**
* Authenticate the application id and key. * Authenticate the application id and key.
* *
* `matched` will be one of * `matched` parameter is one of:
*
* - clientKey * - clientKey
* - javaScriptKey * - javaScriptKey
* - restApiKey * - restApiKey
@ -201,7 +200,7 @@ Application.resetKeys = function (appId, cb) {
* @param {String} key * @param {String} key
* @callback {Function} callback * @callback {Function} callback
* @param {Error} err * @param {Error} err
* @param {String} matched - The matching key * @param {String} matched The matching key
*/ */
Application.authenticate = function (appId, key, cb) { Application.authenticate = function (appId, key, cb) {
this.findById(appId, function (err, app) { this.findById(appId, function (err, app) {

View File

@ -18,11 +18,11 @@ var properties = {
* *
* **Properties** * **Properties**
* *
* - `to` - **{ String }** **required** * - `to` - String (required)
* - `from` - **{ String }** **required** * - `from` - String (required)
* - `subject` - **{ String }** **required** * - `subject` - String (required)
* - `text` - **{ String }** * - `text` - String
* - `html` - **{ String }** * - `html` - String
* *
* @class * @class
* @inherits {Model} * @inherits {Model}
@ -35,19 +35,24 @@ var Email = module.exports = Model.extend('Email', properties);
* *
* Example Options: * Example Options:
* *
* ```json * ```js
* { * {
* from: "Fred Foo <foo@blurdybloop.com>", // sender address * from: "Fred Foo <foo@blurdybloop.com>", // sender address
* to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers
* subject: "Hello", // Subject line * subject: "Hello", // Subject line
* text: "Hello world", // plaintext body * text: "Hello world", // plaintext body
* html: "<b>Hello world</b>" // html body * html: "<b>Hello world</b>" // html body
* } * }
* ``` * ```
* *
* See https://github.com/andris9/Nodemailer for other supported options. * See https://github.com/andris9/Nodemailer for other supported options.
* *
* @param {Object} options * @options {Object} options See below
* @prop {String} from Senders's email address
* @prop {String} to List of one or more recipient email addresses (comma-delimited)
* @prop {String} subject Subject line
* @prop {String} text Body text
* @prop {String} html Body HTML (optional)
* @param {Function} callback Called after the e-mail is sent or the sending failed * @param {Function} callback Called after the e-mail is sent or the sending failed
*/ */

View File

@ -1,222 +0,0 @@
var loopback = require('../loopback');
// "OAuth token"
var OAuthToken = loopback.createModel({
// "access token"
accessToken: {
type: String,
index: {
unique: true
}
}, // key, The string token
clientId: {
type: String,
index: true
}, // The client id
resourceOwner: {
type: String,
index: true
}, // The resource owner (user) id
realm: {
type: String,
index: true
}, // The resource owner realm
issuedAt: {
type: Date,
index: true
}, // The timestamp when the token is issued
expiresIn: Number, // Expiration time in seconds
expiredAt: {
type: Date,
index: {
expires: "1d"
}
}, // The timestamp when the token is expired
scopes: [ String ], // oAuth scopes
parameters: [
{
name: String,
value: String
}
],
authorizationCode: {
type: String,
index: true
}, // The corresponding authorization code that is used to request the
// access token
refreshToken: {
type: String,
index: true
}, // The corresponding refresh token if issued
tokenType: {
type: String,
enum: [ "Bearer", "MAC" ]
}, // The token type, such as Bearer:
// http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16
// or MAC: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
authenticationScheme: String, // HTTP authenticationScheme
hash: String // The SHA-1 hash for
// client-secret/resource-owner-secret-key
});
// "OAuth authorization code"
var OAuthAuthorizationCode = loopback.createModel({
code: {
type: String,
index: {
unique: true
}
}, // key // The string code
clientId: {
type: String,
index: true
}, // The client id
resourceOwner: {
type: String,
index: true
}, // The resource owner (user) id
realm: {
type: String,
index: true
}, // The resource owner realm
issuedAt: {
type: Date,
index: true
}, // The timestamp when the token is issued
expiresIn: Number, // Expiration time in seconds
expiredAt: {
type: Date,
index: {
expires: "1d"
}
}, // The timestamp when the token is expired
scopes: [ String ], // oAuth scopes
parameters: [
{
name: String,
value: String
}
],
used: Boolean, // Is it ever used
redirectURI: String, // The redirectURI from the request, we need to
// check if it's identical to the one used for
// access token
hash: String // The SHA-1 hash for
// client-secret/resource-owner-secret-key
});
// "OAuth client registration record"
var ClientRegistration = loopback.createModel({
id: {
type: String,
index: {
unique: true
}
},
clientId: {
type: String,
index: {
unique: true
}
}, // key; // The client id
clientSecret: String, // The generated client secret
defaultTokenType: String,
accessLevel: Number, // The access level to scopes, -1: disabled, 0:
// basic, 1..N
disabled: Boolean,
name: {
type: String,
index: true
},
email: String,
description: String,
url: String,
iconURL: String,
redirectURIs: [ String ],
type: {
type: String,
enum: [ "CONFIDENTIAL", "PUBLIC" ]
},
userId: {
type: String,
index: true
} // The registered developer
});
// "OAuth permission"
var OAuthPermission = loopback.createModel({
clientId: {
type: String,
index: true
}, // The client id
resourceOwner: {
type: String,
index: true
}, // The resource owner (user) id
realm: {
type: String,
index: true
}, // The resource owner realm
issuedAt: {
type: Date,
index: true
}, // The timestamp when the permission is issued
expiresIn: Number, // Expiration time in seconds
expiredAt: {
type: Date,
index: {
expires: "1d"
}
}, // The timestamp when the permission is expired
scopes: [ String ]
});
// "OAuth scope"
var OAuthScope = loopback.createModel({
scope: {
type: String,
index: {
unique: true
}
}, // key; // The scope name
description: String, // Description of the scope
iconURL: String, // The icon to be displayed on the "Request Permission"
// dialog
expiresIn: Number, // The default maximum lifetime of access token that
// carries the scope
requiredAccessLevel: Number, // The minimum access level required
resourceOwnerAuthorizationRequired: Boolean
// The scope requires authorization from the resource owner
});
// "OAuth protected resource"
var OAuthResource = loopback.createModel({
operations: [
{
type: String,
enum: [ "ALL", "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH" ]
}
], // A list of operations, by default ALL
path: String, // The resource URI path
scopes: [ String ]
// Allowd scopes
});
// Use the schema to register a model
exports.OAuthToken = OAuthToken;
exports.OAuthAuthorizationCode = OAuthAuthorizationCode;
exports.ClientRegistration = ClientRegistration;
exports.OAuthPermission = OAuthPermission;
exports.OAuthScope = OAuthScope;
exports.OAuthResource = OAuthResource;

View File

@ -149,11 +149,14 @@ User.login = function (credentials, include, fn) {
} else if(credentials.username) { } else if(credentials.username) {
query.username = credentials.username; query.username = credentials.username;
} else { } else {
return fn(new Error('must provide username or email')); var err = new Error('username or email is required');
err.statusCode = 400;
return fn(err);
} }
this.findOne({where: query}, function(err, user) { this.findOne({where: query}, function(err, user) {
var defaultError = new Error('login failed'); var defaultError = new Error('login failed');
defaultError.statusCode = 401;
if(err) { if(err) {
debug('An error is reported from User.findOne: %j', err); debug('An error is reported from User.findOne: %j', err);

View File

@ -9,7 +9,7 @@
"Platform", "Platform",
"mBaaS" "mBaaS"
], ],
"version": "1.7.0", "version": "1.7.1",
"scripts": { "scripts": {
"test": "mocha -R spec" "test": "mocha -R spec"
}, },

88
test/integration.test.js Normal file
View File

@ -0,0 +1,88 @@
var net = require('net');
describe('loopback application', function() {
it('pauses request stream during authentication', function(done) {
// This test reproduces the issue reported in
// https://github.com/strongloop/loopback-storage-service/issues/7
var app = loopback();
setupAppWithStreamingMethod();
app.listen(0, function() {
sendHttpRequestInOnePacket(
this.address().port,
'POST /streamers/read HTTP/1.0\n' +
'Content-Length: 1\n' +
'Content-Type: application/x-custom-octet-stream\n' +
'\n' +
'X',
function(err, res) {
if (err) return done(err);
expect(res).to.match(/\nX$/);
done();
});
});
function setupAppWithStreamingMethod() {
app.dataSource('db', {
connector: loopback.Memory,
defaultForType: 'db'
});
var db = app.datasources.db;
loopback.User.attachTo(db);
loopback.AccessToken.attachTo(db);
loopback.Role.attachTo(db);
loopback.ACL.attachTo(db);
loopback.User.hasMany(loopback.AccessToken, { as: 'accessTokens' });
var Streamer = app.model('Streamer', { dataSource: 'db' });
Streamer.read = function(req, res, cb) {
var body = new Buffer(0);
req.on('data', function(chunk) {
body += chunk;
});
req.on('end', function() {
res.end(body.toString());
// we must not call the callback here
// because it will attempt to add response headers
});
req.once('error', function(err) {
cb(err);
});
};
loopback.remoteMethod(Streamer.read, {
http: { method: 'post' },
accepts: [
{ arg: 'req', type: 'Object', http: { source: 'req' } },
{ arg: 'res', type: 'Object', http: { source: 'res' } }
]
});
app.enableAuth();
app.use(loopback.token({ model: app.models.accessToken }));
app.use(loopback.rest());
}
function sendHttpRequestInOnePacket(port, reqString, cb) {
var socket = net.createConnection(port);
var response = new Buffer(0);
socket.on('data', function(chunk) {
response += chunk;
});
socket.on('end', function() {
callCb(null, response.toString());
});
socket.once('error', function(err) {
callCb(err);
});
socket.write(reqString.replace(/\n/g, '\r\n'));
function callCb(err, res) {
if (!cb) return;
cb(err, res);
cb = null;
}
}
});
});

View File

@ -20,6 +20,7 @@ describe('Application', function () {
assert(app.masterKey); assert(app.masterKey);
assert(app.created); assert(app.created);
assert(app.modified); assert(app.modified);
assert.equal(typeof app.id, 'string');
done(err, result); done(err, result);
}); });
}); });

View File

@ -9,6 +9,9 @@ var userMemory = loopback.createDataSource({
describe('User', function(){ describe('User', function(){
var validCredentials = {email: 'foo@bar.com', password: 'bar'}; var validCredentials = {email: 'foo@bar.com', password: 'bar'};
var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
var incompleteCredentials = {password: 'bar1'};
beforeEach(function() { beforeEach(function() {
User = loopback.User.extend('user'); User = loopback.User.extend('user');
User.email = loopback.Email.extend('email'); User.email = loopback.Email.extend('email');
@ -135,6 +138,40 @@ describe('User', function(){
}); });
}); });
it('Login a user over REST by providing invalid credentials', function(done) {
request(app)
.post('/users/login')
.expect('Content-Type', /json/)
.expect(401)
.send(invalidCredentials)
.end(function(err, res){
done();
});
});
it('Login a user over REST by providing incomplete credentials', function(done) {
request(app)
.post('/users/login')
.expect('Content-Type', /json/)
.expect(400)
.send(incompleteCredentials)
.end(function(err, res){
done();
});
});
it('Login a user over REST with the wrong Content-Type', function(done) {
request(app)
.post('/users/login')
.set('Content-Type', null)
.expect('Content-Type', /json/)
.expect(400)
.send(validCredentials)
.end(function(err, res){
done();
});
});
it('Returns current user when `include` is `USER`', function(done) { it('Returns current user when `include` is `USER`', function(done) {
request(app) request(app)
.post('/users/login?include=USER') .post('/users/login?include=USER')