diff --git a/lib/chatbot-api/functions/api-handler/routes/documents.py b/lib/chatbot-api/functions/api-handler/routes/documents.py index d6e6d3c1..641ae137 100644 --- a/lib/chatbot-api/functions/api-handler/routes/documents.py +++ b/lib/chatbot-api/functions/api-handler/routes/documents.py @@ -2,6 +2,7 @@ import genai_core.types import genai_core.upload import genai_core.documents +import genai_core.workspaces from pydantic import BaseModel from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler.appsync import Router @@ -103,6 +104,14 @@ class DocumentSubscriptionStatusRequest(BaseModel): @router.resolver(field_name="getUploadFileURL") @tracer.capture_method def file_upload(input: dict): + + #Check policy for writable permission + user_id = genai_core.auth.get_user_id(router) + is_workspace_readable = genai_core.workspaces.is_workspace_readable(request.workspaceId, user_id) + + if is_workspace_readable == False: + raise genai_core.types.CommonError("Due security policy you are not allowed to upload document into workspace.") + request = FileUploadRequest(**input) _, extension = os.path.splitext(request.fileName) if extension not in allowed_extensions: diff --git a/lib/chatbot-api/functions/api-handler/routes/workspaces.py b/lib/chatbot-api/functions/api-handler/routes/workspaces.py index 1c71a24f..c81c17c0 100644 --- a/lib/chatbot-api/functions/api-handler/routes/workspaces.py +++ b/lib/chatbot-api/functions/api-handler/routes/workspaces.py @@ -21,6 +21,7 @@ class GenericCreateWorkspaceRequest(BaseModel): class CreateWorkspaceAuroraRequest(BaseModel): kind: str name: str + isPublic: bool embeddingsModelProvider: str embeddingsModelName: str crossEncoderModelProvider: str @@ -37,6 +38,7 @@ class CreateWorkspaceAuroraRequest(BaseModel): class CreateWorkspaceOpenSearchRequest(BaseModel): kind: str name: str + isPublic: bool embeddingsModelProvider: str embeddingsModelName: str crossEncoderModelProvider: str @@ -51,6 +53,7 @@ class CreateWorkspaceOpenSearchRequest(BaseModel): class CreateWorkspaceKendraRequest(BaseModel): kind: str name: str + isPublic: bool kendraIndexId: str useAllData: bool @@ -58,7 +61,21 @@ class CreateWorkspaceKendraRequest(BaseModel): @router.resolver(field_name="listWorkspaces") @tracer.capture_method def list_workspaces(): - workspaces = genai_core.workspaces.list_workspaces() + + user_id = genai_core.auth.get_user_id(router) + + #Get Public workspace + publicWorkspace = genai_core.workspaces.list_workspaces_by_user(0) + + #Get workspace affected to current user through policy + workspacesByUser = genai_core.workspaces.list_workspaces_by_user(user_id) + + workspaces = [] + processedItem = [] + for workspaceItem in (workspacesByUser + publicWorkspace): + if not workspaceItem['workspace_id'] in processedItem: + workspaces.append(workspaceItem) + processedItem.append(workspaceItem['workspace_id']) ret_value = [_convert_workspace(workspace) for workspace in workspaces] @@ -68,11 +85,24 @@ def list_workspaces(): @router.resolver(field_name="getWorkspace") @tracer.capture_method def get_workspace(workspaceId: str): + + user_id = genai_core.auth.get_user_id(router) + #Check policy before to fetch a data of workspace + workspace_policy = genai_core.workspaces.is_workspace_readable(workspaceId, user_id) + + if not workspace_policy: + return None + workspace = genai_core.workspaces.get_workspace(workspaceId) if not workspace: return None + key_policies = ['is_owner', 'is_writable'] + + for key_policy in key_policies: + workspace[key_policy] = workspace_policy[key_policy] + ret_value = _convert_workspace(workspace) return ret_value @@ -116,6 +146,7 @@ def create_kendra_workspace(input: dict): def _create_workspace_aurora(request: CreateWorkspaceAuroraRequest, config: dict): + user_id = genai_core.auth.get_user_id(router) workspace_name = request.name.strip() embedding_models = config["rag"]["embeddingsModels"] cross_encoder_models = config["rag"]["crossEncoderModels"] @@ -170,28 +201,32 @@ def _create_workspace_aurora(request: CreateWorkspaceAuroraRequest, config: dict if request.chunkOverlap < 0 or request.chunkOverlap >= request.chunkSize: raise genai_core.types.CommonError("Invalid chunk overlap") + item = genai_core.workspaces.create_workspace_aurora( + workspace_name=workspace_name, + is_public=request.isPublic, + embeddings_model_provider=request.embeddingsModelProvider, + embeddings_model_name=request.embeddingsModelName, + embeddings_model_dimensions=embeddings_model_dimensions, + cross_encoder_model_provider=request.crossEncoderModelProvider, + cross_encoder_model_name=request.crossEncoderModelName, + languages=request.languages, + metric=request.metric, + has_index=request.index, + hybrid_search=request.hybridSearch, + chunking_strategy=request.chunkingStrategy, + chunk_size=request.chunkSize, + chunk_overlap=request.chunkOverlap, + creator_id=user_id, + ) return _convert_workspace( - genai_core.workspaces.create_workspace_aurora( - workspace_name=workspace_name, - embeddings_model_provider=request.embeddingsModelProvider, - embeddings_model_name=request.embeddingsModelName, - embeddings_model_dimensions=embeddings_model_dimensions, - cross_encoder_model_provider=request.crossEncoderModelProvider, - cross_encoder_model_name=request.crossEncoderModelName, - languages=request.languages, - metric=request.metric, - has_index=request.index, - hybrid_search=request.hybridSearch, - chunking_strategy=request.chunkingStrategy, - chunk_size=request.chunkSize, - chunk_overlap=request.chunkOverlap, - ) + get_workspace(item.workspace_id) ) def _create_workspace_open_search( request: CreateWorkspaceOpenSearchRequest, config: dict ): + user_id = genai_core.auth.get_user_id(router) workspace_name = request.name.strip() embedding_models = config["rag"]["embeddingsModels"] cross_encoder_models = config["rag"]["crossEncoderModels"] @@ -246,6 +281,7 @@ def _create_workspace_open_search( return _convert_workspace( genai_core.workspaces.create_workspace_open_search( workspace_name=workspace_name, + is_public=request.isPublic, embeddings_model_provider=request.embeddingsModelProvider, embeddings_model_name=request.embeddingsModelName, embeddings_model_dimensions=embeddings_model_dimensions, @@ -256,11 +292,14 @@ def _create_workspace_open_search( chunking_strategy=request.chunkingStrategy, chunk_size=request.chunkSize, chunk_overlap=request.chunkOverlap, + creator_id=user_id, ) ) def _create_workspace_kendra(request: CreateWorkspaceKendraRequest, config: dict): + + user_id = genai_core.auth.get_user_id(router) workspace_name = request.name.strip() kendra_indexes = genai_core.kendra.get_kendra_indexes() @@ -285,8 +324,11 @@ def _create_workspace_kendra(request: CreateWorkspaceKendraRequest, config: dict return _convert_workspace( genai_core.workspaces.create_workspace_kendra( workspace_name=workspace_name, + is_public=request.isPublic, kendra_index=kendra_index, use_all_data=request.useAllData, + creator_id=user_id + ) ) @@ -297,6 +339,7 @@ def _convert_workspace(workspace: dict): return { "id": workspace["workspace_id"], "name": workspace["name"], + "isPublic": workspace["is_public"], "engine": workspace["engine"], "status": workspace["status"], "languages": workspace.get("languages"), @@ -322,4 +365,6 @@ def _convert_workspace(workspace: dict): "kendraUseAllData": workspace.get("kendra_use_all_data", kendra_index_external), "createdAt": workspace.get("created_at"), "updatedAt": workspace.get("updated_at"), + "is_writable": workspace.get("is_writable"), + "is_owner": workspace.get("is_owner") } diff --git a/lib/chatbot-api/rest-api.ts b/lib/chatbot-api/rest-api.ts index a0ac6271..5b1a2a4d 100644 --- a/lib/chatbot-api/rest-api.ts +++ b/lib/chatbot-api/rest-api.ts @@ -74,6 +74,10 @@ export class ApiResolvers extends Construct { props.ragEngines?.workspacesTable.tableName ?? "", WORKSPACES_BY_OBJECT_TYPE_INDEX_NAME: props.ragEngines?.workspacesByObjectTypeIndexName ?? "", + WORKSPACES_POLICY_TABLE_NAME: + props.ragEngines?.workspacesPolicyTable.tableName ?? "", + WORKSPACES_POLICY_BY_WORKSPACE_ID_INDEX_NAME: + props.ragEngines?.workspacesPolicyByWorkspaceIdIndexName ?? "", DOCUMENTS_TABLE_NAME: props.ragEngines?.documentsTable.tableName ?? "", DOCUMENTS_BY_COMPOUND_KEY_INDEX_NAME: @@ -128,6 +132,10 @@ export class ApiResolvers extends Construct { ); } + if (props.ragEngines?.workspacesPolicyTable) { + props.ragEngines.workspacesPolicyTable.grantReadWriteData(apiHandler); + } + if (props.ragEngines?.auroraPgVector) { props.ragEngines.auroraPgVector.database.secret?.grantRead(apiHandler); props.ragEngines.auroraPgVector.database.connections.allowDefaultPortFrom( diff --git a/lib/chatbot-api/schema/schema.graphql b/lib/chatbot-api/schema/schema.graphql index f844063a..5d5cb081 100644 --- a/lib/chatbot-api/schema/schema.graphql +++ b/lib/chatbot-api/schema/schema.graphql @@ -3,6 +3,7 @@ input CreateWorkspaceAuroraInput { name: String! kind: String! + isPublic: Boolean! embeddingsModelProvider: String! embeddingsModelName: String! crossEncoderModelProvider: String! @@ -19,6 +20,7 @@ input CreateWorkspaceAuroraInput { input CreateWorkspaceKendraInput { name: String! kind: String! + isPublic: Boolean! kendraIndexId: String! useAllData: Boolean! } @@ -26,6 +28,7 @@ input CreateWorkspaceKendraInput { input CreateWorkspaceOpenSearchInput { name: String! kind: String! + isPublic: Boolean! embeddingsModelProvider: String! embeddingsModelName: String! crossEncoderModelProvider: String! @@ -266,6 +269,7 @@ input WebsiteInput { type Workspace @aws_cognito_user_pools { id: String! name: String! + isPublic: Boolean formatVersion: Int engine: String! status: String @@ -291,6 +295,8 @@ type Workspace @aws_cognito_user_pools { kendraUseAllData: Boolean createdAt: AWSDateTime! updatedAt: AWSDateTime! + is_writable: Boolean + is_owner: Boolean } type Channel @aws_iam @aws_cognito_user_pools { diff --git a/lib/rag-engines/index.ts b/lib/rag-engines/index.ts index f89dc688..751847fd 100644 --- a/lib/rag-engines/index.ts +++ b/lib/rag-engines/index.ts @@ -28,6 +28,8 @@ export class RagEngines extends Construct { public readonly documentsTable: dynamodb.Table; public readonly workspacesTable: dynamodb.Table; public readonly workspacesByObjectTypeIndexName: string; + public readonly workspacesPolicyTable: dynamodb.Table; + public readonly workspacesPolicyByWorkspaceIdIndexName: string; public readonly documentsByCompountKeyIndexName: string; public readonly documentsByStatusIndexName: string; public readonly fileImportWorkflow?: sfn.StateMachine; @@ -110,6 +112,8 @@ export class RagEngines extends Construct { this.processingBucket = dataImport.processingBucket; this.workspacesTable = tables.workspacesTable; this.documentsTable = tables.documentsTable; + this.workspacesPolicyTable = tables.workspacesPolicyTable; + this.workspacesPolicyByWorkspaceIdIndexName = tables.workspacesPolicyByWorkspaceIdIndexName; this.workspacesByObjectTypeIndexName = tables.workspacesByObjectTypeIndexName; this.documentsByCompountKeyIndexName = diff --git a/lib/rag-engines/rag-dynamodb-tables/index.ts b/lib/rag-engines/rag-dynamodb-tables/index.ts index 458be9f0..58313082 100644 --- a/lib/rag-engines/rag-dynamodb-tables/index.ts +++ b/lib/rag-engines/rag-dynamodb-tables/index.ts @@ -4,9 +4,12 @@ import { Construct } from "constructs"; export class RagDynamoDBTables extends Construct { public readonly workspacesTable: dynamodb.Table; + public readonly workspacesPolicyTable: dynamodb.Table; public readonly documentsTable: dynamodb.Table; public readonly workspacesByObjectTypeIndexName: string = "by_object_type_idx"; + public readonly workspacesPolicyByWorkspaceIdIndexName: string = + "by_workspace_idx"; public readonly documentsByCompoundKeyIndexName: string = "by_compound_key_idx"; public readonly documentsByStatusIndexName: string = "by_status_idx"; @@ -41,6 +44,29 @@ export class RagDynamoDBTables extends Construct { }, }); + const workspacesPolicyTable = new dynamodb.Table(this, "WorkspacesPolicy", { + partitionKey: { + name: "pk", + type: dynamodb.AttributeType.STRING, + }, + sortKey: { + name: "sk", + type: dynamodb.AttributeType.STRING, + }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + encryption: dynamodb.TableEncryption.AWS_MANAGED, + removalPolicy: cdk.RemovalPolicy.DESTROY, + pointInTimeRecovery: true, + }); + + workspacesPolicyTable.addGlobalSecondaryIndex({ + indexName: this.workspacesPolicyByWorkspaceIdIndexName, + partitionKey: { + name: "workspace_id", + type: dynamodb.AttributeType.STRING, + } + }); + const documentsTable = new dynamodb.Table(this, "Documents", { partitionKey: { name: "workspace_id", @@ -81,6 +107,7 @@ export class RagDynamoDBTables extends Construct { }); this.workspacesTable = workspacesTable; + this.workspacesPolicyTable = workspacesPolicyTable; this.documentsTable = documentsTable; } } diff --git a/lib/shared/layers/python-sdk/python/genai_core/aurora/delete.py b/lib/shared/layers/python-sdk/python/genai_core/aurora/delete.py index 77733ed9..061e3e99 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/aurora/delete.py +++ b/lib/shared/layers/python-sdk/python/genai_core/aurora/delete.py @@ -3,10 +3,12 @@ import genai_core.utils.delete_files_with_prefix from psycopg2 import sql from genai_core.aurora.connection import AuroraConnection +import genai_core.workspaces PROCESSING_BUCKET_NAME = os.environ["PROCESSING_BUCKET_NAME"] UPLOAD_BUCKET_NAME = os.environ["UPLOAD_BUCKET_NAME"] WORKSPACES_TABLE_NAME = os.environ["WORKSPACES_TABLE_NAME"] +WORKSPACES_POLICY_TABLE_NAME = os.environ["WORKSPACES_POLICY_TABLE_NAME"] DOCUMENTS_TABLE_NAME = os.environ.get("DOCUMENTS_TABLE_NAME") WORKSPACE_OBJECT_TYPE = "workspace" @@ -30,6 +32,7 @@ def delete_aurora_workspace(workspace: dict): ) workspaces_table = dynamodb.Table(WORKSPACES_TABLE_NAME) + documents_policy_table = dynamodb.Table(WORKSPACES_POLICY_TABLE_NAME) documents_table = dynamodb.Table(DOCUMENTS_TABLE_NAME) items_to_delete = [] @@ -68,3 +71,18 @@ def delete_aurora_workspace(workspace: dict): ) print(f"Delete Item succeeded: {response}") + + # Delete all workspace policy related to the current deletion workspace + items_policy_to_delete = genai_core.workspaces.list_policy_workspace_by_id(workspace_id) + # Batch delete in groups of 25 + for i in range(0, len(items_policy_to_delete), 25): + with documents_policy_table.batch_writer() as batch: + for item in items_policy_to_delete[i : i + 25]: + batch.delete_item( + Key={ + "pk": item["pk"], + "sk": item["sk"], + } + ) + + print(f"Deleted {len(items_policy_to_delete)} policy items.") diff --git a/lib/shared/layers/python-sdk/python/genai_core/workspaces.py b/lib/shared/layers/python-sdk/python/genai_core/workspaces.py index 7652158f..85055524 100644 --- a/lib/shared/layers/python-sdk/python/genai_core/workspaces.py +++ b/lib/shared/layers/python-sdk/python/genai_core/workspaces.py @@ -6,6 +6,9 @@ from datetime import datetime from genai_core.types import Task +from boto3.dynamodb.conditions import Key +import genai_core.auth + dynamodb = boto3.resource("dynamodb") sfn_client = boto3.client("stepfunctions") @@ -13,6 +16,12 @@ WORKSPACES_BY_OBJECT_TYPE_INDEX_NAME = os.environ.get( "WORKSPACES_BY_OBJECT_TYPE_INDEX_NAME" ) + +WORKSPACES_POLICY_TABLE_NAME = os.environ.get("WORKSPACES_POLICY_TABLE_NAME") +WORKSPACES_POLICY_BY_WORKSPACE_ID_INDEX_NAME = os.environ.get( + "WORKSPACES_POLICY_BY_WORKSPACE_ID_INDEX_NAME" +) + CREATE_AURORA_WORKSPACE_WORKFLOW_ARN = os.environ.get( "CREATE_AURORA_WORKSPACE_WORKFLOW_ARN" ) @@ -30,6 +39,55 @@ table = dynamodb.Table(WORKSPACES_TABLE_NAME) +if WORKSPACES_POLICY_TABLE_NAME: + workspacePolicyTable = dynamodb.Table(WORKSPACES_POLICY_TABLE_NAME) + +def list_workspaces_by_user(userId): + all_items = [] + last_evaluated_key = None + + while True: + if last_evaluated_key: + response = workspacePolicyTable.query( + KeyConditionExpression='pk = :pk AND begins_with ( sk, :sk )', + ExpressionAttributeValues={ + ':pk': f'#userid#{userId}#', + ':sk': '#workspace#' + }, + ExclusiveStartKey=last_evaluated_key, + ScanIndexForward=False + ) + else: + response = workspacePolicyTable.query( + KeyConditionExpression='pk = :pk AND begins_with ( sk, :sk )', + ExpressionAttributeValues={ + ':pk': f'#userid#{userId}#', + ':sk': '#workspace#' + }, + ScanIndexForward=False + ) + + all_items.extend(response["Items"]) + + last_evaluated_key = response.get("LastEvaluatedKey") + if not last_evaluated_key: + break + + workspaces = [] + key_policies = ['is_owner', 'is_writable'] + + for item in all_items: + itemResponse = table.get_item(Key={"workspace_id": item['workspace_id'], "object_type": WORKSPACE_OBJECT_TYPE}) + + workspace = itemResponse.get('Item') + + for key_policy in key_policies: + workspace[key_policy] = item[key_policy] + + workspaces.append(workspace) + + return workspaces + def list_workspaces(): all_items = [] last_evaluated_key = None @@ -62,6 +120,34 @@ def list_workspaces(): return all_items +def list_policy_workspace_by_id(workspaceId): + + all_items = [] + last_evaluated_key = None + + while True: + if last_evaluated_key: + response = workspacePolicyTable.query( + IndexName=WORKSPACES_POLICY_BY_WORKSPACE_ID_INDEX_NAME, + KeyConditionExpression=boto3.dynamodb.conditions.Key("workspace_id").eq(workspaceId), + ExclusiveStartKey=last_evaluated_key, + ScanIndexForward=False + ) + else: + response = workspacePolicyTable.query( + IndexName=WORKSPACES_POLICY_BY_WORKSPACE_ID_INDEX_NAME, + KeyConditionExpression=boto3.dynamodb.conditions.Key("workspace_id").eq(workspaceId), + ScanIndexForward=False + ) + + all_items.extend(response["Items"]) + + last_evaluated_key = response.get("LastEvaluatedKey") + if not last_evaluated_key: + break + + return all_items + def get_workspace(workspace_id: str): response = table.get_item( Key={"workspace_id": workspace_id, "object_type": WORKSPACE_OBJECT_TYPE} @@ -70,6 +156,56 @@ def get_workspace(workspace_id: str): return item +def is_workspace_readable(workspaceId, userId): + + response = workspacePolicyTable.get_item( + Key={"pk": f"#userid#{userId}#", "sk": f"#workspace#{workspaceId}#"} + ) + + item = response.get('Item') + + if item: + print("Workspace policy find and it's readable.") + return item + + #If user does not have explicit access then try to check if the workspace open publicly and readable + response = workspacePolicyTable.get_item( + Key={"pk": f"#userid#0#", "sk": f"#workspace#{workspaceId}#"} + ) + + item = response.get('Item') + + if item: + print("Workspace policy find and it's readable.") + return item + + return None + +def is_workspace_writable(workspaceId, userId): + + response = workspacePolicyTable.get_item( + Key={"pk": f"#userid#{userId}#", "sk": f"#workspace#{workspaceId}#"} + ) + + item = response.get('Item') + + if item and item['is_writable'] == True: + print("Workspace policy find and it's writable.") + return True + + #If user does not have explicit access then try to check if the workspace open publicly and writeable + response = workspacePolicyTable.get_item( + Key={"pk": f"#userid#0#", "sk": f"#workspace#{workspaceId}#"} + ) + + item = response.get('Item') + + if item and item['is_writable'] == True: + print("Workspace policy find and it's writable.") + return True + + + return False def set_status(workspace_id: str, status: str): timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") @@ -91,6 +227,7 @@ def set_status(workspace_id: str, status: str): def create_workspace_aurora( workspace_name: str, + is_public: bool, embeddings_model_provider: str, embeddings_model_name: str, embeddings_model_dimensions: int, @@ -103,6 +240,7 @@ def create_workspace_aurora( chunking_strategy: str, chunk_size: int, chunk_overlap: int, + creator_id: str, ): workspace_id = str(uuid.uuid4()) timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") @@ -118,6 +256,7 @@ def create_workspace_aurora( item = { "workspace_id": workspace_id, "object_type": WORKSPACE_OBJECT_TYPE, + "is_public": is_public, "format_version": 1, "name": workspace_name, "engine": "aurora", @@ -139,11 +278,38 @@ def create_workspace_aurora( "size_in_bytes": 0, "created_at": timestamp, "updated_at": timestamp, + "creator_id": creator_id, } response = table.put_item(Item=item) print(response) + #Create default workspace policy at least for creator + itemWorkspacePolicy = { + "pk": f"#userid#{creator_id}#", + "sk": f"#workspace#{workspace_id}#", + "is_owner": True, + "is_writable": True, + "workspace_id": workspace_id, + "created_at": timestamp, + "updated_at": timestamp + } + + workspacePolicyTableResponse = workspacePolicyTable.put_item(Item=itemWorkspacePolicy) + + if is_public == True: + #Create default workspace policy at least for creator + itemWorkspacePolicy = { + "pk": f"#userid#0#", + "sk": f"#workspace#{workspace_id}#", + "is_owner": False, + "is_writable": True, + "workspace_id": workspace_id, + "created_at": timestamp, + "updated_at": timestamp + } + workspacePolicyTableResponse = workspacePolicyTable.put_item(Item=itemWorkspacePolicy) + response = sfn_client.start_execution( stateMachineArn=CREATE_AURORA_WORKSPACE_WORKFLOW_ARN, input=json.dumps( @@ -160,6 +326,7 @@ def create_workspace_aurora( def create_workspace_open_search( workspace_name: str, + is_public: bool, embeddings_model_provider: str, embeddings_model_name: str, embeddings_model_dimensions: int, @@ -170,6 +337,7 @@ def create_workspace_open_search( chunking_strategy: str, chunk_size: int, chunk_overlap: int, + creator_id: str, ): workspace_id = str(uuid.uuid4()) timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") @@ -185,6 +353,7 @@ def create_workspace_open_search( item = { "workspace_id": workspace_id, "object_type": WORKSPACE_OBJECT_TYPE, + "is_public": is_public, "format_version": 1, "name": workspace_name, "engine": "opensearch", @@ -211,6 +380,32 @@ def create_workspace_open_search( response = table.put_item(Item=item) print(response) + #Create default workspace policy at least for creator + itemWorkspacePolicy = { + "pk": f"#userid#{creator_id}#", + "sk": f"#workspace#{workspace_id}#", + "is_owner": True, + "is_writable": True, + "workspace_id": workspace_id, + "created_at": timestamp, + "updated_at": timestamp + } + + workspacePolicyTableResponse = workspacePolicyTable.put_item(Item=itemWorkspacePolicy) + + if is_public == True: + #Create default workspace policy at least for creator + itemWorkspacePolicy = { + "pk": f"#userid#0#", + "sk": f"#workspace#{workspace_id}#", + "is_owner": False, + "is_writable": True, + "workspace_id": workspace_id, + "created_at": timestamp, + "updated_at": timestamp + } + workspacePolicyTableResponse = workspacePolicyTable.put_item(Item=itemWorkspacePolicy) + response = sfn_client.start_execution( stateMachineArn=CREATE_OPEN_SEARCH_WORKSPACE_WORKFLOW_ARN, input=json.dumps( @@ -226,7 +421,7 @@ def create_workspace_open_search( def create_workspace_kendra( - workspace_name: str, kendra_index: dict, use_all_data: bool + workspace_name: str, is_public: bool, kendra_index: dict, use_all_data: bool ): workspace_id = str(uuid.uuid4()) timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") @@ -237,6 +432,7 @@ def create_workspace_kendra( item = { "workspace_id": workspace_id, "object_type": WORKSPACE_OBJECT_TYPE, + "is_public": is_public, "format_version": 1, "name": workspace_name, "engine": "kendra", @@ -254,6 +450,34 @@ def create_workspace_kendra( response = table.put_item(Item=item) print(response) + #Create default workspace policy at least for creator + itemWorkspacePolicy = { + "pk": f"#userid#{creator_id}#", + "sk": f"#workspace#{workspace_id}#", + "is_owner": True, + "is_writable": True, + "workspace_id": workspace_id, + "created_at": timestamp, + "updated_at": timestamp + } + + workspacePolicyTableResponse = workspacePolicyTable.put_item(Item=itemWorkspacePolicy) + + workspacePolicyTableResponse = workspacePolicyTable.put_item(Item=itemWorkspacePolicy) + + if is_public == True: + #Create default workspace policy at least for creator + itemWorkspacePolicy = { + "pk": f"#userid#0#", + "sk": f"#workspace#{workspace_id}#", + "is_owner": False, + "is_writable": True, + "workspace_id": workspace_id, + "created_at": timestamp, + "updated_at": timestamp + } + workspacePolicyTableResponse = workspacePolicyTable.put_item(Item=itemWorkspacePolicy) + response = sfn_client.start_execution( stateMachineArn=CREATE_KENDRA_WORKSPACE_WORKFLOW_ARN, input=json.dumps( diff --git a/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts b/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts index 58124abc..3b43194f 100644 --- a/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts +++ b/lib/user-interface/react-app/src/common/api-client/workspaces-client.ts @@ -63,6 +63,7 @@ export class WorkspacesClient { chunkingStrategy: string; chunkSize: number; chunkOverlap: number; + isPublic: boolean; }): Promise>> { const result = API.graphql>({ query: createAuroraWorkspace, @@ -84,6 +85,7 @@ export class WorkspacesClient { chunkingStrategy: string; chunkSize: number; chunkOverlap: number; + isPublic: boolean; }): Promise>> { const result = API.graphql>( { @@ -100,6 +102,7 @@ export class WorkspacesClient { name: string; kendraIndexId: string; useAllData: boolean; + isPublic: boolean; }): Promise>> { const result = API.graphql>({ query: createKendraWorkspace, diff --git a/lib/user-interface/react-app/src/common/types.ts b/lib/user-interface/react-app/src/common/types.ts index 8d3517ad..e2851523 100644 --- a/lib/user-interface/react-app/src/common/types.ts +++ b/lib/user-interface/react-app/src/common/types.ts @@ -71,6 +71,7 @@ export interface AuroraWorkspaceCreateInput { hybridSearch: boolean; chunkSize: number; chunkOverlap: number; + isPublic: boolean; } export interface OpenSearchWorkspaceCreateInput { @@ -81,10 +82,12 @@ export interface OpenSearchWorkspaceCreateInput { hybridSearch: boolean; chunkSize: number; chunkOverlap: number; + isPublic: boolean; } export interface KendraWorkspaceCreateInput { name: string; kendraIndex: SelectProps.Option | null; useAllData: boolean; + isPublic: boolean; } diff --git a/lib/user-interface/react-app/src/pages/rag/create-workspace/aurora-form.tsx b/lib/user-interface/react-app/src/pages/rag/create-workspace/aurora-form.tsx index 09533e86..7f7f4a80 100644 --- a/lib/user-interface/react-app/src/pages/rag/create-workspace/aurora-form.tsx +++ b/lib/user-interface/react-app/src/pages/rag/create-workspace/aurora-form.tsx @@ -52,6 +52,17 @@ export default function AuroraForm(props: AuroraFormProps) { /> + + + props.onChange({ isPublic: checked }) + } + > + + + + + + props.onChange({ isPublic: checked }) + } + > + +