Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GSoC19] Add authentication to RC #1

Merged
merged 7 commits into from
Jul 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/client
14 changes: 13 additions & 1 deletion core/server/api/shared/http.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const debug = require('ghost-ignition').debug('api:shared:http');
const shared = require('../shared');
const models = require('../../models');
const _ = require('lodash');

/**
* @description HTTP wrapper.
Expand All @@ -18,6 +19,16 @@ const http = (apiImpl) => {
let apiKey = null;
let integration = null;
let user = null;
let rc_uid = null;
let rc_token = null;

if(req.headers && req.headers.cookie)
_.forEach(req.headers.cookie.split(';'), (v)=>{
if(v.includes('rc_uid'))
rc_uid = v.split('=')[1];
if(v.includes('rc_token'))
rc_token = v.split('=')[1];
});

if (req.api_key) {
apiKey = {
Expand All @@ -39,6 +50,8 @@ const http = (apiImpl) => {
file: req.file,
files: req.files,
query: req.query,
rc_uid: rc_uid,
rc_token: rc_token,
params: req.params,
user: req.user,
context: {
Expand Down Expand Up @@ -93,7 +106,6 @@ const http = (apiImpl) => {
docName: frame.docName,
method: frame.method
};

next(err);
});
};
Expand Down
113 changes: 105 additions & 8 deletions core/server/api/v0.1/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Promise = require('bluebird'),
mail = require('../../services/mail'),
urlService = require('../../services/url'),
localUtils = require('./utils'),
rcUtils = require('../v2/utils/rc-utils'),
models = require('../../models'),
web = require('../../web'),
mailAPI = require('./mail'),
Expand Down Expand Up @@ -61,14 +62,21 @@ function setupTasks(setupData) {
let tasks;

function validateData(setupData) {
return localUtils.checkObject(setupData, 'setup').then((checked) => {
const data = checked.setup[0];

const id = setupData['setup'][0].rc_id;
const token = setupData['setup'][0].rc_token;
const blogTitle = setupData['setup'][0].blogTitle;
const rcUrl = setupData['setup'][0].rc_url;
return rcUtils.checkAdmin(rcUrl, id, token).then((data) => {
const email = data.emails[0].address;
return {
name: data.name,
email: data.email,
password: data.password,
blogTitle: data.blogTitle,
rc_id: id,
rc_username: data.username,
profile_image: data.avatarUrl,
email: email,
password: "qwe123qwe123",//TODO set random password
blogTitle: blogTitle,
serverUrl: rcUrl,
status: 'active'
};
});
Expand Down Expand Up @@ -97,6 +105,7 @@ function setupTasks(setupData) {
function doSettings(data) {
const user = data.user,
blogTitle = data.userData.blogTitle,
serverUrl = data.userData.serverUrl,
context = {context: {user: data.user.id}};

let userSettings;
Expand All @@ -106,6 +115,7 @@ function setupTasks(setupData) {
}

userSettings = [
{key: 'server_url', value: serverUrl},
{key: 'title', value: blogTitle.trim()},
{key: 'description', value: common.i18n.t('common.api.authentication.sampleBlogDescription')}
];
Expand Down Expand Up @@ -437,6 +447,92 @@ authentication = {
return pipeline(tasks, invitation);
},

/**
* ### Add Users
* @param {Object} invitation an invitation object
* @returns {Promise<Object>}
*/
addUser(invitation, option) {
let tasks,
invite;
const options = {context: {internal: true}, withRelated: ['roles']};
const localOptions = {context: {internal: true}};
// 1. if admin adds user, option.
// 2. if user creating account, invitation.
const rc_uid = option.rc_uid || invitation.user[0].rc_uid;
const rc_token = option.rc_token || invitation.user[0].rc_token;

function validateInvitation(invitation) {
return models.Settings.findOne({ key: 'invite_only' }, localOptions)
.then((setting) => {
const inviteOnly = setting.attributes.value;
return rcUtils.getMe(rc_uid, rc_token)
.then((invitedBy) => {
if (!invitedBy.success) {
throw new common.errors.NotFoundError({ message: "User not found. Make Sure you are logged in on RC." });
}
if (inviteOnly) { //Check that rc_uid is of Owner/Admin
return models.User.findOne({ rc_id: rc_uid, role: 'Owner'||'Administrator', status: 'all'}, options)
.then((user) => {
if (user) {
return invitation;
} else {
throw new common.errors.NotFoundError({ message: "You are not authorized to add new authors" });
}
});
} else {// Self Invitation
return invitation;
}
});
});
}

function processInvitation(invitation) {
const data = invitation.user[0];
return rcUtils.getUser(rc_uid, rc_token, data.rc_username)
.then((user) => {
if (user.success && user.user) {
const u = user.user;
if(!u.emails){
throw new common.errors.NotFoundError({ message: "Cannot create account without email." });
}
const email = u.emails[0].address;
const role = data.role.name||'Author';
return models.Role.findOne({name: role})
.then((r) => {
return models.User.add({
rc_id: u._id,
rc_username: u.username,
email: email,
name: u.name,
password: "qwe123qwe123",//TODO Random password
roles: [r]
}, localOptions);
});
} else {
throw new common.errors.NotFoundError({message: "User not found. Make Sure you are logged in on RC."});
}
});
}

function formatResponse() {
return {
invitation: [
{message: 'User Added'}
]
};
}

tasks = [
assertSetupCompleted(true),
validateInvitation,
processInvitation,
formatResponse
];

return pipeline(tasks, invitation);
},

/**
* ### Check for invitation
* @param {Object} options
Expand Down Expand Up @@ -513,7 +609,7 @@ authentication = {
},

/**
* Executes the setup tasks and sends an email to the owner
* Executes the setup tasks and get access_token and user_id and verify with rc-utils
* @param {Object} setupDetails
* @return {Promise<Object>} a user api payload
*/
Expand Down Expand Up @@ -566,7 +662,8 @@ authentication = {
tasks = [
assertSetupCompleted(false),
doSetup,
sendNotification,
// TODO: add mail service from RC.
// sendNotification,
formatResponse
];

Expand Down
10 changes: 9 additions & 1 deletion core/server/api/v0.1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Ghost's JSON API is integral to the workings of Ghost, regardless of whether you want to access data internally,
// from a theme, an app, or from an external app, you'll use the Ghost JSON API to do so.

const {isEmpty} = require('lodash');
const {isEmpty, forEach} = require('lodash');
const Promise = require('bluebird');
const models = require('../../models');
const urlService = require('../../services/url');
Expand Down Expand Up @@ -272,6 +272,14 @@ const http = (apiMethod) => {
}
});

if(req.headers && req.headers.cookie)
forEach(req.headers.cookie.split(';'), (v)=>{
if(v.includes('rc_uid'))
options.rc_uid = v.split('=')[1];
if(v.includes('rc_token'))
options.rc_token = v.split('=')[1];
});

if (req.files) {
options.files = req.files;
}
Expand Down
4 changes: 4 additions & 0 deletions core/server/api/v2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ module.exports = {
return shared.pipeline(require('./invites'), localUtils);
},

get rcapi() {
return shared.pipeline(require('./rcapi'), localUtils);
},

get mail() {
return shared.pipeline(require('./mail'), localUtils);
},
Expand Down
33 changes: 33 additions & 0 deletions core/server/api/v2/rcapi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const rcUtils = require('./utils/rc-utils');
const ALLOWED_INCLUDES = [];

module.exports = {
docName: 'rcapi',

browse: {
options: [
'include',
'name',
'page',
'limit',
'fields',
'filter',
'order',
'debug'
],
validation: {
options: {
include: ALLOWED_INCLUDES,
}
},
permissions: false,
query(frame) {
let username = frame.options.name;

return rcUtils.validateUser(frame.original.rc_uid, frame.original.rc_token, username)
.then((user) =>{
return user;
})
}
}
};
37 changes: 26 additions & 11 deletions core/server/api/v2/session.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Promise = require('bluebird');
const common = require('../../lib/common');
const models = require('../../models');
const rcUtils = require('../v2/utils/rc-utils');
const auth = require('../../services/auth');

const session = {
Expand All @@ -13,33 +14,47 @@ const session = {
*/
return models.User.findOne({id: options.context.user});
},

add(object) {
if (!object || !object.username || !object.password) {
if (!object || !object.rc_id || !object.rc_token) {
return Promise.reject(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
}));
}

return models.User.check({
email: object.username,
password: object.password
return models.User.findOne({
rc_id: object.rc_id
}).then((user) => {
return Promise.resolve((req, res, next) => {
req.brute.reset(function (err) {
if (err) {
return next(err);
if (!user){
throw new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
});
}
return rcUtils.getMe(object.rc_id, object.rc_token)
.then((u) => {
if (!u.success) {
throw new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
});
}
req.user = user;
auth.session.createSession(req, res, next);
return Promise.resolve((req, res, next) => {
req.brute.reset(function (err) {
if (err) {
return next(err);
}
req.user = user;
auth.session.createSession(req, res, next);
});
});
});
});
}).catch((err) => {
throw new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied'),
err
});
});
},

delete() {
return Promise.resolve((req, res, next) => {
auth.session.destroySession(req, res, next);
Expand Down
28 changes: 28 additions & 0 deletions core/server/api/v2/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ module.exports = {
}
},

// TODO: find a better way to check if this setting
// Maybe add a setting in RC and keep both in sync.
inviteOnly: {
options: [],
validation: {
options: {
include: []
}
},
permissions: false,
query(frame) {
const key = 'invite_only';
let setting = settingsCache.get(key, {resolve: false});

if (!setting) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.settings.problemFindingSetting', {
key: key
})
}));
}

return {
[key]: setting
};
}
},

read: {
options: ['key'],
validation: {
Expand Down
Loading