@electric-ax/agents-server 0.4.15 → 0.4.16
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.
- package/dist/entrypoint.js +1176 -232
- package/dist/index.cjs +1179 -226
- package/dist/index.d.cts +1146 -167
- package/dist/index.d.ts +1146 -167
- package/dist/index.js +1181 -228
- package/drizzle/0011_entity_permissions.sql +100 -0
- package/drizzle/0012_horton_user_manage_permission.sql +25 -0
- package/drizzle/0013_worker_user_manage_permission.sql +25 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +7 -7
- package/src/db/schema.ts +198 -0
- package/src/electric-agents-types.ts +76 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +78 -60
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +608 -5
- package/src/index.ts +11 -0
- package/src/permissions.ts +239 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/durable-streams-router.ts +125 -4
- package/src/routing/electric-proxy-router.ts +4 -0
- package/src/routing/entities-router.ts +344 -20
- package/src/routing/entity-types-router.ts +244 -15
- package/src/routing/hooks.ts +1 -0
- package/src/routing/observations-router.ts +2 -1
- package/src/runtime.ts +34 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/utils/server-utils.ts +191 -11
- package/src/wake-registry.ts +8 -0
package/src/entity-registry.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { and, desc, eq, lt, ne, sql } from 'drizzle-orm'
|
|
1
|
+
import { and, desc, eq, inArray, lt, ne, sql } from 'drizzle-orm'
|
|
2
2
|
import { buildTagsIndex, normalizeTags } from '@electric-ax/agents-runtime'
|
|
3
3
|
import {
|
|
4
4
|
consumerClaims,
|
|
5
5
|
entities,
|
|
6
|
+
entityEffectivePermissions,
|
|
6
7
|
entityBridges,
|
|
7
8
|
entityDispatchState,
|
|
8
9
|
entityManifestSources,
|
|
10
|
+
entityLineage,
|
|
11
|
+
entityPermissionGrants,
|
|
9
12
|
entityTypes,
|
|
13
|
+
entityTypePermissionGrants,
|
|
10
14
|
runnerRuntimeDiagnostics,
|
|
11
15
|
runners,
|
|
16
|
+
sharedStateLinks,
|
|
12
17
|
tagStreamOutbox,
|
|
18
|
+
users,
|
|
13
19
|
} from './db/schema.js'
|
|
14
20
|
import {
|
|
15
21
|
assertEntityStatus,
|
|
@@ -30,8 +36,15 @@ import type {
|
|
|
30
36
|
SourceStreamOffset,
|
|
31
37
|
ConsumerClaim,
|
|
32
38
|
DispatchPolicy,
|
|
39
|
+
EntityPermission,
|
|
40
|
+
EntityPermissionGrant,
|
|
41
|
+
EntityPermissionPropagation,
|
|
42
|
+
EntityTypePermission,
|
|
43
|
+
EntityTypePermissionGrant,
|
|
44
|
+
PermissionSubjectKind,
|
|
33
45
|
} from './electric-agents-types.js'
|
|
34
46
|
import type { EntityTags } from '@electric-ax/agents-runtime'
|
|
47
|
+
import type { Principal } from './principal.js'
|
|
35
48
|
|
|
36
49
|
export class EntityAlreadyExistsError extends Error {
|
|
37
50
|
constructor(public readonly url: string) {
|
|
@@ -51,6 +64,8 @@ export interface EntityBridgeRow {
|
|
|
51
64
|
sourceRef: string
|
|
52
65
|
tags: EntityTags
|
|
53
66
|
streamUrl: string
|
|
67
|
+
principalUrl?: string
|
|
68
|
+
principalKind?: string
|
|
54
69
|
shapeHandle?: string
|
|
55
70
|
shapeOffset?: string
|
|
56
71
|
lastObserverActivityAt: Date
|
|
@@ -135,13 +150,43 @@ export interface MaterializeReleasedClaimInput {
|
|
|
135
150
|
releasedAt?: Date
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
export interface CreateEntityTypePermissionGrantInput {
|
|
154
|
+
entityType: string
|
|
155
|
+
permission: EntityTypePermission
|
|
156
|
+
subjectKind: PermissionSubjectKind
|
|
157
|
+
subjectValue: string
|
|
158
|
+
createdBy?: string
|
|
159
|
+
expiresAt?: Date
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface CreateEntityPermissionGrantInput {
|
|
163
|
+
entityUrl: string
|
|
164
|
+
permission: EntityPermission
|
|
165
|
+
subjectKind: PermissionSubjectKind
|
|
166
|
+
subjectValue: string
|
|
167
|
+
propagation?: EntityPermissionPropagation
|
|
168
|
+
copyToChildren?: boolean
|
|
169
|
+
createdBy?: string
|
|
170
|
+
expiresAt?: Date
|
|
171
|
+
}
|
|
172
|
+
|
|
138
173
|
const DEFAULT_RUNNER_LEASE_MS = 30_000
|
|
174
|
+
const PERMISSION_PRUNE_INTERVAL_MS = 30_000
|
|
175
|
+
|
|
176
|
+
type RegistryTransaction = Parameters<
|
|
177
|
+
Parameters<DrizzleDB[`transaction`]>[0]
|
|
178
|
+
>[0]
|
|
139
179
|
|
|
140
180
|
export function runnerWakeStream(runnerId: string): string {
|
|
141
181
|
return `/runners/${runnerId}/wake`
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
export class PostgresRegistry {
|
|
185
|
+
// Electric predicates cannot depend on now(), so expired effective rows need a
|
|
186
|
+
// server-side sweep. Debounce it to keep read/auth paths from writing on every request.
|
|
187
|
+
private lastPermissionPruneStartedAt = 0
|
|
188
|
+
private permissionPrunePromise: Promise<void> | null = null
|
|
189
|
+
|
|
145
190
|
constructor(
|
|
146
191
|
private db: DrizzleDB,
|
|
147
192
|
readonly tenantId: string = DEFAULT_TENANT_ID
|
|
@@ -151,6 +196,18 @@ export class PostgresRegistry {
|
|
|
151
196
|
|
|
152
197
|
close(): void {}
|
|
153
198
|
|
|
199
|
+
async ensureUserForPrincipal(principal: Principal): Promise<void> {
|
|
200
|
+
if (principal.kind !== `user`) return
|
|
201
|
+
|
|
202
|
+
await this.db
|
|
203
|
+
.insert(users)
|
|
204
|
+
.values({
|
|
205
|
+
tenantId: this.tenantId,
|
|
206
|
+
id: principal.id,
|
|
207
|
+
})
|
|
208
|
+
.onConflictDoNothing()
|
|
209
|
+
}
|
|
210
|
+
|
|
154
211
|
async createRunner(
|
|
155
212
|
input: RegisterRunnerInput
|
|
156
213
|
): Promise<ElectricAgentsRunner> {
|
|
@@ -699,6 +756,67 @@ export class PostgresRegistry {
|
|
|
699
756
|
})
|
|
700
757
|
.onConflictDoNothing()
|
|
701
758
|
|
|
759
|
+
await tx
|
|
760
|
+
.insert(entityLineage)
|
|
761
|
+
.values({
|
|
762
|
+
tenantId: this.tenantId,
|
|
763
|
+
ancestorUrl: entity.url,
|
|
764
|
+
descendantUrl: entity.url,
|
|
765
|
+
depth: 0,
|
|
766
|
+
})
|
|
767
|
+
.onConflictDoNothing()
|
|
768
|
+
|
|
769
|
+
if (entity.parent) {
|
|
770
|
+
await tx.execute(sql`
|
|
771
|
+
INSERT INTO ${entityLineage} (
|
|
772
|
+
tenant_id,
|
|
773
|
+
ancestor_url,
|
|
774
|
+
descendant_url,
|
|
775
|
+
depth
|
|
776
|
+
)
|
|
777
|
+
SELECT
|
|
778
|
+
${this.tenantId},
|
|
779
|
+
ancestor_url,
|
|
780
|
+
${entity.url},
|
|
781
|
+
depth + 1
|
|
782
|
+
FROM ${entityLineage}
|
|
783
|
+
WHERE tenant_id = ${this.tenantId}
|
|
784
|
+
AND descendant_url = ${entity.parent}
|
|
785
|
+
ON CONFLICT DO NOTHING
|
|
786
|
+
`)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
await tx.execute(sql`
|
|
790
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
791
|
+
tenant_id,
|
|
792
|
+
entity_url,
|
|
793
|
+
source_entity_url,
|
|
794
|
+
source_grant_id,
|
|
795
|
+
permission,
|
|
796
|
+
subject_kind,
|
|
797
|
+
subject_value,
|
|
798
|
+
expires_at
|
|
799
|
+
)
|
|
800
|
+
SELECT
|
|
801
|
+
${this.tenantId},
|
|
802
|
+
${entity.url},
|
|
803
|
+
grants.entity_url,
|
|
804
|
+
grants.id,
|
|
805
|
+
grants.permission,
|
|
806
|
+
grants.subject_kind,
|
|
807
|
+
grants.subject_value,
|
|
808
|
+
grants.expires_at
|
|
809
|
+
FROM ${entityPermissionGrants} grants
|
|
810
|
+
JOIN ${entityLineage} lineage
|
|
811
|
+
ON lineage.tenant_id = grants.tenant_id
|
|
812
|
+
AND lineage.ancestor_url = grants.entity_url
|
|
813
|
+
AND lineage.descendant_url = ${entity.url}
|
|
814
|
+
WHERE grants.tenant_id = ${this.tenantId}
|
|
815
|
+
AND grants.propagation = 'descendants'
|
|
816
|
+
AND (grants.expires_at IS NULL OR grants.expires_at > now())
|
|
817
|
+
ON CONFLICT DO NOTHING
|
|
818
|
+
`)
|
|
819
|
+
|
|
702
820
|
return parseInt(result[0]!.txid)
|
|
703
821
|
})
|
|
704
822
|
} catch (err) {
|
|
@@ -735,12 +853,9 @@ export class PostgresRegistry {
|
|
|
735
853
|
streamPath: string
|
|
736
854
|
): Promise<ElectricAgentsEntity | null> {
|
|
737
855
|
const mainSuffix = `/main`
|
|
738
|
-
const errorSuffix = `/error`
|
|
739
856
|
let entityUrl: string | null = null
|
|
740
857
|
if (streamPath.endsWith(mainSuffix)) {
|
|
741
858
|
entityUrl = streamPath.slice(0, -mainSuffix.length)
|
|
742
|
-
} else if (streamPath.endsWith(errorSuffix)) {
|
|
743
|
-
entityUrl = streamPath.slice(0, -errorSuffix.length)
|
|
744
859
|
}
|
|
745
860
|
if (!entityUrl) return null
|
|
746
861
|
return this.getEntity(entityUrl)
|
|
@@ -753,6 +868,11 @@ export class PostgresRegistry {
|
|
|
753
868
|
limit?: number
|
|
754
869
|
offset?: number
|
|
755
870
|
created_by?: string
|
|
871
|
+
readableBy?: {
|
|
872
|
+
principalUrl: string
|
|
873
|
+
principalKind: string
|
|
874
|
+
bypass?: boolean
|
|
875
|
+
}
|
|
756
876
|
}): Promise<{ entities: Array<ElectricAgentsEntity>; total: number }> {
|
|
757
877
|
const conditions = [eq(entities.tenantId, this.tenantId)]
|
|
758
878
|
if (filter?.type) conditions.push(eq(entities.type, filter.type))
|
|
@@ -760,6 +880,25 @@ export class PostgresRegistry {
|
|
|
760
880
|
if (filter?.parent) conditions.push(eq(entities.parent, filter.parent))
|
|
761
881
|
if (filter?.created_by)
|
|
762
882
|
conditions.push(eq(entities.createdBy, filter.created_by))
|
|
883
|
+
if (filter?.readableBy && !filter.readableBy.bypass) {
|
|
884
|
+
conditions.push(sql`(
|
|
885
|
+
${entities.createdBy} = ${filter.readableBy.principalUrl}
|
|
886
|
+
OR ${entities.url} IN (
|
|
887
|
+
SELECT ${entityEffectivePermissions.entityUrl}
|
|
888
|
+
FROM ${entityEffectivePermissions}
|
|
889
|
+
WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
|
|
890
|
+
AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
|
|
891
|
+
AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
|
|
892
|
+
AND (
|
|
893
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
894
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
|
|
895
|
+
OR
|
|
896
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
897
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
|
|
898
|
+
)
|
|
899
|
+
)
|
|
900
|
+
)`)
|
|
901
|
+
}
|
|
763
902
|
|
|
764
903
|
const whereClause = and(...conditions)
|
|
765
904
|
|
|
@@ -790,6 +929,431 @@ export class PostgresRegistry {
|
|
|
790
929
|
}
|
|
791
930
|
}
|
|
792
931
|
|
|
932
|
+
async createEntityTypePermissionGrant(
|
|
933
|
+
input: CreateEntityTypePermissionGrantInput
|
|
934
|
+
): Promise<EntityTypePermissionGrant> {
|
|
935
|
+
const [row] = await this.db
|
|
936
|
+
.insert(entityTypePermissionGrants)
|
|
937
|
+
.values({
|
|
938
|
+
tenantId: this.tenantId,
|
|
939
|
+
entityType: input.entityType,
|
|
940
|
+
permission: input.permission,
|
|
941
|
+
subjectKind: input.subjectKind,
|
|
942
|
+
subjectValue: input.subjectValue,
|
|
943
|
+
createdBy: input.createdBy ?? null,
|
|
944
|
+
expiresAt: input.expiresAt ?? null,
|
|
945
|
+
})
|
|
946
|
+
.returning()
|
|
947
|
+
return this.rowToEntityTypePermissionGrant(row!)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async ensureEntityTypePermissionGrant(
|
|
951
|
+
input: CreateEntityTypePermissionGrantInput
|
|
952
|
+
): Promise<EntityTypePermissionGrant> {
|
|
953
|
+
const [existing] = await this.db
|
|
954
|
+
.select()
|
|
955
|
+
.from(entityTypePermissionGrants)
|
|
956
|
+
.where(
|
|
957
|
+
and(
|
|
958
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
959
|
+
eq(entityTypePermissionGrants.entityType, input.entityType),
|
|
960
|
+
eq(entityTypePermissionGrants.permission, input.permission),
|
|
961
|
+
eq(entityTypePermissionGrants.subjectKind, input.subjectKind),
|
|
962
|
+
eq(entityTypePermissionGrants.subjectValue, input.subjectValue),
|
|
963
|
+
input.expiresAt
|
|
964
|
+
? eq(entityTypePermissionGrants.expiresAt, input.expiresAt)
|
|
965
|
+
: sql`${entityTypePermissionGrants.expiresAt} IS NULL`
|
|
966
|
+
)
|
|
967
|
+
)
|
|
968
|
+
.limit(1)
|
|
969
|
+
if (existing) return this.rowToEntityTypePermissionGrant(existing)
|
|
970
|
+
|
|
971
|
+
return await this.createEntityTypePermissionGrant(input)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async listEntityTypePermissionGrants(
|
|
975
|
+
entityType: string
|
|
976
|
+
): Promise<Array<EntityTypePermissionGrant>> {
|
|
977
|
+
const rows = await this.db
|
|
978
|
+
.select()
|
|
979
|
+
.from(entityTypePermissionGrants)
|
|
980
|
+
.where(
|
|
981
|
+
and(
|
|
982
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
983
|
+
eq(entityTypePermissionGrants.entityType, entityType)
|
|
984
|
+
)
|
|
985
|
+
)
|
|
986
|
+
.orderBy(entityTypePermissionGrants.id)
|
|
987
|
+
return rows.map((row) => this.rowToEntityTypePermissionGrant(row))
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async deleteEntityTypePermissionGrant(
|
|
991
|
+
entityType: string,
|
|
992
|
+
grantId: number
|
|
993
|
+
): Promise<boolean> {
|
|
994
|
+
const rows = await this.db
|
|
995
|
+
.delete(entityTypePermissionGrants)
|
|
996
|
+
.where(
|
|
997
|
+
and(
|
|
998
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
999
|
+
eq(entityTypePermissionGrants.entityType, entityType),
|
|
1000
|
+
eq(entityTypePermissionGrants.id, grantId)
|
|
1001
|
+
)
|
|
1002
|
+
)
|
|
1003
|
+
.returning({ id: entityTypePermissionGrants.id })
|
|
1004
|
+
return rows.length > 0
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async hasEntityTypePermission(
|
|
1008
|
+
entityType: string,
|
|
1009
|
+
permission: EntityTypePermission,
|
|
1010
|
+
subject: { principalUrl: string; principalKind: string }
|
|
1011
|
+
): Promise<boolean> {
|
|
1012
|
+
const permissions = [permission, `manage`] as const
|
|
1013
|
+
const rows = await this.db
|
|
1014
|
+
.select({ id: entityTypePermissionGrants.id })
|
|
1015
|
+
.from(entityTypePermissionGrants)
|
|
1016
|
+
.where(
|
|
1017
|
+
and(
|
|
1018
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
1019
|
+
eq(entityTypePermissionGrants.entityType, entityType),
|
|
1020
|
+
inArray(entityTypePermissionGrants.permission, [...permissions]),
|
|
1021
|
+
sql`(${entityTypePermissionGrants.expiresAt} IS NULL OR ${entityTypePermissionGrants.expiresAt} > now())`,
|
|
1022
|
+
sql`(
|
|
1023
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal'
|
|
1024
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
|
|
1025
|
+
OR
|
|
1026
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal_kind'
|
|
1027
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
|
|
1028
|
+
)`
|
|
1029
|
+
)
|
|
1030
|
+
)
|
|
1031
|
+
.limit(1)
|
|
1032
|
+
return rows.length > 0
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async createEntityPermissionGrant(
|
|
1036
|
+
input: CreateEntityPermissionGrantInput
|
|
1037
|
+
): Promise<EntityPermissionGrant> {
|
|
1038
|
+
return await this.db.transaction(async (tx) => {
|
|
1039
|
+
const [row] = await tx
|
|
1040
|
+
.insert(entityPermissionGrants)
|
|
1041
|
+
.values({
|
|
1042
|
+
tenantId: this.tenantId,
|
|
1043
|
+
entityUrl: input.entityUrl,
|
|
1044
|
+
permission: input.permission,
|
|
1045
|
+
subjectKind: input.subjectKind,
|
|
1046
|
+
subjectValue: input.subjectValue,
|
|
1047
|
+
propagation: input.propagation ?? `self`,
|
|
1048
|
+
copyToChildren: input.copyToChildren ?? false,
|
|
1049
|
+
createdBy: input.createdBy ?? null,
|
|
1050
|
+
expiresAt: input.expiresAt ?? null,
|
|
1051
|
+
})
|
|
1052
|
+
.returning()
|
|
1053
|
+
await this.materializeEntityPermissionGrant(tx, row!)
|
|
1054
|
+
return this.rowToEntityPermissionGrant(row!)
|
|
1055
|
+
})
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async listEntityPermissionGrants(
|
|
1059
|
+
entityUrl: string
|
|
1060
|
+
): Promise<Array<EntityPermissionGrant>> {
|
|
1061
|
+
const rows = await this.db
|
|
1062
|
+
.select()
|
|
1063
|
+
.from(entityPermissionGrants)
|
|
1064
|
+
.where(
|
|
1065
|
+
and(
|
|
1066
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1067
|
+
eq(entityPermissionGrants.entityUrl, entityUrl)
|
|
1068
|
+
)
|
|
1069
|
+
)
|
|
1070
|
+
.orderBy(entityPermissionGrants.id)
|
|
1071
|
+
return rows.map((row) => this.rowToEntityPermissionGrant(row))
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async deleteEntityPermissionGrant(
|
|
1075
|
+
entityUrl: string,
|
|
1076
|
+
grantId: number
|
|
1077
|
+
): Promise<boolean> {
|
|
1078
|
+
return await this.db.transaction(async (tx) => {
|
|
1079
|
+
await tx
|
|
1080
|
+
.delete(entityEffectivePermissions)
|
|
1081
|
+
.where(
|
|
1082
|
+
and(
|
|
1083
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1084
|
+
eq(entityEffectivePermissions.sourceGrantId, grantId)
|
|
1085
|
+
)
|
|
1086
|
+
)
|
|
1087
|
+
const rows = await tx
|
|
1088
|
+
.delete(entityPermissionGrants)
|
|
1089
|
+
.where(
|
|
1090
|
+
and(
|
|
1091
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1092
|
+
eq(entityPermissionGrants.entityUrl, entityUrl),
|
|
1093
|
+
eq(entityPermissionGrants.id, grantId)
|
|
1094
|
+
)
|
|
1095
|
+
)
|
|
1096
|
+
.returning({ id: entityPermissionGrants.id })
|
|
1097
|
+
return rows.length > 0
|
|
1098
|
+
})
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async copyEntityPermissionGrantsForSpawn(
|
|
1102
|
+
parentEntityUrl: string,
|
|
1103
|
+
childEntityUrl: string,
|
|
1104
|
+
createdBy?: string
|
|
1105
|
+
): Promise<Array<EntityPermissionGrant>> {
|
|
1106
|
+
const parentGrants = await this.db
|
|
1107
|
+
.select()
|
|
1108
|
+
.from(entityPermissionGrants)
|
|
1109
|
+
.where(
|
|
1110
|
+
and(
|
|
1111
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1112
|
+
eq(entityPermissionGrants.entityUrl, parentEntityUrl),
|
|
1113
|
+
eq(entityPermissionGrants.copyToChildren, true),
|
|
1114
|
+
sql`(${entityPermissionGrants.expiresAt} IS NULL OR ${entityPermissionGrants.expiresAt} > now())`
|
|
1115
|
+
)
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
const copied: Array<EntityPermissionGrant> = []
|
|
1119
|
+
for (const grant of parentGrants) {
|
|
1120
|
+
copied.push(
|
|
1121
|
+
await this.createEntityPermissionGrant({
|
|
1122
|
+
entityUrl: childEntityUrl,
|
|
1123
|
+
permission: grant.permission as EntityPermission,
|
|
1124
|
+
subjectKind: grant.subjectKind as PermissionSubjectKind,
|
|
1125
|
+
subjectValue: grant.subjectValue,
|
|
1126
|
+
propagation: `self`,
|
|
1127
|
+
copyToChildren: grant.copyToChildren,
|
|
1128
|
+
createdBy,
|
|
1129
|
+
expiresAt: grant.expiresAt ?? undefined,
|
|
1130
|
+
})
|
|
1131
|
+
)
|
|
1132
|
+
}
|
|
1133
|
+
return copied
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async hasEntityPermission(
|
|
1137
|
+
entityUrl: string,
|
|
1138
|
+
permission: EntityPermission,
|
|
1139
|
+
subject: { principalUrl: string; principalKind: string }
|
|
1140
|
+
): Promise<boolean> {
|
|
1141
|
+
const permissions = [permission, `manage`] as const
|
|
1142
|
+
const rows = await this.db
|
|
1143
|
+
.select({ id: entityEffectivePermissions.id })
|
|
1144
|
+
.from(entityEffectivePermissions)
|
|
1145
|
+
.where(
|
|
1146
|
+
and(
|
|
1147
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1148
|
+
eq(entityEffectivePermissions.entityUrl, entityUrl),
|
|
1149
|
+
inArray(entityEffectivePermissions.permission, [...permissions]),
|
|
1150
|
+
sql`(${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())`,
|
|
1151
|
+
sql`(
|
|
1152
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1153
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
|
|
1154
|
+
OR
|
|
1155
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1156
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
|
|
1157
|
+
)`
|
|
1158
|
+
)
|
|
1159
|
+
)
|
|
1160
|
+
.limit(1)
|
|
1161
|
+
return rows.length > 0
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async replaceSharedStateLink(
|
|
1165
|
+
ownerEntityUrl: string,
|
|
1166
|
+
manifestKey: string,
|
|
1167
|
+
sharedStateId?: string
|
|
1168
|
+
): Promise<void> {
|
|
1169
|
+
await this.db
|
|
1170
|
+
.delete(sharedStateLinks)
|
|
1171
|
+
.where(
|
|
1172
|
+
and(
|
|
1173
|
+
eq(sharedStateLinks.tenantId, this.tenantId),
|
|
1174
|
+
eq(sharedStateLinks.ownerEntityUrl, ownerEntityUrl),
|
|
1175
|
+
eq(sharedStateLinks.manifestKey, manifestKey)
|
|
1176
|
+
)
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
if (!sharedStateId) return
|
|
1180
|
+
|
|
1181
|
+
await this.db
|
|
1182
|
+
.insert(sharedStateLinks)
|
|
1183
|
+
.values({
|
|
1184
|
+
tenantId: this.tenantId,
|
|
1185
|
+
ownerEntityUrl,
|
|
1186
|
+
manifestKey,
|
|
1187
|
+
sharedStateId,
|
|
1188
|
+
})
|
|
1189
|
+
.onConflictDoUpdate({
|
|
1190
|
+
target: [
|
|
1191
|
+
sharedStateLinks.tenantId,
|
|
1192
|
+
sharedStateLinks.ownerEntityUrl,
|
|
1193
|
+
sharedStateLinks.manifestKey,
|
|
1194
|
+
],
|
|
1195
|
+
set: {
|
|
1196
|
+
sharedStateId,
|
|
1197
|
+
updatedAt: new Date(),
|
|
1198
|
+
},
|
|
1199
|
+
})
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async listSharedStateLinkedEntityUrls(
|
|
1203
|
+
sharedStateId: string
|
|
1204
|
+
): Promise<Array<string>> {
|
|
1205
|
+
const rows = await this.db
|
|
1206
|
+
.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl })
|
|
1207
|
+
.from(sharedStateLinks)
|
|
1208
|
+
.where(
|
|
1209
|
+
and(
|
|
1210
|
+
eq(sharedStateLinks.tenantId, this.tenantId),
|
|
1211
|
+
eq(sharedStateLinks.sharedStateId, sharedStateId)
|
|
1212
|
+
)
|
|
1213
|
+
)
|
|
1214
|
+
return rows.map((row) => row.ownerEntityUrl)
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async pruneExpiredPermissionGrants(
|
|
1218
|
+
now: Date = new Date(),
|
|
1219
|
+
options: { force?: boolean } = {}
|
|
1220
|
+
): Promise<void> {
|
|
1221
|
+
if (this.permissionPrunePromise) return await this.permissionPrunePromise
|
|
1222
|
+
|
|
1223
|
+
const startedAt = Date.now()
|
|
1224
|
+
if (
|
|
1225
|
+
!options.force &&
|
|
1226
|
+
startedAt - this.lastPermissionPruneStartedAt <
|
|
1227
|
+
PERMISSION_PRUNE_INTERVAL_MS
|
|
1228
|
+
) {
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
this.lastPermissionPruneStartedAt = startedAt
|
|
1233
|
+
const promise = this.pruneExpiredPermissionGrantsNow(now)
|
|
1234
|
+
this.permissionPrunePromise = promise
|
|
1235
|
+
try {
|
|
1236
|
+
await promise
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
this.lastPermissionPruneStartedAt = 0
|
|
1239
|
+
throw error
|
|
1240
|
+
} finally {
|
|
1241
|
+
if (this.permissionPrunePromise === promise) {
|
|
1242
|
+
this.permissionPrunePromise = null
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private async pruneExpiredPermissionGrantsNow(now: Date): Promise<void> {
|
|
1248
|
+
await this.db.transaction(async (tx) => {
|
|
1249
|
+
const expiredEntityGrantIds = await tx
|
|
1250
|
+
.select({ id: entityPermissionGrants.id })
|
|
1251
|
+
.from(entityPermissionGrants)
|
|
1252
|
+
.where(
|
|
1253
|
+
and(
|
|
1254
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1255
|
+
sql`${entityPermissionGrants.expiresAt} IS NOT NULL`,
|
|
1256
|
+
lt(entityPermissionGrants.expiresAt, now)
|
|
1257
|
+
)
|
|
1258
|
+
)
|
|
1259
|
+
const ids = expiredEntityGrantIds.map((row) => row.id)
|
|
1260
|
+
if (ids.length > 0) {
|
|
1261
|
+
await tx
|
|
1262
|
+
.delete(entityEffectivePermissions)
|
|
1263
|
+
.where(
|
|
1264
|
+
and(
|
|
1265
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1266
|
+
inArray(entityEffectivePermissions.sourceGrantId, ids)
|
|
1267
|
+
)
|
|
1268
|
+
)
|
|
1269
|
+
await tx
|
|
1270
|
+
.delete(entityPermissionGrants)
|
|
1271
|
+
.where(
|
|
1272
|
+
and(
|
|
1273
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1274
|
+
inArray(entityPermissionGrants.id, ids)
|
|
1275
|
+
)
|
|
1276
|
+
)
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
await tx
|
|
1280
|
+
.delete(entityEffectivePermissions)
|
|
1281
|
+
.where(
|
|
1282
|
+
and(
|
|
1283
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1284
|
+
sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`,
|
|
1285
|
+
lt(entityEffectivePermissions.expiresAt, now)
|
|
1286
|
+
)
|
|
1287
|
+
)
|
|
1288
|
+
await tx
|
|
1289
|
+
.delete(entityTypePermissionGrants)
|
|
1290
|
+
.where(
|
|
1291
|
+
and(
|
|
1292
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
1293
|
+
sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`,
|
|
1294
|
+
lt(entityTypePermissionGrants.expiresAt, now)
|
|
1295
|
+
)
|
|
1296
|
+
)
|
|
1297
|
+
})
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
private async materializeEntityPermissionGrant(
|
|
1301
|
+
tx: RegistryTransaction,
|
|
1302
|
+
grant: typeof entityPermissionGrants.$inferSelect
|
|
1303
|
+
): Promise<void> {
|
|
1304
|
+
await tx
|
|
1305
|
+
.delete(entityEffectivePermissions)
|
|
1306
|
+
.where(
|
|
1307
|
+
and(
|
|
1308
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1309
|
+
eq(entityEffectivePermissions.sourceGrantId, grant.id)
|
|
1310
|
+
)
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
if (grant.propagation === `descendants`) {
|
|
1314
|
+
await tx.execute(sql`
|
|
1315
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
1316
|
+
tenant_id,
|
|
1317
|
+
entity_url,
|
|
1318
|
+
source_entity_url,
|
|
1319
|
+
source_grant_id,
|
|
1320
|
+
permission,
|
|
1321
|
+
subject_kind,
|
|
1322
|
+
subject_value,
|
|
1323
|
+
expires_at
|
|
1324
|
+
)
|
|
1325
|
+
SELECT
|
|
1326
|
+
${this.tenantId},
|
|
1327
|
+
descendant_url,
|
|
1328
|
+
${grant.entityUrl},
|
|
1329
|
+
${grant.id},
|
|
1330
|
+
${grant.permission},
|
|
1331
|
+
${grant.subjectKind},
|
|
1332
|
+
${grant.subjectValue},
|
|
1333
|
+
${grant.expiresAt}
|
|
1334
|
+
FROM ${entityLineage}
|
|
1335
|
+
WHERE tenant_id = ${this.tenantId}
|
|
1336
|
+
AND ancestor_url = ${grant.entityUrl}
|
|
1337
|
+
ON CONFLICT DO NOTHING
|
|
1338
|
+
`)
|
|
1339
|
+
return
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
await tx
|
|
1343
|
+
.insert(entityEffectivePermissions)
|
|
1344
|
+
.values({
|
|
1345
|
+
tenantId: this.tenantId,
|
|
1346
|
+
entityUrl: grant.entityUrl,
|
|
1347
|
+
sourceEntityUrl: grant.entityUrl,
|
|
1348
|
+
sourceGrantId: grant.id,
|
|
1349
|
+
permission: grant.permission,
|
|
1350
|
+
subjectKind: grant.subjectKind,
|
|
1351
|
+
subjectValue: grant.subjectValue,
|
|
1352
|
+
expiresAt: grant.expiresAt,
|
|
1353
|
+
})
|
|
1354
|
+
.onConflictDoNothing()
|
|
1355
|
+
}
|
|
1356
|
+
|
|
793
1357
|
async updateStatus(entityUrl: string, status: EntityStatus): Promise<void> {
|
|
794
1358
|
const whereClause = isTerminalEntityStatus(status)
|
|
795
1359
|
? this.entityWhere(entityUrl)
|
|
@@ -953,6 +1517,8 @@ export class PostgresRegistry {
|
|
|
953
1517
|
sourceRef: string
|
|
954
1518
|
tags: EntityTags
|
|
955
1519
|
streamUrl: string
|
|
1520
|
+
principalUrl: string
|
|
1521
|
+
principalKind: string
|
|
956
1522
|
}): Promise<EntityBridgeRow> {
|
|
957
1523
|
await this.db
|
|
958
1524
|
.insert(entityBridges)
|
|
@@ -961,6 +1527,8 @@ export class PostgresRegistry {
|
|
|
961
1527
|
sourceRef: row.sourceRef,
|
|
962
1528
|
tags: normalizeTags(row.tags),
|
|
963
1529
|
streamUrl: row.streamUrl,
|
|
1530
|
+
principalUrl: row.principalUrl,
|
|
1531
|
+
principalKind: row.principalKind,
|
|
964
1532
|
})
|
|
965
1533
|
.onConflictDoNothing()
|
|
966
1534
|
|
|
@@ -1271,6 +1839,40 @@ export class PostgresRegistry {
|
|
|
1271
1839
|
}
|
|
1272
1840
|
}
|
|
1273
1841
|
|
|
1842
|
+
private rowToEntityTypePermissionGrant(
|
|
1843
|
+
row: typeof entityTypePermissionGrants.$inferSelect
|
|
1844
|
+
): EntityTypePermissionGrant {
|
|
1845
|
+
return {
|
|
1846
|
+
id: row.id,
|
|
1847
|
+
entity_type: row.entityType,
|
|
1848
|
+
permission: row.permission as EntityTypePermission,
|
|
1849
|
+
subject_kind: row.subjectKind as PermissionSubjectKind,
|
|
1850
|
+
subject_value: row.subjectValue,
|
|
1851
|
+
created_by: row.createdBy ?? undefined,
|
|
1852
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1853
|
+
created_at: row.createdAt.toISOString(),
|
|
1854
|
+
updated_at: row.updatedAt.toISOString(),
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
private rowToEntityPermissionGrant(
|
|
1859
|
+
row: typeof entityPermissionGrants.$inferSelect
|
|
1860
|
+
): EntityPermissionGrant {
|
|
1861
|
+
return {
|
|
1862
|
+
id: row.id,
|
|
1863
|
+
entity_url: row.entityUrl,
|
|
1864
|
+
permission: row.permission as EntityPermission,
|
|
1865
|
+
subject_kind: row.subjectKind as PermissionSubjectKind,
|
|
1866
|
+
subject_value: row.subjectValue,
|
|
1867
|
+
propagation: row.propagation as EntityPermissionPropagation,
|
|
1868
|
+
copy_to_children: row.copyToChildren,
|
|
1869
|
+
created_by: row.createdBy ?? undefined,
|
|
1870
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1871
|
+
created_at: row.createdAt.toISOString(),
|
|
1872
|
+
updated_at: row.updatedAt.toISOString(),
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1274
1876
|
private rowToEntity(row: typeof entities.$inferSelect): ElectricAgentsEntity {
|
|
1275
1877
|
return {
|
|
1276
1878
|
url: row.url,
|
|
@@ -1278,7 +1880,6 @@ export class PostgresRegistry {
|
|
|
1278
1880
|
status: assertEntityStatus(row.status),
|
|
1279
1881
|
streams: {
|
|
1280
1882
|
main: `${row.url}/main`,
|
|
1281
|
-
error: `${row.url}/error`,
|
|
1282
1883
|
},
|
|
1283
1884
|
subscription_id: row.subscriptionId,
|
|
1284
1885
|
dispatch_policy:
|
|
@@ -1311,6 +1912,8 @@ export class PostgresRegistry {
|
|
|
1311
1912
|
sourceRef: row.sourceRef,
|
|
1312
1913
|
tags: (row.tags as EntityTags | null | undefined) ?? {},
|
|
1313
1914
|
streamUrl: row.streamUrl,
|
|
1915
|
+
principalUrl: row.principalUrl ?? undefined,
|
|
1916
|
+
principalKind: row.principalKind ?? undefined,
|
|
1314
1917
|
shapeHandle: row.shapeHandle ?? undefined,
|
|
1315
1918
|
shapeOffset: row.shapeOffset ?? undefined,
|
|
1316
1919
|
lastObserverActivityAt: row.lastObserverActivityAt,
|