Skip to content

Commit

Permalink
fix: export should limit by permission (#1215)
Browse files Browse the repository at this point in the history
* fix: export should limit by permission

* fix: import table add projection field option

* fix: inplace import field should be limit by auth
  • Loading branch information
caoxing9 authored Jan 9, 2025
1 parent ba36769 commit aa6ccf8
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ExportOpenApiService } from './export-open-api.service';

@Controller('api/export')
@UseGuards(PermissionGuard)
export class ExportController {
export class ExportOpenApiController {
constructor(private readonly exportOpenService: ExportOpenApiService) {}
@Get(':tableId')
@Permissions('table|export', 'view|read')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { FieldModule } from '../../field/field.module';
import { RecordModule } from '../../record/record.module';
import { ExportController } from './export-open-api.controller';
import { ExportOpenApiController } from './export-open-api.controller';
import { ExportOpenApiService } from './export-open-api.service';

@Module({
imports: [RecordModule, FieldModule],
controllers: [ExportController],
controllers: [ExportOpenApiController],
providers: [ExportOpenApiService],
exports: [ExportOpenApiService],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Readable } from 'stream';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import type { IAttachmentCellValue } from '@teable/core';
import { FieldType, ViewType } from '@teable/core';
import type { IAttachmentCellValue, IFilter } from '@teable/core';
import { FieldType, mergeFilter, ViewType } from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import type { Response } from 'express';
import Papa from 'papaparse';
Expand All @@ -17,7 +17,15 @@ export class ExportOpenApiService {
private readonly recordService: RecordService,
private readonly prismaService: PrismaService
) {}
async exportCsvFromTable(response: Response, tableId: string, viewId?: string) {
async exportCsvFromTable(
response: Response,
tableId: string,
viewId?: string,
exportQuery?: {
projection?: string[];
recordFilter?: IFilter;
}
) {
let count = 0;
let isOver = false;
const csvStream = new Readable({
Expand Down Expand Up @@ -47,6 +55,7 @@ export class ExportOpenApiService {
name: true,
id: true,
type: true,
filter: true,
},
})
.catch((e) => {
Expand All @@ -68,9 +77,17 @@ export class ExportOpenApiService {
csvStream.pipe(response);

// set headers as first row
const headers = await this.fieldService.getFieldsByQuery(tableId, {
viewId: viewRaw?.id ? viewRaw?.id : undefined,
filterHidden: viewRaw?.id ? true : undefined,
const headers = (
await this.fieldService.getFieldsByQuery(tableId, {
viewId: viewRaw?.id ? viewRaw?.id : undefined,
filterHidden: viewRaw?.id ? true : undefined,
})
).filter((field) => {
if (exportQuery?.projection?.length) {
return exportQuery?.projection.includes(field.id);
}

return true;
});
const headerData = Papa.unparse([headers.map((h) => h.name)]);

Expand All @@ -89,12 +106,17 @@ export class ExportOpenApiService {
csvStream.push('\uFEFF');
csvStream.push(headerData);

const mergedFilter = viewRaw?.filter
? mergeFilter(JSON.parse(viewRaw?.filter), exportQuery?.recordFilter)
: exportQuery?.recordFilter;

try {
while (!isOver) {
const { records } = await this.recordService.getRecords(tableId, {
take: 1000,
skip: count,
viewId: viewRaw?.id ? viewRaw?.id : undefined,
filter: mergedFilter,
});
if (records.length === 0) {
isOver = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Worker } from 'worker_threads';
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { Injectable, Logger, BadRequestException, ForbiddenException } from '@nestjs/common';
import type { IFieldRo } from '@teable/core';
import {
FieldType,
Expand All @@ -15,7 +15,7 @@ import type {
IInplaceImportOptionRo,
IImportColumn,
} from '@teable/openapi';
import { toString } from 'lodash';
import { difference, toString } from 'lodash';
import { ClsService } from 'nestjs-cls';
import type { CreateOp } from 'sharedb';
import type { LocalPresence } from 'sharedb/lib/client';
Expand Down Expand Up @@ -128,7 +128,8 @@ export class ImportOpenApiService {
baseId: string,
tableId: string,
inplaceImportRo: IInplaceImportOptionRo,
maxRowCount?: number
maxRowCount?: number,
projection?: string[]
) {
const userId = this.cls.get('user.id');
const { attachmentUrl, fileType, insertConfig, notification = false } = inplaceImportRo;
Expand All @@ -152,6 +153,15 @@ export class ImportOpenApiService {
},
});

if (projection?.length) {
const inplaceFieldIds = Object.keys(sourceColumnMap);
const noUpdateFields = difference(projection, inplaceFieldIds);
if (noUpdateFields.length !== 0) {
const tips = noUpdateFields.join(',');
throw new ForbiddenException(`There is no permission to update there field ${tips}`);
}
}

if (!tableRaw || !fieldRaws) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { useQuery } from '@tanstack/react-query';
import type { IInplaceImportOptionRo, IImportOptionRo } from '@teable/openapi';
import { getTableById as apiGetTableById, getFields as apiGetFields } from '@teable/openapi';
import {
getTableById as apiGetTableById,
getFields as apiGetFields,
getTablePermission,
} from '@teable/openapi';
import { ReactQueryKeys } from '@teable/sdk/config';
import { useBaseId } from '@teable/sdk/hooks';
import { TablePermissionContext } from '@teable/sdk/context/table-permission/TablePermissionContext';
import { useBaseId, useField, useFields, useTable, useTablePermission } from '@teable/sdk/hooks';
import { isEqual } from 'lodash';
import { useMemo } from 'react';
import { useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { InplaceImportOptionPanel } from '../CollapsePanel';
import { InplacePreviewColumn } from './InplacePreviewColumn';
Expand Down Expand Up @@ -45,6 +50,20 @@ const InplaceFieldConfigPanel = (props: IInplaceFieldConfigPanel) => {
queryFn: () => apiGetFields(tableId).then((data) => data.data),
});

const { data: tablePermission } = useQuery({
queryKey: ReactQueryKeys.getTablePermission(baseId!, tableId!),
queryFn: ({ queryKey }) => getTablePermission(queryKey[1], queryKey[2]).then((res) => res.data),
enabled: !!tableId,
});

const hasReadPermissionFields = Object.entries(tablePermission?.field?.fields || {})
.filter(([, value]) => {
return value['field|read'];
})
.map(([key]) => key);

const fieldWithPermission = fields?.filter(({ id }) => hasReadPermissionFields.includes(id));

const optionHandler = (value: IInplaceOption, propertyName: keyof IInplaceOption) => {
const newInsertConfig = {
...insertConfig,
Expand Down Expand Up @@ -82,12 +101,12 @@ const InplaceFieldConfigPanel = (props: IInplaceFieldConfigPanel) => {
</p>
</div>

{fields && (
{fieldWithPermission && (
<div className="my-2 h-[400px] overflow-y-auto rounded-sm border border-secondary">
<InplacePreviewColumn
onChange={columnHandler}
workSheets={workSheets}
fields={fields}
fields={fieldWithPermission}
insertConfig={insertConfig}
></InplacePreviewColumn>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
{t('view.action.rename')}
</Button>
)}
{view.type === 'grid' && permission['view|read'] && (
{view.type === 'grid' && permission['table|export'] && (
<Button
size="xs"
variant="ghost"
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/auth/role/table.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { z } from '../../zod';
import { type FieldAction, type RecordAction, type ViewAction } from '../actions';
import type { TableAction, FieldAction, RecordAction, ViewAction } from '../actions';
import { Role } from './types';

export const TableRole = {
Expand All @@ -13,4 +13,4 @@ export const tableRolesSchema = z.nativeEnum(TableRole);

export type ITableRole = z.infer<typeof tableRolesSchema>;

export type TablePermission = ViewAction | FieldAction | RecordAction;
export type TablePermission = ViewAction | FieldAction | RecordAction | TableAction;

0 comments on commit aa6ccf8

Please sign in to comment.