• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 8421654220

25 Mar 2024 02:22PM CUT coverage: 79.934% (+53.8%) from 26.087%
8421654220

Pull #495

github

web-flow
Merge 4faeebea5 into 1869c986d
Pull Request #495: chore: add licenses for non-NPM packages

3256 of 3853 branches covered (84.51%)

25152 of 31466 relevant lines covered (79.93%)

1188.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.69
/apps/nestjs-backend/src/share-db/share-db-permission.service.ts
1
import { Injectable } from '@nestjs/common';
2✔
2
import { ANONYMOUS_USER_ID, IdPrefix } from '@teable/core';
2✔
3
import type { IShareViewMeta, PermissionAction } from '@teable/core';
2✔
4
import { PrismaService } from '@teable/db-main-prisma';
2✔
5
import { ClsService } from 'nestjs-cls';
2✔
6
import ShareDBClass from 'sharedb';
2✔
7
import { PermissionService } from '../features/auth/permission.service';
2✔
8
import { FieldService } from '../features/field/field.service';
2✔
9
import type { IClsStore } from '../types/cls';
2✔
10
import { getAction, getPrefixAction, isShareViewResourceDoc } from './utils';
2✔
11
import { WsAuthService } from './ws-auth.service';
2✔
12

2✔
13
type IContextDecorator = 'useCls' | 'skipIfBackend';
2✔
14
// eslint-disable-next-line @typescript-eslint/naming-convention
2✔
15
export function ContextDecorator(...args: IContextDecorator[]): MethodDecorator {
2✔
16
  return (_target: unknown, _propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
204✔
17
    const originalMethod = descriptor.value;
204✔
18

204✔
19
    descriptor.value = async function (
204✔
20
      context: IAuthMiddleContext,
196✔
21
      callback: (err?: unknown) => void
196✔
22
    ) {
196✔
23
      // Skip if the context is from the backend
196✔
24
      if (args.includes('skipIfBackend') && context.agent.custom.isBackend) {
196!
25
        callback();
×
26
        return;
×
27
      }
×
28
      // If 'useCls' is specified, set up the CLS context
196✔
29
      if (args.includes('useCls')) {
196✔
30
        const clsService: ClsService<IClsStore> = (this as ShareDbPermissionService).clsService;
96✔
31
        await clsService.runWith({ ...clsService.get() }, async () => {
96✔
32
          try {
96✔
33
            clsService.set('user', context.agent.custom.user);
96✔
34
            clsService.set('shareViewId', context.agent.custom.shareId);
96✔
35
            await originalMethod.apply(this, [context, callback]);
96✔
36
          } catch (error) {
96!
37
            callback(error);
×
38
          }
×
39
        });
96✔
40
        return;
96✔
41
      }
96✔
42
      // If 'useCls' is not specified, just call the original method
100✔
43
      try {
100✔
44
        await originalMethod.apply(this, [context, callback]);
100✔
45
      } catch (error) {
196!
46
        callback(error);
×
47
      }
×
48
    };
196✔
49
  };
204✔
50
}
204✔
51

2✔
52
export type IAuthMiddleContext =
2✔
53
  | ShareDBClass.middleware.ConnectContext
2✔
54
  | ShareDBClass.middleware.ApplyContext
2✔
55
  | ShareDBClass.middleware.ReadSnapshotsContext
2✔
56
  | ShareDBClass.middleware.QueryContext;
2✔
57

2✔
58
@Injectable()
2✔
59
export class ShareDbPermissionService {
2✔
60
  constructor(
64✔
61
    readonly clsService: ClsService<IClsStore>,
64✔
62
    private readonly permissionService: PermissionService,
64✔
63
    private readonly wsAuthService: WsAuthService,
64✔
64
    private readonly prismaService: PrismaService,
64✔
65
    private readonly fieldService: FieldService
64✔
66
  ) {}
64✔
67

64✔
68
  private async clsRunWith(
64✔
69
    context: IAuthMiddleContext,
98✔
70
    callback: (err?: unknown) => void,
98✔
71
    error?: unknown
98✔
72
  ) {
98✔
73
    await this.clsService.runWith(this.clsService.get(), async () => {
98✔
74
      this.clsService.set('user', context.agent.custom.user);
98✔
75
      this.clsService.set('shareViewId', context.agent.custom.shareId);
98✔
76
      callback(error);
98✔
77
    });
98✔
78
  }
98✔
79

64✔
80
  @ContextDecorator('skipIfBackend')
64✔
81
  async authMiddleware(context: IAuthMiddleContext, callback: (err?: unknown) => void) {
100✔
82
    try {
100✔
83
      const { cookie, shareId, sessionId } = context.agent.custom;
100✔
84
      if (shareId) {
100✔
85
        context.agent.custom.user = { id: ANONYMOUS_USER_ID, name: ANONYMOUS_USER_ID, email: '' };
10✔
86
        await this.wsAuthService.checkShareCookie(shareId, cookie);
10✔
87
      } else {
100✔
88
        const user = await this.wsAuthService.checkSession(sessionId);
90✔
89
        context.agent.custom.user = user;
90✔
90
      }
90✔
91
      await this.clsRunWith(context, callback);
98✔
92
    } catch (error) {
100✔
93
      callback(error);
2✔
94
    }
2✔
95
  }
100✔
96

64✔
97
  private async runPermissionCheck(collection: string, permissionAction: PermissionAction) {
64✔
98
    const [docType, collectionId] = collection.split('_');
92✔
99
    try {
92✔
100
      if (docType === IdPrefix.Table) {
92!
101
        await this.permissionService.checkPermissionByBaseId(collectionId, [permissionAction]);
×
102
      } else {
92✔
103
        await this.permissionService.checkPermissionByTableId(collectionId, [permissionAction]);
92✔
104
      }
92✔
105
    } catch (e) {
92!
106
      return e;
×
107
    }
×
108
  }
92✔
109

64✔
110
  @ContextDecorator('skipIfBackend', 'useCls')
64✔
111
  async checkApplyPermissionMiddleware(
52✔
112
    context: ShareDBClass.middleware.ApplyContext,
52✔
113
    callback: (err?: unknown) => void
52✔
114
  ) {
52✔
115
    const { op, collection } = context;
52✔
116
    const [docType] = collection.split('_');
52✔
117
    const prefixAction = getPrefixAction(docType as IdPrefix);
52✔
118
    const action = getAction(op);
52✔
119
    if (!prefixAction || !action) {
52!
120
      callback(`unknown docType: ${docType}`);
×
121
      return;
×
122
    }
×
123
    const error = await this.runPermissionCheck(collection, `${prefixAction}|${action}`);
52✔
124
    callback(error);
52✔
125
  }
52✔
126

64✔
127
  @ContextDecorator('skipIfBackend', 'useCls')
64✔
128
  async checkReadPermissionMiddleware(
44✔
129
    context: ShareDBClass.middleware.ReadSnapshotsContext,
44✔
130
    callback: (err?: unknown) => void
44✔
131
  ) {
44✔
132
    const [docType] = context.collection.split('_');
44✔
133
    const prefixAction = getPrefixAction(docType as IdPrefix);
44✔
134
    if (!prefixAction) {
44!
135
      callback(`unknown docType: ${docType}`);
×
136
      return;
×
137
    }
×
138
    // view share permission validation
44✔
139
    const shareId = context.agent.custom.shareId;
44✔
140
    if (shareId && isShareViewResourceDoc(docType as IdPrefix)) {
44✔
141
      const error = await this.checkReadViewSharePermission(
4✔
142
        shareId,
4✔
143
        context.collection,
4✔
144
        context.snapshots
4✔
145
      );
4✔
146
      callback(error);
4✔
147
      return;
4✔
148
    }
4✔
149
    const error = await this.runPermissionCheck(context.collection, `${prefixAction}|read`);
40✔
150
    callback(error);
40✔
151
  }
40✔
152

64✔
153
  async checkReadViewSharePermission(
64✔
154
    shareId: string,
4✔
155
    collection: string,
4✔
156
    snapshots: ShareDBClass.Snapshot[]
4✔
157
  ) {
4✔
158
    const [docType, tableId] = collection.split('_');
4✔
159
    const view = await this.prismaService.txClient().view.findFirst({
4✔
160
      where: { shareId, tableId, deletedTime: null, enableShare: true },
4✔
161
    });
4✔
162
    if (!view) {
4!
163
      return `invalid shareId: ${shareId}`;
×
164
    }
×
165
    const shareMeta = (JSON.parse(view.shareMeta as string) as IShareViewMeta) || {};
4✔
166
    const checkSnapshot = (checkSnapshotMethod: (snapshot: ShareDBClass.Snapshot) => boolean) =>
4✔
167
      snapshots.every(checkSnapshotMethod);
4✔
168

4✔
169
    // share view resource (field, view in share)
4✔
170
    switch (docType as IdPrefix) {
4✔
171
      case IdPrefix.Field:
4✔
172
        {
2✔
173
          const { ids } = await this.fieldService.getDocIdsByQuery(tableId, {
2✔
174
            viewId: view.id,
2✔
175
            filterHidden: !shareMeta.includeHiddenField,
2✔
176
          });
2✔
177
          const fieldIds = new Set(ids);
2✔
178
          if (!checkSnapshot((snapshot) => fieldIds.has(snapshot.id)))
2✔
179
            return 'no permission read field';
2!
180
        }
2✔
181
        break;
2✔
182
      case IdPrefix.View:
4✔
183
        {
2✔
184
          if (!checkSnapshot((snapshot) => view.id === snapshot.id))
2✔
185
            return 'no permission read view';
2!
186
        }
2✔
187
        break;
2✔
188
      case IdPrefix.Record:
4!
189
        return;
×
190
      default:
4!
191
        return 'unknown docType for read permission check';
×
192
    }
4✔
193
  }
4✔
194
}
64✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc