Merge branch 'release/1.7.1' into production
This commit is contained in:
commit
5a97ceafa8
|
@ -279,11 +279,16 @@ app.enableAuth = function() {
|
|||
var modelId = modelInstance && modelInstance.id || req.param('id');
|
||||
|
||||
if(Model.checkAccess) {
|
||||
// Pause the request before checking access
|
||||
// See https://github.com/strongloop/loopback-storage-service/issues/7
|
||||
req.pause();
|
||||
Model.checkAccess(
|
||||
req.accessToken,
|
||||
modelId,
|
||||
method.name,
|
||||
function(err, allowed) {
|
||||
// Emit any cached data events that fired while checking access.
|
||||
req.resume();
|
||||
if(err) {
|
||||
console.log(err);
|
||||
next(err);
|
||||
|
|
|
@ -57,7 +57,7 @@ loopback.mime = express.mime;
|
|||
*/
|
||||
loopback.compat = require('./compat');
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Create an loopback application.
|
||||
*
|
||||
* @return {Function}
|
||||
|
@ -305,7 +305,7 @@ loopback.autoAttachModel = function(ModelCtor) {
|
|||
}
|
||||
};
|
||||
|
||||
/*
|
||||
/*!
|
||||
* Built in models / services
|
||||
*/
|
||||
|
||||
|
|
|
@ -194,8 +194,8 @@ Principal.SCOPE = 'SCOPE';
|
|||
|
||||
/**
|
||||
* Compare if two principals are equal
|
||||
* @param p The other principal
|
||||
* @returns {boolean}
|
||||
* Returns true if argument principal is equal to this principal.
|
||||
* @param {Object} principal The other principal
|
||||
*/
|
||||
Principal.prototype.equals = function (p) {
|
||||
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} property
|
||||
* @param {String} accessType The access type
|
||||
* @param {String} permission The permission
|
||||
* @param {String} permission The requested permission
|
||||
* @returns {AccessRequest}
|
||||
* @class
|
||||
*/
|
||||
|
|
|
@ -50,7 +50,7 @@ var PushNotificationSettingSchema = {
|
|||
* Data model for Application
|
||||
*/
|
||||
var ApplicationSchema = {
|
||||
id: {type: String, id: true, generated: true},
|
||||
id: {type: String, id: true},
|
||||
// Basic information
|
||||
name: {type: String, required: true}, // The name
|
||||
description: String, // The description
|
||||
|
@ -90,7 +90,7 @@ var ApplicationSchema = {
|
|||
modified: {type: Date, default: Date}
|
||||
};
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Application management functions
|
||||
*/
|
||||
|
||||
|
@ -189,8 +189,7 @@ Application.resetKeys = function (appId, cb) {
|
|||
/**
|
||||
* Authenticate the application id and key.
|
||||
*
|
||||
* `matched` will be one of
|
||||
*
|
||||
* `matched` parameter is one of:
|
||||
* - clientKey
|
||||
* - javaScriptKey
|
||||
* - restApiKey
|
||||
|
@ -201,7 +200,7 @@ Application.resetKeys = function (appId, cb) {
|
|||
* @param {String} key
|
||||
* @callback {Function} callback
|
||||
* @param {Error} err
|
||||
* @param {String} matched - The matching key
|
||||
* @param {String} matched The matching key
|
||||
*/
|
||||
Application.authenticate = function (appId, key, cb) {
|
||||
this.findById(appId, function (err, app) {
|
||||
|
|
|
@ -18,11 +18,11 @@ var properties = {
|
|||
*
|
||||
* **Properties**
|
||||
*
|
||||
* - `to` - **{ String }** **required**
|
||||
* - `from` - **{ String }** **required**
|
||||
* - `subject` - **{ String }** **required**
|
||||
* - `text` - **{ String }**
|
||||
* - `html` - **{ String }**
|
||||
* - `to` - String (required)
|
||||
* - `from` - String (required)
|
||||
* - `subject` - String (required)
|
||||
* - `text` - String
|
||||
* - `html` - String
|
||||
*
|
||||
* @class
|
||||
* @inherits {Model}
|
||||
|
@ -35,19 +35,24 @@ var Email = module.exports = Model.extend('Email', properties);
|
|||
*
|
||||
* 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
|
||||
* subject: "Hello ✔", // Subject line
|
||||
* text: "Hello world ✔", // plaintext body
|
||||
* html: "<b>Hello world ✔</b>" // html body
|
||||
* subject: "Hello", // Subject line
|
||||
* text: "Hello world", // plaintext body
|
||||
* html: "<b>Hello world</b>" // html body
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -149,11 +149,14 @@ User.login = function (credentials, include, fn) {
|
|||
} else if(credentials.username) {
|
||||
query.username = credentials.username;
|
||||
} 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) {
|
||||
var defaultError = new Error('login failed');
|
||||
defaultError.statusCode = 401;
|
||||
|
||||
if(err) {
|
||||
debug('An error is reported from User.findOne: %j', err);
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"Platform",
|
||||
"mBaaS"
|
||||
],
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"scripts": {
|
||||
"test": "mocha -R spec"
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -20,6 +20,7 @@ describe('Application', function () {
|
|||
assert(app.masterKey);
|
||||
assert(app.created);
|
||||
assert(app.modified);
|
||||
assert.equal(typeof app.id, 'string');
|
||||
done(err, result);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,9 @@ var userMemory = loopback.createDataSource({
|
|||
|
||||
describe('User', function(){
|
||||
var validCredentials = {email: 'foo@bar.com', password: 'bar'};
|
||||
var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
|
||||
var incompleteCredentials = {password: 'bar1'};
|
||||
|
||||
beforeEach(function() {
|
||||
User = loopback.User.extend('user');
|
||||
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) {
|
||||
request(app)
|
||||
.post('/users/login?include=USER')
|
||||
|
|
Loading…
Reference in New Issue