Skip to content

Commit 8b45b90

Browse files
authored
Merge pull request #206 from import-ai/refactor/resources
refactor(resources): add cycle detection
2 parents e0dca1e + be71b1a commit 8b45b90

14 files changed

+276
-107
lines changed

src/interceptor/user.interceptor.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { Observable } from 'rxjs';
88
import { tap, finalize } from 'rxjs/operators';
99
import { trace, context } from '@opentelemetry/api';
10+
import { Socket } from 'socket.io';
1011

1112
const LOGIN_URLS = ['/api/v1/login', '/api/v1/sign-up/confirm'];
1213

@@ -16,24 +17,33 @@ export class UserInterceptor implements NestInterceptor {
1617
executionContext: ExecutionContext,
1718
next: CallHandler,
1819
): Observable<any> {
19-
const req = executionContext.switchToHttp().getRequest();
2020
const tracer = trace.getTracer('user-interceptor');
21-
2221
return tracer.startActiveSpan(
2322
'UserInterceptor',
2423
{},
2524
context.active(),
2625
(span) => {
2726
return next.handle().pipe(
2827
tap((responseBody) => {
29-
if (req.user?.id) {
30-
span.setAttribute('user.id', req.user.id);
31-
} else if (
32-
LOGIN_URLS.includes(req.url) &&
33-
req.method === 'POST' &&
34-
responseBody?.id
35-
) {
36-
span.setAttribute('user.id', responseBody.id);
28+
const ctxType = executionContext.getType();
29+
let userId: string | null = null;
30+
if (ctxType === 'http') {
31+
const httpReq = executionContext.switchToHttp().getRequest();
32+
if (httpReq.user?.id) {
33+
userId = httpReq.user.id;
34+
} else if (
35+
LOGIN_URLS.includes(httpReq.url) &&
36+
httpReq.method === 'POST' &&
37+
responseBody?.id
38+
) {
39+
userId = responseBody.id;
40+
}
41+
} else if (ctxType === 'ws') {
42+
const client = executionContext.switchToWs().getClient<Socket>();
43+
userId = client.data.userId;
44+
}
45+
if (userId) {
46+
span.setAttribute('user.id', userId);
3747
}
3848
}),
3949
finalize(() => span.end()),
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {
2+
Injectable,
3+
NestInterceptor,
4+
ExecutionContext,
5+
CallHandler,
6+
} from '@nestjs/common';
7+
import { Observable } from 'rxjs';
8+
import { finalize } from 'rxjs/operators';
9+
import { trace, context } from '@opentelemetry/api';
10+
11+
@Injectable()
12+
export class WsSpanInterceptor implements NestInterceptor {
13+
intercept(
14+
executionContext: ExecutionContext,
15+
next: CallHandler,
16+
): Observable<any> {
17+
const wsContext = executionContext.switchToWs();
18+
const messageName = wsContext.getPattern();
19+
const spanName = `SOCKET /api/v1/socket.io/wizard/${messageName}`;
20+
const tracer = trace.getTracer('ws-span-interceptor');
21+
return tracer.startActiveSpan(spanName, {}, context.active(), (span) => {
22+
return next.handle().pipe(finalize(() => span.end()));
23+
});
24+
}
25+
}

src/namespace-resources/file-resources.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class FileResourcesController {
3737
parentId,
3838
undefined,
3939
);
40-
return await this.namespaceResourcesService.getPath({
40+
return await this.namespaceResourcesService.getResource({
4141
namespaceId,
4242
userId,
4343
resourceId: newResource.id,
@@ -92,7 +92,7 @@ export class FileResourcesController {
9292
mimetype,
9393
parentId,
9494
);
95-
return await this.namespaceResourcesService.getPath({
95+
return await this.namespaceResourcesService.getResource({
9696
namespaceId,
9797
userId: req.user!.id,
9898
resourceId: newResource.id,

src/namespace-resources/namespace-resources.controller.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class NamespaceResourcesController {
5353
userId,
5454
data,
5555
);
56-
return await this.namespaceResourcesService.getPath({
56+
return await this.namespaceResourcesService.getResource({
5757
namespaceId: data.namespaceId,
5858
resourceId: newResource.id,
5959
userId: userId,
@@ -68,9 +68,10 @@ export class NamespaceResourcesController {
6868
) {
6969
const newResource = await this.namespaceResourcesService.duplicate(
7070
req.user!.id,
71+
namespaceId,
7172
resourceId,
7273
);
73-
return await this.namespaceResourcesService.getPath({
74+
return await this.namespaceResourcesService.getResource({
7475
namespaceId,
7576
userId: req.user!.id,
7677
resourceId: newResource.id,
@@ -141,7 +142,7 @@ export class NamespaceResourcesController {
141142
@Param('namespaceId') namespaceId: string,
142143
@Param('resourceId') resourceId: string,
143144
) {
144-
return await this.namespaceResourcesService.getPath({
145+
return await this.namespaceResourcesService.getResource({
145146
namespaceId,
146147
resourceId,
147148
userId: req.user!.id,
@@ -165,7 +166,7 @@ export class NamespaceResourcesController {
165166
throw new ForbiddenException('Not authorized');
166167
}
167168
await this.namespaceResourcesService.update(req.user!.id, resourceId, data);
168-
return await this.namespaceResourcesService.getPath({
169+
return await this.namespaceResourcesService.getResource({
169170
namespaceId,
170171
resourceId,
171172
userId: req.user!.id,
@@ -189,6 +190,7 @@ export class NamespaceResourcesController {
189190
}
190191
return await this.namespaceResourcesService.delete(
191192
req.user!.id,
193+
namespaceId,
192194
resourceId,
193195
);
194196
}
@@ -200,7 +202,7 @@ export class NamespaceResourcesController {
200202
@Param('resourceId') resourceId: string,
201203
) {
202204
await this.namespaceResourcesService.restore(req.user!.id, resourceId);
203-
return await this.namespaceResourcesService.getPath({
205+
return await this.namespaceResourcesService.getResource({
204206
namespaceId,
205207
resourceId,
206208
userId: req.user!.id,

src/namespace-resources/namespace-resources.service.ts

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,11 @@ export class NamespaceResourcesService {
202202
);
203203
}
204204

205-
async duplicate(userId: string, resourceId: string) {
206-
const resource = await this.get(resourceId);
205+
async duplicate(userId: string, namespaceId: string, resourceId: string) {
206+
const resource = await this.resourcesService.getResourceOrFail(
207+
namespaceId,
208+
resourceId,
209+
);
207210
if (!resource.parentId) {
208211
throw new BadRequestException('Cannot duplicate root resource.');
209212
}
@@ -361,9 +364,9 @@ export class NamespaceResourcesService {
361364
// Self and child exclusions
362365
if (excludeResourceId) {
363366
const resourceChildren = await this.getSubResourcesByUser(
367+
userId,
364368
namespaceId,
365369
excludeResourceId,
366-
userId,
367370
);
368371
where.id = Not(
369372
In([
@@ -390,9 +393,9 @@ export class NamespaceResourcesService {
390393
}
391394

392395
async getSubResourcesByUser(
396+
userId: string,
393397
namespaceId: string,
394398
resourceId: string,
395-
userId: string,
396399
): Promise<ResourceMetaDto[]> {
397400
const parents = await this.resourcesService.getParentResourcesOrFail(
398401
namespaceId,
@@ -452,8 +455,8 @@ export class NamespaceResourcesService {
452455
);
453456
const resources = [...parents, ...children, ...subChildren];
454457
const permissionMap = await this.permissionsService.getCurrentPermissions(
455-
namespaceId,
456458
userId,
459+
namespaceId,
457460
resources,
458461
);
459462

@@ -494,7 +497,7 @@ export class NamespaceResourcesService {
494497
return count > 0 ? SpaceType.TEAM : SpaceType.PRIVATE;
495498
}
496499

497-
async getPath({
500+
async getResource({
498501
userId,
499502
namespaceId,
500503
resourceId,
@@ -503,7 +506,10 @@ export class NamespaceResourcesService {
503506
namespaceId: string;
504507
resourceId: string;
505508
}): Promise<ResourceDto> {
506-
const resource = await this.get(resourceId);
509+
const resource = await this.resourcesService.getResourceOrFail(
510+
namespaceId,
511+
resourceId,
512+
);
507513
if (resource.namespaceId !== namespaceId) {
508514
throw new NotFoundException('Not found');
509515
}
@@ -541,18 +547,6 @@ export class NamespaceResourcesService {
541547
);
542548
}
543549

544-
async get(id: string) {
545-
const resource = await this.resourceRepository.findOne({
546-
where: {
547-
id,
548-
},
549-
});
550-
if (!resource) {
551-
throw new NotFoundException('Resource not found.');
552-
}
553-
return resource;
554-
}
555-
556550
async update(userId: string, resourceId: string, data: UpdateResourceDto) {
557551
await this.resourcesService.updateResource(
558552
data.namespaceId,
@@ -567,8 +561,11 @@ export class NamespaceResourcesService {
567561
);
568562
}
569563

570-
async delete(userId: string, id: string) {
571-
const resource = await this.get(id);
564+
async delete(userId: string, namespaceId: string, id: string) {
565+
const resource = await this.resourcesService.getResourceOrFail(
566+
namespaceId,
567+
id,
568+
);
572569
if (!resource.parentId) {
573570
throw new BadRequestException('Cannot delete root resource.');
574571
}
@@ -658,7 +655,10 @@ export class NamespaceResourcesService {
658655
const encodedName = encodeFileName(fileName);
659656
let resource: Resource;
660657
if (resourceId) {
661-
resource = await this.get(resourceId);
658+
resource = await this.resourcesService.getResourceOrFail(
659+
namespaceId,
660+
resourceId,
661+
);
662662
if (resource.resourceType !== ResourceType.FILE) {
663663
throw new BadRequestException('Resource is not a file.');
664664
}
@@ -714,7 +714,10 @@ export class NamespaceResourcesService {
714714

715715
let resource: Resource;
716716
if (resourceId) {
717-
resource = await this.get(resourceId);
717+
resource = await this.resourcesService.getResourceOrFail(
718+
namespaceId,
719+
resourceId,
720+
);
718721
if (resource.resourceType !== ResourceType.FILE) {
719722
throw new BadRequestException('Resource is not a file.');
720723
}
@@ -778,30 +781,17 @@ export class NamespaceResourcesService {
778781
return { fileStream, resource };
779782
}
780783

781-
async listAllUserAccessibleResources(
782-
namespaceId: string,
784+
async getAllResourcesByUser(
783785
userId: string,
786+
namespaceId: string,
784787
includeRoot: boolean = false,
785-
) {
786-
const resources = await this.resourceRepository.find({
787-
where: { namespaceId, deletedAt: IsNull() },
788-
});
789-
const filteredResources = await this.permissionFilter(
790-
namespaceId,
788+
): Promise<ResourceMetaDto[]> {
789+
const resources = await this.permissionsService.filterResourcesByPermission(
791790
userId,
792-
resources.filter((res) => res.parentId !== null || includeRoot),
793-
);
794-
795-
// Load tags for filtered resources
796-
const tagsMap = await this.getTagsForResources(
797791
namespaceId,
798-
filteredResources,
792+
await this.resourcesService.getAllResources(namespaceId),
799793
);
800-
801-
return filteredResources.map((resource) => ({
802-
...resource,
803-
tags: tagsMap.get(resource.id) || [],
804-
}));
794+
return resources.filter((res) => res.parentId !== null || includeRoot);
805795
}
806796

807797
async listAllResources(offset: number, limit: number) {

src/permissions/permissions.service.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,17 +162,21 @@ export class PermissionsService {
162162
return curPermission || ResourcePermission.NO_ACCESS;
163163
}
164164

165+
/**
166+
* Get the current permissions for each specified resource.
167+
* For each non-root resource specified, it's required that all its parents are also specified.
168+
*/
165169
async getCurrentPermissions(
166-
namespaceId: string,
167170
userId: string,
171+
namespaceId: string,
168172
resources: ResourceMetaDto[],
169173
entityManager?: EntityManager,
170174
): Promise<Map<string, ResourcePermission>> {
171175
if (!entityManager) {
172176
return await this.dataSource.transaction((entityManager) =>
173177
this.getCurrentPermissions(
174-
namespaceId,
175178
userId,
179+
namespaceId,
176180
resources,
177181
entityManager,
178182
),
@@ -495,6 +499,31 @@ export class PermissionsService {
495499
);
496500
return comparePermission(permission, requiredPermission) >= 0;
497501
}
502+
503+
/**
504+
* Filter resources by permission.
505+
* For each non-root resource specified, it's required that all its parents are also specified.
506+
*/
507+
async filterResourcesByPermission(
508+
userId: string,
509+
namespaceId: string,
510+
resources: ResourceMetaDto[],
511+
requiredPermission: ResourcePermission = ResourcePermission.CAN_VIEW,
512+
entityManager?: EntityManager,
513+
): Promise<ResourceMetaDto[]> {
514+
const permissions = await this.getCurrentPermissions(
515+
userId,
516+
namespaceId,
517+
resources,
518+
entityManager,
519+
);
520+
return resources.filter((res) => {
521+
const permission = permissions.get(res.id);
522+
return (
523+
permission && comparePermission(permission, requiredPermission) >= 0
524+
);
525+
});
526+
}
498527
}
499528

500529
function getGlobalPermission(

0 commit comments

Comments
 (0)