Skip to content

Commit

Permalink
Merge pull request #170 from UnitedEffects/issue169
Browse files Browse the repository at this point in the history
resolves #169
  • Loading branch information
theBoEffect authored Mar 10, 2022
2 parents bd24e0a + 7d6d591 commit 80a021c
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 30 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ue-auth",
"altName": "UE-Auth",
"version": "0.40.0",
"version": "1.0.0",
"description": "UE Auth is a multi-tenant OIDC Provider, User Management, B2B Product Access, and Roles/Permissions Management system intended to create a single hybrid solution to serve as Identity and Access for both self-registered B2C Apps and Enterprise B2B Solutions",
"private": false,
"license": "SEE LICENSE IN ./LICENSE.md",
Expand Down
62 changes: 61 additions & 1 deletion src/api/accounts/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,48 @@ const cryptoRandomString = require('crypto-random-string');
const config = require('../../config');

export default {
async importAccounts(authGroup, global, array, creator, customDomain) {
let failed = [];
let success = [];
let ok = 0;
const attempted = array.length;
const accounts = [];
array.map((acc) => {
const data = {
email: acc.email,
authGroup: authGroup.id,
username: acc.username || acc.email,
password: cryptoRandomString({length: 16, type: 'url-safe'}),
phone: acc.phone,
modifiedBy: creator
};
if(acc.id) data._id = acc.id;
accounts.push(data);
});
try {
const result = await dal.writeMany(accounts);
ok = result?.length || 0;
success = JSON.parse(JSON.stringify(result));
} catch (error) {
ok = error?.insertedDocs?.length || 0;
failed = error?.writeErrors;
success = error?.insertedDocs;
}
// todo - build bulk notification system to make this possible
/*
if (global.notifications.enabled === true &&
authGroup.pluginOptions.notification.enabled === true &&
authGroup.config.autoVerify === true) {
try {
await this.bulkResetOrVerify(authGroup, global, success, creator, false, customDomain);
} catch (er) {
console.error(er);
failed.push({ warning: er.message, message: 'some accounts may not have received a notification' });
}
}
*/
return { warning: 'Auto verify does not work with bulk imports. You will need to send password reset notifications or direct your users to the self-service password reset page.', attempted, ok, failed, success };
},
async writeAccount(data, creator = undefined) {
data.email = data.email.toLowerCase();
if(!data.username) data.username = data.email;
Expand Down Expand Up @@ -131,6 +173,24 @@ export default {
const aliasDns = authGroup.aliasDnsOIDC || undefined;
return this.resetOrVerify(authGroup, settings, user, ['email'], undefined, true, aliasDns);
},

// @notTested
// todo - incomplete implementation for bulk user import
async bulkResetOrVerify(authGroup, globalSettings, users, activeUser = undefined, aliasDns = undefined) {
const iAccessTokens = await iat.generateManyIAT(900, ['auth_group'], authGroup, users);
await Promise.all(users.map(async(user) => {
const findToken = iAccessTokens.filter((t) => {
return (t.payload.sub === user._id);
});
if(findToken.length !== 0) {
const data = this.verifyAccountOptions(authGroup, user, findToken[0].payload.jti, [], activeUser, aliasDns);
// todo - you'll still need to create a bulk notification system and only hit DB once for auth + notify objects
// todo n.notify will not work well for this...
return n.notify(globalSettings, data, authGroup);
}
return user;
}));
},
// @notTested
async resetOrVerify(authGroup, globalSettings, user, formats = [], activeUser = undefined, reset=true, aliasDns = undefined) {
let iAccessToken;
Expand All @@ -144,7 +204,7 @@ export default {
let data;
if(reset === true){
data = this.resetPasswordOptions(authGroup, user, iAccessToken, formats, activeUser, aliasDns);
} else data = this.verifyAccountOptions(authGroup, user, iAccessToken, formats = [], activeUser, aliasDns);
} else data = this.verifyAccountOptions(authGroup, user, iAccessToken, formats, activeUser, aliasDns);

return n.notify(globalSettings, data, authGroup);
} catch (error) {
Expand Down
14 changes: 13 additions & 1 deletion src/api/accounts/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,25 @@ const config = require('../../config');
const RESOURCE = 'Account';

const api = {
async importAccounts(req, res, next) {
try {
if (req.authGroup.active === false) throw Boom.forbidden('You can not add members to an inactive group');
if (!Array.isArray(req.body)) throw Boom.badRequest('You are expected to send an array of accounts');
if(req.body.length > 1000) throw Boom.badRequest('At this time, we can only import 1000 accounts a time');
const result = await acct.importAccounts(req.authGroup, req.globalSettings, req.body, req.user.sub || 'SYSTEM_ADMIN', req.customDomain);
return res.respond(say.created(result, RESOURCE));
} catch(error) {
ueEvents.emit(req.authGroup.id, 'ue.account.import.error', error);
next(error);
}
},
async writeAccount(req, res, next) {
try {
if (req.groupActivationEvent === true) return api.activateGroupWithAccount(req, res, next);
if (req.authGroup.active === false) throw Boom.forbidden('You can not add members to an inactive group');
if (!req.body.email) throw Boom.preconditionRequired('username is required');
if (req.body.generatePassword === true) {
req.body.password = cryptoRandomString({length: 32, type: 'url-safe'});
req.body.password = cryptoRandomString({length: 16, type: 'url-safe'});
}
if (!req.body.password) throw Boom.preconditionRequired('password is required');
const password = req.body.password;
Expand Down
3 changes: 3 additions & 0 deletions src/api/accounts/dal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export default {
const account = new Account(data);
return account.save();
},
async writeMany(accounts) {
return Account.insertMany(accounts, { ordered: false });
},
async getAccounts(g, query) {
query.query.authGroup = g;
return Account.find(query.query).select(query.projection).sort(query.sort).skip(query.skip).limit(query.limit);
Expand Down
36 changes: 20 additions & 16 deletions src/api/oidc/initialAccess/dal.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import IAT from '../models/initialAccessToken';

export default {
async updateAuthGroup(id, meta) {
const update = {
'payload.auth_group': meta.auth_group
};
if(meta.sub) update['payload.sub'] = meta.sub;
if(meta.email) update['payload.email'] = meta.email;
if(meta.uid) update['payload.uid'] = meta.uid;
async updateAuthGroup(id, meta) {
const update = {
'payload.auth_group': meta.auth_group
};
if(meta.sub) update['payload.sub'] = meta.sub;
if(meta.email) update['payload.email'] = meta.email;
if(meta.uid) update['payload.uid'] = meta.uid;

return IAT.findOneAndUpdate( { _id: id }, update, {new: true});
},
return IAT.findOneAndUpdate( { _id: id }, update, {new: true});
},

async getOne(id, authGroupId) {
return IAT.findOne({ _id: id, 'payload.auth_group': authGroupId });
},
async getOne(id, authGroupId) {
return IAT.findOne({ _id: id, 'payload.auth_group': authGroupId });
},

async deleteOne(id, authGroupId) {
return IAT.findOneAndRemove( { _id: id , 'payload.auth_group': authGroupId });
}
}
async deleteOne(id, authGroupId) {
return IAT.findOneAndRemove( { _id: id , 'payload.auth_group': authGroupId });
},

async insertMany(docs) {
return IAT.insertMany(docs, { ordered: false });
}
};
14 changes: 14 additions & 0 deletions src/api/oidc/initialAccess/iat.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import oidc from '../oidc';
import dal from './dal';

export default {
// todo - incomplete implementation for bulk user import
async generateManyIAT(expiresIn, policies, authGroup, users) {
if(!authGroup) throw new Error('authGroup not defined');
const setOfIATs = [];
await Promise.all((users.map(async(x) => {
const iat = new (oidc(authGroup).InitialAccessToken)({ expiresIn, policies });
iat.payload.auth_group = authGroup.id;
iat.payload.sub = x._id || x.id;
iat.payload.email = x.email;
setOfIATs.push(iat);
return x;
})));
return dal.insertMany(setOfIATs);
},
async generateIAT(expiresIn, policies, authGroup, meta = {}) {
if(!authGroup) throw new Error('authGroup not defined');
return new (oidc(authGroup).InitialAccessToken)({ expiresIn, policies }).save().then(async (x) => {
Expand Down
3 changes: 2 additions & 1 deletion src/events/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ const OP_EVENTS = {
'ue.account.create',
'ue.account.edit',
'ue.account.destroy',
'ue.account.error'
'ue.account.error',
'ue.account.import.error'
],
group: [
'ue.group.create',
Expand Down
9 changes: 9 additions & 0 deletions src/routes/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ router.post('/:group/account', [
m.permissions,
m.access('accounts')
], account.writeAccount);
router.post('/:group/accounts', [
m.validateAuthGroup,
m.schemaCheck,
m.isAuthenticated,
m.getGlobalPluginSettings,
m.organizationContext,
m.permissions,
m.access('accounts')
], account.importAccounts);
router.get('/:group/accounts', [
m.validateAuthGroup,
m.isAuthenticated,
Expand Down
11 changes: 8 additions & 3 deletions src/routes/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import m from '../middleware';

const router = express.Router();
const pJson = require('../../package.json');
const config = require('../config');

router.get('/', m.validateHostDomain, (req, res) => {
const date = new Date();
Expand All @@ -12,19 +13,23 @@ router.get('/', m.validateHostDomain, (req, res) => {
version: pJson.version,
by: `${req.authGroup.name} Platform`,
url: req.authGroup.primaryDomain,
company: req.authGroup.name,
year: date.getFullYear(),
home: pJson.homepage,
custom: true
custom: true,
production: (config.ENV === 'production')
});
}
return res.render('index', {
title: pJson.name, version: pJson.version,
description: pJson.description,
by: pJson.author,
url: pJson.person.url,
url: config.INIT_ROOT_PRIMARY_DOMAIN,
company: config.ROOT_COMPANY_NAME,
year: date.getFullYear(),
home: pJson.homepage,
custom: false
custom: false,
production: (config.ENV === 'production')
});
});

Expand Down
89 changes: 88 additions & 1 deletion swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ paths:
tags:
- Users
summary: Register a new user
description: Register a new user to an Auth Group independant of any organization or other access considerations. Self-registration is possible if the Auth Group has defined locked=false. Otherwise, an appropriately permissioned member of the Auth Group must create the user. Please note that the only data provided by this endpoint (or any Account API) is the id, username, and email (and creat/modify/active meta data) of the user within this AuthGroup. The Account record holds no personal information aside from this email address and an optional phone number which is only visible to that Account owner. It is possible to add personal data into the metadata field, but this is a discouraged action.
description: Register a new user to an Auth Group independant of any organization or other access considerations. Self-registration is possible if the Auth Group has defined locked=false. Otherwise, an appropriately permissioned member of the Auth Group must create the user. Please note that the only data provided by this endpoint (or any Account API) is the id, username, and email (and creat/modify/active meta data) of the user within this AuthGroup. The Account record holds no personal information aside from this email address and an optional phone number which is only visible to that Account owner; however, if you supply profile information, a secured profile record will be generated which the account holder can administrate later.
operationId: writeAccount
parameters:
- name: group
Expand Down Expand Up @@ -854,6 +854,69 @@ paths:
- bearer: []
- openId: []
/api/{group}/accounts:
post:
tags:
- Users
summary: Import users to the Auth Group
description: This API allows you to import an array of users to the Auth Group independant of any organization or other access considerations. This API cannot be used for self-registration. Passwords are automatically generated and all users must claim their accounts. No profile data can be added with this API.
operationId: importAccounts
parameters:
- name: group
in: path
description: the auth group ID associated to your business account
schema:
type: string
required: true
responses:
'201':
description: successful operation
content:
application/json:
schema:
properties:
type:
type: string
example: 'Accounts'
data:
type: object
properties:
attempted:
type: number
description: how many attempted
ok:
type: number
description: how many successfully written?
failed:
type: array
items:
type: object
description: which accounts failed and why
success:
type: array
items:
$ref: '#/components/schemas/getAccount'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'405':
$ref: '#/components/responses/InvalidInput'
'417':
$ref: '#/components/responses/ExpectationFailed'
security:
- bearer: [ ]
- openId: [ ]
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/importAccount'
description: Account data to be written
required: true
get:
tags:
- Users
Expand Down Expand Up @@ -7513,6 +7576,30 @@ components:
type: string
format: uri

importAccount:
type: object
additionalProperties: false
required:
- email
properties:
id:
type: string
format: uuid
description: if provided, the system will attempt to use this ID as long as it is unique.
username:
type: string
description: optional identifier, must be unique in the authGroup. If not provided, set to email.
email:
type: string
description: email address
phone:
type: object
properties:
txt:
type: string
pattern: '\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{1,14}$'
description: used for notifications. only visible to the user in question.

writeAccount:
type: object
additionalProperties: false
Expand Down
7 changes: 4 additions & 3 deletions views/index.pug
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ block content
a.btn.btn-outline-dark.m-r-5(href='/swagger') Swagger 
a.btn.btn-outline-dark.m-r-5(href='/api' style="margin: 5px;") Documentation 
if custom===false
a.btn.btn-outline-dark.m-r-5(href=url style="margin: 5px;") United Effects 
a.btn.btn-outline-dark.m-r-5(href=home style="margin: 5px;") Project 
a.btn.btn-outline-dark.m-r-5(href='https://github.com/UnitedEffects/ueauth' style="margin: 5px;") Code 
a.btn.btn-outline-dark.m-r-5(href=url style="margin: 5px;") #{company} 
if production!==true
a.btn.btn-outline-dark.m-r-5(href=home style="margin: 5px;") Project 
a.btn.btn-outline-dark.m-r-5(href='https://github.com/UnitedEffects/ueauth' style="margin: 5px;") Code 
else
a.btn.btn-outline-dark.m-r-5(href=url style="margin: 5px;") Home 
if custom===true
Expand Down
7 changes: 4 additions & 3 deletions views/login/loginWithMFA.pug
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ block loginWithMFA
div#mfa
div#mfa-try-again(style='visibility: hidden')
span#reconnect(style="font-size: 20px;").m-t-10 If you need to reconnect your device to your account, use the 'Restore MFA' link below. Otherwise click to retry login.
a.btn.btn-outline-dark.federated.m-t-10(href='/' + authGroup + '/interaction/' + uid)
i.fas.fa-undo
span.p-l-10 Try Again
div
a.btn.btn-outline-dark.federated.m-t-10(href='/' + authGroup + '/interaction/' + uid)
i.fas.fa-undo
span.p-l-10 Try Again

0 comments on commit 80a021c

Please sign in to comment.