Skip to content

Commit

Permalink
Merge pull request #466 from curveball/all-principal
Browse files Browse the repository at this point in the history
Adding a special group principal $all
  • Loading branch information
evert authored Sep 24, 2023
2 parents 6ce46ae + 798f507 commit 8f812a9
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 50 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ inspect: build

inspect-brk: build
node --inspect-brk dist/app.js

src/db-types.js:
./bin/generate-db-types.mjs
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Changelog
* Update to curveball 0.21
* Refactored the privilege system to make internally easier to use. There
should be no end-user effects to this.
* Added a system group principal , which allows admins to set privileges for
every user in the system.


0.23.1 (2023-03-29)
Expand Down
61 changes: 25 additions & 36 deletions src/db-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,26 @@ interface Tables {
export type ChangelogRecord = {
id: number;
timestamp: number | null;
}

};
export type GroupMembersRecord = {
user_id: number;
group_id: number;
}

};
export type Oauth2ClientsRecord = {
id: number;
client_id: string;
client_secret: string;
allowed_grant_types: string;
user_id: number;
require_pkce: number | null;
}

require_pkce: number;
};
export type Oauth2CodesRecord = {
id: number;
client_id: number;
code: string;
principal_id: number;
code_challenge: string | null;
code_challenge_method: 'plain' | 'S256' | null;
code_challenge: string | null;
created_at: number;
browser_session_id: string | null;

Expand All @@ -65,14 +62,12 @@ export type Oauth2CodesRecord = {
* OpenID Connect Nonce
*/
nonce: string | null;
}

};
export type Oauth2RedirectUrisRecord = {
id: number;
oauth2_client_id: number;
uri: string;
}

};
export type Oauth2TokensRecord = {
id: number;
oauth2_client_id: number;
Expand All @@ -85,16 +80,15 @@ export type Oauth2TokensRecord = {
browser_session_id: string | null;

/**
* 1=implicit, 2=client_credentials, 3=password, 4=authorization_code, 5=authorization_code with secret
* 1=implicit, 2=client_credentials, 3=password, 4=authorization_code, 5=authorization_code with secret,6=one-time-token
*/
grant_type: number | null;

/**
* OAuth2 scopes, comma separated
* OAuth2 scopes, space separated
*/
scope: string | null;
}

};
export type PrincipalsRecord = {
id: number;
identity: string;
Expand All @@ -104,30 +98,31 @@ export type PrincipalsRecord = {
active: number;

/**
* 1 = user, 2 = app
* 1 = user, 2 = app, 3 = group
*/
type: number;
modified_at: number;
}

/**
* System are built-in and cannot be deleted
*/
system: number;
};
export type PrivilegesRecord = {
privilege: string;
description: string;
}

};
export type ResetPasswordTokenRecord = {
id: number;
user_id: number;
token: string;
expires_at: number;
created_at: number;
}

};
export type ServerSettingsRecord = {
setting: string;
value: string | null;
}

};
export type UserAppPermissionsRecord = {
id: number;

Expand Down Expand Up @@ -160,8 +155,7 @@ export type UserAppPermissionsRecord = {
* Last time this application issued or refreshed an access token
*/
last_used_at: number | null;
}

};
export type UserLogRecord = {
id: number;
time: number;
Expand All @@ -170,36 +164,31 @@ export type UserLogRecord = {
ip: string;
user_agent: string | null;
country: string | null;
}

};
export type UserPasswordsRecord = {
user_id: number;
password: string;
}

};
export type UserPrivilegesRecord = {
id: number;
user_id: number;
resource: string;
privilege: string;
}

};
export type UserTotpRecord = {
user_id: number;
secret: string;
failures: number;
created: number;
}

};
export type UserWebauthnRecord = {
id: number;
user_id: number;
credential_id: string;
public_key: string;
counter: number;
created: number;
}

};


}
22 changes: 15 additions & 7 deletions src/group/controller/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,21 @@ class GroupController extends Controller {

const principalPrivileges = await privilegeService.get(group);

ctx.response.body = hal.item(
group,
principalPrivileges.getAll(),
isAdmin,
await groupService.findGroupsForPrincipal(group),
members,
);
if (group.system && group.externalId === '$all') {
ctx.response.body = hal.itemAllUsers(
group,
principalPrivileges.getAll(),
isAdmin,
);
} else {
ctx.response.body = hal.item(
group,
principalPrivileges.getAll(),
isAdmin,
await groupService.findGroupsForPrincipal(group),
members,
);
}

}

Expand Down
38 changes: 38 additions & 0 deletions src/group/formats/hal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function item(group: Group, privileges: PrivilegeMap, isAdmin: boolean, g
createdAt: group.createdAt,
modifiedAt: group.modifiedAt,
type: group.type,
system: group.system || undefined,
privileges
};

Expand All @@ -94,6 +95,43 @@ export function item(group: Group, privileges: PrivilegeMap, isAdmin: boolean, g

}

/**
* Generate a HAL response for the 'all users' group.
*
* This group is special as it contains a virtual representation of all users in the system.
* We don't want to allow modifying the members list of listing the members generally.
*/
export function itemAllUsers(group: Group, privileges: PrivilegeMap, isAdmin: boolean): HalResource {

const hal: HalResource = {
_links: {
'self': {href: group.href, title: group.nickname },
'me': { href: group.identity, title: group.nickname },
'up' : { href: '/group', title: 'List of groups' },
'describedby': {
href: 'https://curveballjs.org/schemas/a12nserver/group.json',
type: 'application/schema+json',
}
},
nickname: group.nickname,
createdAt: group.createdAt,
modifiedAt: group.modifiedAt,
type: group.type,
system: true,
privileges
};

if (isAdmin) {
hal._links['privileges'] = {
href: `${group.href}/edit/privileges`,
title: 'Change privilege policy',
};
}

return hal;

}

function addMemberForm(group: Group): HalFormsTemplate {

return {
Expand Down
35 changes: 35 additions & 0 deletions src/migrations/20230924194452_all_users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Knex } from 'knex';


export async function up(knex: Knex): Promise<void> {

await knex.schema.alterTable('principals', table => {

table
.tinyint('system')
.notNullable()
.defaultTo(0)
.comment('System principals are built-in and cannot be deleted');

});

await knex('principals').insert({
identity: 'urn:uuid:5793d1ce-d21e-4955-9091-c2b9debb9ec1',
nickname: 'All Users',
created_at: Date.now(),
active: 1,
type: 3, // Group
modified_at: Date.now(),
external_id: '$all',
system: 1,
});

}


export async function down(knex: Knex): Promise<void> {

throw new Error('Reverting this migration is not supported.');

}

1 change: 1 addition & 0 deletions src/oauth2/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export function generateTokenDeveloperToken(options: GenerateTokenDeveloperToken
type: 'app',
nickname: 'a12n-server system user',
active: true,
system: true,
}
};
return generateTokenInternal({
Expand Down
3 changes: 2 additions & 1 deletion src/principal/privileged-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export function recordToModel(user: PrincipalsRecord): Principal {
createdAt: new Date(user.created_at),
modifiedAt: new Date(user.modified_at),
type: userTypeIntToUserType(user.type),
active: !!user.active
active: !!user.active,
system: !!user.system,
};

}
12 changes: 8 additions & 4 deletions src/principal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const fieldNames: Array<keyof PrincipalsRecord> = [
'created_at',
'modified_at',
'type',
'active'
'active',
'system',
];

export async function findAll(type: 'user'): Promise<User[]>;
Expand Down Expand Up @@ -228,6 +229,7 @@ export async function save<T extends PrincipalType>(principal: BasePrincipal<T>|
active: principal.active ? 1 : 0,
modified_at: Date.now(),
created_at: Date.now(),
system: 0,
};

const result = await insertAndGetId('principals', newPrincipalsRecord);
Expand All @@ -236,7 +238,8 @@ export async function save<T extends PrincipalType>(principal: BasePrincipal<T>|
id: result,
href: `/${principal.type}/${externalId}`,
externalId,
...principal
system: false,
...principal,
};

} else {
Expand All @@ -249,7 +252,7 @@ export async function save<T extends PrincipalType>(principal: BasePrincipal<T>|

principal.modifiedAt = new Date();

const updatePrincipalsRecord: Omit<PrincipalsRecord, 'id' | 'created_at' | 'type' | 'external_id'> = {
const updatePrincipalsRecord: Omit<PrincipalsRecord, 'id' | 'created_at' | 'type' | 'external_id' | 'system'> = {
identity: principal.identity,
nickname: principal.nickname,
active: principal.active ? 1 : 0,
Expand Down Expand Up @@ -300,7 +303,8 @@ export function recordToModel(user: PrincipalsRecord): Principal {
createdAt: new Date(user.created_at),
modifiedAt: new Date(user.modified_at),
type: userTypeIntToUserType(user.type),
active: !!user.active
active: !!user.active,
system: !!user.system,
};

}
Expand Down
24 changes: 24 additions & 0 deletions src/privilege/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ async function getRecursiveGroupIds(principalId: number): Promise<number[]> {
async function getPrivilegesForPrincipal(principal: Principal): Promise<PrivilegeMap> {

const recursiveGroupIds = await getRecursiveGroupIds(principal.id);
if (principal.type === 'user') recursiveGroupIds.push(await getAllUsersGroupId());

const result = await query(
`SELECT resource, privilege FROM user_privileges WHERE user_id IN (${recursiveGroupIds.map(_ => '?').join(',')})`,
Expand All @@ -272,3 +273,26 @@ async function getPrivilegesForPrincipal(principal: Principal): Promise<Privileg

}

let allUsersGroupId: number|null = null;

/**
* Returns the set of privileges for the $all group
*/
async function getAllUsersGroupId() {

if (allUsersGroupId) return allUsersGroupId;
const allPrincipal = await db('principals')
.select('id')
.first()
.where({
external_id: '$all',
type: 3
});

if (!allPrincipal) {
throw new Error('Could not find the $all group in the database!');
}
allUsersGroupId = allPrincipal.id;
return allUsersGroupId;

}
Loading

0 comments on commit 8f812a9

Please sign in to comment.