Ghost/core/server/services/auth/auth-strategies.js
Katharina Irrgang c6a95c6478
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 10:53:53 +01:00

214 lines
7.9 KiB
JavaScript

var _ = require('lodash'),
models = require('../../models'),
common = require('../../lib/common'),
security = require('../../lib/security'),
strategies;
strategies = {
/**
* ClientPasswordStrategy
*
* This strategy is used to authenticate registered OAuth clients. It is
* employed to protect the `token` endpoint, which consumers use to obtain
* access tokens. The OAuth 2.0 specification suggests that clients use the
* HTTP Basic scheme to authenticate (not implemented yet).
* Use of the client password strategy is implemented to support ember-simple-auth.
*/
clientPasswordStrategy: function clientPasswordStrategy(clientId, clientSecret, done) {
return models.Client.findOne({slug: clientId}, {withRelated: ['trustedDomains']})
.then(function then(model) {
if (model) {
var client = model.toJSON({withRelated: ['trustedDomains']});
if (client.status === 'enabled' && client.secret === clientSecret) {
return done(null, client);
}
}
return done(null, false);
});
},
/**
* BearerStrategy
*
* This strategy is used to authenticate users based on an access token (aka a
* bearer token). The user must have previously authorized a client
* application, which is issued an access token to make requests on behalf of
* the authorizing user.
*/
bearerStrategy: function bearerStrategy(accessToken, done) {
return models.Accesstoken.findOne({token: accessToken})
.then(function then(model) {
if (model) {
var token = model.toJSON();
if (token.expires > Date.now()) {
return models.User.findOne({id: token.user_id})
.then(function then(model) {
if (!model) {
return done(null, false);
}
if (!model.isActive()) {
throw new common.errors.NoPermissionError({
message: common.i18n.t('errors.models.user.accountSuspended')
});
}
var user = model.toJSON(),
info = {scope: '*'};
return done(null, {id: user.id}, info);
})
.catch(function (err) {
return done(err);
});
} else {
return done(null, false);
}
} else {
return done(null, false);
}
});
},
/**
* Ghost Strategy
* ghostAuthRefreshToken: will be null for now, because we don't need it right now
*
* CASES:
* - via invite token
* - via normal sign in
* - via setup
*/
ghostStrategy: function ghostStrategy(req, ghostAuthAccessToken, ghostAuthRefreshToken, profile, done) {
var inviteToken = req.body.inviteToken,
options = {context: {internal: true}},
handleInviteToken, handleSetup, handleSignIn;
// CASE: socket hangs up for example
if (!ghostAuthAccessToken || !profile) {
return done(new common.errors.NoPermissionError({
help: 'Please try again.'
}));
}
handleInviteToken = function handleInviteToken() {
var user, invite;
inviteToken = security.url.decodeBase64(inviteToken);
return models.Invite.findOne({token: inviteToken}, options)
.then(function addInviteUser(_invite) {
invite = _invite;
if (!invite) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.invites.inviteNotFound')
});
}
if (invite.get('expires') < Date.now()) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.invites.inviteExpired')
});
}
return models.User.add({
email: profile.email,
name: profile.name,
password: security.identifier.uid(50),
roles: [invite.toJSON().role_id],
ghost_auth_id: profile.id,
ghost_auth_access_token: ghostAuthAccessToken
}, options);
})
.then(function destroyInvite(_user) {
user = _user;
return invite.destroy(options);
})
.then(function () {
return user;
});
};
handleSetup = function handleSetup() {
return models.User.findOne({slug: 'ghost-owner', status: 'inactive'}, options)
.then(function fetchedOwner(owner) {
if (!owner) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.models.user.userNotFound')
});
}
// CASE: slug null forces regenerating the slug (ghost-owner is default and needs to be overridden)
return models.User.edit({
email: profile.email,
name: profile.name,
slug: null,
status: 'active',
ghost_auth_id: profile.id,
ghost_auth_access_token: ghostAuthAccessToken
}, _.merge({id: owner.id}, options));
});
};
handleSignIn = function handleSignIn() {
var user;
return models.User.findOne({ghost_auth_id: profile.id}, options)
.then(function (_user) {
user = _user;
if (!user) {
throw new common.errors.NotFoundError();
}
if (!user.isActive()) {
throw new common.errors.NoPermissionError({
message: common.i18n.t('errors.models.user.accountSuspended')
});
}
return models.User.edit({
email: profile.email,
name: profile.name,
ghost_auth_id: profile.id,
ghost_auth_access_token: ghostAuthAccessToken
}, _.merge({id: user.id}, options));
})
.then(function () {
return user;
});
};
if (inviteToken) {
return handleInviteToken()
.then(function (user) {
done(null, user, profile);
})
.catch(function (err) {
done(err);
});
}
handleSignIn()
.then(function (user) {
done(null, user, profile);
})
.catch(function (err) {
if (!(err instanceof common.errors.NotFoundError)) {
return done(err);
}
handleSetup()
.then(function (user) {
done(null, user, profile);
})
.catch(function (err) {
done(err);
});
});
}
};
module.exports = strategies;