2020-04-29 18:44:27 +03:00
|
|
|
const should = require('should');
|
|
|
|
const sinon = require('sinon');
|
2021-10-06 13:12:21 +03:00
|
|
|
const testUtils = require('../../../../utils');
|
2020-04-29 18:44:27 +03:00
|
|
|
const Promise = require('bluebird');
|
2021-10-06 13:12:21 +03:00
|
|
|
const models = require('../../../../../core/server/models');
|
|
|
|
const providers = require('../../../../../core/server/services/permissions/providers');
|
2017-09-25 12:17:06 +03:00
|
|
|
|
2017-10-05 22:01:34 +03:00
|
|
|
describe('Permission Providers', function () {
|
2017-09-25 12:17:06 +03:00
|
|
|
before(function () {
|
|
|
|
models.init();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(function () {
|
2019-01-21 19:53:44 +03:00
|
|
|
sinon.restore();
|
2017-09-25 12:17:06 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('User', function () {
|
|
|
|
it('errors if user cannot be found', function (done) {
|
2020-04-29 18:44:27 +03:00
|
|
|
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
|
2017-09-25 12:17:06 +03:00
|
|
|
return Promise.resolve();
|
|
|
|
});
|
|
|
|
|
2017-10-05 22:01:34 +03:00
|
|
|
providers.user(1)
|
2017-09-25 12:17:06 +03:00
|
|
|
.then(function () {
|
|
|
|
done(new Error('Should have thrown a user not found error'));
|
|
|
|
})
|
|
|
|
.catch(function (err) {
|
|
|
|
findUserSpy.callCount.should.eql(1);
|
|
|
|
err.errorType.should.eql('NotFoundError');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can load user with role, and permissions', function (done) {
|
|
|
|
// This test requires quite a lot of unique setup work
|
2020-04-29 18:44:27 +03:00
|
|
|
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
|
2017-09-25 12:17:06 +03:00
|
|
|
// Create a fake model
|
2020-04-29 18:44:27 +03:00
|
|
|
const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
|
2020-05-05 21:37:53 +03:00
|
|
|
fakeUser.set('status', 'active');
|
2020-04-29 18:44:27 +03:00
|
|
|
|
|
|
|
// Roles & Permissions need to be collections
|
|
|
|
const fakeAdminRole = models.Roles.forge(testUtils.DataGenerator.Content.roles[0]);
|
|
|
|
|
|
|
|
const fakeAdminRolePermissions = models.Permissions.forge(testUtils.DataGenerator.Content.permissions);
|
2017-09-25 12:17:06 +03:00
|
|
|
|
|
|
|
// ## Fake the relations
|
|
|
|
// User is related to roles & permissions
|
|
|
|
fakeUser.relations = {
|
|
|
|
roles: fakeAdminRole,
|
|
|
|
permissions: fakeAdminRolePermissions
|
|
|
|
};
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
2017-09-25 12:17:06 +03:00
|
|
|
// We use this inside toJSON.
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
fakeUser.withRelated = ['roles', 'permissions', 'roles.permissions'];
|
2017-09-25 12:17:06 +03:00
|
|
|
|
|
|
|
return Promise.resolve(fakeUser);
|
|
|
|
});
|
|
|
|
|
2017-10-05 22:01:34 +03:00
|
|
|
// Get permissions for the user
|
|
|
|
providers.user(1)
|
2017-09-25 12:17:06 +03:00
|
|
|
.then(function (res) {
|
|
|
|
findUserSpy.callCount.should.eql(1);
|
|
|
|
|
|
|
|
res.should.be.an.Object().with.properties('permissions', 'roles');
|
|
|
|
|
|
|
|
res.permissions.should.be.an.Array().with.lengthOf(10);
|
|
|
|
res.roles.should.be.an.Array().with.lengthOf(1);
|
|
|
|
|
|
|
|
// @TODO fix this!
|
|
|
|
// Permissions is an array of models
|
|
|
|
// Roles is a JSON array
|
|
|
|
res.permissions[0].should.be.an.Object().with.properties('attributes', 'id');
|
|
|
|
res.roles[0].should.be.an.Object().with.properties('id', 'name', 'description');
|
|
|
|
res.permissions[0].should.be.instanceOf(models.Base.Model);
|
|
|
|
res.roles[0].should.not.be.instanceOf(models.Base.Model);
|
|
|
|
|
|
|
|
done();
|
|
|
|
})
|
|
|
|
.catch(done);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can load user with role, and role.permissions', function (done) {
|
|
|
|
// This test requires quite a lot of unique setup work
|
2020-04-29 18:44:27 +03:00
|
|
|
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
|
2017-09-25 12:17:06 +03:00
|
|
|
// Create a fake model
|
2020-04-29 18:44:27 +03:00
|
|
|
const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
|
2020-05-05 21:37:53 +03:00
|
|
|
fakeUser.set('status', 'active');
|
2020-04-29 18:44:27 +03:00
|
|
|
|
|
|
|
// Roles & Permissions need to be collections
|
|
|
|
const fakeAdminRole = models.Roles.forge(testUtils.DataGenerator.Content.roles[0]);
|
|
|
|
|
|
|
|
const fakeAdminRolePermissions = models.Permissions.forge(testUtils.DataGenerator.Content.permissions);
|
2017-09-25 12:17:06 +03:00
|
|
|
|
|
|
|
// ## Fake the relations
|
|
|
|
// Roles are related to permissions
|
|
|
|
fakeAdminRole.models[0].relations = {
|
|
|
|
permissions: fakeAdminRolePermissions
|
|
|
|
};
|
|
|
|
// User is related to roles
|
|
|
|
fakeUser.relations = {
|
|
|
|
roles: fakeAdminRole
|
|
|
|
};
|
|
|
|
// We use this inside toJSON.
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
fakeUser.withRelated = ['roles', 'permissions', 'roles.permissions'];
|
2017-09-25 12:17:06 +03:00
|
|
|
|
|
|
|
return Promise.resolve(fakeUser);
|
|
|
|
});
|
|
|
|
|
2017-10-05 22:01:34 +03:00
|
|
|
// Get permissions for the user
|
|
|
|
providers.user(1)
|
2017-09-25 12:17:06 +03:00
|
|
|
.then(function (res) {
|
|
|
|
findUserSpy.callCount.should.eql(1);
|
|
|
|
|
|
|
|
res.should.be.an.Object().with.properties('permissions', 'roles');
|
|
|
|
|
|
|
|
res.permissions.should.be.an.Array().with.lengthOf(10);
|
|
|
|
res.roles.should.be.an.Array().with.lengthOf(1);
|
|
|
|
|
|
|
|
// @TODO fix this!
|
|
|
|
// Permissions is an array of models
|
|
|
|
// Roles is a JSON array
|
|
|
|
res.permissions[0].should.be.an.Object().with.properties('attributes', 'id');
|
|
|
|
res.roles[0].should.be.an.Object().with.properties('id', 'name', 'description');
|
|
|
|
res.permissions[0].should.be.instanceOf(models.Base.Model);
|
|
|
|
res.roles[0].should.not.be.instanceOf(models.Base.Model);
|
|
|
|
|
|
|
|
done();
|
|
|
|
})
|
|
|
|
.catch(done);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can load user with role, permissions and role.permissions and deduplicate them', function (done) {
|
|
|
|
// This test requires quite a lot of unique setup work
|
2020-04-29 18:44:27 +03:00
|
|
|
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
|
2017-09-25 12:17:06 +03:00
|
|
|
// Create a fake model
|
2020-04-29 18:44:27 +03:00
|
|
|
const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
|
2020-05-05 21:37:53 +03:00
|
|
|
fakeUser.set('status', 'active');
|
2020-04-29 18:44:27 +03:00
|
|
|
|
|
|
|
// Roles & Permissions need to be collections
|
|
|
|
const fakeAdminRole = models.Roles.forge(testUtils.DataGenerator.Content.roles[0]);
|
|
|
|
|
|
|
|
const fakeAdminRolePermissions = models.Permissions.forge(testUtils.DataGenerator.Content.permissions);
|
2017-09-25 12:17:06 +03:00
|
|
|
|
|
|
|
// ## Fake the relations
|
|
|
|
// Roles are related to permissions
|
|
|
|
fakeAdminRole.models[0].relations = {
|
|
|
|
permissions: fakeAdminRolePermissions
|
|
|
|
};
|
|
|
|
// User is related to roles and permissions
|
|
|
|
fakeUser.relations = {
|
|
|
|
roles: fakeAdminRole,
|
|
|
|
permissions: fakeAdminRolePermissions
|
|
|
|
};
|
|
|
|
// We use this inside toJSON.
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
fakeUser.withRelated = ['roles', 'permissions', 'roles.permissions'];
|
2017-09-25 12:17:06 +03:00
|
|
|
|
|
|
|
return Promise.resolve(fakeUser);
|
|
|
|
});
|
|
|
|
|
2017-10-05 22:01:34 +03:00
|
|
|
// Get permissions for the user
|
|
|
|
providers.user(1)
|
2017-09-25 12:17:06 +03:00
|
|
|
.then(function (res) {
|
|
|
|
findUserSpy.callCount.should.eql(1);
|
|
|
|
|
|
|
|
res.should.be.an.Object().with.properties('permissions', 'roles');
|
|
|
|
|
|
|
|
res.permissions.should.be.an.Array().with.lengthOf(10);
|
|
|
|
res.roles.should.be.an.Array().with.lengthOf(1);
|
|
|
|
|
|
|
|
// @TODO fix this!
|
|
|
|
// Permissions is an array of models
|
|
|
|
// Roles is a JSON array
|
|
|
|
res.permissions[0].should.be.an.Object().with.properties('attributes', 'id');
|
|
|
|
res.roles[0].should.be.an.Object().with.properties('id', 'name', 'description');
|
|
|
|
res.permissions[0].should.be.instanceOf(models.Base.Model);
|
|
|
|
res.roles[0].should.not.be.instanceOf(models.Base.Model);
|
|
|
|
|
|
|
|
done();
|
|
|
|
})
|
|
|
|
.catch(done);
|
|
|
|
});
|
2020-05-05 21:37:53 +03:00
|
|
|
|
|
|
|
it('throws when user with non-active status is loaded', function (done) {
|
|
|
|
// This test requires quite a lot of unique setup work
|
|
|
|
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
|
|
|
|
// Create a fake model
|
|
|
|
const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
|
|
|
|
fakeUser.set('status', 'locked');
|
|
|
|
|
|
|
|
return Promise.resolve(fakeUser);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Get permissions for the user
|
|
|
|
providers.user(1)
|
|
|
|
.then(function (res) {
|
|
|
|
done(new Error('Locked user should should throw an error'));
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
err.errorType.should.equal('UnauthorizedError');
|
|
|
|
findUserSpy.callCount.should.eql(1);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2017-09-25 12:17:06 +03:00
|
|
|
});
|
|
|
|
|
2019-01-18 14:17:12 +03:00
|
|
|
describe('API Key', function () {
|
|
|
|
it('errors if api_key cannot be found', function (done) {
|
2019-01-21 19:53:44 +03:00
|
|
|
let findApiKeySpy = sinon.stub(models.ApiKey, 'findOne');
|
2019-01-18 14:17:12 +03:00
|
|
|
findApiKeySpy.returns(new Promise.resolve());
|
|
|
|
providers.apiKey(1)
|
|
|
|
.then(() => {
|
|
|
|
done(new Error('Should have thrown an api key not found error'));
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
findApiKeySpy.callCount.should.eql(1);
|
|
|
|
err.errorType.should.eql('NotFoundError');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
it('can load api_key with role, and role.permissions', function (done) {
|
2019-01-21 19:53:44 +03:00
|
|
|
const findApiKeySpy = sinon.stub(models.ApiKey, 'findOne').callsFake(function () {
|
2019-01-18 14:17:12 +03:00
|
|
|
const fakeApiKey = models.ApiKey.forge(testUtils.DataGenerator.Content.api_keys[0]);
|
|
|
|
const fakeAdminRole = models.Role.forge(testUtils.DataGenerator.Content.roles[0]);
|
|
|
|
const fakeAdminRolePermissions = models.Permissions.forge(testUtils.DataGenerator.Content.permissions);
|
|
|
|
fakeAdminRole.relations = {
|
|
|
|
permissions: fakeAdminRolePermissions
|
|
|
|
};
|
|
|
|
fakeApiKey.relations = {
|
|
|
|
role: fakeAdminRole
|
|
|
|
};
|
|
|
|
fakeApiKey.withRelated = ['role', 'role.permissions'];
|
|
|
|
return Promise.resolve(fakeApiKey);
|
|
|
|
});
|
|
|
|
providers.apiKey(1).then((res) => {
|
|
|
|
findApiKeySpy.callCount.should.eql(1);
|
|
|
|
res.should.be.an.Object().with.properties('permissions', 'roles');
|
|
|
|
res.roles.should.be.an.Array().with.lengthOf(1);
|
|
|
|
res.permissions[0].should.be.an.Object().with.properties('attributes', 'id');
|
|
|
|
res.roles[0].should.be.an.Object().with.properties('id', 'name', 'description');
|
|
|
|
res.permissions[0].should.be.instanceOf(models.Base.Model);
|
|
|
|
res.roles[0].should.not.be.instanceOf(models.Base.Model);
|
|
|
|
done();
|
|
|
|
}).catch(done);
|
|
|
|
});
|
|
|
|
});
|
2017-09-25 12:17:06 +03:00
|
|
|
});
|