bazis 0.128.0 → 0.130.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/core/access/Access.module.ts +36 -0
  2. package/core/access/catalog/AccessCatalog.ts +73 -0
  3. package/core/access/catalog/IAccessCatalog.service.ts +17 -0
  4. package/core/access/catalog/moduleFields.ts +153 -0
  5. package/core/access/catalog/permissions.ts +127 -0
  6. package/core/access/check/Access.ts +191 -0
  7. package/core/access/check/IAccess.service.ts +20 -0
  8. package/core/access/check/accessConfig.ts +18 -0
  9. package/core/access/check/can.ts +21 -0
  10. package/core/access/check/conditions.ts +132 -0
  11. package/core/access/check/filter.ts +132 -0
  12. package/core/access/contracts/AccessPolicy.ts +29 -0
  13. package/core/access/contracts/AttributeLookup.ts +17 -0
  14. package/core/access/contracts/References.ts +33 -0
  15. package/core/access/contracts/errors.ts +13 -0
  16. package/core/access/contracts/types.ts +66 -0
  17. package/core/access/generated/deps.ts +14 -0
  18. package/core/access/http/AccessCatalog.controller.ts +51 -0
  19. package/core/access/index.ts +35 -0
  20. package/core/access/roles/RolesAccessPolicy.ts +64 -0
  21. package/core/access/roles/roles.ts +62 -0
  22. package/core/access/testing/index.ts +54 -0
  23. package/core/background/BackgroundService.ts +1 -1
  24. package/core/di/README.md +5 -5
  25. package/core/di/errors/ClassDependenciesMismatchError.ts +1 -1
  26. package/core/di/internal/GraphValidator.ts +1 -1
  27. package/core/di/module/autoDeps.ts +11 -1
  28. package/core/di/module/createContainer.ts +1 -1
  29. package/core/di/module/providerSelection.ts +1 -1
  30. package/core/grpc/{GrpcClient.ts → GrpcClientBase.ts} +1 -1
  31. package/core/grpc/grpcClients.ts +79 -0
  32. package/core/grpc/index.ts +2 -2
  33. package/core/http/options.ts +1 -1
  34. package/core/http-client/httpClients.ts +82 -0
  35. package/core/http-client/index.ts +11 -91
  36. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  37. package/core/scripts/di-generate-target.ts +9 -0
  38. package/core/scripts/di-generate.ts +177 -9
  39. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +15 -11
  40. package/core/workflow/definitions_module/access/Definitions.permissions.ts +23 -0
  41. package/core/workflow/definitions_module/access/Process.references.ts +27 -0
  42. package/core/workflow/definitions_module/code/CodeProcessCatalog.ts +4 -3
  43. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +2 -2
  44. package/core/workflow/definitions_module/http/Processes.controller.ts +23 -14
  45. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +9 -0
  46. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +4 -0
  47. package/core/workflow/generated/deps.ts +56 -0
  48. package/core/workflow/index.ts +1 -1
  49. package/core/workflow/internal/WorkflowTools.ts +2 -2
  50. package/core/workflow/internal/http.ts +18 -17
  51. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +17 -34
  52. package/core/workflow/runtime_module/access/Runtime.permissions.ts +28 -0
  53. package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
  54. package/core/workflow/runtime_module/background/WorkflowWorker.ts +1 -10
  55. package/core/workflow/runtime_module/config/Runtime.config.ts +11 -1
  56. package/core/workflow/runtime_module/http/Events.controller.ts +5 -3
  57. package/core/workflow/runtime_module/http/Instances.controller.ts +25 -15
  58. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +4 -3
  59. package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +3 -0
  60. package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +16 -5
  61. package/core/workflow/tasks_module/WorkflowTasks.module.ts +8 -10
  62. package/core/workflow/tasks_module/access/Tasks.permissions.ts +13 -0
  63. package/core/workflow/tasks_module/events/TaskEvents.ts +29 -8
  64. package/core/workflow/tasks_module/http/Tasks.controller.ts +21 -12
  65. package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +8 -3
  66. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +41 -42
  67. package/core/workflow/testing/index.ts +1 -0
  68. package/index.ts +1 -1
  69. package/library/http-client/{HttpClient.ts → HttpClientBase.ts} +5 -5
  70. package/library/http-client/HttpClientFactory.ts +5 -5
  71. package/library/http-client/index.ts +3 -3
  72. package/library/terminal/messages.ts +2 -2
  73. package/package.json +3 -1
  74. package/core/grpc/grpcClientProvider.ts +0 -9
  75. package/core/workflow/access/WorkflowAccess.ts +0 -52
@@ -4,6 +4,7 @@
4
4
  * extension points. The document format is `bazis/library/workflow`.
5
5
  */
6
6
  import type { CodeProcess } from "./definitions_module/code/defineProcess";
7
+ import "./generated/deps";
7
8
 
8
9
  export { WorkflowModule } from "./Workflow.module";
9
10
  export { defineProcess, type CodeProcess, type ProcessDefinition } from "./definitions_module/code/defineProcess";
@@ -49,7 +50,6 @@ export { workflowRuntimeConfig } from "./runtime_module/config/Runtime.config";
49
50
  export type { HistoryType } from "./runtime_module/model/WfHistory.model";
50
51
  export type { InstanceStatus, StartedVia } from "./runtime_module/model/WfInstance.model";
51
52
  export type { TokenStatus } from "./runtime_module/model/WfToken.model";
52
- export { WorkflowAccess, DenyWorkflowAccess, type WorkflowAccessTarget, type WorkflowAction } from "./access/WorkflowAccess";
53
53
  export { WorkflowInputValidator, DefaultWorkflowInputValidator, type WorkflowInputContext } from "./contracts/WorkflowInputValidator";
54
54
  export { WorkflowError, type WorkflowErrorCode } from "./contracts/WorkflowError";
55
55
  export type { WorkflowActor, WorkflowPage, WorkflowPaging } from "./contracts/types";
@@ -1,4 +1,4 @@
1
- import type { ServiceResolver } from "../../di";
1
+ import type { ServiceProvider } from "../../di";
2
2
  import { ToolRunner, type AgentToolContract, type BoundToolRunner } from "../../agent";
3
3
 
4
4
  /** The workflow caller id of TOOLS-001: processes run only Tools whose `callers` list it. */
@@ -11,7 +11,7 @@ export const WORKFLOW_CALLER = "workflow";
11
11
  export class WorkflowTools {
12
12
  private readonly runner: BoundToolRunner | undefined;
13
13
 
14
- constructor(services: ServiceResolver) {
14
+ constructor(services: ServiceProvider) {
15
15
  this.runner = services.tryResolve(ToolRunner)?.as(WORKFLOW_CALLER);
16
16
  }
17
17
 
@@ -1,25 +1,26 @@
1
- import { ForbiddenError, UnauthorizedError, type HttpContext } from "../../http";
2
- import { WorkflowAccess, type WorkflowAccessTarget, type WorkflowAction } from "../access/WorkflowAccess";
1
+ import { IAccess, type AccessObject, type SubjectAccess } from "../../access";
2
+ import { ForbiddenError, type HttpContext } from "../../http";
3
3
  import type { WorkflowActor } from "../contracts/types";
4
4
  import { WorkflowError } from "../contracts/WorkflowError";
5
5
 
6
- /**
7
- * Asks the application's {@link WorkflowAccess} who calls and whether the action
8
- * is allowed. Without a registered access every route answers 403.
9
- */
10
- export async function authorize(ctx: HttpContext, action: WorkflowAction, target: WorkflowAccessTarget = {}): Promise<WorkflowActor> {
11
- const access = ctx.services.tryResolve(WorkflowAccess);
12
- if (access === undefined) throw new ForbiddenError("Workflow HTTP is closed: register a WorkflowAccess implementation");
13
- const actor = await access.actor(ctx);
14
- if (actor === undefined) throw new UnauthorizedError();
15
- if (!await access.can(ctx, action, target)) throw new ForbiddenError();
16
- return actor;
6
+ /** Who calls a route: their rights and how they are recorded as an actor. */
7
+ export interface WorkflowCaller {
8
+ readonly access: SubjectAccess;
9
+ readonly actor: WorkflowActor;
17
10
  }
18
11
 
19
- /** Whom the caller's tasks are addressed to, from the application's {@link WorkflowAccess}. */
20
- export async function assigneesOf(ctx: HttpContext, actor: WorkflowActor): Promise<readonly string[]> {
21
- const access = ctx.services.tryResolve(WorkflowAccess);
22
- return access === undefined ? [] : await access.assignees(ctx, actor);
12
+ /**
13
+ * The caller of a route; 401 without a subject. With `permission` and `object`
14
+ * also checks the object the route's `can` could not know: 403 without the
15
+ * right. The actor's id is the first owner id, so that `{ own: true }` means
16
+ * the instances the subject started.
17
+ */
18
+ export async function callerOf(ctx: HttpContext, permission?: string, object?: AccessObject): Promise<WorkflowCaller> {
19
+ const access = await ctx.services.resolve(IAccess).for(ctx);
20
+ if (permission !== undefined && !await access.can(permission, object)) throw new ForbiddenError();
21
+ const sub = access.subject.sub;
22
+ const [id = sub.slice(sub.lastIndexOf(":") + 1)] = await access.ownerIds();
23
+ return { access, actor: { kind: "user", id } };
23
24
  }
24
25
 
25
26
  /** A route parameter as a string. */
@@ -1,10 +1,8 @@
1
- import { DI, Module, SERVICE_PROVIDER, scoped } from "../../di";
2
- import { EventBus, LOGGER } from "../../kernel";
3
- import { WorkflowClock } from "../contracts/WorkflowClock";
1
+ import { AccessModule } from "../../access";
2
+ import { Module, scoped, singleton } from "../../di";
4
3
  import { DefaultWorkflowInputValidator, WorkflowInputValidator } from "../contracts/WorkflowInputValidator";
5
4
  import { WorkflowDefinitionsModule } from "../definitions_module/WorkflowDefinitions.module";
6
- import { IWorkflowDefinitions } from "../definitions_module/services/IWorkflowDefinitions.service";
7
- import { WorkflowTools } from "../internal/WorkflowTools";
5
+ import { runtimePermissions } from "./access/Runtime.permissions";
8
6
  import { WorkflowRetention } from "./background/WorkflowRetention";
9
7
  import { WorkflowWorker } from "./background/WorkflowWorker";
10
8
  import { workflowRuntimeConfig } from "./config/Runtime.config";
@@ -21,40 +19,25 @@ import { WorkflowRuntime } from "./services/WorkflowRuntime.service";
21
19
 
22
20
  /**
23
21
  * Runs published versions: instances, tokens, jobs, history, the worker and
24
- * retention ([passport](MODULE.md)). Dependencies are explicit: the framework's
25
- * classes are not covered by the application's codegen.
22
+ * retention ([passport](MODULE.md)). The application replaces the input
23
+ * validator by registering its own `WorkflowInputValidator`.
26
24
  */
27
25
  @Module({
28
- imports: [WorkflowDefinitionsModule],
26
+ imports: [AccessModule, WorkflowDefinitionsModule],
27
+ permissions: runtimePermissions,
29
28
  config: [workflowRuntimeConfig],
30
29
  ormBazis: { context: RuntimeDbContext, entities: [...RUNTIME_ENTITIES], ownedStore: runtimeStore, registerRepositories: false },
31
30
  providers: [
32
- DI.singleton(DI.factoryProvider(GraphCache, [], () => new GraphCache())),
33
- DI.scoped(DI.factoryProviderWithResolver(WorkflowEngine, [], (resolver) => new WorkflowEngine(
34
- resolver.resolve(RuntimeDbContext),
35
- resolver.resolve(IWorkflowDefinitions),
36
- resolver.resolve(GraphCache),
37
- resolver.resolve(EventBus),
38
- resolver,
39
- resolver.tryResolve(LOGGER),
40
- resolver.resolve(WorkflowClock),
41
- ))),
42
- scoped(ToolStep, ToolStep, [RuntimeDbContext, WorkflowEngine, WorkflowTools] as const),
43
- scoped(TimerStep, TimerStep, [RuntimeDbContext, WorkflowEngine] as const),
44
- scoped(Schedules, Schedules, [RuntimeDbContext, WorkflowEngine, IWorkflowDefinitions] as const),
45
- // The application's input validator, if it registered one anywhere; the built-in one otherwise.
46
- DI.scoped(DI.factoryProviderWithResolver(IWorkflowRuntime, [], (resolver) => new WorkflowRuntime(
47
- resolver.resolve(RuntimeDbContext),
48
- resolver.resolve(WorkflowEngine),
49
- resolver.resolve(IWorkflowDefinitions),
50
- resolver.resolve(SERVICE_PROVIDER).tryResolve(WorkflowInputValidator) ?? new DefaultWorkflowInputValidator(),
51
- ))),
31
+ singleton(GraphCache),
32
+ scoped(WorkflowEngine),
33
+ scoped(ToolStep),
34
+ scoped(TimerStep),
35
+ scoped(Schedules),
36
+ singleton(WorkflowInputValidator, DefaultWorkflowInputValidator),
37
+ scoped(IWorkflowRuntime, WorkflowRuntime),
52
38
  ],
53
- controllers: [DI.bindDeps(InstancesController, IWorkflowRuntime), DI.bindDeps(EventsController, IWorkflowRuntime)],
54
- background: [
55
- DI.bindDeps(WorkflowWorker, SERVICE_PROVIDER, workflowRuntimeConfig.token),
56
- DI.bindDeps(WorkflowRetention, SERVICE_PROVIDER, workflowRuntimeConfig.token),
57
- ],
58
- exports: [IWorkflowRuntime],
39
+ controllers: [InstancesController, EventsController],
40
+ background: [WorkflowWorker, WorkflowRetention],
41
+ exports: [IWorkflowRuntime, WorkflowInputValidator],
59
42
  })
60
43
  export class WorkflowRuntimeModule {}
@@ -0,0 +1,28 @@
1
+ import { definePermissions } from "../../../access";
2
+
3
+ const process = { ref: "workflow.process", column: "processKey" };
4
+
5
+ /** The rights of running processes (WORKFLOW-002 §6.2). */
6
+ export const runtimePermissions = definePermissions({
7
+ "workflow.instances.start": {
8
+ title: { en: "Start processes", ru: "Запускать процессы" },
9
+ scope: "request",
10
+ on: { process },
11
+ },
12
+ "workflow.instances.read": {
13
+ title: { en: "See instances", ru: "Видеть экземпляры" },
14
+ scope: { column: "partition" },
15
+ on: { process },
16
+ owner: "startedById",
17
+ },
18
+ "workflow.instances.manage": {
19
+ title: { en: "Cancel and retry instances", ru: "Отменять и повторять экземпляры" },
20
+ scope: { column: "partition" },
21
+ on: { process },
22
+ },
23
+ "workflow.events.emit": {
24
+ title: { en: "Send events to processes", ru: "Отправлять события процессам" },
25
+ scope: "request",
26
+ on: { event: { type: "string" } },
27
+ },
28
+ });
@@ -5,7 +5,7 @@ import { WorkflowClock } from "../../contracts/WorkflowClock";
5
5
  import { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
6
6
  import { WORKFLOW_INSTANCES_DELETED } from "../events/Runtime.events";
7
7
  import { RuntimeDbContext } from "../model/Runtime.dbContext";
8
- import type { WorkflowSettings } from "./WorkflowWorker";
8
+ import type { WorkflowSettings } from "../config/Runtime.config";
9
9
 
10
10
  const DAY_MS = 86_400_000;
11
11
 
@@ -2,7 +2,7 @@ import { BackgroundService, delay } from "../../../background";
2
2
  import type { ServiceProvider } from "../../../di";
3
3
  import { WorkflowClock } from "../../contracts/WorkflowClock";
4
4
  import type { ConfigView } from "../../../kernel";
5
- import type { workflowRuntimeConfig } from "../config/Runtime.config";
5
+ import type { WorkflowSettings } from "../config/Runtime.config";
6
6
  import { WorkflowJobs } from "../internal/WorkflowJobs";
7
7
 
8
8
  /** How often the scheduled starts are matched against the published versions. */
@@ -51,12 +51,3 @@ export class WorkflowWorker extends BackgroundService {
51
51
  }
52
52
  }
53
53
 
54
- /** The resolved settings of {@link workflowRuntimeConfig}. */
55
- export interface WorkflowSettings {
56
- readonly "worker.enabled": boolean;
57
- readonly "worker.intervalMs": number;
58
- readonly "worker.batchSize": number;
59
- readonly "retention.enabled": boolean;
60
- readonly "retention.intervalMs": number;
61
- readonly "retention.batchSize": number;
62
- }
@@ -5,7 +5,17 @@ import { defineConfig } from "../../../kernel";
5
5
  * application configuration: `BAZIS_WORKFLOW__WORKER__ENABLED=false` keeps an
6
6
  * instance from running jobs, for example a web-only one.
7
7
  */
8
- export const workflowRuntimeConfig = defineConfig("workflow", {
8
+ /** The settings of the workflow background services, `workflow.*` in the configuration. */
9
+ export interface WorkflowSettings {
10
+ readonly "worker.enabled": boolean;
11
+ readonly "worker.intervalMs": number;
12
+ readonly "worker.batchSize": number;
13
+ readonly "retention.enabled": boolean;
14
+ readonly "retention.intervalMs": number;
15
+ readonly "retention.batchSize": number;
16
+ }
17
+
18
+ export const workflowRuntimeConfig = defineConfig<WorkflowSettings>("workflow", {
9
19
  default: {
10
20
  "worker.enabled": true,
11
21
  "worker.intervalMs": 1000,
@@ -1,6 +1,7 @@
1
1
  import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
2
- import { Controller, Post, type HttpContext } from "../../../http";
3
- import { authorize, body, optionalString, param } from "../../internal/http";
2
+ import { can } from "../../../access";
3
+ import { Authorize, Controller, Post, type HttpContext } from "../../../http";
4
+ import { body, callerOf, optionalString, param } from "../../internal/http";
4
5
  import { IWorkflowRuntime } from "../services/IWorkflowRuntime.service";
5
6
 
6
7
  /** Events from outside — a webhook, another service — over HTTP (WORKFLOW-001 §4). */
@@ -9,11 +10,12 @@ export class EventsController {
9
10
  constructor(private readonly runtime: IWorkflowRuntime) {}
10
11
 
11
12
  @Post(":eventKey")
13
+ @Authorize(can("workflow.events.emit"))
12
14
  async emit(ctx: HttpContext) {
13
15
  const eventKey = param(ctx, "eventKey");
14
16
  const request = await body(ctx);
15
17
  const partition = optionalString(request.partition, "partition") ?? null;
16
- const actor = await authorize(ctx, "events.emit", { partition });
18
+ const { actor } = await callerOf(ctx, "workflow.events.emit", { scope: partition, event: eventKey });
17
19
  const result = await this.runtime.emit(eventKey, request.payload ?? {}, {
18
20
  correlation: optionalString(request.correlation, "correlation") ?? null,
19
21
  partition,
@@ -1,21 +1,22 @@
1
1
  import { contextOnlyController } from "../../../http/Routing/contextOnlyControllers";
2
- import { Controller, Created, Get, Post, type HttpContext } from "../../../http";
3
- import { WorkflowError } from "../../contracts/WorkflowError";
4
- import { authorize, body, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
2
+ import { can, type AccessObject } from "../../../access";
3
+ import { Authorize, Controller, Created, Get, Post, type HttpContext } from "../../../http";
4
+ import { body, callerOf, optionalString, pagingQuery, param, requiredString } from "../../internal/http";
5
5
  import type { InstanceStatus } from "../model/WfInstance.model";
6
- import { IWorkflowRuntime } from "../services/IWorkflowRuntime.service";
6
+ import { IWorkflowRuntime, type InstanceSummary } from "../services/IWorkflowRuntime.service";
7
7
 
8
- /** Instances over HTTP (WORKFLOW-001 §4); rights come from `WorkflowAccess` with the instance's partition. */
8
+ /** Instances over HTTP (WORKFLOW-001 §4); rights are `workflow.instances.*` (WORKFLOW-002 §6.2). */
9
9
  @Controller("workflow/instances")
10
10
  export class InstancesController {
11
11
  constructor(private readonly runtime: IWorkflowRuntime) {}
12
12
 
13
13
  @Post()
14
+ @Authorize(can("workflow.instances.start"))
14
15
  async start(ctx: HttpContext) {
15
16
  const request = await body(ctx);
16
17
  const processKey = requiredString(request.processKey, "processKey", 64);
17
18
  const partition = optionalString(request.partition, "partition") ?? null;
18
- const actor = await authorize(ctx, "instances.start", { processKey, partition });
19
+ const { actor } = await callerOf(ctx, "workflow.instances.start", { scope: partition, process: processKey });
19
20
  const instance = await this.runtime.start(processKey, {
20
21
  input: request.input,
21
22
  partition,
@@ -26,14 +27,14 @@ export class InstancesController {
26
27
  }
27
28
 
28
29
  @Get()
30
+ @Authorize(can("workflow.instances.read"))
29
31
  async list(ctx: HttpContext) {
30
- const partition = optionalString(ctx.query("partition"), "partition");
31
- const processKey = optionalString(ctx.query("processKey"), "processKey");
32
- await authorize(ctx, "instances.read", { partition: partition ?? null, ...(processKey === undefined ? {} : { processKey }) });
32
+ const { access } = await callerOf(ctx);
33
33
  const page = await this.runtime.instances({
34
34
  ...pagingQuery(ctx),
35
- partition,
36
- processKey,
35
+ access: await access.filter("workflow.instances.read"),
36
+ partition: optionalString(ctx.query("partition"), "partition"),
37
+ processKey: optionalString(ctx.query("processKey"), "processKey"),
37
38
  status: optionalString(ctx.query("status"), "status") as InstanceStatus | undefined,
38
39
  businessKey: optionalString(ctx.query("businessKey"), "businessKey"),
39
40
  startedById: optionalString(ctx.query("startedById"), "startedById"),
@@ -42,33 +43,42 @@ export class InstancesController {
42
43
  }
43
44
 
44
45
  @Get(":id")
46
+ @Authorize(can("workflow.instances.read"))
45
47
  async get(ctx: HttpContext) {
46
48
  const instance = await this.runtime.instance(param(ctx, "id"));
47
- await authorize(ctx, "instances.read", { processKey: instance.processKey, partition: instance.partition, instanceId: instance.id });
49
+ await callerOf(ctx, "workflow.instances.read", objectOf(instance));
48
50
  return instance;
49
51
  }
50
52
 
51
53
  @Get(":id/history")
54
+ @Authorize(can("workflow.instances.read"))
52
55
  async history(ctx: HttpContext) {
53
56
  const instance = await this.runtime.instance(param(ctx, "id"));
54
- await authorize(ctx, "instances.read", { processKey: instance.processKey, partition: instance.partition, instanceId: instance.id });
57
+ await callerOf(ctx, "workflow.instances.read", objectOf(instance));
55
58
  return { data: await this.runtime.history(instance.id) };
56
59
  }
57
60
 
58
61
  @Post(":id/cancel")
62
+ @Authorize(can("workflow.instances.manage"))
59
63
  async cancel(ctx: HttpContext) {
60
64
  const instance = await this.runtime.instance(param(ctx, "id"));
61
65
  const request = await body(ctx);
62
- const actor = await authorize(ctx, "instances.manage", { processKey: instance.processKey, partition: instance.partition, instanceId: instance.id });
66
+ const { actor } = await callerOf(ctx, "workflow.instances.manage", objectOf(instance));
63
67
  return await this.runtime.cancel(instance.id, { reason: optionalString(request.reason, "reason", 1000) ?? null, actor });
64
68
  }
65
69
 
66
70
  @Post(":id/retry")
71
+ @Authorize(can("workflow.instances.manage"))
67
72
  async retry(ctx: HttpContext) {
68
73
  const instance = await this.runtime.instance(param(ctx, "id"));
69
- const actor = await authorize(ctx, "instances.manage", { processKey: instance.processKey, partition: instance.partition, instanceId: instance.id });
74
+ const { actor } = await callerOf(ctx, "workflow.instances.manage", objectOf(instance));
70
75
  return await this.runtime.retry(instance.id, { actor });
71
76
  }
72
77
  }
73
78
  contextOnlyController(InstancesController);
74
79
 
80
+ /** The instance as the object of a permission. */
81
+ function objectOf(instance: InstanceSummary): AccessObject {
82
+ return { scope: instance.partition, process: instance.processKey, startedById: instance.startedBy.id };
83
+ }
84
+
@@ -1,4 +1,4 @@
1
- import type { ServiceResolver } from "../../../di";
1
+ import type { ServiceProvider } from "../../../di";
2
2
  import { EventBus, type EventToken, type Logger } from "../../../kernel";
3
3
  import { durationMs, evaluateCondition, nodeKindOf, readPath, type FormSpec, type InstanceData, type ProcessFlow, type ProcessNode } from "../../../../library/workflow";
4
4
  import type { WorkflowActor } from "../../contracts/types";
@@ -52,9 +52,10 @@ export class WorkflowEngine {
52
52
  private readonly definitions: IWorkflowDefinitions,
53
53
  private readonly graphs: GraphCache,
54
54
  private readonly events: EventBus,
55
- private readonly scope: ServiceResolver,
56
- private readonly logger: Logger | undefined,
55
+ /** The scope of the engine: event handlers run in it. */
56
+ private readonly scope: ServiceProvider,
57
57
  private readonly clock: WorkflowClock,
58
+ private readonly logger?: Logger,
58
59
  ) {}
59
60
 
60
61
  /** The engine's time ({@link WorkflowClock}). */
@@ -1,3 +1,4 @@
1
+ import type { AccessFilter } from "../../../access";
1
2
  import type { FormIssue } from "../../../../library/workflow";
2
3
  import type { WorkflowActor, WorkflowPage, WorkflowPaging } from "../../contracts/types";
3
4
  import type { HistoryType } from "../model/WfHistory.model";
@@ -75,6 +76,8 @@ export interface EmitResult {
75
76
  }
76
77
 
77
78
  export interface InstanceListQuery extends WorkflowPaging {
79
+ /** Which instances the caller may see, from `IAccess`; without it the calling code has decided. */
80
+ readonly access?: AccessFilter;
78
81
  readonly partition?: string;
79
82
  readonly status?: InstanceStatus;
80
83
  readonly processKey?: string;
@@ -1,7 +1,9 @@
1
+ import { grantsToWhere } from "../../../access";
1
2
  import type { FormIssue, FormSpec } from "../../../../library/workflow";
2
3
  import { isUuid, pagingOf, type WorkflowActor, type WorkflowPage } from "../../contracts/types";
3
4
  import { WorkflowError } from "../../contracts/WorkflowError";
4
- import type { WorkflowInputValidator } from "../../contracts/WorkflowInputValidator";
5
+ import type { ServiceProvider } from "../../../di";
6
+ import { WorkflowInputValidator } from "../../contracts/WorkflowInputValidator";
5
7
  import type { IWorkflowDefinitions } from "../../definitions_module/services/IWorkflowDefinitions.service";
6
8
  import type { WorkflowEngine } from "../internal/WorkflowEngine";
7
9
  import type { RuntimeDbContext } from "../model/Runtime.dbContext";
@@ -27,7 +29,8 @@ export class WorkflowRuntime extends IWorkflowRuntime {
27
29
  private readonly db: RuntimeDbContext,
28
30
  private readonly engine: WorkflowEngine,
29
31
  private readonly definitions: IWorkflowDefinitions,
30
- private readonly validator: WorkflowInputValidator,
32
+ /** The scope: the input validator is asked here, so that the application's own registration replaces the default. */
33
+ private readonly services: ServiceProvider,
31
34
  ) {
32
35
  super();
33
36
  }
@@ -39,7 +42,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
39
42
  const version = await this.definitions.latest(processKey);
40
43
  const start = Object.entries(version.document.nodes).find(([, node]) => node.kind === "manualStart");
41
44
  if (!start) throw new WorkflowError("wrong_status", `Process ${processKey} has no manual start`);
42
- const { value, issues } = await this.validator.validate(version.document.input, options.input, { purpose: "input", processKey, partition });
45
+ const { value, issues } = await this.validator().validate(version.document.input, options.input, { purpose: "input", processKey, partition });
43
46
  if (issues.length > 0) throw new WorkflowError("invalid_input", "The process input is invalid", issues);
44
47
  const id = await this.engine.begin(this.engine.cache(version), start[0], { input: value, partition, businessKey, startedVia: "manual", actor: options.actor });
45
48
  return await this.instance(id);
@@ -81,7 +84,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
81
84
  if (node?.kind !== "userTask" || !current || current.status !== "waiting" || step.instance.status !== "running") {
82
85
  throw new WorkflowError("wrong_status", "The token does not wait for a task");
83
86
  }
84
- const { value, issues } = await this.validator.validate(node.config?.form as FormSpec | undefined, output, { purpose: "task", processKey: step.instance.processKey, partition: step.instance.partition });
87
+ const { value, issues } = await this.validator().validate(node.config?.form as FormSpec | undefined, output, { purpose: "task", processKey: step.instance.processKey, partition: step.instance.partition });
85
88
  if (issues.length > 0) throw new WorkflowError("invalid_input", "The task form is invalid", issues);
86
89
  await this.engine.complete(step, tokenId, value, actor);
87
90
  return true;
@@ -117,7 +120,7 @@ export class WorkflowRuntime extends IWorkflowRuntime {
117
120
  for (const [nodeId, node] of Object.entries(version.document.nodes)) {
118
121
  if (node.kind !== "eventStart" || node.config?.event !== eventKey) continue;
119
122
  // An event carries more than a process needs: fields the input does not declare are dropped, not refused.
120
- const { value, issues: all } = await this.validator.validate(version.document.input, data, { purpose: "input", processKey: version.key, partition });
123
+ const { value, issues: all } = await this.validator().validate(version.document.input, data, { purpose: "input", processKey: version.key, partition });
121
124
  const issues = all.filter((issue) => issue.code !== "unknown");
122
125
  if (issues.length > 0) {
123
126
  rejected.push({ processKey: version.key, issues });
@@ -132,7 +135,10 @@ export class WorkflowRuntime extends IWorkflowRuntime {
132
135
  async instances(query: InstanceListQuery = {}): Promise<WorkflowPage<InstanceSummary>> {
133
136
  const { limit, offset } = pagingOf(query);
134
137
  if (query.status !== undefined && !STATUSES.has(query.status)) throw new WorkflowError("invalid_input", "status must be running, completed, cancelled or failed");
138
+ const where = query.access === undefined ? "all" : grantsToWhere(query.access);
139
+ if (where === "none") return { items: [], total: 0 };
135
140
  let source = this.db.instances.asNoTracking();
141
+ if (where !== "all") source = source.where(() => where);
136
142
  if (query.partition !== undefined) source = source.where((i) => i.partition.eq(query.partition!));
137
143
  if (query.status !== undefined) source = source.where((i) => i.status.eq(query.status!));
138
144
  if (query.processKey !== undefined) source = source.where((i) => i.processKey.eq(query.processKey!));
@@ -158,6 +164,11 @@ export class WorkflowRuntime extends IWorkflowRuntime {
158
164
  const rows = await this.db.history.asNoTracking().where((h) => h.instanceId.eq(instanceId)).orderBy((h) => h.id).toList();
159
165
  return rows.map(historyOf);
160
166
  }
167
+
168
+ /** The application's `WorkflowInputValidator`, or the module's `DefaultWorkflowInputValidator`. */
169
+ private validator(): WorkflowInputValidator {
170
+ return this.services.resolve(WorkflowInputValidator);
171
+ }
161
172
  }
162
173
 
163
174
  function notFound(instanceId: string): WorkflowError {
@@ -1,9 +1,9 @@
1
- import { DI, Module, scoped } from "../../di";
2
- import { WorkflowClock } from "../contracts/WorkflowClock";
1
+ import { AccessModule } from "../../access";
2
+ import { Module, scoped } from "../../di";
3
3
  import { WorkflowDefinitionsModule } from "../definitions_module/WorkflowDefinitions.module";
4
4
  import { withEventSubscribers } from "../../kernel";
5
- import { IWorkflowRuntime } from "../runtime_module/services/IWorkflowRuntime.service";
6
5
  import { WorkflowRuntimeModule } from "../runtime_module/WorkflowRuntime.module";
6
+ import { tasksPermissions } from "./access/Tasks.permissions";
7
7
  import { TaskEvents } from "./events/TaskEvents";
8
8
  import { TasksController } from "./http/Tasks.controller";
9
9
  import { TasksDbContext, tasksStore } from "./model/Tasks.dbContext";
@@ -13,19 +13,17 @@ import { WorkflowTasks } from "./services/WorkflowTasks.service";
13
13
 
14
14
  /**
15
15
  * Tasks for people: the inbox of `userTask` steps ([passport](MODULE.md)).
16
- * Dependencies are explicit: the framework's classes are not covered by the
17
- * application's codegen.
18
16
  */
19
17
  @Module({
20
- imports: [WorkflowDefinitionsModule, WorkflowRuntimeModule],
18
+ imports: [AccessModule, WorkflowDefinitionsModule, WorkflowRuntimeModule],
19
+ permissions: tasksPermissions,
21
20
  ormBazis: { context: TasksDbContext, entities: [WfTask], ownedStore: tasksStore, registerRepositories: false },
22
21
  providers: [
23
- scoped(WorkflowTasks, WorkflowTasks, [TasksDbContext, IWorkflowRuntime, WorkflowClock] as const),
24
- DI.scoped(DI.factoryProviderWithResolver(IWorkflowTasks, [], (resolver) => resolver.resolve(WorkflowTasks))),
25
- scoped(TaskEvents, TaskEvents, [WorkflowTasks] as const),
22
+ scoped(IWorkflowTasks, WorkflowTasks),
23
+ scoped(TaskEvents),
26
24
  ...withEventSubscribers(TaskEvents),
27
25
  ],
28
- controllers: [DI.bindDeps(TasksController, IWorkflowTasks)],
26
+ controllers: [TasksController],
29
27
  exports: [IWorkflowTasks],
30
28
  })
31
29
  export class WorkflowTasksModule {}
@@ -0,0 +1,13 @@
1
+ import { definePermissions } from "../../../access";
2
+
3
+ /**
4
+ * The right to work on tasks (WORKFLOW-002 §6.2). Which tasks are the
5
+ * subject's is decided by their addressees, not by an owner column.
6
+ */
7
+ export const tasksPermissions = definePermissions({
8
+ "workflow.tasks.work": {
9
+ title: { en: "Work on tasks", ru: "Работать с задачами" },
10
+ scope: { column: "partition" },
11
+ on: { process: { ref: "workflow.process", column: "processKey" } },
12
+ },
13
+ });
@@ -1,23 +1,44 @@
1
1
  import { OnEvent } from "../../../kernel";
2
+ import { UniqueViolationError } from "../../../orm";
2
3
  import { WORKFLOW_INSTANCES_DELETED, WORKFLOW_TASK_REQUESTED, WORKFLOW_TOKEN_CLOSED, type WorkflowInstancesDeleted, type WorkflowTaskRequested, type WorkflowTokenClosed } from "../../runtime_module/events/Runtime.events";
3
- import type { WorkflowTasks } from "../services/WorkflowTasks.service";
4
+ import type { TasksDbContext } from "../model/Tasks.dbContext";
5
+ import { WfTask } from "../model/WfTask.model";
4
6
 
5
7
  /** Keeps the inbox in line with the steps: a waiting user task gets a task, a closed one cancels it. */
6
8
  export class TaskEvents {
7
- constructor(private readonly tasks: WorkflowTasks) {}
9
+ constructor(private readonly db: TasksDbContext) {}
8
10
 
11
+ /** A step started waiting for a person: its task. Repeated events of one token make one task. */
9
12
  @OnEvent(WORKFLOW_TASK_REQUESTED)
10
- requested(event: WorkflowTaskRequested): Promise<void> {
11
- return this.tasks.requested(event);
13
+ async requested(event: WorkflowTaskRequested): Promise<void> {
14
+ const assignee = "role" in event.assignee ? `role:${event.assignee.role}` : `user:${event.assignee.user}`;
15
+ this.db.tasks.add(Object.assign(new WfTask(), {
16
+ tokenId: event.tokenId,
17
+ instanceId: event.instanceId,
18
+ processKey: event.processKey,
19
+ nodeId: event.nodeId,
20
+ partition: event.partition,
21
+ title: event.title,
22
+ assignee,
23
+ form: event.form,
24
+ dueAt: event.dueAt,
25
+ }));
26
+ try {
27
+ await this.db.saveChanges();
28
+ } catch (error) {
29
+ if (!(error instanceof UniqueViolationError)) throw error;
30
+ }
12
31
  }
13
32
 
33
+ /** The step closed otherwise than by its task: the task is cancelled. */
14
34
  @OnEvent(WORKFLOW_TOKEN_CLOSED)
15
- closed(event: WorkflowTokenClosed): Promise<void> {
16
- return this.tasks.closed(event);
35
+ async closed(event: WorkflowTokenClosed): Promise<void> {
36
+ await this.db.tasks.asNoTracking().where((t) => t.tokenId.eq(event.tokenId).and(t.status.in(["open", "claimed"]))).executeUpdate({ status: "cancelled" });
17
37
  }
18
38
 
39
+ /** Retention deleted the instances: their tasks go too. */
19
40
  @OnEvent(WORKFLOW_INSTANCES_DELETED)
20
- deleted(event: WorkflowInstancesDeleted): Promise<void> {
21
- return this.tasks.deleted(event);
41
+ async deleted(event: WorkflowInstancesDeleted): Promise<void> {
42
+ if (event.instanceIds.length > 0) await this.db.tasks.asNoTracking().where((t) => t.instanceId.in([...event.instanceIds])).executeDelete();
22
43
  }
23
44
  }