Added PAT support

This commit is contained in:
Wyatt Johnson
2017-06-15 18:12:53 -04:00
parent 0c7f9cdb1e
commit b78d4f29ea
13 changed files with 343 additions and 2 deletions
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const program = require('./commander');
const mongoose = require('../services/mongoose');
const TokensService = require('../services/tokens');
const util = require('./util');
const Table = require('cli-table');
// Regeister the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
async function listTokens(userID) {
try {
let tokens = await TokensService.list(userID);
let table = new Table({
head: [
'ID',
'Name',
'Status'
]
});
tokens.forEach((token) => {
table.push([
token.id,
token.name,
token.active ? 'Active' : 'Revoked'
]);
});
console.log(table.toString());
util.shutdown();
} catch (e) {
console.error(e);
util.shutdown(1);
}
}
async function revokeToken(tokenID) {
try {
await TokensService.revoke(null, tokenID);
util.shutdown();
} catch (e) {
console.error(e);
util.shutdown(1);
}
}
async function createToken(userID, tokenName) {
try {
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
util.shutdown();
} catch (e) {
console.error(e);
util.shutdown(1);
}
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('list <userID>')
.description('list tokens for a user')
.action(listTokens);
program
.command('revoke <tokenID>')
.description('revokes a token with a given id')
.action(revokeToken);
program
.command('create <userID> <tokenName>')
.description('create a token for a user with a given name')
.action(createToken);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
util.shutdown();
}
+2
View File
@@ -4,6 +4,7 @@ const debug = require('debug')('talk:graph:mutators');
const Comment = require('./comment');
const Action = require('./action');
const Tag = require('./tag');
const Token = require('./token');
const User = require('./user');
const plugins = require('../../services/plugins');
@@ -14,6 +15,7 @@ let mutators = [
Comment,
Action,
Tag,
Token,
User,
// Load the plugin mutators from the manager.
+41
View File
@@ -0,0 +1,41 @@
const errors = require('../../errors');
const TokensService = require('../../services/tokens');
const {
CREATE_TOKEN,
REVOKE_TOKEN
} = require('../../perms/constants');
// Creates a new token for a user.
const createToken = async ({user}, {name}) => {
let {pat, jwt} = await TokensService.create(user.id, name);
// Attach the token to the PAT.
pat.jwt = jwt;
// Return that PAT!
return pat;
};
// Revokes the token from the user.
const revokeToken = async ({user}, {id}) => {
return TokensService.revoke(user.id, id);
};
module.exports = (context) => {
let mutators = {
Token: {
create: () => Promise.reject(errors.ErrNotAuthorized),
revoke: () => Promise.reject(errors.ErrNotAuthorized)
}
};
if (context.user && context.user.can(CREATE_TOKEN)) {
mutators.Token.create = (input) => createToken(context, input);
}
if (context.user && context.user.can(REVOKE_TOKEN)) {
mutators.Token.revoke = (input) => revokeToken(context, input);
}
return mutators;
};
+6
View File
@@ -39,6 +39,12 @@ const RootMutation = {
},
removeTag(_, {tag}, {mutators: {Tag}}) {
return wrapResponse(null)(Tag.remove(tag));
},
createToken(_, {input}, {mutators: {Token}}) {
return wrapResponse('token')(Token.create(input));
},
revokeToken(_, {input}, {mutators: {Token}}) {
return wrapResponse(null)(Token.revoke(input));
}
};
+9 -1
View File
@@ -5,7 +5,8 @@ const {
SEARCH_OTHER_USERS,
SEARCH_OTHERS_COMMENTS,
UPDATE_USER_ROLES,
SEARCH_COMMENT_METRICS
SEARCH_COMMENT_METRICS,
LIST_OWN_TOKENS
} = require('../../perms/constants');
const User = {
@@ -46,6 +47,13 @@ const User = {
return null;
},
tokens({id, tokens}, args, {user}) {
if (!user || ((user.id !== id) && !user.can(LIST_OWN_TOKENS))) {
return null;
}
return tokens;
},
ignoredUsers({id}, args, {user, loaders: {Users}}) {
// Only allow a logged in user that is either the current user or is a staff
+57
View File
@@ -36,6 +36,23 @@ enum USER_ROLES {
MODERATOR
}
# Token is a personal access token associated with a given user.
type Token {
# ID is the unique identifier for the token.
id: ID!
# Name is the description for the token.
name: String!
# Active determines if the token is available to hit the API.
active: Boolean!
# JWT is the actual token to use for authentication, this is only available
# on token creation, otherwise it will be null.
jwt: String
}
type UserProfile {
# the id is an identifier for the user profile (email, facebook id, etc)
id: String!
@@ -78,6 +95,9 @@ type User {
# ignored users.
ignoredUsers: [User!]
# Tokens are the personal access tokens for a given user.
tokens: [Token!]
# returns all comments based on a query.
comments(query: CommentsQuery): CommentConnection!
@@ -889,6 +909,37 @@ type EditCommentResponse implements Response {
errors: [UserError!]
}
# CreateTokenInput contains the input to create the token.
input CreateTokenInput {
# Name is the description for the token.
name: String!
}
# CreateTokenResponse contains the errors related to creating a token.
type CreateTokenResponse implements Response {
# Token is the Token that was created, or null if it failed.
token: Token
# An array of errors relating to the mutation that occured.
errors: [UserError!]
}
# RevokeTokenInput contains the input to revoke the token.
input RevokeTokenInput {
# ID is the JTI for the token.
id: ID!
}
# RevokeTokenResponse contains the errors related to revoking a token.
type RevokeTokenResponse implements Response {
# An array of errors relating to the mutation that occured.
errors: [UserError!]
}
# All mutations for the application are defined on this object.
type RootMutation {
@@ -928,6 +979,12 @@ type RootMutation {
# Ignore comments by another user
ignoreUser(id: ID!): IgnoreUserResponse
# CreateToken will create a token that is attached to the current user.
createToken(input: CreateTokenInput!): CreateTokenResponse!
# RevokeToken will revoke an existing token.
revokeToken(input: RevokeTokenInput!): RevokeTokenResponse!
# Stop Ignoring comments by another user
stopIgnoringUser(id: ID!): StopIgnoringUserResponse
}
+16
View File
@@ -0,0 +1,16 @@
const mongoose = require('../../services/mongoose');
const Schema = mongoose.Schema;
const TokenSchema = new Schema({
// ID is the JTI of a given JWT's identifier.
id: String,
// Name is given by the user on token creation.
name: String,
// Active is used to determine if the token is valid.
active: Boolean
});
module.exports = TokenSchema;
+4
View File
@@ -3,6 +3,7 @@ const bcrypt = require('bcrypt');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLinkSchema = require('./schema/tag_link');
const TokenSchema = require('./schema/token');
const intersection = require('lodash/intersection');
const can = require('../perms');
@@ -86,6 +87,9 @@ const UserSchema = new Schema({
// addresses.
profiles: [ProfileSchema],
// Tokens are the individual personal access tokens for a given user.
tokens: [TokenSchema],
// Roles provides an array of roles (as strings) that is associated with a
// user.
roles: [{
+4 -1
View File
@@ -14,6 +14,8 @@ module.exports = {
REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG',
UPDATE_USER_ROLES: 'UPDATE_USER_ROLES',
UPDATE_CONFIG: 'UPDATE_CONFIG',
CREATE_TOKEN: 'CREATE_TOKEN',
REVOKE_TOKEN: 'REVOKE_TOKEN',
// queries
SEARCH_ASSETS: 'SEARCH_ASSETS',
@@ -21,5 +23,6 @@ module.exports = {
SEARCH_ACTIONS: 'SEARCH_ACTIONS',
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS',
SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS',
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS'
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS'
};
+4
View File
@@ -29,6 +29,10 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN', 'MODERATOR']);
case types.UPDATE_CONFIG:
return check(user, ['ADMIN', 'MODERATOR']);
case types.CREATE_TOKEN:
return check(user, ['ADMIN']);
case types.REVOKE_TOKEN:
return check(user, ['ADMIN']);
default:
break;
}
+2
View File
@@ -15,6 +15,8 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN', 'MODERATOR']);
case types.SEARCH_COMMENT_METRICS:
return check(user, ['ADMIN', 'MODERATOR']);
case types.LIST_OWN_TOKENS:
return check(user, ['ADMIN']);
default:
break;
}
+18
View File
@@ -34,6 +34,23 @@ const GenerateToken = (user) => JWT.sign({}, JWT_SECRET, {
audience: JWT_AUDIENCE
});
// GeneratePersonalAccessToken will sign a token to include all the
// authorization information needed for the front end for headless access.
const GeneratePersonalAccessToken = (userID) => {
const payload = {
jti: uuid.v4(),
iss: JWT_ISSUER,
aud: JWT_AUDIENCE,
sub: userID,
pat: true
};
// Sign the payload.
const jwt = JWT.sign(payload, JWT_SECRET, {});
return {payload, jwt};
};
// SetTokenForSafari sends the token in a cookie for Safari clients.
const SetTokenForSafari = (req, res, token) => {
const browser = bowser._detect(req.headers['user-agent']);
@@ -474,5 +491,6 @@ module.exports = {
HandleAuthPopupCallback,
HandleGenerateCredentials,
HandleLogout,
GeneratePersonalAccessToken,
CheckBlacklisted
};
+82
View File
@@ -0,0 +1,82 @@
const UserModel = require('../models/user');
const {GeneratePersonalAccessToken} = require('./passport');
/**
* TokenService manages Personal Access Tokens for users. These tokens are
* persisted in the database and attached to the user.
*/
module.exports = class TokenService {
/**
* Creates a token for a user with a given name.
*
* @param {String} userID the id of the user owning the token
* @param {String} tokenName the name of the token to be created
*/
static async create(userID, tokenName) {
// Create the token.
let {payload, jwt} = GeneratePersonalAccessToken(userID);
// Create the PAT.
let pat = {
id: payload.jti,
name: tokenName,
active: true
};
// Wait to update the user model with the new PAT.
await UserModel.update({id: userID}, {
$push: {
tokens: pat
}
});
return {payload, jwt, pat};
}
/**
* Revokes a token and prevents the token from being used. Once a token has
* been revoked, it cannot be re-enabled.
*
* @param {String} userID the id of the user owning the token
* @param {String} tokenID the jti of the token to revoke
*/
static async revoke(userID, tokenID) {
let query = {
tokens: {
$elemMatch: {
id: tokenID
}
}
};
if (userID) {
query.id = userID;
}
// Revoke the token id.
await UserModel.update(query, {
$set: {
'tokens.$.active': false
}
});
}
/**
* Lists the tokens owned by the user.
*
* @param {String} userID the id of the user owning the token
*/
static async list(userID) {
// Get the user specified by the id.
let user = await UserModel.findOne({id: userID}).select('tokens');
if (!user || !user.tokens) {
return [];
}
return user.tokens;
}
};