diff --git a/README.md b/README.md index d049198c..3f7bb033 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ as illustrated below: * Clients * [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-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) * Tools diff --git a/docs.json b/docs.json index 5822b153..c79bfa0d 100644 --- a/docs.json +++ b/docs.json @@ -5,21 +5,22 @@ "lib/loopback.js", "lib/runtime.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-context.js", "lib/models/acl.js", "lib/models/application.js", "lib/models/email.js", - "lib/models/model.js", - "lib/models/data-model.js", "lib/models/role.js", "lib/models/user.js", - "lib/models/change.js", - "docs/api-datasource.md", - "docs/api-geopoint.md", - "docs/api-model.md", - "docs/api-model-remote.md" + "lib/models/change.js" ], "assets": "/docs/assets" } diff --git a/docs/api-datasource.md b/docs/api-datasource.md deleted file mode 100644 index 90b55a86..00000000 --- a/docs/api-datasource.md +++ /dev/null @@ -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 - }, - ... -} -``` diff --git a/docs/api-geopoint.md b/docs/api-geopoint.md deleted file mode 100644 index 588a5246..00000000 --- a/docs/api-geopoint.md +++ /dev/null @@ -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. diff --git a/docs/api-model-remote.md b/docs/api-model-remote.md deleted file mode 100644 index c4dfe3ca..00000000 --- a/docs/api-model-remote.md +++ /dev/null @@ -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 Argument description. The `callback` argument is assumed; do not specify. | -| returns | No | Describes the remote method's callback arguments; See Argument description. The `err` argument is assumed; do not specify. | -| http | No | HTTP routing information: -| description | No | A text description of the method. This is used by API documentation generators like Swagger. - - -**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 below. - -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'} -] -``` - - -**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). diff --git a/docs/api-model.md b/docs/api-model.md deleted file mode 100644 index 7aaf3687..00000000 --- a/docs/api-model.md +++ /dev/null @@ -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)). diff --git a/docs/assets/lb-modules.png b/docs/assets/lb-modules.png index 390b86ea..c8f750e3 100644 Binary files a/docs/assets/lb-modules.png and b/docs/assets/lb-modules.png differ diff --git a/lib/application.js b/lib/application.js index 1e89f6b0..f421f706 100644 --- a/lib/application.js +++ b/lib/application.js @@ -114,58 +114,55 @@ app.disuse = function (route) { */ app.model = function (Model, config) { - if(arguments.length === 1) { - assert(Model.prototype instanceof registry.Model, - 'Model must be a descendant of loopback.Model'); - if(Model.sharedClass) { - this.remotes().addClass(Model.sharedClass); + var isPublic = true; + if (arguments.length > 1) { + config = config || {}; + if (typeof Model === 'string') { + // 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); - Model.shared = true; - Model.app = this; - Model.emit('attached', this); - return Model; + + configureModel(Model, config, this); + isPublic = config.public !== false; + } else { + assert(Model.prototype instanceof loopback.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; this.models[modelName] = - this.models[classify(modelName)] = - this.models[camelize(modelName)] = Model; + this.models[classify(modelName)] = + this.models[camelize(modelName)] = Model; - if (config.public !== false) { - this.model(Model); + this.models().push(Model); + + if (isPublic && Model.sharedClass) { + this.remotes().addClass(Model.sharedClass); + clearHandlerCache(this); } + Model.shared = isPublic; + Model.app = this; + Model.emit('attached', this); return Model; }; - /** * Get the models exported by the app. Returns only models defined using `app.model()` * @@ -337,7 +334,7 @@ app.enableAuth = function() { Model.checkAccess( req.accessToken, modelId, - method.name, + method, function(err, allowed) { // Emit any cached data events that fired while checking access. req.resume(); diff --git a/lib/loopback.js b/lib/loopback.js index 893f37d2..032f5d34 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -159,6 +159,7 @@ loopback.template = function (file) { return ejs.compile(str); }; + /*! * Built in models / services */ diff --git a/lib/middleware/rest.js b/lib/middleware/rest.js index 3f87c126..5e91c7f4 100644 --- a/lib/middleware/rest.js +++ b/lib/middleware/rest.js @@ -1,17 +1,24 @@ -/** +/*! * Module dependencies. */ var loopback = require('../loopback'); -/** +/*! * Export the middleware. */ 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() { diff --git a/lib/middleware/status.js b/lib/middleware/status.js index 02068548..085c5975 100644 --- a/lib/middleware/status.js +++ b/lib/middleware/status.js @@ -1,9 +1,22 @@ -/** +/*! * Export the middleware. */ 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() { var started = new Date(); diff --git a/lib/middleware/token.js b/lib/middleware/token.js index 88a93c24..6087fd31 100644 --- a/lib/middleware/token.js +++ b/lib/middleware/token.js @@ -12,14 +12,17 @@ var assert = require('assert'); 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 - * - `headers` - An `Array` of header names - * - `params` - An `Array` of param names - * - `model` - Specify an AccessToken class to use + * - `access_token` + * - `X-Access-Token` + * - `authorization` + * + * 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 * and header called `foo-auth`. @@ -28,23 +31,16 @@ module.exports = token; * app.use(loopback.token({ * cookies: ['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) { diff --git a/lib/middleware/urlNotFound.js b/lib/middleware/urlNotFound.js index 9f9735e6..a73f6804 100644 --- a/lib/middleware/urlNotFound.js +++ b/lib/middleware/urlNotFound.js @@ -1,13 +1,14 @@ -/** +/*! * Export the middleware. + * See discussion in Connect pull request #954 for more details + * https://github.com/senchalabs/connect/pull/954. */ module.exports = urlNotFound; /** * Convert any request not handled so far to a 404 error * to be handled by error-handling middleware. - * See discussion in Connect pull request #954 for more details - * https://github.com/senchalabs/connect/pull/954 + * @header loopback.urlNotFound() */ function urlNotFound() { return function raiseUrlNotFoundError(req, res, next) { diff --git a/lib/models/access-context.js b/lib/models/access-context.js index b748db1b..681c366c 100644 --- a/lib/models/access-context.js +++ b/lib/models/access-context.js @@ -36,7 +36,18 @@ function AccessContext(context) { this.property = context.property || AccessContext.ALL; 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.accessToken = context.accessToken || AccessToken.ANONYMOUS; @@ -79,7 +90,6 @@ AccessContext.permissionOrder = { DENY: 4 }; - /** * Add a principal to the context * @param {String} principalType The principal type @@ -96,8 +106,6 @@ AccessContext.prototype.addPrincipal = function (principalType, principalId, pri } } this.principals.push(principal); - - debug('adding principal %j', principal); return true; }; @@ -213,7 +221,7 @@ Principal.prototype.equals = function (p) { * @returns {AccessRequest} * @class */ -function AccessRequest(model, property, accessType, permission) { +function AccessRequest(model, property, accessType, permission, methodNames) { if (!(this instanceof AccessRequest)) { return new AccessRequest(model, property, accessType); } @@ -224,26 +232,20 @@ function AccessRequest(model, property, accessType, permission) { this.property = obj.property || AccessContext.ALL; this.accessType = obj.accessType || AccessContext.ALL; this.permission = obj.permission || AccessContext.DEFAULT; + this.methodNames = methodNames || []; } else { this.model = model || AccessContext.ALL; this.property = property || AccessContext.ALL; this.accessType = accessType || AccessContext.ALL; this.permission = permission || AccessContext.DEFAULT; - } - - 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()); + this.methodNames = methodNames || []; } } /** - * Is the request a wildcard - * @returns {boolean} + * Does the request contain any wildcards? + * + * @returns {Boolean} */ AccessRequest.prototype.isWildcard = function () { return this.model === AccessContext.ALL || @@ -251,6 +253,47 @@ AccessRequest.prototype.isWildcard = function () { 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.Principal = Principal; module.exports.AccessRequest = AccessRequest; diff --git a/lib/models/acl.js b/lib/models/acl.js index 7dc1d03c..a3126b87 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -61,6 +61,7 @@ var ACLSchema = { /** * Name of the access type - READ/WRITE/EXEC + * @property accessType {String} Name of the access type - READ/WRITE/EXEC */ accessType: String, @@ -114,17 +115,20 @@ ACL.SCOPE = Principal.SCOPE; * Calculate the matching score for the given rule and request * @param {ACL} rule The ACL entry * @param {AccessRequest} req The request - * @returns {number} + * @returns {Number} */ ACL.getMatchingScore = function getMatchingScore(rule, req) { var props = ['model', 'property', 'accessType']; var score = 0; + for (var i = 0; i < props.length; i++) { // Shift the score by 4 for each of the properties as the weight score = score * 4; var val1 = rule[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 score += 3; } else if (val1 === ACL.ALL) { @@ -186,6 +190,16 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) { 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 * @param {Object[]) acls The list of ACLs @@ -200,14 +214,13 @@ ACL.resolvePermission = function resolvePermission(acls, req) { acls = acls.sort(function (rule1, rule2) { return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req); }); - if(debug.enabled) { - debug('ACLs by order: %j', acls); - } var permission = ACL.DEFAULT; var score = 0; + for (var i = 0; i < acls.length; i++) { score = ACL.getMatchingScore(acls[i], req); if (score < 0) { + // the highest scored ACL did not match break; } if (!req.isWildcard()) { @@ -215,11 +228,7 @@ ACL.resolvePermission = function resolvePermission(acls, req) { permission = acls[i].permission; break; } else { - if(acls[i].model === req.model && - acls[i].property === req.property && - acls[i].accessType === req.accessType - ) { - // We should stop at the exact match + if(req.exactlyMatches(acls[i])) { permission = acls[i].permission; 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, permission || ACL.DEFAULT); return res; @@ -253,11 +270,9 @@ ACL.getStaticACLs = function getStaticACLs(model, property) { property: acl.property || ACL.ALL, principalType: acl.principalType, principalId: acl.principalId, // TODO: Should it be a name? - accessType: acl.accessType, + accessType: acl.accessType || ACL.ALL, permission: acl.permission })); - - staticACLs[staticACLs.length - 1].debug('Adding ACL'); }); } 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 - * @param {String} principalType The principal type - * @param {String} principalId The principal id - * @param {String} model The model name - * @param {String} property The property/method/relation name - * @param {String} accessType The access type - * @param {Function} callback The callback function - * - * @callback callback + * @param {String} principalType The principal type. + * @param {String} principalId The principal ID. + * @param {String} model The model name. + * @param {String} property The property/method/relation name. + * @param {String} accessType The access type. + * @callback {Function} callback Callback function. * @param {String|Error} err The error object * @param {AccessRequest} result The access permission */ @@ -350,15 +363,16 @@ ACL.prototype.debug = function() { } /** - * Check if the request has the permission to access - * @param {Object} context - * @property {Object[]} principals An array of principals - * @property {String|Model} model The model name or model class - * @property {*} id The model instance id - * @property {String} property The property/method/relation name - * @property {String} accessType The access type - * @param {Function} callback + * Check if the request has the permission to access. + * @options {Object} context See below. + * @property {Object[]} principals An array of principals. + * @property {String|Model} model The model name or model class. + * @property {*} id The model instance ID. + * @property {String} property The property/method/relation name. + * @property {String} accessType The access type: READE, WRITE, or EXEC. + * @param {Function} callback Callback function */ + ACL.checkAccessForContext = function (context, callback) { if(!(context instanceof AccessContext)) { context = new AccessContext(context); @@ -367,11 +381,13 @@ ACL.checkAccessForContext = function (context, callback) { var model = context.model; var property = context.property; 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 req = new AccessRequest(model.modelName, property, accessType); + var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames); var effectiveACLs = []; var staticACLs = this.getStaticACLs(model.modelName, property); @@ -404,9 +420,6 @@ ACL.checkAccessForContext = function (context, callback) { inRoleTasks.push(function (done) { roleModel.isInRole(acl.principalId, context, function (err, inRole) { - if(debug.enabled) { - debug('In role %j: %j', acl.principalId, inRole); - } if (!err && inRole) { effectiveACLs.push(acl); } @@ -425,21 +438,20 @@ ACL.checkAccessForContext = function (context, callback) { if(resolved && resolved.permission === ACL.DEFAULT) { resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW; } - debug('checkAccessForContext() returns: %j', resolved); + debug('---Resolved---'); + resolved.debug(); callback && callback(null, resolved); }); }); }; - /** * Check if the given access token can invoke the method * @param {AccessToken} token The access token * @param {String} model The model name * @param {*} modelId The model id * @param {String} method The method name - * @end - * @callback {Function} callback + * @callback {Function} callback Callback function * @param {String|Error} err The error object * @param {Boolean} allowed is the request allowed */ @@ -454,10 +466,6 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) { modelId: modelId }); - context.accessType = context.model._getAccessTypeForMethod(method); - - context.debug(); - this.checkAccessForContext(context, function (err, access) { if (err) { callback && callback(err); diff --git a/lib/models/application.js b/lib/models/application.js index 1201fca3..f395b6a1 100644 --- a/lib/models/application.js +++ b/lib/models/application.js @@ -46,7 +46,7 @@ var PushNotificationSettingSchema = { gcm: GcmSettingsSchema }; -/** +/*! * Data model for Application */ var ApplicationSchema = { @@ -133,10 +133,10 @@ Application.beforeCreate = function (next) { /** * Register a new application - * @param owner Owner's user id - * @param name Name of the application - * @param options Other options - * @param cb Callback function + * @param {String} owner Owner's user ID. + * @param {String} name Name of the application + * @param {Object} options Other options + * @param {Function} callback Callback function */ Application.register = function (owner, name, options, cb) { assert(owner, 'owner is required'); diff --git a/lib/models/model.js b/lib/models/model.js index fa94aeaa..1cfc50d4 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -7,7 +7,6 @@ var juggler = require('loopback-datasource-juggler'); var ModelBuilder = juggler.ModelBuilder; var DataSource = juggler.DataSource; var modeler = new ModelBuilder(); -var async = require('async'); var assert = require('assert'); var _ = require('underscore'); var SharedClass = require('strong-remoting').SharedClass; @@ -222,26 +221,34 @@ Model._ACL = function getACL(ACL) { return _aclModel; }; - /** * Check if the given access token can invoke the method * * @param {AccessToken} token The access token - * @param {*} modelId The model id - * @param {String} method The method name - * @param callback The callback function - * - * @callback {Function} callback + * @param {*} modelId The model ID. + * @param {SharedMethod} sharedMethod The method in question + * @callback {Function} callback The callback function * @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; token = token || ANONYMOUS; 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 Model.setup(); + diff --git a/lib/models/role.js b/lib/models/role.js index 8b64796f..072b7a76 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -319,12 +319,13 @@ Role.registerResolver(Role.EVERYONE, function (role, context, callback) { * @param {Boolean} isInRole */ Role.isInRole = function (role, context, callback) { - debug('isInRole(): %s %j', role, context); - if (!(context instanceof AccessContext)) { context = new AccessContext(context); } + debug('isInRole(): %s', role); + context.debug(); + var resolver = Role.resolvers[role]; if (resolver) { 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 */ Role.getRoles = function (context, callback) { - debug('getRoles(): %j', context); - if(!(context instanceof AccessContext)) { context = new AccessContext(context); } diff --git a/lib/models/user.js b/lib/models/user.js index ae3ad569..c06493ee 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -8,8 +8,6 @@ var PersistedModel = require('../loopback').PersistedModel , SALT_WORK_FACTOR = 10 , crypto = require('crypto') , bcrypt = require('bcryptjs') - , passport = require('passport') - , LocalStrategy = require('passport-local').Strategy , BaseAccessToken = require('./access-token') , DEFAULT_TTL = 1209600 // 2 weeks 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); +/** + * 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`. * @@ -144,6 +159,7 @@ var User = module.exports = PersistedModel.extend('User', properties, options); */ User.login = function (credentials, include, fn) { + var self = this; if (typeof include === 'function') { fn = include; include = undefined; @@ -162,7 +178,7 @@ User.login = function (credentials, include, fn) { return fn(err); } - this.findOne({where: query}, function(err, user) { + self.findOne({where: query}, function(err, user) { var defaultError = new Error('login failed'); defaultError.statusCode = 401; @@ -175,9 +191,7 @@ User.login = function (credentials, include, fn) { debug('An error is reported from User.hasPassword: %j', err); fn(defaultError); } else if(isMatch) { - user.accessTokens.create({ - ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL) - }, function(err, token) { + user.createAccessToken(credentials.ttl, function(err, token) { if (err) return fn(err); if (include === 'user') { // NOTE(bajtos) We can't set token.user here: diff --git a/package.json b/package.json index a1601bf0..8132da28 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,8 @@ "debug": "~0.8.1", "express": "4.x", "body-parser": "~1.2.2", - "strong-remoting": "2.0.0-beta3", + "strong-remoting": "2.0.0-beta4", "inflection": "~1.3.5", - "passport": "~0.2.0", - "passport-local": "~1.0.0", "nodemailer": "~0.6.5", "ejs": "~1.0.0", "bcryptjs": "~0.7.12", @@ -55,11 +53,11 @@ "errorhandler": "~1.0.1", "serve-favicon": "~2.0.0", "loopback-datasource-juggler": "2.0.0-beta1", - "mocha": "~1.18.0", + "mocha": "~1.20.1", "strong-task-emitter": "0.0.x", "supertest": "~0.13.0", "chai": "~1.9.1", - "loopback-testing": "~0.1.3", + "loopback-testing": "~0.2.0", "browserify": "~4.1.6", "grunt": "~0.4.5", "grunt-browserify": "~2.1.0", diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 0ef59c7b..575baebd 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -70,8 +70,11 @@ describe('access control - integration', function () { lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser); - lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/users'); - lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users'); + lt.it.shouldBeAllowedWhenCalledAnonymously( + 'POST', '/api/users', newUserData()); + + lt.it.shouldBeAllowedWhenCalledByUser( + CURRENT_USER, 'POST', '/api/users', newUserData()); lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout'); @@ -117,6 +120,15 @@ describe('access control - integration', function () { function urlForUser() { 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 () { diff --git a/test/acl.test.js b/test/acl.test.js index 3f8892bd..ba97dda9 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -102,11 +102,15 @@ describe('security ACLs', function () { property: 'find', accessType: 'WRITE' }; + + acls = acls.map(function(a) { return new ACL(a)}); + var perm = ACL.resolvePermission(acls, req); assert.deepEqual(perm, { model: 'account', property: 'find', accessType: 'WRITE', - permission: 'ALLOW' }); + permission: 'ALLOW', + methodNames: []}); }); it("should allow access to models for the given principal by wildcard", function () { @@ -297,7 +301,6 @@ describe('security ACLs', function () { }); }); }); - }); diff --git a/test/app.test.js b/test/app.test.js index f7b6d609..8f9be923 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -38,6 +38,15 @@ describe('app', function() { 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) { app.use(loopback.rest()); 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'); }); + + 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() { diff --git a/test/user.test.js b/test/user.test.js index 25b1df8f..2b3805a1 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1,6 +1,5 @@ var User; var AccessToken = loopback.AccessToken; -var passport = require('passport'); var MailConnector = require('../lib/connectors/mail'); var userMemory = loopback.createDataSource({ @@ -9,6 +8,7 @@ var userMemory = loopback.createDataSource({ describe('User', function(){ 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 incompleteCredentials = {password: 'bar1'}; @@ -117,6 +117,44 @@ describe('User', function(){ 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) { request(app)