Skip to content

Commit 8ec0bc2

Browse files
authored
Merge pull request #1865 from rocket-admin/backend_agent_table_public_permissions
Backend agent table public permissions
2 parents 24d01cb + 5c6d6cc commit 8ec0bc2

19 files changed

Lines changed: 343 additions & 5 deletions

backend/src/common/data-injection.tokens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export enum UseCaseType {
183183
AGENTS_VALIDATE_TABLE_AI_REQUEST = 'AGENTS_VALIDATE_TABLE_AI_REQUEST',
184184
AGENTS_VALIDATE_CONNECTION_EDIT = 'AGENTS_VALIDATE_CONNECTION_EDIT',
185185
AGENTS_SET_PUBLIC_PERMISSIONS = 'AGENTS_SET_PUBLIC_PERMISSIONS',
186+
AGENTS_SET_SITE_RUNTIME_POLICY = 'AGENTS_SET_SITE_RUNTIME_POLICY',
186187
AGENTS_GET_AI_CONNECTION_CONTEXT = 'AGENTS_GET_AI_CONNECTION_CONTEXT',
187188
AGENTS_GET_AI_CONNECTION_TABLES = 'AGENTS_GET_AI_CONNECTION_TABLES',
188189
AGENTS_GET_AI_TABLE_STRUCTURE = 'AGENTS_GET_AI_TABLE_STRUCTURE',

backend/src/entities/connection/connection.entity.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ export class ConnectionEntity {
120120
@Column({ type: 'text', nullable: true, default: null })
121121
public_cedar_policy?: string | null;
122122

123+
// Server-side site data contract for the SiteNova generated-site runtime API (plan 13 §4),
124+
// served to universal-backend inside the internal credentials response. Presence marks the
125+
// connection as backing a real generated site (Step 2a allow-list); its shape (auth/write/
126+
// owned-read rules) is consumed by universal-backend's SiteContractService in Step 2b. NULL for
127+
// every ordinary admin-panel connection.
128+
// jsonb value typed `Record<string, any>` to match the existing jsonb columns
129+
// (e.g. TableFiltersEntity.filters) and keep TypeORM's update() deep-partial typings happy.
130+
@Column({ type: 'jsonb', nullable: true, default: null })
131+
site_runtime_policy?: Record<string, any> | null;
132+
123133
/**
124134
* Non-persisted flag indicating whether credentials are currently in decrypted state.
125135
* Used by @BeforeUpdate to decide whether encryption is needed.

backend/src/entities/connection/repository/connection.repository.interface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ export interface IConnectionRepository {
3232

3333
updateConnectionPublicCedarPolicy(connectionId: string, publicCedarPolicy: string | null): Promise<void>;
3434

35+
getConnectionSiteRuntimePolicy(connectionId: string): Promise<Record<string, unknown> | null>;
36+
37+
updateConnectionSiteRuntimePolicy(
38+
connectionId: string,
39+
siteRuntimePolicy: Record<string, unknown> | null,
40+
): Promise<void>;
41+
3542
findOneAgentConnectionByToken(connectionToken: string): Promise<ConnectionEntity | null>;
3643

3744
decryptConnectionField(field: string): string;

backend/src/entities/connection/repository/custom-connection-repository-extension.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
216216
.execute();
217217
},
218218

219+
async getConnectionSiteRuntimePolicy(connectionId: string): Promise<Record<string, unknown> | null> {
220+
const connection = await this.createQueryBuilder('connection')
221+
.select(['connection.id', 'connection.site_runtime_policy'])
222+
.where('connection.id = :connectionId', { connectionId })
223+
.getOne();
224+
return connection?.site_runtime_policy ?? null;
225+
},
226+
227+
async updateConnectionSiteRuntimePolicy(
228+
connectionId: string,
229+
siteRuntimePolicy: Record<string, unknown> | null,
230+
): Promise<void> {
231+
await this.createQueryBuilder()
232+
.update(ConnectionEntity)
233+
.set({ site_runtime_policy: siteRuntimePolicy })
234+
.where('id = :connectionId', { connectionId })
235+
.execute();
236+
},
237+
219238
async isUserFromConnection(userId: string, connectionId: string): Promise<boolean> {
220239
const qb = this.createQueryBuilder('connection')
221240
.leftJoin('connection.groups', 'group')

backend/src/exceptions/text/messages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export const Messages = {
105105
PUBLIC_POLICY_ACTION_NOT_ALLOWED:
106106
'Public permissions may only grant QueryTable (table:query) and ColumnRead (column:read)',
107107
PUBLIC_ACCESS_NOT_CONFIGURED: 'Public access is not configured for this connection',
108+
PUBLIC_GRANT_ON_AUTH_TABLE_NOT_ALLOWED:
109+
"Public read cannot be granted on the site's visitor accounts table or its credential column",
110+
SITE_RUNTIME_POLICY_INVALID: 'Site runtime policy must be a JSON object',
108111
CSV_EXPORT_FAILED: 'CSV export failed',
109112
CSV_EXPORT_DISABLED: 'CSV export is disabled',
110113
CSV_IMPORT_FAILED: 'CSV import failed',

backend/src/microservices/agents-microservice/agents.controller.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
CompanySubscriptionInfoRO,
1717
PermissionAllowedRO,
1818
PublicPermissionsRO,
19+
SiteRuntimePolicyRO,
1920
ValidatedUserTokenRO,
2021
} from './data-structures/agents-responses.ds.js';
2122
import {
@@ -28,6 +29,7 @@ import {
2829
import { ValidateConnectionEditDto, ValidateTableAiRequestDto, ValidateUserTokenDto } from './dto/agents-auth.dtos.js';
2930
import { GetCompanySubscriptionInfoDto } from './dto/agents-company.dtos.js';
3031
import { SetAgentsPublicPermissionsDto } from './dto/agents-public-permissions.dtos.js';
32+
import { SetSiteRuntimePolicyDto } from './dto/agents-site-runtime-policy.dtos.js';
3133
import {
3234
IExecuteAiAggregationPipeline,
3335
IExecuteAiRawQuery,
@@ -38,6 +40,7 @@ import {
3840
IGetCompanySubscriptionInfo,
3941
IScanAndCreateSettings,
4042
ISetPublicPermissions,
43+
ISetSiteRuntimePolicy,
4144
IValidateConnectionEdit,
4245
IValidateTableAiRequest,
4346
IValidateUserToken,
@@ -59,6 +62,8 @@ export class AgentsController {
5962
private readonly validateConnectionEditUseCase: IValidateConnectionEdit,
6063
@Inject(UseCaseType.AGENTS_SET_PUBLIC_PERMISSIONS)
6164
private readonly setPublicPermissionsUseCase: ISetPublicPermissions,
65+
@Inject(UseCaseType.AGENTS_SET_SITE_RUNTIME_POLICY)
66+
private readonly setSiteRuntimePolicyUseCase: ISetSiteRuntimePolicy,
6267
@Inject(UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT)
6368
private readonly getAiConnectionContextUseCase: IGetAiConnectionContext,
6469
@Inject(UseCaseType.AGENTS_GET_AI_CONNECTION_TABLES)
@@ -127,6 +132,26 @@ export class AgentsController {
127132
);
128133
}
129134

135+
@ApiOperation({
136+
summary: 'Write the site data contract (runtime manifest) for a generated site — agent finalize flow',
137+
description:
138+
'Re-checks Cedar connection:edit for the given user, then stores the manifest on the connection ' +
139+
'(site_runtime_policy, replacing any previous one). universal-backend enforces it at the generated-site ' +
140+
'runtime: auth-table pinning, write allow-lists with ownership, owner-scoped reads (plan 13 Step 2b).',
141+
})
142+
@ApiResponse({ status: 201, type: SiteRuntimePolicyRO })
143+
@ApiBody({ type: SetSiteRuntimePolicyDto })
144+
@Post('/connection/site-runtime-policy/:connectionId')
145+
public async setSiteRuntimePolicy(
146+
@SlugUuid('connectionId') connectionId: string,
147+
@Body() body: SetSiteRuntimePolicyDto,
148+
): Promise<SiteRuntimePolicyRO> {
149+
return await this.setSiteRuntimePolicyUseCase.execute(
150+
{ connectionId, userId: body.userId, policy: body.policy },
151+
InTransactionEnum.OFF,
152+
);
153+
}
154+
130155
@ApiOperation({ summary: 'Get AI-relevant connection context (type, schema, MongoDB flag)' })
131156
@ApiResponse({ status: 201, type: AiConnectionContextRO })
132157
@ApiBody({ type: AiDataRequestBaseDto })

backend/src/microservices/agents-microservice/agents.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { GetAiTableStructureUseCase } from './use-cases/get-ai-table-structure.u
1313
import { GetCompanySubscriptionInfoUseCase } from './use-cases/get-company-subscription-info.use.case.js';
1414
import { ScanAndCreateSettingsUseCase } from './use-cases/scan-and-create-settings.use.case.js';
1515
import { SetPublicPermissionsUseCase } from './use-cases/set-public-permissions.use.case.js';
16+
import { SetSiteRuntimePolicyUseCase } from './use-cases/set-site-runtime-policy.use.case.js';
1617
import { ValidateConnectionEditUseCase } from './use-cases/validate-connection-edit.use.case.js';
1718
import { ValidateTableAiRequestUseCase } from './use-cases/validate-table-ai-request.use.case.js';
1819
import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.case.js';
@@ -40,6 +41,10 @@ import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.ca
4041
provide: UseCaseType.AGENTS_SET_PUBLIC_PERMISSIONS,
4142
useClass: SetPublicPermissionsUseCase,
4243
},
44+
{
45+
provide: UseCaseType.AGENTS_SET_SITE_RUNTIME_POLICY,
46+
useClass: SetSiteRuntimePolicyUseCase,
47+
},
4348
{
4449
provide: UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT,
4550
useClass: GetAiConnectionContextUseCase,

backend/src/microservices/agents-microservice/data-structures/agents-responses.ds.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ export class PublicPermissionsRO {
3434
tables: Array<{ tableName: string; readableColumns?: Array<string> }>;
3535
}
3636

37+
export class SiteRuntimePolicyRO {
38+
@ApiProperty({
39+
type: 'object',
40+
additionalProperties: true,
41+
description: 'The site runtime policy (data contract) now stored on the connection.',
42+
})
43+
policy: Record<string, unknown>;
44+
}
45+
3746
export class AiConnectionContextRO {
3847
@ApiProperty()
3948
connectionId: string;

backend/src/microservices/agents-microservice/data-structures/agents.ds.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export class SetPublicPermissionsDs {
1818
mode: 'merge' | 'replace';
1919
}
2020

21+
export class SetSiteRuntimePolicyDs {
22+
userId: string;
23+
connectionId: string;
24+
policy: Record<string, unknown>;
25+
}
26+
2127
export class AiDataRequestDs {
2228
connectionId: string;
2329
userId: string;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsObject, IsString } from 'class-validator';
3+
4+
export class SetSiteRuntimePolicyDto {
5+
@ApiProperty({ description: 'The user on whose behalf the manifest is written — must hold connection:edit (Cedar).' })
6+
@IsNotEmpty()
7+
@IsString()
8+
userId: string;
9+
10+
@ApiProperty({
11+
type: 'object',
12+
additionalProperties: true,
13+
description:
14+
'The site data contract (plan 13 §4): {auth, ownedRead, write}. Replaces the stored policy wholesale — ' +
15+
'the generation agent always proposes the complete manifest it derived from the schema it created. ' +
16+
'universal-backend parses it fail-closed, so unknown keys are inert and missing sections grant nothing.',
17+
})
18+
@IsObject()
19+
policy: Record<string, unknown>;
20+
}

0 commit comments

Comments
 (0)