From cc4a4ec7963ff23ce70f5b05267066b5226ae5e4 Mon Sep 17 00:00:00 2001 From: Charles Zhou Date: Mon, 17 Jun 2024 03:06:32 -0500 Subject: [PATCH] feat: permission and security fixes (#5266) --- api/controllers/console/app/app.py | 15 +++++ api/controllers/console/app/workflow.py | 54 +++++++++++++++++- .../console/auth/data_source_bearer_auth.py | 18 ++++-- web/app/(commonLayout)/apps/AppCard.tsx | 38 ++++++------- web/app/components/app-sidebar/app-info.tsx | 13 +++-- .../datasets/create/website/index.tsx | 10 ++-- .../data-source-notion/index.tsx | 2 +- .../config-firecrawl-modal.tsx | 4 +- .../data-source-website/index.tsx | 48 +++++++++------- .../data-source-page/panel/config-item.tsx | 4 +- .../data-source-page/panel/index.tsx | 56 +++++++++---------- web/models/common.ts | 22 +++----- web/service/datasets.ts | 6 +- 13 files changed, 186 insertions(+), 104 deletions(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 4b852c1a49..082838334a 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -129,6 +129,10 @@ class AppApi(Resource): @marshal_with(app_detail_fields_with_site) def put(self, app_model): """Update app""" + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('name', type=str, required=True, nullable=False, location='json') parser.add_argument('description', type=str, location='json') @@ -147,6 +151,7 @@ class AppApi(Resource): @get_app_model def delete(self, app_model): """Delete app""" + # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() @@ -203,6 +208,10 @@ class AppNameApi(Resource): @get_app_model @marshal_with(app_detail_fields) def post(self, app_model): + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('name', type=str, required=True, location='json') args = parser.parse_args() @@ -220,6 +229,10 @@ class AppIconApi(Resource): @get_app_model @marshal_with(app_detail_fields) def post(self, app_model): + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('icon', type=str, location='json') parser.add_argument('icon_background', type=str, location='json') @@ -241,6 +254,7 @@ class AppSiteStatus(Resource): # The role of the current user in the ta table must be admin, owner, or editor if not current_user.is_editor: raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('enable_site', type=bool, required=True, location='json') args = parser.parse_args() @@ -261,6 +275,7 @@ class AppApiStatus(Resource): # The role of the current user in the ta table must be admin or owner if not current_user.is_admin_or_owner: raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('enable_api', type=bool, required=True, location='json') args = parser.parse_args() diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 641997f3f3..668a722bf7 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -3,7 +3,7 @@ import logging from flask import abort, request from flask_restful import Resource, marshal_with, reqparse -from werkzeug.exceptions import InternalServerError, NotFound +from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.console import api @@ -36,6 +36,10 @@ class DraftWorkflowApi(Resource): """ Get draft workflow """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + # fetch draft workflow by app_model workflow_service = WorkflowService() workflow = workflow_service.get_draft_workflow(app_model=app_model) @@ -54,6 +58,10 @@ class DraftWorkflowApi(Resource): """ Sync draft workflow """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + content_type = request.headers.get('Content-Type') if 'application/json' in content_type: @@ -110,6 +118,10 @@ class AdvancedChatDraftWorkflowRunApi(Resource): """ Run draft workflow """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('inputs', type=dict, location='json') parser.add_argument('query', type=str, required=True, location='json', default='') @@ -146,6 +158,10 @@ class AdvancedChatDraftRunIterationNodeApi(Resource): """ Run draft workflow iteration node """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('inputs', type=dict, location='json') args = parser.parse_args() @@ -179,6 +195,10 @@ class WorkflowDraftRunIterationNodeApi(Resource): """ Run draft workflow iteration node """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('inputs', type=dict, location='json') args = parser.parse_args() @@ -212,6 +232,10 @@ class DraftWorkflowRunApi(Resource): """ Run draft workflow """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('inputs', type=dict, required=True, nullable=False, location='json') parser.add_argument('files', type=list, required=False, location='json') @@ -243,6 +267,10 @@ class WorkflowTaskStopApi(Resource): """ Stop workflow task """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id) return { @@ -260,6 +288,10 @@ class DraftWorkflowNodeRunApi(Resource): """ Run draft workflow node """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('inputs', type=dict, required=True, nullable=False, location='json') args = parser.parse_args() @@ -286,6 +318,10 @@ class PublishedWorkflowApi(Resource): """ Get published workflow """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + # fetch published workflow by app_model workflow_service = WorkflowService() workflow = workflow_service.get_published_workflow(app_model=app_model) @@ -301,6 +337,10 @@ class PublishedWorkflowApi(Resource): """ Publish workflow """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + workflow_service = WorkflowService() workflow = workflow_service.publish_workflow(app_model=app_model, account=current_user) @@ -319,6 +359,10 @@ class DefaultBlockConfigsApi(Resource): """ Get default block config """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + # Get default block configs workflow_service = WorkflowService() return workflow_service.get_default_block_configs() @@ -333,6 +377,10 @@ class DefaultBlockConfigApi(Resource): """ Get default block config """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + parser = reqparse.RequestParser() parser.add_argument('q', type=str, location='args') args = parser.parse_args() @@ -363,6 +411,10 @@ class ConvertToWorkflowApi(Resource): Convert expert mode of chatbot app to workflow mode Convert Completion App to Workflow App """ + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + if request.data: parser = reqparse.RequestParser() parser.add_argument('name', type=str, required=False, nullable=True, location='json') diff --git a/api/controllers/console/auth/data_source_bearer_auth.py b/api/controllers/console/auth/data_source_bearer_auth.py index 81678f61fc..f79b93b74f 100644 --- a/api/controllers/console/auth/data_source_bearer_auth.py +++ b/api/controllers/console/auth/data_source_bearer_auth.py @@ -16,15 +16,21 @@ class ApiKeyAuthDataSource(Resource): @login_required @account_initialization_required def get(self): - # The role of the current user in the table must be admin or owner - if not current_user.is_admin_or_owner: - raise Forbidden() data_source_api_key_bindings = ApiKeyAuthService.get_provider_auth_list(current_user.current_tenant_id) if data_source_api_key_bindings: return { - 'settings': [data_source_api_key_binding.to_dict() for data_source_api_key_binding in - data_source_api_key_bindings]} - return {'settings': []} + 'sources': [{ + 'id': data_source_api_key_binding.id, + 'category': data_source_api_key_binding.category, + 'provider': data_source_api_key_binding.provider, + 'disabled': data_source_api_key_binding.disabled, + 'created_at': int(data_source_api_key_binding.created_at.timestamp()), + 'updated_at': int(data_source_api_key_binding.updated_at.timestamp()), + } + for data_source_api_key_binding in + data_source_api_key_bindings] + } + return {'sources': []} class ApiKeyAuthDataSourceBinding(Resource): diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 59040f207d..c36090d476 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -279,27 +279,27 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { 'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]', tags.length ? 'flex' : '!hidden group-hover:!flex', )}> -
{ - e.stopPropagation() - e.preventDefault() - }}> -
- tag.id)} - selectedTags={tags} - onCacheUpdate={setTags} - onChange={onRefresh} - /> -
-
{isCurrentWorkspaceEditor && ( <> +
{ + e.stopPropagation() + e.preventDefault() + }}> +
+ tag.id)} + selectedTags={tags} + onCacheUpdate={setTags} + onChange={onRefresh} + /> +
+
{ setShowConfirmDelete(false) }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t]) + const { isCurrentWorkspaceEditor } = useAppContext() + if (!appDetail) return null @@ -154,10 +156,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => { >
setOpen(v => !v)} + onClick={() => { + if (isCurrentWorkspaceEditor) + setOpen(v => !v) + }} className='block' > -
+
{
{appDetail.name}
- + {isCurrentWorkspaceEditor && }
{appDetail.mode === 'advanced-chat' && ( diff --git a/web/app/components/datasets/create/website/index.tsx b/web/app/components/datasets/create/website/index.tsx index 14ac40163a..e06fbb4a12 100644 --- a/web/app/components/datasets/create/website/index.tsx +++ b/web/app/components/datasets/create/website/index.tsx @@ -5,8 +5,8 @@ import NoData from './no-data' import Firecrawl from './firecrawl' import { useModalContext } from '@/context/modal-context' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' -import { fetchFirecrawlApiKey } from '@/service/datasets' -import { type DataSourceWebsiteItem, WebsiteProvider } from '@/models/common' +import { fetchDataSources } from '@/service/datasets' +import { type DataSourceItem, DataSourceProvider } from '@/models/common' type Props = { onPreview: (payload: CrawlResultItem) => void @@ -29,9 +29,9 @@ const Website: FC = ({ const [isLoaded, setIsLoaded] = useState(false) const [isSetFirecrawlApiKey, setIsSetFirecrawlApiKey] = useState(false) const checkSetApiKey = useCallback(async () => { - const res = await fetchFirecrawlApiKey() as any - const list = res.settings.filter((item: DataSourceWebsiteItem) => item.provider === WebsiteProvider.fireCrawl && !item.disabled) - setIsSetFirecrawlApiKey(list.length > 0) + const res = await fetchDataSources() as any + const isFirecrawlSet = res.sources.some((item: DataSourceItem) => item.provider === DataSourceProvider.fireCrawl) + setIsSetFirecrawlApiKey(isFirecrawlSet) }, []) useEffect(() => { diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx index f5541999a4..b0db511550 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx @@ -58,7 +58,7 @@ const DataSourceNotion: FC = ({ type={DataSourceType.notion} isConfigured={connected} onConfigure={handleConnectNotion} - readonly={!isCurrentWorkspaceManager} + readOnly={!isCurrentWorkspaceManager} isSupportList configuredList={workspaces.map(workspace => ({ id: workspace.id, diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx index 21277c8ec1..983fb29b03 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx @@ -11,7 +11,7 @@ import Button from '@/app/components/base/button' import type { FirecrawlConfig } from '@/models/common' import Field from '@/app/components/datasets/create/website/firecrawl/base/field' import Toast from '@/app/components/base/toast' -import { createFirecrawlApiKey } from '@/service/datasets' +import { createDataSourceApiKeyBinding } from '@/service/datasets' import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' type Props = { onCancel: () => void @@ -76,7 +76,7 @@ const ConfigFirecrawlModal: FC = ({ } try { setIsSaving(true) - await createFirecrawlApiKey(postData) + await createDataSourceApiKeyBinding(postData) Toast.notify({ type: 'success', message: t('common.api.success'), diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx index b6ac22436c..63ad8df0d8 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx @@ -7,15 +7,15 @@ import cn from 'classnames' import Panel from '../panel' import { DataSourceType } from '../panel/types' import ConfigFirecrawlModal from './config-firecrawl-modal' -import { fetchFirecrawlApiKey, removeFirecrawlApiKey } from '@/service/datasets' +import { fetchDataSources, removeDataSourceApiKeyBinding } from '@/service/datasets' import type { - DataSourceWebsiteItem, + DataSourceItem, } from '@/models/common' import { useAppContext } from '@/context/app-context' import { - WebsiteProvider, + DataSourceProvider, } from '@/models/common' import Toast from '@/app/components/base/toast' @@ -24,11 +24,11 @@ type Props = {} const DataSourceWebsite: FC = () => { const { t } = useTranslation() const { isCurrentWorkspaceManager } = useAppContext() - const [list, setList] = useState([]) + const [sources, setSources] = useState([]) const checkSetApiKey = useCallback(async () => { - const res = await fetchFirecrawlApiKey() as any - const list = res.settings.filter((item: DataSourceWebsiteItem) => item.provider === WebsiteProvider.fireCrawl && !item.disabled) - setList(list) + const res = await fetchDataSources() as any + const list = res.sources + setSources(list) }, []) useEffect(() => { @@ -46,23 +46,33 @@ const DataSourceWebsite: FC = () => { hideConfig() }, [checkSetApiKey, hideConfig]) - const handleRemove = useCallback(async () => { - await removeFirecrawlApiKey(list[0].id) - setList([]) - Toast.notify({ - type: 'success', - message: t('common.api.remove'), - }) - }, [list, t]) + const getIdByProvider = (provider: string): string | undefined => { + const source = sources.find(item => item.provider === provider) + return source?.id + } + + const handleRemove = useCallback((provider: string) => { + return async () => { + const dataSourceId = getIdByProvider(provider) + if (dataSourceId) { + await removeDataSourceApiKeyBinding(dataSourceId) + setSources(sources.filter(item => item.provider !== provider)) + Toast.notify({ + type: 'success', + message: t('common.api.remove'), + }) + } + } + }, [sources, t]) return ( <> 0} + isConfigured={sources.length > 0} onConfigure={showConfig} - readonly={!isCurrentWorkspaceManager} - configuredList={list.map(item => ({ + readOnly={!isCurrentWorkspaceManager} + configuredList={sources.map(item => ({ id: item.id, logo: ({ className }: { className: string }) => (
🔥
@@ -70,7 +80,7 @@ const DataSourceWebsite: FC = () => { name: 'FireCrawl', isActive: true, }))} - onRemove={handleRemove} + onRemove={handleRemove(DataSourceProvider.fireCrawl)} /> {isShowConfig && ( diff --git a/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx index 376c4aea7b..0b27dd7392 100644 --- a/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx +++ b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx @@ -26,6 +26,7 @@ type Props = { notionActions?: { onChangeAuthorizedPage: () => void } + readOnly: boolean } const ConfigItem: FC = ({ @@ -33,6 +34,7 @@ const ConfigItem: FC = ({ payload, onRemove, notionActions, + readOnly, }) => { const { t } = useTranslation() const isNotion = type === DataSourceType.notion @@ -65,7 +67,7 @@ const ConfigItem: FC = ({ )} { - isWebsite && ( + isWebsite && !readOnly && (
diff --git a/web/app/components/header/account-setting/data-source-page/panel/index.tsx b/web/app/components/header/account-setting/data-source-page/panel/index.tsx index b0f6f4ad13..2c27005d1d 100644 --- a/web/app/components/header/account-setting/data-source-page/panel/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/panel/index.tsx @@ -14,7 +14,7 @@ type Props = { type: DataSourceType isConfigured: boolean onConfigure: () => void - readonly: boolean + readOnly: boolean isSupportList?: boolean configuredList: ConfigItemType[] onRemove: () => void @@ -27,7 +27,7 @@ const Panel: FC = ({ type, isConfigured, onConfigure, - readonly, + readOnly, configuredList, isSupportList, onRemove, @@ -67,7 +67,7 @@ const Panel: FC = ({ className={ `flex items-center ml-3 px-3 h-7 bg-white border border-gray-200 rounded-md text-xs font-medium text-gray-700 - ${!readonly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}` + ${!readOnly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}` } onClick={onConfigure} > @@ -79,7 +79,7 @@ const Panel: FC = ({ {isSupportList &&
@@ -96,10 +96,10 @@ const Panel: FC = ({
{t('common.dataSource.configure')}
@@ -108,28 +108,28 @@ const Panel: FC = ({
{ isConfigured && ( -
-
- {isNotion ? t('common.dataSource.notion.connectedWorkspace') : t('common.dataSource.website.configuredCrawlers')} + <> +
+
+ {isNotion ? t('common.dataSource.notion.connectedWorkspace') : t('common.dataSource.website.configuredCrawlers')} +
+
-
-
- ) - } - { - isConfigured && ( -
- { - configuredList.map(item => ( - - )) - } -
+
+ { + configuredList.map(item => ( + + )) + } +
+ ) }
diff --git a/web/models/common.ts b/web/models/common.ts index 730cfee05d..6a941655c5 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -175,34 +175,26 @@ export type DataSourceNotion = { export enum DataSourceCategory { website = 'website', } -export enum WebsiteProvider { +export enum DataSourceProvider { fireCrawl = 'firecrawl', } -export type WebsiteCredentials = { - auth_type: 'bearer' - config: { - base_url: string - api_key: string - } -} - export type FirecrawlConfig = { api_key: string base_url: string } -export type DataSourceWebsiteItem = { +export type DataSourceItem = { id: string - category: DataSourceCategory.website - provider: WebsiteProvider - credentials: WebsiteCredentials + category: DataSourceCategory + provider: DataSourceProvider disabled: boolean created_at: number updated_at: number } -export type DataSourceWebsite = { - settings: DataSourceWebsiteItem[] + +export type DataSources = { + sources: DataSourceItem[] } export type GithubRepo = { diff --git a/web/service/datasets.ts b/web/service/datasets.ts index a382ee8ec8..35330a0dec 100644 --- a/web/service/datasets.ts +++ b/web/service/datasets.ts @@ -231,15 +231,15 @@ export const fetchDatasetApiBaseUrl: Fetcher<{ api_base_url: string }, string> = return get<{ api_base_url: string }>(url) } -export const fetchFirecrawlApiKey = () => { +export const fetchDataSources = () => { return get('api-key-auth/data-source') } -export const createFirecrawlApiKey: Fetcher> = (body) => { +export const createDataSourceApiKeyBinding: Fetcher> = (body) => { return post('api-key-auth/data-source/binding', { body }) } -export const removeFirecrawlApiKey: Fetcher = (id: string) => { +export const removeDataSourceApiKeyBinding: Fetcher = (id: string) => { return del(`api-key-auth/data-source/${id}`) }