Skip to content

Commit

Permalink
🎨 public config endpoint (#7631)
Browse files Browse the repository at this point in the history
closes #7628

With this PR we expose a public configuration endpoint.
When /ghost is requested, we don't load and render the configurations into the template anymore. Instead, Ghost-Admin can request the public configuration endpoint.

* 🎨  make configuration endpoint public
* 🔥  remove loading configurations in admin app
- do not render them into the default html page
* ✨  load client credentials in configuration endpoint
- this is not a security issue, because we have exposed this information anyway before (by rendering them into the requested html page)
* 🎨  extend existing configuration integration test
* ✨  tests: add ghost-auth to data generator
* ✨  add functional test
* 🔥  remove type/value pattern
* 🎨  do not return stringified JSON objects
  • Loading branch information
kirrg001 authored and ErisDS committed Oct 28, 2016
1 parent e11e3a2 commit a55fb0b
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 63 deletions.
33 changes: 1 addition & 32 deletions core/server/admin/controller.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
var debug = require('debug')('ghost:admin:controller'),
_ = require('lodash'),
Promise = require('bluebird'),
api = require('../api'),
config = require('../config'),
logging = require('../logging'),
updateCheck = require('../update-check'),
i18n = require('../i18n');
Expand All @@ -14,35 +12,6 @@ module.exports = function adminController(req, res) {
/*jslint unparam:true*/
debug('index called');

function renderIndex() {
var configuration,
fetch = {
configuration: api.configuration.read().then(function (res) { return res.configuration[0]; }),
client: api.clients.read({slug: 'ghost-admin'}).then(function (res) { return res.clients[0]; }),
ghostAuth: api.clients.read({slug: 'ghost-auth'})
.then(function (res) { return res.clients[0]; })
.catch(function () {
return;
})
};

return Promise.props(fetch).then(function renderIndex(result) {
configuration = result.configuration;

configuration.clientId = {value: result.client.slug, type: 'string'};
configuration.clientSecret = {value: result.client.secret, type: 'string'};

if (result.ghostAuth && config.get('auth:type') === 'ghost') {
configuration.ghostAuthId = {value: result.ghostAuth.uuid, type: 'string'};
}

debug('rendering default template');
res.render('default', {
configuration: configuration
});
});
}

updateCheck().then(function then() {
return updateCheck.showUpdateNotification();
}).then(function then(updateVersion) {
Expand All @@ -64,6 +33,6 @@ module.exports = function adminController(req, res) {
}
});
}).finally(function noMatterWhat() {
renderIndex();
res.render('default');
}).catch(logging.logError);
};
2 changes: 1 addition & 1 deletion core/server/api/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function apiRoutes() {
apiRouter.options('*', cors);

// ## Configuration
apiRouter.get('/configuration', authenticatePrivate, api.http(api.configuration.read));
apiRouter.get('/configuration', api.http(api.configuration.read));
apiRouter.get('/configuration/:key', authenticatePrivate, api.http(api.configuration.read));
apiRouter.get('/configuration/timezones', authenticatePrivate, api.http(api.configuration.read));

Expand Down
46 changes: 32 additions & 14 deletions core/server/api/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@
var _ = require('lodash'),
config = require('../config'),
ghostVersion = require('../utils/ghost-version'),
models = require('../models'),
Promise = require('bluebird'),

configuration;

function labsFlag(key) {
return {
value: (config[key] === true),
type: 'bool'
};
}

function fetchAvailableTimezones() {
var timezones = require('../data/timezones.json');
return timezones;
Expand All @@ -30,18 +24,22 @@ function getAboutConfig() {

function getBaseConfig() {
return {
fileStorage: {value: (config.fileStorage !== false), type: 'bool'},
useGravatar: {value: !config.isPrivacyDisabled('useGravatar'), type: 'bool'},
publicAPI: labsFlag('publicAPI'),
blogUrl: {value: config.get('url').replace(/\/$/, ''), type: 'string'},
blogTitle: {value: config.get('theme').title, type: 'string'},
routeKeywords: {value: JSON.stringify(config.get('routeKeywords')), type: 'json'}
fileStorage: config.get('fileStorage') !== false,
useGravatar: !config.isPrivacyDisabled('useGravatar'),
publicAPI: config.get('publicAPI') === true,
blogUrl: config.get('url').replace(/\/$/, ''),
blogTitle: config.get('theme').title,
routeKeywords: config.get('routeKeywords')
};
}

/**
* ## Configuration API Methods
*
* We need to load the client credentials dynamically.
* For example: on bootstrap ghost-auth get's created and if we load them here in parallel,
* it can happen that we won't get any client credentials or wrong credentials.
*
* **See:** [API Methods](index.js.html#api%20methods)
*/
configuration = {
Expand All @@ -54,9 +52,29 @@ configuration = {
*/
read: function read(options) {
options = options || {};
var ops = {};

if (!options.key) {
return Promise.resolve({configuration: [getBaseConfig()]});
ops.ghostAdmin = models.Client.findOne({slug: 'ghost-admin'});

if (config.get('auth:type') === 'ghost') {
ops.ghostAuth = models.Client.findOne({slug: 'ghost-auth'});
}

return Promise.props(ops)
.then(function (result) {
var configuration = getBaseConfig();

configuration.clientId = result.ghostAdmin.get('slug');
configuration.clientSecret = result.ghostAdmin.get('secret');

if (result.ghostAuth) {
configuration.ghostAuthId = result.ghostAuth.get('uuid');
configuration.ghostAuthUrl = config.get('auth:url');
}

return {configuration: [configuration]};
});
}

if (options.key === 'about') {
Expand Down
41 changes: 41 additions & 0 deletions core/test/functional/routes/api/configuration_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
var testUtils = require('../../../utils'),
should = require('should'),
supertest = require('supertest'),
ghost = testUtils.startGhost,
request;

describe('Configuration API', function () {
var accesstoken = '';

before(function (done) {
// starting ghost automatically populates the db
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
ghost().then(function (ghostServer) {
request = supertest.agent(ghostServer.rootApp);
}).then(function () {
return testUtils.doAuth(request, 'posts');
}).then(function (token) {
accesstoken = token;
done();
}).catch(done);
});

after(function (done) {
testUtils.clearData().then(function () {
done();
}).catch(done);
});

describe('success', function () {
it('can retrieve public configuration', function (done) {
request.get(testUtils.API.getApiQuery('configuration/'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
should.exist(res.body.configuration);
done();
});
});
});
});
49 changes: 34 additions & 15 deletions core/test/integration/api/api_configuration_spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
var testUtils = require('../../utils'),
should = require('should'),
rewire = require('rewire'),
var testUtils = require('../../utils'),
configUtils = require('../../utils/configUtils'),
should = require('should'),
rewire = require('rewire'),

// Stuff we are testing
ConfigurationAPI = rewire('../../../server/api/configuration');
ConfigurationAPI = rewire('../../../server/api/configuration');

describe('Configuration API', function () {
// Keep the DB clean
before(testUtils.teardown);
afterEach(testUtils.teardown);
beforeEach(testUtils.setup('clients'));
afterEach(function () {
configUtils.restore();
return testUtils.teardown();
});

should.exist(ConfigurationAPI);

it('can read basic config and get all expected properties', function (done) {
configUtils.set('auth:type', 'ghost');

ConfigurationAPI.read().then(function (response) {
var props;

Expand All @@ -21,17 +28,29 @@ describe('Configuration API', function () {
response.configuration.should.be.an.Array().with.lengthOf(1);
props = response.configuration[0];

// Check the structure
props.should.have.property('blogUrl').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('blogTitle').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('routeKeywords').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('fileStorage').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('useGravatar').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('publicAPI').which.is.an.Object().with.properties('type', 'value');
props.blogUrl.should.eql('http://127.0.0.1:2369');
props.routeKeywords.should.eql({
tag: 'tag',
author: 'author',
page: 'page',
preview: 'p',
private: 'private',
subscribe: 'subscribe',
amp: 'amp'
});

// Check a few values
props.blogUrl.should.have.property('value', 'http://127.0.0.1:2369');
props.fileStorage.should.have.property('value', true);
props.fileStorage.should.eql(true);
props.useGravatar.should.eql(true);
props.publicAPI.should.eql(false);
props.clientId.should.eql('ghost-admin');
props.clientSecret.should.eql('not_available');
props.ghostAuthUrl.should.eql('http://devauth.ghost.org:8080');

// value not available, because settings API was not called yet
props.hasOwnProperty('blogTitle').should.eql(true);

// uuid
props.hasOwnProperty('ghostAuthId').should.eql(true);

done();
}).catch(done);
Expand Down
3 changes: 2 additions & 1 deletion core/test/utils/fixtures/data-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,8 @@ DataGenerator.forKnex = (function () {

clients = [
createClient({name: 'Ghost Admin', slug: 'ghost-admin', type: 'ua'}),
createClient({name: 'Ghost Scheduler', slug: 'ghost-scheduler', type: 'web'})
createClient({name: 'Ghost Scheduler', slug: 'ghost-scheduler', type: 'web'}),
createClient({name: 'Ghost Auth', slug: 'ghost-auth', type: 'web'})
];

roles_users = [
Expand Down

0 comments on commit a55fb0b

Please sign in to comment.