0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Added member login resource to Admin API (#11607)

no issue

- Adds 'GET /members/:id/signin_urls' endpoint to Admin API allowing to fetch login URL for member. This URL allows to log in as a member which is useful in situations when you need to impersonate a member (for example to debug some issue they are having)
- Added member_signin_urls permission with migrations. Only the "Owner" user can read "signin_urls" resource. Admin and other users will be denied access
This commit is contained in:
Naz 2020-02-27 11:48:02 +08:00 committed by GitHub
parent 258bcc71bf
commit b0ff1e7cac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 192 additions and 1 deletions

View file

@ -71,6 +71,10 @@ module.exports = {
return shared.pipeline(require('./members'), localUtils);
},
get memberSigninUrls() {
return shared.pipeline(require('./memberSigninUrls.js'), localUtils);
},
get labels() {
return shared.pipeline(require('./labels'), localUtils);
},

View file

@ -0,0 +1,30 @@
const models = require('../../models');
const common = require('../../lib/common');
const membersService = require('../../services/members');
module.exports = {
docName: 'member_signin_urls',
permissions: true,
read: {
data: [
'id'
],
permissions: true,
async query(frame) {
let model = await models.Member.findOne(frame.data, frame.options);
if (!model) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.members.memberNotFound')
});
}
const magicLink = membersService.api.getMagicLink(model.get('email'));
return {
member_id: model.get('id'),
url: magicLink
};
}
}
};

View file

@ -63,6 +63,10 @@ module.exports = {
return require('./members');
},
get member_signin_urls() {
return require('./member-signin_urls');
},
get images() {
return require('./images');
},

View file

@ -0,0 +1,7 @@
module.exports = {
read(data, apiConfig, frame) {
frame.response = {
member_signin_urls: [data]
};
}
};

View file

@ -0,0 +1,58 @@
const _ = require('lodash');
const utils = require('../../../schema/fixtures/utils');
const permissions = require('../../../../services/permissions');
const logging = require('../../../../lib/common/logging');
const resources = ['member_signin_url'];
const _private = {};
_private.getPermissions = function getPermissions(resource) {
return utils.findModelFixtures('Permission', {object_type: resource});
};
_private.getRelations = function getRelations(resource) {
return utils.findPermissionRelationsForObject(resource);
};
_private.printResult = function printResult(result, message) {
if (result.done === result.expected) {
logging.info(message);
} else {
logging.warn(`(${result.done}/${result.expected}) ${message}`);
}
};
module.exports.config = {
transaction: true
};
module.exports.up = (options) => {
const localOptions = _.merge({
context: {internal: true}
}, options);
return Promise.map(resources, (resource) => {
const modelToAdd = _private.getPermissions(resource);
const relationToAdd = _private.getRelations(resource);
return utils.addFixturesForModel(modelToAdd, localOptions)
.then(result => _private.printResult(result, `Adding permissions fixtures for ${resource}s`))
.then(() => utils.addFixturesForRelation(relationToAdd, localOptions))
.then(result => _private.printResult(result, `Adding permissions_roles fixtures for ${resource}s`))
.then(() => permissions.init(localOptions));
});
};
module.exports.down = (options) => {
const localOptions = _.merge({
context: {internal: true}
}, options);
return Promise.map(resources, (resource) => {
const modelToRemove = _private.getPermissions(resource);
// permission model automatically cleans up permissions_roles on .destroy()
return utils.removeFixturesForModel(modelToRemove, localOptions)
.then(result => _private.printResult(result, `Removing permissions fixtures for ${resource}s`));
});
};

View file

@ -412,6 +412,11 @@
"name": "Delete labels",
"action_type": "destroy",
"object_type": "label"
},
{
"name": "Read member signin urls",
"action_type": "read",
"object_type": "member_signin_url"
}
]
},

View file

@ -100,6 +100,8 @@ module.exports = function apiRoutes() {
router.put('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.edit));
router.del('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.destroy));
router.get('/members/:id/signin_urls', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.memberSigninUrls.read));
// ## Labels
router.get('/labels', mw.authAdminApi, http(apiCanary.labels.browse));
router.get('/labels/:id', mw.authAdminApi, http(apiCanary.labels.read));

View file

@ -0,0 +1,80 @@
const path = require('path');
const should = require('should');
const supertest = require('supertest');
const sinon = require('sinon');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const config = require('../../../../../server/config');
const labs = require('../../../../../server/services/labs');
const ghost = testUtils.startGhost;
let request;
describe('Members Sigin URL API', function () {
before(function () {
sinon.stub(labs, 'isSet').withArgs('members').returns(true);
});
after(function () {
sinon.restore();
});
describe('As Owner', function () {
before(function () {
return ghost()
.then(function () {
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request, 'member');
});
});
it('Can read', function () {
return request
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[0].id}/signin_urls/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.member_signin_urls);
jsonResponse.member_signin_urls.should.have.length(1);
localUtils.API.checkResponse(jsonResponse.member_signin_urls[0], 'member_signin_url');
});
});
});
describe('As non-Owner', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
request = supertest.agent(config.get('url'));
})
.then(function () {
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'admin+1@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[0].name
});
})
.then(function (admin) {
request.user = admin;
return localUtils.doAuth(request, 'member');
});
});
it('Cannot read', function () {
return request
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[0].id}/signin_urls/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
});
});

View file

@ -59,6 +59,7 @@ const expectedProperties = {
.concat('comped')
.concat('labels')
,
member_signin_url: ['member_id', 'url'],
role: _(schema.roles)
.keys()
,

View file

@ -20,7 +20,7 @@ var should = require('should'),
describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = '7cd198f085844aa5725964069b051189';
const currentFixturesHash = '0ca1c9a6d3dab21d8a1e0b6a988fd83f';
const currentFixturesHash = 'b2e26827d712513907054782a0be5735';
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
// and the values above will need updating as confirmation