0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor(schemas): sentinel first version

This commit is contained in:
Gao Sun 2023-09-18 16:31:23 +08:00
parent d0a44e93f8
commit d065cbc623
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
5 changed files with 136 additions and 25 deletions

View file

@ -0,0 +1,79 @@
import { type CommonQueryMethods, sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const getDatabaseName = async (pool: CommonQueryMethods) => {
const { currentDatabase } = await pool.one<{ currentDatabase: string }>(sql`
select current_database();
`);
return currentDatabase.replaceAll('-', '_');
};
const alteration: AlterationScript = {
up: async (pool) => {
const database = await getDatabaseName(pool);
const baseRoleId = sql.identifier([`logto_tenant_${database}`]);
await pool.query(sql`
create type sentinel_action_result as enum ('Success', 'Failed');
create type sentinel_decision as enum ('Undecided', 'Allowed', 'Blocked', 'Challenge');
create table sentinel_activities (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
/** The target that the action was performed on. */
target_type varchar(32) /* @use SentinelActivityTargetType */ not null,
/** The target hashed identifier. */
target_hash varchar(64) not null,
/** The action name that was performed. */
action varchar(64) /* @use SentinelActivityAction */ not null,
/** If the action was successful or not. */
action_result sentinel_action_result not null,
/** Additional payload data if any. */
payload jsonb /* @use SentinelActivityPayload */ not null,
/** The sentinel decision for the action. */
decision sentinel_decision not null,
/** The expiry date of the decision. */
decision_expires_at timestamptz not null default(now()),
/** The time the activity was created. */
created_at timestamptz not null default(now()),
primary key (id)
);
create index sentinel_activities__id
on sentinel_activities (tenant_id, id);
create index sentinel_activities__target_type_target_hash_action_action_result_decision
on sentinel_activities (tenant_id, target_type, target_hash, action, action_result, decision);
create trigger set_tenant_id before insert on sentinel_activities
for each row execute procedure set_tenant_id();
alter table sentinel_activities enable row level security;
create policy sentinel_activities_tenant_id on sentinel_activities
as restrictive
using (tenant_id = (select id from tenants where db_user = current_user));
create policy sentinel_activities_modification on sentinel_activities
using (true);
grant select, insert, update, delete on sentinel_activities to ${baseRoleId};
`);
},
down: async (pool) => {
await pool.query(sql`
drop policy sentinel_activities_tenant_id on sentinel_activities;
drop policy sentinel_activities_modification on sentinel_activities;
drop table sentinel_activities;
drop type sentinel_action_result;
drop type sentinel_decision;
`);
},
};
export default alteration;

View file

@ -339,14 +339,6 @@ export const domainStatusGuard = z.nativeEnum(DomainStatus);
/* === Sentinel activities === */
/** The subject (actor) type of a sentinel activity. */
export enum SentinelActivitySubjectType {
User = 'User',
App = 'App',
Sentinel = 'Sentinel',
}
export const sentinelActivitySubjectTypeGuard = z.nativeEnum(SentinelActivitySubjectType);
/** The action target type of a sentinel activity. */
export enum SentinelActivityTargetType {
User = 'User',
@ -357,19 +349,19 @@ export const sentinelActivityTargetTypeGuard = z.nativeEnum(SentinelActivityTarg
/** The action type of a sentinel activity. */
export enum SentinelActivityAction {
/**
* The subject tries to pass a verification for a target.
* The subject tries to pass a verification by inputting a password.
*
* For example, a user (subject) who inputted a verification code or password for themselves
* For example, a user (subject) who inputted a password (action) to authenticate themselves
* (target).
*/
Verification = 'Verification',
Password = 'Password',
/**
* The subject tries to block the target from passing a verification.
* The subject tries to pass a verification by inputting a verification code.
*
* For example, the sentinel (subject) who blocked a user (target) from passing a verification
* for 10 minutes.
* For example, a user (subject) who inputted a verification code (action) to authenticate
* themselves (target).
*/
BlockVerification = 'BlockVerification',
VerificationCode = 'VerificationCode',
}
export const sentinelActivityActionGuard = z.nativeEnum(SentinelActivityAction);

View file

@ -18,3 +18,4 @@ export * from './theme.js';
export * from './cookie.js';
export * from './dashboard.js';
export * from './domain.js';
export * from './sentinel.js';

View file

@ -0,0 +1,32 @@
import { type SentinelDecision, type SentinelActivity } from '../db-entries/index.js';
/** The activity payload to be sent to the sentinel. */
export type ActivityReport = Pick<
SentinelActivity,
'targetType' | 'targetHash' | 'action' | 'actionResult' | 'payload'
>;
/**
* The sentinel class interface.
*
* Sentinels are responsible for accepting activity reports and making decisions based on them.
*
* For example, for a user sign-in activity, the sentinel might decide to:
*
* - Accept since the user uses the same device and location as usual;
* - Require a MFA code since the user uses a new device or location;
* - Block the user since the user tried to sign-in too many times with an incorrect password.
*
* The implementation should be privacy-aware and not store any personal identifiable information.
*/
export abstract class Sentinel {
/**
* Report an activity to the sentinel. The sentinel should make a decision based on the activity
* (also the history, if needed) and return the result.
*
* @param activity The activity data to be reported.
* @returns A Promise that resolves to the sentinel decision.
* @see {@link SentinelDecision}
*/
abstract reportActivity(activity: ActivityReport): Promise<SentinelDecision>;
}

View file

@ -1,25 +1,32 @@
create type sentinel_activity_result as enum ('Success', 'Failed');
create type sentinel_action_result as enum ('Success', 'Failed');
create type sentinel_decision as enum ('Undecided', 'Allowed', 'Blocked', 'Challenge');
create table sentinel_activities (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
/** The subject (actor) that performed the action. */
subject_type varchar(32) /* @use SentinelActivitySubjectType */ not null,
/** The target that the action was performed on. */
target_type varchar(32) /* @use SentinelActivityTargetType */ not null,
/** The target identifier. */
target_id varchar(21) not null
references users (id) on update cascade on delete cascade,
/** The related log id if any. */
log_id varchar(21)
references logs (id) on update cascade on delete cascade,
/** The target hashed identifier. */
target_hash varchar(64) not null,
/** The action name that was performed. */
action varchar(64) /* @use SentinelActivityAction */ not null,
/** If the action was successful or not. */
result sentinel_activity_result not null,
action_result sentinel_action_result not null,
/** Additional payload data if any. */
payload jsonb /* @use SentinelActivityPayload */ not null,
/** The sentinel decision for the action. */
decision sentinel_decision not null,
/** The expiry date of the decision. */
decision_expires_at timestamptz not null default(now()),
/** The time the activity was created. */
created_at timestamptz not null default(now()),
primary key (id)
);
create index sentinel_activities__id
on sentinel_activities (tenant_id, id);
create index sentinel_activities__target_type_target_hash_action_action_result_decision
on sentinel_activities (tenant_id, target_type, target_hash, action, action_result, decision);