Skip to content

Commit

Permalink
feat: api webhooks (#2543)
Browse files Browse the repository at this point in the history
* dev: initiate external apis

* dev: external api

* dev: external public api implementation

* dev: add prefix to all api tokens

* dev: flag to enable disable api token api access

* dev: webhook model create and apis

* dev: webhook settings

* fix: webhook logs

* chore: removed drf spectacular

* dev: remove retry_count and fix api logging for get requests

* dev: refactor webhook logic

* fix: celery retry mechanism

* chore: event and action change

* chore: migrations changes

* dev: proxy setup for apis

* chore: changed retry time and cleanup

* chore: added issue comment and inbox issue api endpoints

* fix: migration files

* fix: added env variables

* fix: removed issue attachment from proxy

* fix: added new migration file

* fix: restricted wehbook access

* chore: changed urls

* chore: fixed porject serializer

* fix: set expire for api token

* fix: retrive endpoint for api token

* feat: Api Token screens & api integration

* dev: webhook endpoint changes

* dev: add fields for webhook updates

* feat: Download Api secret key

* chore: removed BASE API URL

* feat: revoke token access

* dev: migration fixes

* feat: workspace webhooks (#2748)

* feat: workspace webhook store, services integeration and rendered webhook list and create

* chore: handled webhook update and rengenerate token in workspace webhooks

* feat: regenerate key and delete functionality

---------

Co-authored-by: Ramesh Kumar <[email protected]>
Co-authored-by: gurusainath <[email protected]>
Co-authored-by: Ramesh Kumar Chandra <[email protected]>

* fix: url validation added

* fix: seperated env for webhook and api

* Web hooks refactoring

* add show option for generated hook key

* Api token restructure

* webhook minor fixes

* fix build errors

* chore: improvements in file structring

* dev: rate limiting the open apis

---------

Co-authored-by: pablohashescobar <[email protected]>
Co-authored-by: LAKHAN BAHETI <[email protected]>
Co-authored-by: rahulramesha <[email protected]>
Co-authored-by: Ramesh Kumar <[email protected]>
Co-authored-by: gurusainath <[email protected]>
Co-authored-by: Ramesh Kumar Chandra <[email protected]>
Co-authored-by: Nikhil <[email protected]>
Co-authored-by: sriram veeraghanta <[email protected]>
Co-authored-by: rahulramesha <[email protected]>
  • Loading branch information
10 people authored Nov 15, 2023
1 parent 7b96517 commit 79347ec
Show file tree
Hide file tree
Showing 94 changed files with 3,743 additions and 163 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80

# Set it to 0, to disable it
ENABLE_WEBHOOK=1

# Set it to 0, to disable it
ENABLE_API=1
9 changes: 8 additions & 1 deletion apiserver/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,12 @@ ENABLE_MAGIC_LINK_LOGIN="0"
# Email redirections and minio domain settings
WEB_URL="http://localhost"

# Set it to 0, to disable it
ENABLE_WEBHOOK=1

# Set it to 0, to disable it
ENABLE_API=1

# Gunicorn Workers
GUNICORN_WORKERS=2
GUNICORN_WORKERS=2

19 changes: 17 additions & 2 deletions apiserver/plane/api/permissions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission, WorkspaceUserPermission
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission

from .workspace import (
WorkSpaceBasePermission,
WorkspaceOwnerPermission,
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceViewerPermission,
WorkspaceUserPermission,
)
from .project import (
ProjectBasePermission,
ProjectEntityPermission,
ProjectMemberPermission,
ProjectLitePermission,
)


18 changes: 16 additions & 2 deletions apiserver/plane/api/permissions/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ def has_permission(self, request, view):
).exists()


class WorkspaceOwnerPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False

return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role=Owner,
).exists()


class WorkSpaceAdminPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
Expand Down Expand Up @@ -93,10 +105,12 @@ def has_permission(self, request, view):


class WorkspaceUserPermission(BasePermission):

def has_permission(self, request, view):
if request.user.is_anonymous:
return False

return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
)
).exists()
4 changes: 3 additions & 1 deletion apiserver/plane/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
ModuleFavoriteSerializer,
)

from .api_token import APITokenSerializer
from .api import APITokenSerializer, APITokenReadSerializer

from .integration import (
IntegrationSerializer,
Expand Down Expand Up @@ -100,3 +100,5 @@
from .notification import NotificationSerializer

from .exporter import ExporterHistorySerializer

from .webhook import WebhookSerializer, WebhookLogSerializer
31 changes: 31 additions & 0 deletions apiserver/plane/api/serializers/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from .base import BaseSerializer
from plane.db.models import APIToken, APIActivityLog


class APITokenSerializer(BaseSerializer):

class Meta:
model = APIToken
fields = "__all__"
read_only_fields = [
"token",
"expired_at",
"created_at",
"updated_at",
"workspace",
"user",
]


class APITokenReadSerializer(BaseSerializer):

class Meta:
model = APIToken
exclude = ('token',)


class APIActivityLogSerializer(BaseSerializer):

class Meta:
model = APIActivityLog
fields = "__all__"
14 changes: 0 additions & 14 deletions apiserver/plane/api/serializers/api_token.py

This file was deleted.

2 changes: 1 addition & 1 deletion apiserver/plane/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def get_members(self, obj):
"member__display_name",
"member__avatar",
)
return project_members
return list(project_members)

class Meta:
model = Project
Expand Down
30 changes: 30 additions & 0 deletions apiserver/plane/api/serializers/webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Third party imports
from rest_framework import serializers

# Module imports
from .base import DynamicBaseSerializer
from plane.db.models import Webhook, WebhookLog
from plane.db.models.webhook import validate_domain, validate_schema

class WebhookSerializer(DynamicBaseSerializer):
url = serializers.URLField(validators=[validate_schema, validate_domain])

class Meta:
model = Webhook
fields = "__all__"
read_only_fields = [
"workspace",
"secret_key",
]


class WebhookLogSerializer(DynamicBaseSerializer):

class Meta:
model = WebhookLog
fields = "__all__"
read_only_fields = [
"workspace",
"webhook"
]

12 changes: 12 additions & 0 deletions apiserver/plane/api/urls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
from .user import urlpatterns as user_urls
from .views import urlpatterns as view_urls
from .workspace import urlpatterns as workspace_urls
from .api import urlpatterns as api_urls
from .webhook import urlpatterns as webhook_urls


# Django imports
from django.conf import settings


urlpatterns = [
Expand All @@ -44,3 +50,9 @@
*view_urls,
*workspace_urls,
]

if settings.ENABLE_WEBHOOK:
urlpatterns += webhook_urls

if settings.ENABLE_API:
urlpatterns += api_urls
17 changes: 17 additions & 0 deletions apiserver/plane/api/urls/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.urls import path
from plane.api.views import ApiTokenEndpoint

urlpatterns = [
# API Tokens
path(
"workspaces/<str:slug>/api-tokens/",
ApiTokenEndpoint.as_view(),
name="api-tokens",
),
path(
"workspaces/<str:slug>/api-tokens/<uuid:pk>/",
ApiTokenEndpoint.as_view(),
name="api-tokens",
),
## End API Tokens
]
31 changes: 31 additions & 0 deletions apiserver/plane/api/urls/webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.urls import path

from plane.api.views import (
WebhookEndpoint,
WebhookLogsEndpoint,
WebhookSecretRegenerateEndpoint,
)


urlpatterns = [
path(
"workspaces/<str:slug>/webhooks/",
WebhookEndpoint.as_view(),
name="webhooks",
),
path(
"workspaces/<str:slug>/webhooks/<uuid:pk>/",
WebhookEndpoint.as_view(),
name="webhooks",
),
path(
"workspaces/<str:slug>/webhooks/<uuid:pk>/regenerate/",
WebhookSecretRegenerateEndpoint.as_view(),
name="webhooks",
),
path(
"workspaces/<str:slug>/webhook-logs/<uuid:webhook_id>/",
WebhookLogsEndpoint.as_view(),
name="webhooks",
),
]
6 changes: 4 additions & 2 deletions apiserver/plane/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from .oauth import OauthEndpoint

from .base import BaseAPIView, BaseViewSet
from .base import BaseAPIView, BaseViewSet, WebhookMixin

from .workspace import (
WorkSpaceViewSet,
Expand Down Expand Up @@ -115,7 +115,7 @@
ModuleFavoriteViewSet,
)

from .api_token import ApiTokenEndpoint
from .api import ApiTokenEndpoint

from .integration import (
WorkspaceIntegrationViewSet,
Expand Down Expand Up @@ -172,3 +172,5 @@
from .exporter import ExportIssuesEndpoint

from .config import ConfigurationEndpoint

from .webhook import WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint
78 changes: 78 additions & 0 deletions apiserver/plane/api/views/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Python import
from uuid import uuid4

# Third party
from rest_framework.response import Response
from rest_framework import status

# Module import
from .base import BaseAPIView
from plane.db.models import APIToken, Workspace
from plane.api.serializers import APITokenSerializer, APITokenReadSerializer
from plane.api.permissions import WorkspaceOwnerPermission


class ApiTokenEndpoint(BaseAPIView):
permission_classes = [
WorkspaceOwnerPermission,
]

def post(self, request, slug):
label = request.data.get("label", str(uuid4().hex))
description = request.data.get("description", "")
workspace = Workspace.objects.get(slug=slug)
expired_at = request.data.get("expired_at", None)

# Check the user type
user_type = 1 if request.user.is_bot else 0

api_token = APIToken.objects.create(
label=label,
description=description,
user=request.user,
workspace=workspace,
user_type=user_type,
expired_at=expired_at,
)

serializer = APITokenSerializer(api_token)
# Token will be only visible while creating
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
)

def get(self, request, slug, pk=None):
if pk == None:
api_tokens = APIToken.objects.filter(
user=request.user, workspace__slug=slug
)
serializer = APITokenReadSerializer(api_tokens, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
api_tokens = APIToken.objects.get(
user=request.user, workspace__slug=slug, pk=pk
)
serializer = APITokenReadSerializer(api_tokens)
return Response(serializer.data, status=status.HTTP_200_OK)

def delete(self, request, slug, pk):
api_token = APIToken.objects.get(
workspace__slug=slug,
user=request.user,
pk=pk,
)
api_token.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

def patch(self, request, slug, pk):
api_token = APIToken.objects.get(
workspace__slug=slug,
user=request.user,
pk=pk,
)
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Loading

0 comments on commit 79347ec

Please sign in to comment.