Merge branch 'master' into 2.0

Conflicts:
	docs.json
	lib/application.js
	lib/loopback.js
	lib/models/data-model.js
	lib/models/model.js
	lib/models/user.js
	lib/registry.js
	package.json
	test/app.test.js
This commit is contained in:
Miroslav Bajtoš 2014-06-13 10:09:25 +02:00
commit d21669b844
24 changed files with 340 additions and 1103 deletions

View File

@ -41,9 +41,9 @@ as illustrated below:
* Clients * Clients
* [loopback-ios](https://github.com/strongloop/loopback-ios) * [loopback-ios](https://github.com/strongloop/loopback-ios)
* [loopback-remoting-ios](https://github.com/strongloop/loopback-remoting-ios) * [strong-remoting-ios](https://github.com/strongloop/strong-remoting-ios)
* [loopback-android](https://github.com/strongloop/loopback-android) * [loopback-android](https://github.com/strongloop/loopback-android)
* [loopback-remoting-android](https://github.com/strongloop/loopback-remoting-android) * [strong-remoting-android](https://github.com/strongloop/strong-remoting-android)
* [loopback-angular](https://github.com/strongloop/loopback-angular) * [loopback-angular](https://github.com/strongloop/loopback-angular)
* Tools * Tools

View File

@ -5,21 +5,22 @@
"lib/loopback.js", "lib/loopback.js",
"lib/runtime.js", "lib/runtime.js",
"lib/registry.js", "lib/registry.js",
"lib/middleware/token.js", { "title": "Base models", "depth": 2 },
"lib/models/model.js",
"lib/models/persisted-model.js",
{ "title": "Middleware", "depth": 2 },
"lib/middleware/rest.js",
"lib/middleware/status.js",
"lib/middleware/token.js",
"lib/middleware/urlNotFound.js",
{ "title": "Built-in models", "depth": 2 },
"lib/models/access-token.js", "lib/models/access-token.js",
"lib/models/access-context.js",
"lib/models/acl.js", "lib/models/acl.js",
"lib/models/application.js", "lib/models/application.js",
"lib/models/email.js", "lib/models/email.js",
"lib/models/model.js",
"lib/models/data-model.js",
"lib/models/role.js", "lib/models/role.js",
"lib/models/user.js", "lib/models/user.js",
"lib/models/change.js", "lib/models/change.js"
"docs/api-datasource.md",
"docs/api-geopoint.md",
"docs/api-model.md",
"docs/api-model-remote.md"
], ],
"assets": "/docs/assets" "assets": "/docs/assets"
} }

View File

@ -1,212 +0,0 @@
## Data Source object
LoopBack models can manipulate data via the DataSource object. Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`; some of the added methods may be remote methods.
Define a data source for persisting models.
```js
var oracle = loopback.createDataSource({
connector: 'oracle',
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
```
### Methods
#### dataSource.createModel(name, properties, options)
Define a model and attach it to a `DataSource`.
```js
var Color = oracle.createModel('color', {name: String});
```
You can define an ACL when you create a new data source with the `DataSource.create()` method. For example:
```js
var Customer = ds.createModel('Customer', {
name: {
type: String,
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
}
}, {
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
});
```
#### dataSource.discoverModelDefinitions([username], fn)
Discover a set of model definitions (table or collection names) based on tables or collections in a data source.
```js
oracle.discoverModelDefinitions(function (err, models) {
models.forEach(function (def) {
// def.name ~ the model name
oracle.discoverSchema(null, def.name, function (err, schema) {
console.log(schema);
});
});
});
```
#### dataSource.discoverSchema([owner], name, fn)
Discover the schema of a specific table or collection.
**Example schema from oracle connector:**
```js
{
"name": "Product",
"options": {
"idInjection": false,
"oracle": {
"schema": "BLACKPOOL",
"table": "PRODUCT"
}
},
"properties": {
"id": {
"type": "String",
"required": true,
"length": 20,
"id": 1,
"oracle": {
"columnName": "ID",
"dataType": "VARCHAR2",
"dataLength": 20,
"nullable": "N"
}
},
"name": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "NAME",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"audibleRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "AUDIBLE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"effectiveRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "EFFECTIVE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"rounds": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "ROUNDS",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"extras": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "EXTRAS",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"fireModes": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "FIRE_MODES",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
}
}
}
```
#### dataSource.enableRemote(operation)
Enable remote access to a data source operation. Each [connector](#connector) has its own set of set remotely enabled and disabled operations. You can always list these by calling `dataSource.operations()`.
#### dataSource.disableRemote(operation)
Disable remote access to a data source operation. Each [connector](#connector) has its own set of set enabled and disabled operations. You can always list these by calling `dataSource.operations()`.
```js
// all rest data source operations are
// disabled by default
var oracle = loopback.createDataSource({
connector: require('loopback-connector-oracle'),
host: '...',
...
});
// or only disable it as a remote method
oracle.disableRemote('destroyAll');
```
**Notes:**
- Disabled operations will not be added to attached models.
- Disabling the remoting for a method only affects client access (it will still be available from server models).
- Data sources must enable / disable operations before attaching or creating models.
#### dataSource.operations()
List the enabled and disabled operations.
console.log(oracle.operations());
Output:
```js
{
find: {
remoteEnabled: true,
accepts: [...],
returns: [...]
enabled: true
},
save: {
remoteEnabled: true,
prototype: true,
accepts: [...],
returns: [...],
enabled: true
},
...
}
```

View File

@ -1,69 +0,0 @@
## GeoPoint object
The GeoPoint object represents a physical location.
Use the `GeoPoint` class.
```js
var GeoPoint = require('loopback').GeoPoint;
```
Embed a latitude / longitude point in a [Model](#model).
```js
var CoffeeShop = loopback.createModel('coffee-shop', {
location: 'GeoPoint'
});
```
You can query LoopBack models with a GeoPoint property and an attached data source using geo-spatial filters and sorting. For example, the following code finds the three nearest coffee shops.
```js
CoffeeShop.attachTo(oracle);
var here = new GeoPoint({lat: 10.32424, lng: 5.84978});
CoffeeShop.find({where: {location: {near: here}}, limit:3}, function(err, nearbyShops) {
console.info(nearbyShops); // [CoffeeShop, ...]
});
```
### Distance Types
**Note:** all distance methods use `miles` by default.
- `miles`
- `radians`
- `kilometers`
- `meters`
- `miles`
- `feet`
- `degrees`
### Methods
#### geoPoint.distanceTo(geoPoint, options)
Get the distance to another `GeoPoint`; for example:
```js
var here = new GeoPoint({lat: 10, lng: 10});
var there = new GeoPoint({lat: 5, lng: 5});
console.log(here.distanceTo(there, {type: 'miles'})); // 438
```
#### GeoPoint.distanceBetween(a, b, options)
Get the distance between two points; for example:
```js
GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
```
### Properties
#### geoPoint.lat
The latitude point in degrees. Range: -90 to 90.
#### geoPoint.lng
The longitude point in degrees. Range: -180 to 180.

View File

@ -1,200 +0,0 @@
## Remote methods and hooks
You can expose a Model's instance and static methods to clients. A remote method must accept a callback with the conventional `fn(err, result, ...)` signature.
### Static Methods
#### loopback.remoteMethod(fn, [options])
Expose a remote method.
```js
Product.stats = function(fn) {
var statsResult = {
totalPurchased: 123456
};
var err = null;
// callback with an error and the result
fn(err, statsResult);
}
loopback.remoteMethod(
Product.stats,
{
returns: {arg: 'stats', type: 'object'},
http: {path: '/info', verb: 'get'}
}
);
```
**Options**
The options argument is a JSON object, described in the following table.
| Option | Required? | Description |
| ----- | ----- | ----- |
| accepts | No | Describes the remote method's arguments; See <a href="#argdesc">Argument description</a>. The `callback` argument is assumed; do not specify. |
| returns | No | Describes the remote method's callback arguments; See <a href="#argdesc">Argument description</a>. The `err` argument is assumed; do not specify. |
| http | No | HTTP routing information: <ul><li> **http.path**: path (relative to the model) at which the method is exposed. May be a path fragment (for example, `/:myArg`) that will be populated by an arg of the same name in the `accepts` description. For example, the `stats` method above will be at the whole path `/products/stats`.</li><li> **http.verb**: HTTP method (verb) from which the method is available (one of: get, post, put, del, or all).</li></ul>
| description | No | A text description of the method. This is used by API documentation generators like Swagger.
<a name="argdesc"></a>
**Argument description**
The arguments description defines either a single argument as an object or an ordered set of arguments as an array. Each individual argument has keys for:
* arg: argument name
* type: argument datatype; must be a [loopback type](http://wiki.strongloop.com/display/DOC/LoopBack+types).
* required: Boolean value indicating if argument is required.
* root: For callback arguments: set this property to `true` if your function
has a single callback argument to use as the root object
returned to remote caller. Otherwise the root object returned is a map (argument-name to argument-value).
* http: For input arguments: a function or an object describing mapping from HTTP request
to the argument value, as explained <a href="#argdesc-http">below</a>.
For example, a single argument, specified as an object:
```js
{arg: 'myArg', type: 'number'}
```
Multiple arguments, specified as an array:
```js
[
{arg: 'arg1', type: 'number', required: true},
{arg: 'arg2', type: 'array'}
]
```
<a name="argdesc-http"></a>
**HTTP mapping of input arguments**
There are two ways to specify HTTP mapping for input parameters (what the method accepts):
* Provide an object with a `source` property
* Specify a custom mapping function
To use the first way to specify HTTP mapping for input parameters, provide an object with a `source` property
that has one of the values shown in the following table.
| Value of source property | Description |
|---|---|
| body | The whole request body is used as the value. |
| form | The value is looked up using `req.param`, which searches route arguments, the request body and the query string.|
| query | An alias for form (see above). |
| path | An alias for form (see above). |
| req | The whole HTTP reqest object is used as the value. |
For example, an argument getting the whole request body as the value:
```js
{ arg: 'data', type: 'object', http: { source: 'body' } }
```
The use the second way to specify HTTP mapping for input parameters, specify a custom mapping function
that looks like this:
```js
{
arg: 'custom',
type: 'number',
http: function(ctx) {
// ctx is LoopBack Context object
// 1. Get the HTTP request object as provided by Express
var req = ctx.req;
// 2. Get 'a' and 'b' from query string or form data
// and return their sum as the value
return +req.param('a') + req.param('b');
}
}
```
If you don't specify a mapping, LoopBack will determine the value
as follows (assuming `name` as the name of the input parameter to resolve):
1. If there is a HTTP request parameter `args` with a JSON content,
then the value of `args['name']` is used if it is defined.
2. Otherwise `req.param('name')` is returned.
### Remote hooks
Run a function before or after a remote method is called by a client.
```js
// *.save === prototype.save
User.beforeRemote('*.save', function(ctx, user, next) {
if(ctx.user) {
next();
} else {
next(new Error('must be logged in to update'))
}
});
User.afterRemote('*.save', function(ctx, user, next) {
console.log('user has been saved', user);
next();
});
```
Remote hooks also support wildcards. Run a function before any remote method is called.
```js
// ** will match both prototype.* and *.*
User.beforeRemote('**', function(ctx, user, next) {
console.log(ctx.methodString, 'was invoked remotely'); // users.prototype.save was invoked remotely
next();
});
```
Other wildcard examples
```js
// run before any static method eg. User.find
User.beforeRemote('*', ...);
// run before any instance method eg. User.prototype.save
User.beforeRemote('prototype.*', ...);
// prevent password hashes from being sent to clients
User.afterRemote('**', function (ctx, user, next) {
if(ctx.result) {
if(Array.isArray(ctx.result)) {
ctx.result.forEach(function (result) {
result.password = undefined;
});
} else {
ctx.result.password = undefined;
}
}
next();
});
```
### Context
Remote hooks are provided with a Context `ctx` object which contains transport specific data (eg. for http: `req` and `res`). The `ctx` object also has a set of consistent apis across transports.
#### ctx.req.accessToken
The `accessToken` of the user calling the method remotely. **Note:** this is undefined if the remote method is not invoked by a logged in user (or other principal).
#### ctx.result
During `afterRemote` hooks, `ctx.result` will contain the data about to be sent to a client. Modify this object to transform data before it is sent.
#### REST
When [loopback.rest](#loopbackrest) is used the following additional `ctx` properties are available.
##### ctx.req
The express ServerRequest object. [See full documentation](http://expressjs.com/api.html#req).
##### ctx.res
The express ServerResponse object. [See full documentation](http://expressjs.com/api.html#res).

View File

@ -1,448 +0,0 @@
## Model object
A Loopback `Model` is a vanilla JavaScript class constructor with an attached set of properties and options. A `Model` instance is created by passing a data object containing properties to the `Model` constructor. A `Model` constructor will clean the object passed to it and only set the values matching the properties you define.
```js
// valid color
var Color = loopback.createModel('color', {name: String});
var red = new Color({name: 'red'});
console.log(red.name); // red
// invalid color
var foo = new Color({bar: 'bat baz'});
console.log(foo.bar); // undefined
```
**Properties**
A model defines a list of property names, types and other validation metadata. A [DataSource](#data-source) uses this definition to validate a `Model` during operations such as `save()`.
**Options**
Some [DataSources](#data-source) may support additional `Model` options.
Define A Loopbackmodel.
```js
var User = loopback.createModel('user', {
first: String,
last: String,
age: Number
});
```
### Methods
#### Model.attachTo(dataSource)
Attach a model to a [DataSource](#data-source). Attaching a [DataSource](#data-source) updates the model with additional methods and behaviors.
```js
var oracle = loopback.createDataSource({
connector: require('loopback-connector-oracle'),
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
User.attachTo(oracle);
```
NOTE: until a model is attached to a data source it will not have any attached methods.
### Properties
#### Model.properties
An object containing a normalized set of properties supplied to `loopback.createModel(name, properties)`.
Example:
```js
var props = {
a: String,
b: {type: 'Number'},
c: {type: 'String', min: 10, max: 100},
d: Date,
e: loopback.GeoPoint
};
var MyModel = loopback.createModel('foo', props);
console.log(MyModel.properties);
```
Outputs:
```js
{
"a": {type: String},
"b": {type: Number},
"c": {
"type": String,
"min": 10,
"max": 100
},
"d": {type: Date},
"e": {type: GeoPoint},
"id": {
"id": 1
}
}
```
### CRUD and Query Mixins
Mixins are added by attaching a vanilla model to a [data source](#data-source) with a [connector](#connectors). Each [connector](#connectors) enables its own set of operations that are mixed into a `Model` as methods. To see available methods for a data source call `dataSource.operations()`.
Log the available methods for a memory data source.
```js
var ops = loopback
.createDataSource({connector: loopback.Memory})
.operations();
console.log(Object.keys(ops));
```
Outputs:
```js
[ 'create',
'updateOrCreate',
'upsert',
'findOrCreate',
'exists',
'findById',
'find',
'all',
'findOne',
'destroyAll',
'deleteAll',
'count',
'include',
'relationNameFor',
'hasMany',
'belongsTo',
'hasAndBelongsToMany',
'save',
'isNewRecord',
'destroy',
'delete',
'updateAttribute',
'updateAttributes',
'reload' ]
```
Here is the definition of the `count()` operation.
```js
{
accepts: [ { arg: 'where', type: 'object' } ],
http: { verb: 'get', path: '/count' },
remoteEnabled: true,
name: 'count'
}
```
### Static Methods
**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation.
#### Model.create(data, [callback])
Create an instance of Model with given data and save to the attached data source. Callback is optional.
```js
User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
console.log(user instanceof User); // true
});
```
**Note:** You must include a callback and use the created model provided in the callback if your code depends on your model being saved or having an `id`.
#### Model.count([query], callback)
Query count of Model instances in data source. Optional query param allows to count filtered set of Model instances.
```js
User.count({approved: true}, function(err, count) {
console.log(count); // 2081
});
```
#### Model.find(filter, callback)
Find all instances of Model, matched by query. Fields used for filter and sort should be declared with `{index: true}` in model definition.
**filter**
- **where** `Object` { key: val, key2: {gt: 'val2'}} The search criteria
- Format: {key: val} or {key: {op: val}}
- Operations:
- gt: >
- gte: >=
- lt: <
- lte: <=
- between
- inq: IN
- nin: NOT IN
- neq: !=
- like: LIKE
- nlike: NOT LIKE
- **include** `String`, `Object` or `Array` Allows you to load relations of several objects and optimize numbers of requests.
- Format:
- 'posts': Load posts
- ['posts', 'passports']: Load posts and passports
- {'owner': 'posts'}: Load owner and owner's posts
- {'owner': ['posts', 'passports']}: Load owner, owner's posts, and owner's passports
- {'owner': [{posts: 'images'}, 'passports']}: Load owner, owner's posts, owner's posts' images, and owner's passports
- **order** `String` The sorting order
- Format: 'key1 ASC, key2 DESC'
- **limit** `Number` The maximum number of instances to be returned
- **skip** `Number` Skip the number of instances
- **offset** `Number` Alias for skip
- **fields** `Object|Array|String` The included/excluded fields
- `['foo']` or `'foo'` - include only the foo property
- `['foo', 'bar']` - include the foo and bar properties
- `{foo: true}` - include only foo
- `{bat: false}` - include all properties, exclude bat
Find the second page of 10 users over age 21 in descending order exluding the password property.
```js
User.find({
where: {
age: {gt: 21}},
order: 'age DESC',
limit: 10,
skip: 10,
fields: {password: false}
},
console.log
);
```
**Note:** See the specific connector's [docs](#connectors) for more info.
#### Model.destroyAll([where], callback)
Delete all Model instances from data source. **Note:** destroyAll method does not perform destroy hooks.
```js
Product.destroyAll({price: {gt: 99}}, function(err) {
// removed matching products
});
```
> **NOTE:* `where` is optional and a where object... do NOT pass a filter object
#### Model.findById(id, callback)
Find instance by id.
```js
User.findById(23, function(err, user) {
console.info(user.id); // 23
});
```
#### Model.findOne(where, callback)
Find a single instance that matches the given where expression.
```js
User.findOne({where: {id: 23}}, function(err, user) {
console.info(user.id); // 23
});
```
#### Model.upsert(data, callback)
Update when record with id=data.id found, insert otherwise. **Note:** no setters, validations or hooks applied when using upsert.
#### Custom static methods
Define a static model method.
```js
User.login = function (username, password, fn) {
var passwordHash = hashPassword(password);
this.findOne({username: username}, function (err, user) {
var failErr = new Error('login failed');
if(err) {
fn(err);
} else if(!user) {
fn(failErr);
} else if(user.password === passwordHash) {
MyAccessTokenModel.create({userId: user.id}, function (err, accessToken) {
fn(null, accessToken.id);
});
} else {
fn(failErr);
}
});
}
```
Setup the static model method to be exposed to clients as a [remote method](#remote-method).
```js
loopback.remoteMethod(
User.login,
{
accepts: [
{arg: 'username', type: 'string', required: true},
{arg: 'password', type: 'string', required: true}
],
returns: {arg: 'sessionId', type: 'any'},
http: {path: '/sign-in'}
}
);
```
### Instance methods
**Note:** These are the default mixin methods for a `Model` attached to a data source. See the specific connector for additional API documentation.
#### model.save([options], [callback])
Save an instance of a Model to the attached data source.
```js
var joe = new User({first: 'Joe', last: 'Bob'});
joe.save(function(err, user) {
if(user.errors) {
console.log(user.errors);
} else {
console.log(user.id);
}
});
```
#### model.updateAttributes(data, [callback])
Save specified attributes to the attached data source.
```js
user.updateAttributes({
first: 'updatedFirst',
name: 'updatedLast'
}, fn);
```
#### model.destroy([callback])
Remove a model from the attached data source.
```js
model.destroy(function(err) {
// model instance destroyed
});
```
#### Custom instance methods
Define an instance method.
```js
User.prototype.logout = function (fn) {
MySessionModel.destroyAll({userId: this.id}, fn);
}
```
Define a remote model instance method.
```js
loopback.remoteMethod(User.prototype.logout)
```
### Relationships
#### Model.hasMany(Model, options)
Define a "one to many" relationship.
```js
// by referencing model
Book.hasMany(Chapter);
// specify the name
Book.hasMany('chapters', {model: Chapter});
```
Query and create the related models.
```js
Book.create(function(err, book) {
// create a chapter instance
// ready to be saved in the data source
var chapter = book.chapters.build({name: 'Chapter 1'});
// save the new chapter
chapter.save();
// you can also call the Chapter.create method with
// the `chapters` property which will build a chapter
// instance and save the it in the data source
book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) {
// this callback is optional
});
// query chapters for the book using the
book.chapters(function(err, chapters) {
// all chapters with bookId = book.id
console.log(chapters);
});
book.chapters({where: {name: 'test'}, function(err, chapters) {
// all chapters with bookId = book.id and name = 'test'
console.log(chapters);
});
});
```
#### Model.belongsTo(Model, options)
A `belongsTo` relation sets up a one-to-one connection with another model, such
that each instance of the declaring model "belongs to" one instance of the other
model. For example, if your application includes users and posts, and each post
can be written by exactly one user.
```js
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
```
The code above basically says Post has a reference called `author` to User using
the `userId` property of Post as the foreign key. Now we can access the author
in one of the following styles:
```js
post.author(callback); // Get the User object for the post author asynchronously
post.author(); // Get the User object for the post author synchronously
post.author(user) // Set the author to be the given user
```
#### Model.hasAndBelongsToMany(Model, options)
A `hasAndBelongsToMany` relation creates a direct many-to-many connection with
another model, with no intervening model. For example, if your application
includes users and groups, with each group having many users and each user
appearing in many groups, you could declare the models this way,
```js
User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
user.groups(callback); // get groups of the user
user.groups.create(data, callback); // create a new group and connect it with the user
user.groups.add(group, callback); // connect an existing group with the user
user.groups.remove(group, callback); // remove the user from the group
```
### Shared methods
Any static or instance method can be decorated as `shared`. These methods are exposed over the provided transport (eg. [loopback.rest](#rest)).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

After

Width:  |  Height:  |  Size: 325 KiB

View File

@ -114,58 +114,55 @@ app.disuse = function (route) {
*/ */
app.model = function (Model, config) { app.model = function (Model, config) {
if(arguments.length === 1) { var isPublic = true;
assert(Model.prototype instanceof registry.Model, if (arguments.length > 1) {
'Model must be a descendant of loopback.Model'); config = config || {};
if(Model.sharedClass) { if (typeof Model === 'string') {
this.remotes().addClass(Model.sharedClass); // create & attach the model - backwards compatibility
// create config for loopback.modelFromConfig
var modelConfig = extend({}, config);
modelConfig.options = extend({}, config.options);
modelConfig.name = Model;
// modeller does not understand `dataSource` option
delete modelConfig.dataSource;
Model = registry.createModel(modelConfig);
// delete config options already applied
['relations', 'base', 'acls', 'hidden'].forEach(function(prop) {
delete config[prop];
if (config.options) delete config.options[prop];
});
delete config.properties;
} }
this.models().push(Model);
clearHandlerCache(this); configureModel(Model, config, this);
Model.shared = true; isPublic = config.public !== false;
Model.app = this; } else {
Model.emit('attached', this); assert(Model.prototype instanceof loopback.Model,
return Model; 'Model must be a descendant of loopback.Model');
} }
config = config || {};
if (typeof Model === 'string') {
// create & attach the model - loopback 1.x compatibility
// create config for loopback.modelFromConfig
var modelConfig = extend({}, config);
modelConfig.options = extend({}, config.options);
modelConfig.name = Model;
// modeller does not understand `dataSource` option
delete modelConfig.dataSource;
Model = registry.createModel(modelConfig);
// delete config options already applied
['relations', 'base', 'acls', 'hidden'].forEach(function(prop) {
delete config[prop];
if (config.options) delete config.options[prop];
});
delete config.properties;
}
configureModel(Model, config, this);
var modelName = Model.modelName; var modelName = Model.modelName;
this.models[modelName] = this.models[modelName] =
this.models[classify(modelName)] = this.models[classify(modelName)] =
this.models[camelize(modelName)] = Model; this.models[camelize(modelName)] = Model;
if (config.public !== false) { this.models().push(Model);
this.model(Model);
if (isPublic && Model.sharedClass) {
this.remotes().addClass(Model.sharedClass);
clearHandlerCache(this);
} }
Model.shared = isPublic;
Model.app = this;
Model.emit('attached', this);
return Model; return Model;
}; };
/** /**
* Get the models exported by the app. Returns only models defined using `app.model()` * Get the models exported by the app. Returns only models defined using `app.model()`
* *
@ -337,7 +334,7 @@ app.enableAuth = function() {
Model.checkAccess( Model.checkAccess(
req.accessToken, req.accessToken,
modelId, modelId,
method.name, method,
function(err, allowed) { function(err, allowed) {
// Emit any cached data events that fired while checking access. // Emit any cached data events that fired while checking access.
req.resume(); req.resume();

View File

@ -159,6 +159,7 @@ loopback.template = function (file) {
return ejs.compile(str); return ejs.compile(str);
}; };
/*! /*!
* Built in models / services * Built in models / services
*/ */

View File

@ -1,17 +1,24 @@
/** /*!
* Module dependencies. * Module dependencies.
*/ */
var loopback = require('../loopback'); var loopback = require('../loopback');
/** /*!
* Export the middleware. * Export the middleware.
*/ */
module.exports = rest; module.exports = rest;
/** /**
* Build a temp app for mounting resources. * Expose models over REST.
*
* For example:
* ```js
* app.use(loopback.rest());
* ```
* For more information, see [Exposing models over a REST API](http://docs.strongloop.com/display/DOC/Exposing+models+over+a+REST+API).
* @header loopback.rest()
*/ */
function rest() { function rest() {

View File

@ -1,9 +1,22 @@
/** /*!
* Export the middleware. * Export the middleware.
*/ */
module.exports = status; module.exports = status;
/**
* Return [HTTP response](http://expressjs.com/4x/api.html#res.send) with basic application status information:
* date the application was started and uptime, in JSON format.
* For example:
* ```js
* {
* "started": "2014-06-05T00:26:49.750Z",
* "uptime": 9.394
* }
* ```
*
* @header loopback.status()
*/
function status() { function status() {
var started = new Date(); var started = new Date();

View File

@ -12,14 +12,17 @@ var assert = require('assert');
module.exports = token; module.exports = token;
/** /**
* **Options** * Check for an access token in cookies, headers, and query string parameters.
* This function always checks for the following:
* *
* - `cookies` - An `Array` of cookie names * - `access_token`
* - `headers` - An `Array` of header names * - `X-Access-Token`
* - `params` - An `Array` of param names * - `authorization`
* - `model` - Specify an AccessToken class to use *
* It checks for these values in cookies, headers, and query string parameters _in addition_ to the items
* specified in the options parameter.
* *
* Each array is used to add additional keys to find an `accessToken` for a `request`. * **NOTE:** This function only checks for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
* *
* The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter * The following example illustrates how to check for an `accessToken` in a custom cookie, query string parameter
* and header called `foo-auth`. * and header called `foo-auth`.
@ -28,23 +31,16 @@ module.exports = token;
* app.use(loopback.token({ * app.use(loopback.token({
* cookies: ['foo-auth'], * cookies: ['foo-auth'],
* headers: ['foo-auth', 'X-Foo-Auth'], * headers: ['foo-auth', 'X-Foo-Auth'],
* cookies: ['foo-auth', 'foo_auth'] * params: ['foo-auth', 'foo_auth']
* })); * }));
* ``` * ```
*
* **Defaults**
*
* By default the following names will be checked. These names are appended to any optional names. They will always
* be checked, but any names specified will be checked first.
*
* - **access_token**
* - **X-Access-Token**
* - **authorization**
* - **access_token**
*
* **NOTE:** The `loopback.token()` middleware will only check for [signed cookies](http://expressjs.com/api.html#req.signedCookies).
* *
* @header loopback.token(options) * @options {Object} [options] Each option array is used to add additional keys to find an `accessToken` for a `request`.
* @property {Array} [cookies] Array of cookie names.
* @property {Array} [headers] Array of header names.
* @property {Array} [params] Array of param names.
* @property {Array} [model] An AccessToken object to use.
* @header loopback.token([options])
*/ */
function token(options) { function token(options) {

View File

@ -1,13 +1,14 @@
/** /*!
* Export the middleware. * Export the middleware.
* See discussion in Connect pull request #954 for more details
* https://github.com/senchalabs/connect/pull/954.
*/ */
module.exports = urlNotFound; module.exports = urlNotFound;
/** /**
* Convert any request not handled so far to a 404 error * Convert any request not handled so far to a 404 error
* to be handled by error-handling middleware. * to be handled by error-handling middleware.
* See discussion in Connect pull request #954 for more details * @header loopback.urlNotFound()
* https://github.com/senchalabs/connect/pull/954
*/ */
function urlNotFound() { function urlNotFound() {
return function raiseUrlNotFoundError(req, res, next) { return function raiseUrlNotFoundError(req, res, next) {

View File

@ -36,7 +36,18 @@ function AccessContext(context) {
this.property = context.property || AccessContext.ALL; this.property = context.property || AccessContext.ALL;
this.method = context.method; this.method = context.method;
this.sharedMethod = context.sharedMethod;
this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass;
if(this.sharedMethod) {
this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]);
} else {
this.methodNames = [];
}
if(this.sharedMethod) {
this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod);
}
this.accessType = context.accessType || AccessContext.ALL; this.accessType = context.accessType || AccessContext.ALL;
this.accessToken = context.accessToken || AccessToken.ANONYMOUS; this.accessToken = context.accessToken || AccessToken.ANONYMOUS;
@ -79,7 +90,6 @@ AccessContext.permissionOrder = {
DENY: 4 DENY: 4
}; };
/** /**
* Add a principal to the context * Add a principal to the context
* @param {String} principalType The principal type * @param {String} principalType The principal type
@ -96,8 +106,6 @@ AccessContext.prototype.addPrincipal = function (principalType, principalId, pri
} }
} }
this.principals.push(principal); this.principals.push(principal);
debug('adding principal %j', principal);
return true; return true;
}; };
@ -213,7 +221,7 @@ Principal.prototype.equals = function (p) {
* @returns {AccessRequest} * @returns {AccessRequest}
* @class * @class
*/ */
function AccessRequest(model, property, accessType, permission) { function AccessRequest(model, property, accessType, permission, methodNames) {
if (!(this instanceof AccessRequest)) { if (!(this instanceof AccessRequest)) {
return new AccessRequest(model, property, accessType); return new AccessRequest(model, property, accessType);
} }
@ -224,26 +232,20 @@ function AccessRequest(model, property, accessType, permission) {
this.property = obj.property || AccessContext.ALL; this.property = obj.property || AccessContext.ALL;
this.accessType = obj.accessType || AccessContext.ALL; this.accessType = obj.accessType || AccessContext.ALL;
this.permission = obj.permission || AccessContext.DEFAULT; this.permission = obj.permission || AccessContext.DEFAULT;
this.methodNames = methodNames || [];
} else { } else {
this.model = model || AccessContext.ALL; this.model = model || AccessContext.ALL;
this.property = property || AccessContext.ALL; this.property = property || AccessContext.ALL;
this.accessType = accessType || AccessContext.ALL; this.accessType = accessType || AccessContext.ALL;
this.permission = permission || AccessContext.DEFAULT; this.permission = permission || AccessContext.DEFAULT;
} this.methodNames = methodNames || [];
if(debug.enabled) {
debug('---AccessRequest---');
debug(' model %s', this.model);
debug(' property %s', this.property);
debug(' accessType %s', this.accessType);
debug(' permission %s', this.permission);
debug(' isWildcard() %s', this.isWildcard());
} }
} }
/** /**
* Is the request a wildcard * Does the request contain any wildcards?
* @returns {boolean} *
* @returns {Boolean}
*/ */
AccessRequest.prototype.isWildcard = function () { AccessRequest.prototype.isWildcard = function () {
return this.model === AccessContext.ALL || return this.model === AccessContext.ALL ||
@ -251,6 +253,47 @@ AccessRequest.prototype.isWildcard = function () {
this.accessType === AccessContext.ALL; this.accessType === AccessContext.ALL;
}; };
/**
* Does the given `ACL` apply to this `AccessRequest`.
*
* @param {ACL} acl
*/
AccessRequest.prototype.exactlyMatches = function(acl) {
var matchesModel = acl.model === this.model;
var matchesProperty = acl.property === this.property;
var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1;
var matchesAccessType = acl.accessType === this.accessType;
if(matchesModel && matchesAccessType) {
return matchesProperty || matchesMethodName;
}
return false;
}
/**
* Is the request for access allowed?
*
* @returns {Boolean}
*/
AccessRequest.prototype.isAllowed = function() {
return this.permission !== require('./acl').ACL.DENY;
}
AccessRequest.prototype.debug = function() {
if(debug.enabled) {
debug('---AccessRequest---');
debug(' model %s', this.model);
debug(' property %s', this.property);
debug(' accessType %s', this.accessType);
debug(' permission %s', this.permission);
debug(' isWildcard() %s', this.isWildcard());
debug(' isAllowed() %s', this.isAllowed());
}
}
module.exports.AccessContext = AccessContext; module.exports.AccessContext = AccessContext;
module.exports.Principal = Principal; module.exports.Principal = Principal;
module.exports.AccessRequest = AccessRequest; module.exports.AccessRequest = AccessRequest;

View File

@ -61,6 +61,7 @@ var ACLSchema = {
/** /**
* Name of the access type - READ/WRITE/EXEC * Name of the access type - READ/WRITE/EXEC
* @property accessType {String} Name of the access type - READ/WRITE/EXEC
*/ */
accessType: String, accessType: String,
@ -114,17 +115,20 @@ ACL.SCOPE = Principal.SCOPE;
* Calculate the matching score for the given rule and request * Calculate the matching score for the given rule and request
* @param {ACL} rule The ACL entry * @param {ACL} rule The ACL entry
* @param {AccessRequest} req The request * @param {AccessRequest} req The request
* @returns {number} * @returns {Number}
*/ */
ACL.getMatchingScore = function getMatchingScore(rule, req) { ACL.getMatchingScore = function getMatchingScore(rule, req) {
var props = ['model', 'property', 'accessType']; var props = ['model', 'property', 'accessType'];
var score = 0; var score = 0;
for (var i = 0; i < props.length; i++) { for (var i = 0; i < props.length; i++) {
// Shift the score by 4 for each of the properties as the weight // Shift the score by 4 for each of the properties as the weight
score = score * 4; score = score * 4;
var val1 = rule[props[i]] || ACL.ALL; var val1 = rule[props[i]] || ACL.ALL;
var val2 = req[props[i]] || ACL.ALL; var val2 = req[props[i]] || ACL.ALL;
if (val1 === val2) { var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(val1) !== -1;
if (val1 === val2 || isMatchingMethodName) {
// Exact match // Exact match
score += 3; score += 3;
} else if (val1 === ACL.ALL) { } else if (val1 === ACL.ALL) {
@ -186,6 +190,16 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
return score; return score;
}; };
/**
* Get matching score for the given `AccessRequest`.
* @param {AccessRequest} req The request
* @returns {Number} score
*/
ACL.prototype.score = function(req) {
return this.constructor.getMatchingScore(this, req);
}
/*! /*!
* Resolve permission from the ACLs * Resolve permission from the ACLs
* @param {Object[]) acls The list of ACLs * @param {Object[]) acls The list of ACLs
@ -200,14 +214,13 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
acls = acls.sort(function (rule1, rule2) { acls = acls.sort(function (rule1, rule2) {
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req); return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
}); });
if(debug.enabled) {
debug('ACLs by order: %j', acls);
}
var permission = ACL.DEFAULT; var permission = ACL.DEFAULT;
var score = 0; var score = 0;
for (var i = 0; i < acls.length; i++) { for (var i = 0; i < acls.length; i++) {
score = ACL.getMatchingScore(acls[i], req); score = ACL.getMatchingScore(acls[i], req);
if (score < 0) { if (score < 0) {
// the highest scored ACL did not match
break; break;
} }
if (!req.isWildcard()) { if (!req.isWildcard()) {
@ -215,11 +228,7 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
permission = acls[i].permission; permission = acls[i].permission;
break; break;
} else { } else {
if(acls[i].model === req.model && if(req.exactlyMatches(acls[i])) {
acls[i].property === req.property &&
acls[i].accessType === req.accessType
) {
// We should stop at the exact match
permission = acls[i].permission; permission = acls[i].permission;
break; break;
} }
@ -231,6 +240,14 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
} }
} }
if(debug.enabled) {
debug('The following ACLs were searched: ');
acls.forEach(function(acl) {
acl.debug();
debug('with score:', acl.score(req));
});
}
var res = new AccessRequest(req.model, req.property, req.accessType, var res = new AccessRequest(req.model, req.property, req.accessType,
permission || ACL.DEFAULT); permission || ACL.DEFAULT);
return res; return res;
@ -253,11 +270,9 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
property: acl.property || ACL.ALL, property: acl.property || ACL.ALL,
principalType: acl.principalType, principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name? principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType, accessType: acl.accessType || ACL.ALL,
permission: acl.permission permission: acl.permission
})); }));
staticACLs[staticACLs.length - 1].debug('Adding ACL');
}); });
} }
var prop = modelClass && var prop = modelClass &&
@ -282,14 +297,12 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
/** /**
* Check if the given principal is allowed to access the model/property * Check if the given principal is allowed to access the model/property
* @param {String} principalType The principal type * @param {String} principalType The principal type.
* @param {String} principalId The principal id * @param {String} principalId The principal ID.
* @param {String} model The model name * @param {String} model The model name.
* @param {String} property The property/method/relation name * @param {String} property The property/method/relation name.
* @param {String} accessType The access type * @param {String} accessType The access type.
* @param {Function} callback The callback function * @callback {Function} callback Callback function.
*
* @callback callback
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {AccessRequest} result The access permission * @param {AccessRequest} result The access permission
*/ */
@ -350,15 +363,16 @@ ACL.prototype.debug = function() {
} }
/** /**
* Check if the request has the permission to access * Check if the request has the permission to access.
* @param {Object} context * @options {Object} context See below.
* @property {Object[]} principals An array of principals * @property {Object[]} principals An array of principals.
* @property {String|Model} model The model name or model class * @property {String|Model} model The model name or model class.
* @property {*} id The model instance id * @property {*} id The model instance ID.
* @property {String} property The property/method/relation name * @property {String} property The property/method/relation name.
* @property {String} accessType The access type * @property {String} accessType The access type: READE, WRITE, or EXEC.
* @param {Function} callback * @param {Function} callback Callback function
*/ */
ACL.checkAccessForContext = function (context, callback) { ACL.checkAccessForContext = function (context, callback) {
if(!(context instanceof AccessContext)) { if(!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
@ -367,11 +381,13 @@ ACL.checkAccessForContext = function (context, callback) {
var model = context.model; var model = context.model;
var property = context.property; var property = context.property;
var accessType = context.accessType; var accessType = context.accessType;
var modelName = context.modelName;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]}; var methodNames = context.methodNames;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]}; var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var req = new AccessRequest(model.modelName, property, accessType); var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
var effectiveACLs = []; var effectiveACLs = [];
var staticACLs = this.getStaticACLs(model.modelName, property); var staticACLs = this.getStaticACLs(model.modelName, property);
@ -404,9 +420,6 @@ ACL.checkAccessForContext = function (context, callback) {
inRoleTasks.push(function (done) { inRoleTasks.push(function (done) {
roleModel.isInRole(acl.principalId, context, roleModel.isInRole(acl.principalId, context,
function (err, inRole) { function (err, inRole) {
if(debug.enabled) {
debug('In role %j: %j', acl.principalId, inRole);
}
if (!err && inRole) { if (!err && inRole) {
effectiveACLs.push(acl); effectiveACLs.push(acl);
} }
@ -425,21 +438,20 @@ ACL.checkAccessForContext = function (context, callback) {
if(resolved && resolved.permission === ACL.DEFAULT) { if(resolved && resolved.permission === ACL.DEFAULT) {
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW; resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
} }
debug('checkAccessForContext() returns: %j', resolved); debug('---Resolved---');
resolved.debug();
callback && callback(null, resolved); callback && callback(null, resolved);
}); });
}); });
}; };
/** /**
* Check if the given access token can invoke the method * Check if the given access token can invoke the method
* @param {AccessToken} token The access token * @param {AccessToken} token The access token
* @param {String} model The model name * @param {String} model The model name
* @param {*} modelId The model id * @param {*} modelId The model id
* @param {String} method The method name * @param {String} method The method name
* @end * @callback {Function} callback Callback function
* @callback {Function} callback
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed * @param {Boolean} allowed is the request allowed
*/ */
@ -454,10 +466,6 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
modelId: modelId modelId: modelId
}); });
context.accessType = context.model._getAccessTypeForMethod(method);
context.debug();
this.checkAccessForContext(context, function (err, access) { this.checkAccessForContext(context, function (err, access) {
if (err) { if (err) {
callback && callback(err); callback && callback(err);

View File

@ -46,7 +46,7 @@ var PushNotificationSettingSchema = {
gcm: GcmSettingsSchema gcm: GcmSettingsSchema
}; };
/** /*!
* Data model for Application * Data model for Application
*/ */
var ApplicationSchema = { var ApplicationSchema = {
@ -133,10 +133,10 @@ Application.beforeCreate = function (next) {
/** /**
* Register a new application * Register a new application
* @param owner Owner's user id * @param {String} owner Owner's user ID.
* @param name Name of the application * @param {String} name Name of the application
* @param options Other options * @param {Object} options Other options
* @param cb Callback function * @param {Function} callback Callback function
*/ */
Application.register = function (owner, name, options, cb) { Application.register = function (owner, name, options, cb) {
assert(owner, 'owner is required'); assert(owner, 'owner is required');

View File

@ -7,7 +7,6 @@ var juggler = require('loopback-datasource-juggler');
var ModelBuilder = juggler.ModelBuilder; var ModelBuilder = juggler.ModelBuilder;
var DataSource = juggler.DataSource; var DataSource = juggler.DataSource;
var modeler = new ModelBuilder(); var modeler = new ModelBuilder();
var async = require('async');
var assert = require('assert'); var assert = require('assert');
var _ = require('underscore'); var _ = require('underscore');
var SharedClass = require('strong-remoting').SharedClass; var SharedClass = require('strong-remoting').SharedClass;
@ -222,26 +221,34 @@ Model._ACL = function getACL(ACL) {
return _aclModel; return _aclModel;
}; };
/** /**
* Check if the given access token can invoke the method * Check if the given access token can invoke the method
* *
* @param {AccessToken} token The access token * @param {AccessToken} token The access token
* @param {*} modelId The model id * @param {*} modelId The model ID.
* @param {String} method The method name * @param {SharedMethod} sharedMethod The method in question
* @param callback The callback function * @callback {Function} callback The callback function
*
* @callback {Function} callback
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed * @param {Boolean} allowed True if the request is allowed; false otherwise.
*/ */
Model.checkAccess = function(token, modelId, method, callback) { Model.checkAccess = function(token, modelId, sharedMethod, callback) {
var ANONYMOUS = require('./access-token').ANONYMOUS; var ANONYMOUS = require('./access-token').ANONYMOUS;
token = token || ANONYMOUS; token = token || ANONYMOUS;
var aclModel = Model._ACL(); var aclModel = Model._ACL();
var methodName = 'string' === typeof method? method: method && method.name;
aclModel.checkAccessForToken(token, this.modelName, modelId, methodName, callback); aclModel.checkAccessForContext({
accessToken: token,
model: this,
property: sharedMethod.name,
method: sharedMethod.name,
sharedMethod: sharedMethod,
modelId: modelId,
accessType: this._getAccessTypeForMethod(sharedMethod)
}, function(err, accessRequest) {
if(err) return callback(err);
callback(null, accessRequest.isAllowed());
});
}; };
/*! /*!
@ -370,3 +377,4 @@ Model.scopeRemoting = function(relationName, relation, define) {
// setup the initial model // setup the initial model
Model.setup(); Model.setup();

View File

@ -319,12 +319,13 @@ Role.registerResolver(Role.EVERYONE, function (role, context, callback) {
* @param {Boolean} isInRole * @param {Boolean} isInRole
*/ */
Role.isInRole = function (role, context, callback) { Role.isInRole = function (role, context, callback) {
debug('isInRole(): %s %j', role, context);
if (!(context instanceof AccessContext)) { if (!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
} }
debug('isInRole(): %s', role);
context.debug();
var resolver = Role.resolvers[role]; var resolver = Role.resolvers[role];
if (resolver) { if (resolver) {
debug('Custom resolver found for role %s', role); debug('Custom resolver found for role %s', role);
@ -409,8 +410,6 @@ Role.isInRole = function (role, context, callback) {
* @param {String[]} An array of role ids * @param {String[]} An array of role ids
*/ */
Role.getRoles = function (context, callback) { Role.getRoles = function (context, callback) {
debug('getRoles(): %j', context);
if(!(context instanceof AccessContext)) { if(!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
} }

View File

@ -8,8 +8,6 @@ var PersistedModel = require('../loopback').PersistedModel
, SALT_WORK_FACTOR = 10 , SALT_WORK_FACTOR = 10
, crypto = require('crypto') , crypto = require('crypto')
, bcrypt = require('bcryptjs') , bcrypt = require('bcryptjs')
, passport = require('passport')
, LocalStrategy = require('passport-local').Strategy
, BaseAccessToken = require('./access-token') , BaseAccessToken = require('./access-token')
, DEFAULT_TTL = 1209600 // 2 weeks in seconds , DEFAULT_TTL = 1209600 // 2 weeks in seconds
, DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds , DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds
@ -128,6 +126,23 @@ var options = {
var User = module.exports = PersistedModel.extend('User', properties, options); var User = module.exports = PersistedModel.extend('User', properties, options);
/**
* Create access token for the logged in user. This method can be overridden to
* customize how access tokens are generated
*
* @param [Number} ttl The requested ttl
* @callack {Function} cb The callback function
* @param {String|Error} err The error string or object
* @param {AccessToken} token The generated access token object
*/
User.prototype.createAccessToken = function(ttl, cb) {
var userModel = this.constructor;
ttl = Math.min(ttl || userModel.settings.ttl, userModel.settings.maxTTL);
this.accessTokens.create({
ttl: ttl
}, cb);
};
/** /**
* Login a user by with the given `credentials`. * Login a user by with the given `credentials`.
* *
@ -144,6 +159,7 @@ var User = module.exports = PersistedModel.extend('User', properties, options);
*/ */
User.login = function (credentials, include, fn) { User.login = function (credentials, include, fn) {
var self = this;
if (typeof include === 'function') { if (typeof include === 'function') {
fn = include; fn = include;
include = undefined; include = undefined;
@ -162,7 +178,7 @@ User.login = function (credentials, include, fn) {
return fn(err); return fn(err);
} }
this.findOne({where: query}, function(err, user) { self.findOne({where: query}, function(err, user) {
var defaultError = new Error('login failed'); var defaultError = new Error('login failed');
defaultError.statusCode = 401; defaultError.statusCode = 401;
@ -175,9 +191,7 @@ User.login = function (credentials, include, fn) {
debug('An error is reported from User.hasPassword: %j', err); debug('An error is reported from User.hasPassword: %j', err);
fn(defaultError); fn(defaultError);
} else if(isMatch) { } else if(isMatch) {
user.accessTokens.create({ user.createAccessToken(credentials.ttl, function(err, token) {
ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL)
}, function(err, token) {
if (err) return fn(err); if (err) return fn(err);
if (include === 'user') { if (include === 'user') {
// NOTE(bajtos) We can't set token.user here: // NOTE(bajtos) We can't set token.user here:

View File

@ -34,10 +34,8 @@
"debug": "~0.8.1", "debug": "~0.8.1",
"express": "4.x", "express": "4.x",
"body-parser": "~1.2.2", "body-parser": "~1.2.2",
"strong-remoting": "2.0.0-beta3", "strong-remoting": "2.0.0-beta4",
"inflection": "~1.3.5", "inflection": "~1.3.5",
"passport": "~0.2.0",
"passport-local": "~1.0.0",
"nodemailer": "~0.6.5", "nodemailer": "~0.6.5",
"ejs": "~1.0.0", "ejs": "~1.0.0",
"bcryptjs": "~0.7.12", "bcryptjs": "~0.7.12",
@ -55,11 +53,11 @@
"errorhandler": "~1.0.1", "errorhandler": "~1.0.1",
"serve-favicon": "~2.0.0", "serve-favicon": "~2.0.0",
"loopback-datasource-juggler": "2.0.0-beta1", "loopback-datasource-juggler": "2.0.0-beta1",
"mocha": "~1.18.0", "mocha": "~1.20.1",
"strong-task-emitter": "0.0.x", "strong-task-emitter": "0.0.x",
"supertest": "~0.13.0", "supertest": "~0.13.0",
"chai": "~1.9.1", "chai": "~1.9.1",
"loopback-testing": "~0.1.3", "loopback-testing": "~0.2.0",
"browserify": "~4.1.6", "browserify": "~4.1.6",
"grunt": "~0.4.5", "grunt": "~0.4.5",
"grunt-browserify": "~2.1.0", "grunt-browserify": "~2.1.0",

View File

@ -70,8 +70,11 @@ describe('access control - integration', function () {
lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser); lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser);
lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser);
lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/users'); lt.it.shouldBeAllowedWhenCalledAnonymously(
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users'); 'POST', '/api/users', newUserData());
lt.it.shouldBeAllowedWhenCalledByUser(
CURRENT_USER, 'POST', '/api/users', newUserData());
lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout'); lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout');
@ -117,6 +120,15 @@ describe('access control - integration', function () {
function urlForUser() { function urlForUser() {
return '/api/users/' + this.randomUser.id; return '/api/users/' + this.randomUser.id;
} }
var userCounter;
function newUserData() {
userCounter = userCounter ? ++userCounter : 1;
return {
email: 'new-' + userCounter + '@test.test',
password: 'test'
};
}
}); });
describe('/banks', function () { describe('/banks', function () {

View File

@ -102,11 +102,15 @@ describe('security ACLs', function () {
property: 'find', property: 'find',
accessType: 'WRITE' accessType: 'WRITE'
}; };
acls = acls.map(function(a) { return new ACL(a)});
var perm = ACL.resolvePermission(acls, req); var perm = ACL.resolvePermission(acls, req);
assert.deepEqual(perm, { model: 'account', assert.deepEqual(perm, { model: 'account',
property: 'find', property: 'find',
accessType: 'WRITE', accessType: 'WRITE',
permission: 'ALLOW' }); permission: 'ALLOW',
methodNames: []});
}); });
it("should allow access to models for the given principal by wildcard", function () { it("should allow access to models for the given principal by wildcard", function () {
@ -297,7 +301,6 @@ describe('security ACLs', function () {
}); });
}); });
}); });
}); });

View File

@ -38,6 +38,15 @@ describe('app', function() {
expect(classes).to.contain('color'); expect(classes).to.contain('color');
}); });
it('registers existing models to app.models', function() {
var Color = db.createModel('color', {name: String});
app.model(Color);
expect(Color.app).to.be.equal(app);
expect(Color.shared).to.equal(true);
expect(app.models.color).to.equal(Color);
expect(app.models.Color).to.equal(Color);
});
it.onServer('updates REST API when a new model is added', function(done) { it.onServer('updates REST API when a new model is added', function(done) {
app.use(loopback.rest()); app.use(loopback.rest());
request(app).get('/colors').expect(404, function(err, res) { request(app).get('/colors').expect(404, function(err, res) {
@ -120,6 +129,24 @@ describe('app', function() {
expect(app.models.foo.definition.settings.base).to.equal('Application'); expect(app.models.foo.definition.settings.base).to.equal('Application');
}); });
it('honors config.public options', function() {
app.model('foo', {
dataSource: 'db',
public: false
});
expect(app.models.foo.app).to.equal(app);
expect(app.models.foo.shared).to.equal(false);
});
it('defaults config.public to be true', function() {
app.model('foo', {
dataSource: 'db'
});
expect(app.models.foo.app).to.equal(app);
expect(app.models.foo.shared).to.equal(true);
});
}); });
describe('app.model(ModelCtor, config)', function() { describe('app.model(ModelCtor, config)', function() {

View File

@ -1,6 +1,5 @@
var User; var User;
var AccessToken = loopback.AccessToken; var AccessToken = loopback.AccessToken;
var passport = require('passport');
var MailConnector = require('../lib/connectors/mail'); var MailConnector = require('../lib/connectors/mail');
var userMemory = loopback.createDataSource({ var userMemory = loopback.createDataSource({
@ -9,6 +8,7 @@ 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 validCredentialsWithTTL = {email: 'foo@bar.com', password: 'bar', ttl: 3600};
var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'}; var invalidCredentials = {email: 'foo1@bar.com', password: 'bar1'};
var incompleteCredentials = {password: 'bar1'}; var incompleteCredentials = {password: 'bar1'};
@ -117,6 +117,44 @@ describe('User', function(){
done(); done();
}); });
}); });
it('Login a user by providing credentials with TTL', function(done) {
User.login(validCredentialsWithTTL, function (err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, validCredentialsWithTTL.ttl);
assert.equal(accessToken.id.length, 64);
done();
});
});
it('Login a user using a custom createAccessToken', function(done) {
var createToken = User.prototype.createAccessToken; // Save the original method
// Override createAccessToken
User.prototype.createAccessToken = function(ttl, cb) {
// Reduce the ttl by half for testing purpose
this.accessTokens.create({ttl: ttl /2 }, cb);
};
User.login(validCredentialsWithTTL, function (err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 1800);
assert.equal(accessToken.id.length, 64);
User.findById(accessToken.userId, function(err, user) {
user.createAccessToken(120, function (err, accessToken) {
assert(accessToken.userId);
assert(accessToken.id);
assert.equal(accessToken.ttl, 60);
assert.equal(accessToken.id.length, 64);
// Restore create access token
User.prototype.createAccessToken = createToken;
done();
});
});
});
});
it('Login a user over REST by providing credentials', function(done) { it('Login a user over REST by providing credentials', function(done) {
request(app) request(app)