Skip to content

Commit

Permalink
Merge pull request #521 from curveball/auto-gen-passowrd
Browse files Browse the repository at this point in the history
Auto-generate a 'diceware' password when creating users.
  • Loading branch information
evert authored Aug 24, 2024
2 parents 4628fdf + b9f0854 commit 58d90ca
Show file tree
Hide file tree
Showing 17 changed files with 428 additions and 152 deletions.
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

0.26.2 (????-??-??)
-------------------

* Allow admins to auto-generate an intitial 'diceware' password when creating
new users, which should make onboaring new users and testing easier.


0.26.1 (2024-08-12)
-------------------

Expand Down
345 changes: 222 additions & 123 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"csv-stringify": "^6.0.5",
"db-errors": "^0.2.3",
"dotenv": "^16.0.3",
"eff-diceware-passphrase": "^3.0.0",
"geoip-lite": "^1.0.10",
"handlebars": "^4.7.7",
"jose": "^5.1.0",
Expand All @@ -84,6 +85,7 @@
"@types/bcrypt": "^5.0.0",
"@types/chai": "^4.2.15",
"@types/chai-as-promised": "^7.1.3",
"@types/eff-diceware-passphrase": "^3.0.2",
"@types/geoip-lite": "^1.4.1",
"@types/node": "^18.19.39",
"@types/nodemailer": "^6.4.1",
Expand Down
6 changes: 5 additions & 1 deletion schemas/user-new-form.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
},
"markEmailValid": {
"type": "string",
"description": "Automatically mark email address as validated. Shoudl be Javacript thruthy string."
"description": "Automatically mark email address as validated. Should be Javacript thruthy string."
},
"autoGeneratePassword": {
"type": "string",
"description": "Request a password to be generated. Should be a Javascript thruthy string."
}
}
}
44 changes: 44 additions & 0 deletions schemas/user-new-result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://curveballjs.org/schemas/a12nserver/user-new-result.json",
"type": "object",
"title": "UserNewResult",
"description": "After a user is created, this object will be returned to the client. It's mostly similar to the regular \"user\" object, with some modifications.",

"required": ["nickname", "active", "createdAt", "modifiedAt", "type"],
"additionalProperties": false,

"properties": {
"_links": {
"description": "HAL Links"
},
"nickname": {
"type": "string",
"minLength": 3,
"description": "Human-readable displayname for the user."
},
"active": {
"type": "boolean",
"description": "If false, the user will not be able to log in."
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Creation date and time."
},
"modifiedAt": {
"type": "string",
"format": "date-time",
"description": "Last time the user was modified."
},
"type": {
"const": "user",
"description": "May be 'user', 'app' or 'group'"
},
"password": {
"type": "string",
"description": "Password for the newly created user. This password will only be displayed this one time. You are recommended to change the password immediately after."
}

}
}
48 changes: 47 additions & 1 deletion src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,55 @@ export interface UserNewFormBody {
nickname: string;
email: string | "";
/**
* Automatically mark email address as validated. Shoudl be Javacript thruthy string.
* Automatically mark email address as validated. Should be Javacript thruthy string.
*/
markEmailValid: string;
/**
* Request a password to be generated. Should be a Javascript thruthy string.
*/
autoGeneratePassword?: string;
}
/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/

/**
* After a user is created, this object will be returned to the client. It's mostly similar to the regular "user" object, with some modifications.
*/
export interface UserNewResult {
/**
* HAL Links
*/
_links?: {
[k: string]: unknown;
};
/**
* Human-readable displayname for the user.
*/
nickname: string;
/**
* If false, the user will not be able to log in.
*/
active: boolean;
/**
* Creation date and time.
*/
createdAt: string;
/**
* Last time the user was modified.
*/
modifiedAt: string;
/**
* May be 'user', 'app' or 'group'
*/
type: "user";
/**
* Password for the newly created user. This password will only be displayed this one time. You are recommended to change the password immediately after.
*/
password?: string;
}
/* eslint-disable */
/**
Expand Down
2 changes: 1 addition & 1 deletion src/app/controller/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class AppCollectionController extends Controller {
if (identity) {
await services.principalIdentity.create({
uri: identity.href,
principalId: app.id,
principal: app,
isPrimary: true,
label: identity.title ?? null,
markVerified: false,
Expand Down
2 changes: 1 addition & 1 deletion src/app/controller/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class CreateAppController extends Controller {
if (identity) {
await services.principalIdentity.create({
uri: identity,
principalId: newApp.id,
principal: newApp,
isPrimary: true,
label: null,
markVerified: false,
Expand Down
12 changes: 12 additions & 0 deletions src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as crypto from 'node:crypto';
import generatePassphrase from 'eff-diceware-passphrase';

/**
* Centralized place for all crypto-related functions
Expand Down Expand Up @@ -38,6 +39,17 @@ export function uuidUrn() {

}

/**
* Generates a random 'diceware' password, which is a memorable password
* consisting of several words.
*/
export function generatePassword(): string {

return generatePassphrase.entropy(45).join('-');

}


function generateUrlSafeString(bytes: number): Promise<string> {

return new Promise<string>((res, rej) => {
Expand Down
39 changes: 25 additions & 14 deletions src/principal-identity/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Principal, PrincipalIdentity, NewPrincipalIdentity } from '../types.js';
import knex from '../database.js';
import knex, { insertAndGetId } from '../database.js';
import { PrincipalIdentitiesRecord } from 'knex/types/tables.js';
import { NotFound } from '@curveball/http-errors';
import { generatePublicId } from '../crypto.js';
Expand Down Expand Up @@ -75,19 +75,30 @@ export async function findByUri(arg1: Principal|string, arg2?:string): Promise<P

}

export async function create(identity: NewPrincipalIdentity): Promise<void> {
export async function create(identity: NewPrincipalIdentity): Promise<PrincipalIdentity> {

await knex('principal_identities')
.insert({
uri: identity.uri,
external_id: await generatePublicId(),
principal_id: identity.principalId,
label: identity.label ?? null,
is_primary: identity.isPrimary ? 1 : 0,
verified_at: identity.markVerified ? Date.now() : null,
created_at: Date.now(),
modified_at: Date.now(),
});
const externalId = await generatePublicId();

const id = await insertAndGetId('principal_identities', {
uri: identity.uri,
external_id: externalId,
principal_id: identity.principal.id,
label: identity.label ?? null,
is_primary: identity.isPrimary ? 1 : 0,
verified_at: identity.markVerified ? Date.now() : null,
created_at: Date.now(),
modified_at: Date.now(),
});

return {
id,
href: `${identity.principal.href}/identity/${externalId}`,
externalId,
...identity,
verifiedAt: new Date(),
createdAt: new Date(),
modifiedAt: new Date(),
};

}
export async function markVerified(identity: PrincipalIdentity): Promise<void> {
Expand All @@ -108,7 +119,7 @@ function recordToModel(principal: Principal, record: PrincipalIdentitiesRecord):
return {
id: record.id,
uri: record.uri,
principalId: record.principal_id,
principal,
href: `${principal.href}/identity/${record.external_id}`,
externalId: record.external_id,
label: record.label,
Expand Down
2 changes: 1 addition & 1 deletion src/principal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class PrincipalService {
pi = identity;
}

return this.findById(pi.principalId);
return this.findById(pi.principal.id);

}

Expand Down
2 changes: 1 addition & 1 deletion src/register/controller/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class UserRegistrationController extends Controller {
await services.principalIdentity.create(
{
uri: 'mailto:' + body.emailAddress,
principalId: user.id,
principal: user,
label: null,
isPrimary: true,
// If this was the first run, we assume the email is verified
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export type PrincipalIdentity = {
/**
* Associated principal
*/
principalId: number;
principal: Principal;

/**
* If this is the 'main' ID for a user, this is set to true.
Expand Down
2 changes: 1 addition & 1 deletion src/user/controller/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class UserCollectionController extends Controller {
}

await services.principalIdentity.create({
principalId: user.id,
principal: user,
isPrimary: true,
uri: identity,
label: null,
Expand Down
20 changes: 16 additions & 4 deletions src/user/controller/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { NotFound } from '@curveball/http-errors';
import { createUserForm } from '../formats/html.js';
import * as services from '../../services.js';
import { UserNewFormBody } from '../../api-types.js';
import { generatePassword } from '../../crypto.js';
import { newUserResult } from '../formats/hal.js';

class CreateUserController extends Controller {

Expand Down Expand Up @@ -49,20 +51,30 @@ class CreateUserController extends Controller {
active: true,
});

let identityModel = null;
if (identity) {
await services.principalIdentity.create(
identityModel = await services.principalIdentity.create(
{
uri: 'mailto:' + ctx.request.body.email,
principalId: newUser.id,
principal: newUser,
isPrimary: true,
label: null,
markVerified: !!ctx.request.body.markEmailValid,
}
);
}

ctx.response.status = 303;
ctx.response.headers.set('Location', newUser.href);
let password = null;
if (ctx.request.body.autoGeneratePassword) {
password = generatePassword();
await services.user.createPassword(
newUser,
password,
);
}

ctx.response.status = 201;
ctx.response.body = newUserResult(newUser, password, identityModel ? [identityModel] : []);

}

Expand Down
38 changes: 36 additions & 2 deletions src/user/formats/hal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PrivilegeMap } from '../../privilege/types.js';
import { Principal, Group, User, PrincipalIdentity } from '../../types.js';
import { HalResource } from 'hal-types';
import { LazyPrivilegeBox } from '../../privilege/service.js';
import { UserNewResult } from '../../api-types.js';

export function collection(users: User[]): HalResource {

Expand Down Expand Up @@ -55,8 +56,8 @@ export function item(user: User, privileges: PrivilegeMap, hasControl: boolean,
},
nickname: user.nickname,
active: user.active,
createdAt: user.createdAt,
modifiedAt: user.modifiedAt,
createdAt: user.createdAt.toISOString(),
modifiedAt: user.modifiedAt.toISOString(),
type: user.type,
privileges
};
Expand Down Expand Up @@ -219,3 +220,36 @@ export function editPrivileges(principal: Principal, userPrivileges: PrivilegeMa
},
};
}


/**
* Generate a HAL response after new user creation.
*
* hasControl should only be true if the *current* user is same as the user
* we're generating the repsonse for, or if the current authenticated user
* has full admin privileges
*/
export function newUserResult(user: User, password: string|null, identities: PrincipalIdentity[]): HalResource<UserNewResult> {

const hal: HalResource<UserNewResult> = {
_links: {
'self': {href: user.href, title: user.nickname },
'me': identities.map( identity => (
{ href: identity.uri, title: user.nickname ?? undefined }
)),
'describedby': {
href: 'https://curveballjs.org/schemas/a12nserver/user-new-result.json',
type: 'application/schema+json',
}
},
nickname: user.nickname,
active: user.active,
password: password ?? undefined,
createdAt: user.createdAt.toISOString(),
modifiedAt: user.modifiedAt.toISOString(),
type: user.type
};

return hal;

}
7 changes: 6 additions & 1 deletion templates/create-user.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@

<label class="checkbox">
<span>Mark email as valididated.</span>
<input type="checkbox" name="markEmailValid" />
<input type="checkbox" name="markEmailValid" checked />
</label>

<label class="checkbox">
<span>Auto-generate random password</span>
<input type="checkbox" name="autoGeneratePassword" />
</label>
</fieldset>

Expand Down

0 comments on commit 58d90ca

Please sign in to comment.