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

Adding a special group principal $all #466

Merged
merged 4 commits into from
Sep 24, 2023
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
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
Loading