Skip to content

Commit

Permalink
Authorization/Permissions Prompt studio (#112)
Browse files Browse the repository at this point in the history
* intial commit for permission in prompt studio

* Create prompts to be part of prompt studio core

* added share to prompt project

* Prettier fix

* File upload changes

* Profile manger creation from prompt studio

* Removing relation from custom tool on user deletion

* Refractored the code

* Refractored thr code to avoid exposing unused endpoints

* added permission around prompt

* Support for sharing the exported tool

* Support for sharing the exported tool and access validation

* Conflict resolution models

* Conflict resolution jsx files

* Check prompt registry is associated with custom tool

* implemented UI for tool sharing

* code clean up

* Updated migrations

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Removed commented code

* Fixed code climate issues

* Fix code duplication

* Removed redudant checks

* Update backend/prompt_studio/permission.py

Signed-off-by: Chandrasekharan M <[email protected]>

---------

Signed-off-by: Rahul Johny <[email protected]>
Signed-off-by: Chandrasekharan M <[email protected]>
Co-authored-by: jagadeeswaran-zipstack <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: siddhiq <[email protected]>
Co-authored-by: Chandrasekharan M <[email protected]>
Co-authored-by: Neha <[email protected]>
  • Loading branch information
6 people authored Apr 9, 2024
1 parent cef6fca commit 72cdd4a
Show file tree
Hide file tree
Showing 41 changed files with 852 additions and 436 deletions.
4 changes: 3 additions & 1 deletion backend/account/authentication_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,10 @@ def remove_users_from_organization(
is_removed = False
if is_removed:
OrganizationMember.objects.filter(user__in=ids_list).delete()
# removing adapter relations on user removal
# removing user m2m relations , while removing user
for user_id in ids_list:
User.objects.get(pk=user_id).shared_exported_tools.clear()
User.objects.get(pk=user_id).shared_custom_tool.clear()
User.objects.get(pk=user_id).shared_adapters.clear()
return is_removed

Expand Down
2 changes: 1 addition & 1 deletion backend/account/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Meta:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "email")
fields = ("id", "username")


class OrganizationSignupResponseSerializer(serializers.Serializer):
Expand Down
10 changes: 2 additions & 8 deletions backend/adapter_processor/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from typing import Any

from account.models import User
from account.serializer import UserSerializer
from adapter_processor.adapter_processor import AdapterProcessor
from adapter_processor.constants import AdapterKeys
from cryptography.fernet import Fernet
Expand Down Expand Up @@ -120,16 +120,10 @@ def to_representation(self, instance: AdapterInstance) -> dict[str, str]:
return rep


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "username")


class SharedUserListSerializer(BaseAdapterSerializer):
"""Inherits BaseAdapterSerializer.
Used for listing adapters
Used for listing adapter users
"""

shared_users = UserSerializer(many=True)
Expand Down
15 changes: 0 additions & 15 deletions backend/file_management/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,5 @@
path("file/download", file_downlaod, name="download"),
path("file/upload", file_upload, name="upload"),
path("file/delete", file_delete, name="delete"),
path(
"prompt-studio/file/upload",
prompt_studio_file_upload,
name="prompt_studio_upload",
),
path(
"prompt-studio/file/fetch_contents",
prompt_studio_fetch_content,
name="tool_studio_fetch",
),
path(
"prompt-studio/file",
prompt_studio_file_list,
name="prompt_studio_list",
),
]
)
111 changes: 0 additions & 111 deletions backend/file_management/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import logging
import os
from typing import Any

from connector.models import ConnectorInstance
from django.http import HttpRequest
from file_management.constants import FileViewTypes
from file_management.exceptions import (
ConnectorInstanceNotFound,
ConnectorOAuthError,
Expand All @@ -15,16 +13,11 @@
from file_management.serializer import (
FileInfoIdeSerializer,
FileInfoSerializer,
FileListRequestIdeSerializer,
FileListRequestSerializer,
FileUploadIdeSerializer,
FileUploadSerializer,
)
from oauth2client.client import HttpAccessTokenRefreshError
from prompt_studio.prompt_studio_document_manager.models import DocumentManager
from prompt_studio.prompt_studio_document_manager.prompt_studio_document_helper import (
PromptStudioDocumentHelper,
)
from rest_framework import serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
Expand Down Expand Up @@ -125,110 +118,6 @@ def upload(self, request: HttpRequest) -> Response:
)
return Response({"message": "Files are uploaded successfully!"})

@action(detail=True, methods=["post"])
def upload_for_ide(self, request: HttpRequest) -> Response:
serializer = FileUploadIdeSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
uploaded_files: Any = serializer.validated_data.get("file")
tool_id: str = request.query_params.get("tool_id")
file_path = FileManagerHelper.handle_sub_directory_for_tenants(
request.org_id,
is_create=True,
user_id=request.user.user_id,
tool_id=tool_id,
)
file_system = LocalStorageFS(settings={"path": file_path})

documents = []
for uploaded_file in uploaded_files:
file_name = uploaded_file.name

# Create a record in the db for the file
document = PromptStudioDocumentHelper.create(
tool_id=tool_id, document_name=file_name
)
# Create a dictionary to store document data
doc = {
"document_id": document.document_id,
"document_name": document.document_name,
"tool": document.tool.tool_id,
}
# Store file
logger.info(
f"Uploading file: {file_name}"
if file_name
else "Uploading file"
)
FileManagerHelper.upload_file(
file_system,
file_path,
uploaded_file,
file_name,
)
documents.append(doc)
return Response({"data": documents})

@action(detail=True, methods=["get"])
def fetch_contents_ide(self, request: HttpRequest) -> Response:
serializer = FileInfoIdeSerializer(data=request.GET)
serializer.is_valid(raise_exception=True)
document_id: str = serializer.validated_data.get("document_id")
document: DocumentManager = DocumentManager.objects.get(pk=document_id)
file_name: str = document.document_name
tool_id: str = serializer.validated_data.get("tool_id")
view_type: str = serializer.validated_data.get("view_type")

filename_without_extension = file_name.rsplit(".", 1)[0]
if view_type == FileViewTypes.EXTRACT:
file_name = (
f"{FileViewTypes.EXTRACT.lower()}/"
f"{filename_without_extension}.txt"
)
if view_type == FileViewTypes.SUMMARIZE:
file_name = (
f"{FileViewTypes.SUMMARIZE.lower()}/"
f"{filename_without_extension}.txt"
)

file_path = file_path = (
FileManagerHelper.handle_sub_directory_for_tenants(
request.org_id,
is_create=True,
user_id=request.user.user_id,
tool_id=tool_id,
)
)
file_system = LocalStorageFS(settings={"path": file_path})
if not file_path.endswith("/"):
file_path += "/"
file_path += file_name
contents = FileManagerHelper.fetch_file_contents(file_system, file_path)
return Response({"data": contents}, status=status.HTTP_200_OK)

@action(detail=True, methods=["get"])
def list_ide(self, request: HttpRequest) -> Response:
serializer = FileListRequestIdeSerializer(data=request.GET)
serializer.is_valid(raise_exception=True)
tool_id: str = serializer.validated_data.get("tool_id")
file_path = FileManagerHelper.handle_sub_directory_for_tenants(
request.org_id,
is_create=True,
user_id=request.user.user_id,
tool_id=tool_id,
)
file_system = LocalStorageFS(settings={"path": file_path})
try:
files = FileManagerHelper.list_files(file_system, file_path)
serializer = FileInfoSerializer(files, many=True)
# fetching only the name from path
for file in serializer.data:
file_name = os.path.basename(file.get("name"))
file["name"] = file_name
return Response(serializer.data)
except Exception as error:
logger.error(f"Exception thrown from file list, error {error}")
raise InternalServerError()

@action(detail=True, methods=["get"])
def delete(self, request: HttpRequest) -> Response:
serializer = FileInfoIdeSerializer(data=request.GET)
Expand Down
21 changes: 21 additions & 0 deletions backend/prompt_studio/permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Any

from rest_framework import permissions
from rest_framework.request import Request
from rest_framework.views import APIView


class PromptAcesssToUser(permissions.BasePermission):
"""Is the crud to Prompt/Notes allowed to user."""

def has_object_permission(
self, request: Request, view: APIView, obj: Any
) -> bool:
return (
True
if (
obj.tool_id.created_by == request.user
or obj.tool_id.shared_users.filter(pk=request.user.pk).exists()
)
else False
)
6 changes: 0 additions & 6 deletions backend/prompt_studio/prompt_profile_manager/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from .views import ProfileManagerView

profile_manager_list = ProfileManagerView.as_view({"post": "create"})
profile_manager_detail = ProfileManagerView.as_view(
{
"get": "retrieve",
Expand All @@ -16,11 +15,6 @@

urlpatterns = format_suffix_patterns(
[
path(
"profile-manager/",
profile_manager_list,
name="profile-manager-list",
),
path(
"profile-manager/<uuid:pk>/",
profile_manager_detail,
Expand Down
8 changes: 2 additions & 6 deletions backend/prompt_studio/prompt_profile_manager/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,9 @@ def get_queryset(self) -> Optional[QuerySet]:
ProfileManagerKeys.CREATED_BY,
)
if filter_args:
queryset = ProfileManager.objects.filter(
created_by=self.request.user, **filter_args
)
queryset = ProfileManager.objects.filter(**filter_args)
else:
queryset = ProfileManager.objects.filter(
created_by=self.request.user
)
queryset = ProfileManager.objects.all()
return queryset

def create(
Expand Down
8 changes: 0 additions & 8 deletions backend/prompt_studio/prompt_studio/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@

from .views import ToolStudioPromptView

prompt_studio_prompt_list = ToolStudioPromptView.as_view(
{"get": "list", "post": "create"}
)
prompt_studio_prompt_detail = ToolStudioPromptView.as_view(
{
"get": "retrieve",
Expand All @@ -17,11 +14,6 @@

urlpatterns = format_suffix_patterns(
[
path(
"prompt/",
prompt_studio_prompt_list,
name="prompt-studio-prompt-list",
),
path(
"prompt/<uuid:pk>/",
prompt_studio_prompt_detail,
Expand Down
38 changes: 7 additions & 31 deletions backend/prompt_studio/prompt_studio/views.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import logging
from typing import Any, Optional
from typing import Optional

from account.custom_exceptions import DuplicateData
from django.db import IntegrityError
from django.db.models import QuerySet
from django.http import HttpRequest
from prompt_studio.prompt_studio.constants import (
ToolStudioPromptErrors,
ToolStudioPromptKeys,
)
from rest_framework import status, viewsets
from rest_framework.response import Response
from prompt_studio.permission import PromptAcesssToUser
from prompt_studio.prompt_studio.constants import ToolStudioPromptKeys
from rest_framework import viewsets
from rest_framework.versioning import URLPathVersioning
from utils.filtering import FilterHelper

Expand All @@ -35,33 +29,15 @@ class ToolStudioPromptView(viewsets.ModelViewSet):

versioning_class = URLPathVersioning
serializer_class = ToolStudioPromptSerializer
permission_classes: list[type[PromptAcesssToUser]] = [PromptAcesssToUser]

def get_queryset(self) -> Optional[QuerySet]:
filter_args = FilterHelper.build_filter_args(
self.request,
ToolStudioPromptKeys.TOOL_ID,
)
if filter_args:
queryset = ToolStudioPrompt.objects.filter(
created_by=self.request.user, **filter_args
)
queryset = ToolStudioPrompt.objects.filter(**filter_args)
else:
queryset = ToolStudioPrompt.objects.filter(
created_by=self.request.user,
)
queryset = ToolStudioPrompt.objects.all()
return queryset

def create(
self, request: HttpRequest, *args: tuple[Any], **kwargs: dict[str, Any]
) -> Response:
serializer = self.get_serializer(data=request.data)
# TODO : Handle model related exceptions.
serializer.is_valid(raise_exception=True)
try:
self.perform_create(serializer)
except IntegrityError:
raise DuplicateData(
f"{ToolStudioPromptErrors.PROMPT_NAME_EXISTS}, \
{ToolStudioPromptErrors.DUPLICATE_API}"
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
6 changes: 6 additions & 0 deletions backend/prompt_studio/prompt_studio_core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ class ToolStudioPromptKeys:
NOTES = "NOTES"


class FileViewTypes:
ORIGINAL = "ORIGINAL"
EXTRACT = "EXTRACT"
SUMMARIZE = "SUMMARIZE"


class LogLevels:
INFO = "INFO"
ERROR = "ERROR"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.1 on 2024-03-26 04:26

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
(
"prompt_studio_core",
"0011_alter_customtool_postamble_alter_customtool_preamble",
),
]

operations = [
migrations.AddField(
model_name="customtool",
name="shared_users",
field=models.ManyToManyField(
related_name="shared_custom_tool", to=settings.AUTH_USER_MODEL
),
),
]
Loading

0 comments on commit 72cdd4a

Please sign in to comment.