diff --git a/backend/api/urls.py b/backend/api/urls.py index 24ca222e..d21da490 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,8 +1,8 @@ from django.urls import path, include -from .views import GoogleLoginView, GitHubLoginView, GitLabLoginView +from .views.auth import GoogleLoginView, GitHubLoginView, GitLabLoginView urlpatterns = [ - path('google/', GoogleLoginView.as_view(), name='google'), - path('github/', GitHubLoginView.as_view(), name='github'), - path('gitlab/', GitLabLoginView.as_view(), name='gitlab') + path("google/", GoogleLoginView.as_view(), name="google"), + path("github/", GitHubLoginView.as_view(), name="github"), + path("gitlab/", GitLabLoginView.as_view(), name="gitlab"), ] diff --git a/backend/api/views/auth.py b/backend/api/views/auth.py new file mode 100644 index 00000000..67e51451 --- /dev/null +++ b/backend/api/views/auth.py @@ -0,0 +1,288 @@ +import requests +import json +import base64 +import jwt +import os +from api.serializers import ( + ServiceTokenSerializer, + UserTokenSerializer, +) +from api.models import ServiceToken, UserToken, CustomUser +from api.emails import send_login_email +from api.utils.syncing.auth import store_oauth_token +from backend.api.notifier import notify_slack +from api.utils.rest import ( + get_token_type, + token_is_expired_or_deleted, +) + +from django.conf import settings +from django.contrib.auth import logout +from django.views.decorators.csrf import csrf_exempt +from django.shortcuts import redirect +from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework import status + +from dj_rest_auth.registration.views import SocialLoginView +from allauth.socialaccount import app_settings +from allauth.socialaccount.providers.oauth2.client import OAuth2Client, OAuth2Error +from allauth.socialaccount.providers.github.provider import GitHubProvider +from allauth.socialaccount.providers.gitlab.provider import GitLabProvider +from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter +from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter +from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter + + +CLOUD_HOSTED = settings.APP_HOST == "cloud" + + +def github_callback(request): + code = request.GET.get("code") + state = request.GET.get("state") + + client_id = os.getenv("GITHUB_INTEGRATION_CLIENT_ID") + client_secret = os.getenv("GITHUB_INTEGRATION_CLIENT_SECRET") + + state_decoded = base64.b64decode(state).decode("utf-8") + state = json.loads(state_decoded) + + original_url = state.get("returnUrl", "/") + org_id = state.get("orgId") + + # Exchange code for token + response = requests.post( + "https://github.com/login/oauth/access_token", + headers={"Accept": "application/json"}, + data={ + "client_id": client_id, + "client_secret": client_secret, + "code": code, + "redirect_uri": f"{os.getenv('ALLOWED_ORIGINS')}/service/oauth/github/callback", + }, + ) + + access_token = response.json().get("access_token") + + store_oauth_token("github", access_token, org_id) + + # Redirect back to Next.js app with token and original URL + return redirect(f"{os.getenv('ALLOWED_ORIGINS')}{original_url}") + + +# for custom gitlab adapter class +def _check_errors(response): + # 403 error's are presented as user-facing errors + if response.status_code == 403: + msg = response.content + raise OAuth2Error("Invalid data from GitLab API: %r" % (msg)) + + try: + data = response.json() + except ValueError: # JSONDecodeError on py3 + raise OAuth2Error("Invalid JSON from GitLab API: %r" % (response.text)) + + if response.status_code >= 400 or "error" in data: + # For errors, we expect the following format: + # {"error": "error_name", "error_description": "Oops!"} + # For example, if the token is not valid, we will get: + # {"message": "status_code - message"} + error = data.get("error", "") or response.status_code + desc = data.get("error_description", "") or data.get("message", "") + + raise OAuth2Error("GitLab error: %s (%s)" % (error, desc)) + + # The expected output from the API follows this format: + # {"id": 12345, ...} + if "id" not in data: + # If the id is not present, the output is not usable (no UID) + raise OAuth2Error("Invalid data from GitLab API: %r" % (data)) + + return data + + +class CustomGoogleOAuth2Adapter(GoogleOAuth2Adapter): + def complete_login(self, request, app, token, response, **kwargs): + try: + identity_data = jwt.decode( + response["id_token"], # another nested id_token was returned + options={ + "verify_signature": False, + "verify_iss": True, + "verify_aud": True, + "verify_exp": True, + }, + issuer=self.id_token_issuer, + audience=app.client_id, + ) + except jwt.PyJWTError as e: + raise OAuth2Error("Invalid id_token") from e + login = self.get_provider().sociallogin_from_response(request, identity_data) + email = login.email_addresses[0] + + if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): + try: + # Notify Slack + notify_slack(f"New user signup: {email}") + except Exception as e: + print(f"Error notifying Slack: {e}") + + try: + send_login_email(request, email, "Google") + except Exception as e: + print(f"Error sending email: {e}") + + return login + + +class CustomGitHubOAuth2Adapter(GitHubOAuth2Adapter): + provider_id = GitHubProvider.id + settings = app_settings.PROVIDERS.get(provider_id, {}) + + if "GITHUB_URL" in settings: + web_url = settings.get("GITHUB_URL").rstrip("/") + api_url = "{0}/api/v3".format(web_url) + else: + web_url = "https://github.com" + api_url = "https://api.github.com" + + access_token_url = "{0}/login/oauth/access_token".format(web_url) + authorize_url = "{0}/login/oauth/authorize".format(web_url) + profile_url = "{0}/user".format(api_url) + emails_url = "{0}/user/emails".format(api_url) + + def complete_login(self, request, app, token, **kwargs): + headers = {"Authorization": "token {}".format(token.token)} + resp = requests.get(self.profile_url, headers=headers) + resp.raise_for_status() + extra_data = resp.json() + if app_settings.QUERY_EMAIL and not extra_data.get("email"): + extra_data["email"] = self.get_email(headers) + + email = extra_data["email"] + + if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): + try: + # Notify Slack + notify_slack(f"New user signup: {email}") + except Exception as e: + print(f"Error notifying Slack: {e}") + + try: + send_login_email(request, email, "GitHub") + except Exception as e: + print(f"Error sending email: {e}") + + return self.get_provider().sociallogin_from_response(request, extra_data) + + +class CustomGitLabOAuth2Adapter(OAuth2Adapter): + provider_id = GitLabProvider.id + provider_default_url = "https://gitlab.com" + provider_api_version = "v4" + + settings = app_settings.PROVIDERS.get(provider_id, {}) + provider_base_url = settings.get("GITLAB_URL", provider_default_url) + + access_token_url = "{0}/oauth/token".format(provider_base_url) + authorize_url = "{0}/oauth/authorize".format(provider_base_url) + profile_url = "{0}/api/{1}/user".format(provider_base_url, provider_api_version) + + def complete_login(self, request, app, token, response): + response = requests.get(self.profile_url, params={"access_token": token.token}) + data = _check_errors(response) + login = self.get_provider().sociallogin_from_response(request, data) + + email = login.email_addresses[0] + + if CLOUD_HOSTED: + # Check if user exists and notify Slack for new user signup + if not CustomUser.objects.filter(email=email).exists(): + try: + notify_slack(f"New user signup: {email}") + except Exception as e: + print(f"Error notifying Slack: {e}") + + try: + send_login_email(request, email, "GitLab") + except Exception as e: + print(f"Error sending email: {e}") + + return login + + +class GoogleLoginView(SocialLoginView): + authentication_classes = [] + adapter_class = CustomGoogleOAuth2Adapter + callback_url = settings.OAUTH_REDIRECT_URI + client_class = OAuth2Client + + +class GitHubLoginView(SocialLoginView): + authentication_classes = [] + adapter_class = CustomGitHubOAuth2Adapter + callback_url = settings.OAUTH_REDIRECT_URI + client_class = OAuth2Client + + +class GitLabLoginView(SocialLoginView): + authentication_classes = [] + adapter_class = CustomGitLabOAuth2Adapter + callback_url = settings.OAUTH_REDIRECT_URI + client_class = OAuth2Client + + +def logout_view(request): + logout(request) + return JsonResponse({"message": "Logged out"}) + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def health_check(request): + return JsonResponse({"status": "alive"}) + + +def user_token_kms(request): + auth_token = request.headers["authorization"] + + token = auth_token.split(" ")[2] + + user_token = UserToken.objects.get(token=token) + + serializer = UserTokenSerializer(user_token) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +def service_token_kms(request): + auth_token = request.headers["authorization"] + + token = auth_token.split(" ")[2] + + service_token = ServiceToken.objects.get(token=token) + + serializer = ServiceTokenSerializer(service_token) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def secrets_tokens(request): + auth_token = request.headers["authorization"] + + if token_is_expired_or_deleted(auth_token): + return HttpResponse(status=403) + + token_type = get_token_type(auth_token) + + if token_type == "Service": + return service_token_kms(request) + elif token_type == "User": + return user_token_kms(request) + else: + return HttpResponse(status=403) diff --git a/backend/api/views/graphql.py b/backend/api/views/graphql.py new file mode 100644 index 00000000..7f70f712 --- /dev/null +++ b/backend/api/views/graphql.py @@ -0,0 +1,7 @@ +from graphene_django.views import GraphQLView +from django.contrib.auth.mixins import LoginRequiredMixin + + +class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): + raise_exception = True + pass diff --git a/backend/api/views/kms.py b/backend/api/views/kms.py new file mode 100644 index 00000000..fd0bb8e9 --- /dev/null +++ b/backend/api/views/kms.py @@ -0,0 +1,43 @@ +from datetime import datetime + +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from django.http import JsonResponse, HttpResponse +from api.utils.rest import ( + get_client_ip, +) +from logs.models import KMSDBLog +from api.models import ( + App, +) + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def kms(request, app_id): + auth_token = request.headers["authorization"] + event_type = request.headers["eventtype"] + phase_node = request.headers["phasenode"] + ph_size = request.headers["phsize"] + ip_address = get_client_ip(request) + app_token = auth_token.split("Bearer ")[1] + + if not app_token: + return HttpResponse(status=404) + try: + app = App.objects.get(app_token=app_token) + try: + timestamp = datetime.now().timestamp() * 1000 + KMSDBLog.objects.create( + app_id=app_id, + event_type=event_type, + phase_node=phase_node, + ph_size=float(ph_size), + ip_address=ip_address, + timestamp=timestamp, + ) + except: + pass + return JsonResponse({"wrappedKeyShare": app.wrapped_key_share}) + except: + return HttpResponse(status=404) diff --git a/backend/api/views/lockbox.py b/backend/api/views/lockbox.py new file mode 100644 index 00000000..6f68d30b --- /dev/null +++ b/backend/api/views/lockbox.py @@ -0,0 +1,63 @@ +from api.serializers import ( + LockboxSerializer, +) +from api.models import ( + Lockbox, +) + +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt +from django.utils import timezone + +from django.db.models import Q + +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from djangorestframework_camel_case.parser import CamelCaseJSONParser +from djangorestframework_camel_case.render import ( + CamelCaseJSONRenderer, +) + + +class LockboxView(APIView): + permission_classes = [ + AllowAny, + ] + parser_classes = [ + CamelCaseJSONParser, + ] + renderer_classes = [ + CamelCaseJSONRenderer, + ] + + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + return super(LockboxView, self).dispatch(request, *args, **kwargs) + + def get(self, request, box_id): + try: + box = Lockbox.objects.get( + Q(id=box_id) + & (Q(expires_at__gte=timezone.now()) | Q(expires_at__isnull=True)) + ) + if box.allowed_views is None or box.views < box.allowed_views: + serializer = LockboxSerializer(box) + return Response(serializer.data, status=status.HTTP_200_OK) + else: + return HttpResponse(status=status.HTTP_403_FORBIDDEN) + + except Lockbox.DoesNotExist: + return HttpResponse(status=status.HTTP_404_NOT_FOUND) + + def put(self, request, box_id): + try: + box = Lockbox.objects.get(id=box_id) + box.views += 1 + box.save() + return HttpResponse(status=status.HTTP_200_OK) + + except Lockbox.DoesNotExist: + return HttpResponse(status=status.HTTP_404_NOT_FOUND) diff --git a/backend/api/views.py b/backend/api/views/secrets.py similarity index 56% rename from backend/api/views.py rename to backend/api/views/secrets.py index 19c1ca59..7da12244 100644 --- a/backend/api/views.py +++ b/backend/api/views/secrets.py @@ -1,14 +1,14 @@ -from datetime import datetime -import json +from api.auth import PhaseTokenAuthentication +from api.models import ( + Environment, + Secret, + SecretEvent, + SecretTag, + ServerEnvironmentKey, +) from api.serializers import ( - LockboxSerializer, SecretSerializer, - ServiceTokenSerializer, - UserTokenSerializer, ) -from api.emails import send_login_email -from api.utils.permissions import user_can_access_environment -from api.utils.syncing.auth import store_oauth_token from api.utils.secrets import ( check_for_duplicates_blind, create_environment_folder_structure, @@ -16,346 +16,26 @@ compute_key_digest, get_environment_keys, ) +from api.utils.permissions import user_can_access_environment from api.utils.audit_logging import log_secret_event -from api.auth import PhaseTokenAuthentication + from api.utils.crypto import encrypt_asymmetric -from dj_rest_auth.registration.views import SocialLoginView -from django.contrib.auth.mixins import LoginRequiredMixin -from graphene_django.views import GraphQLView -from django.conf import settings -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework.response import Response -from django.http import JsonResponse, HttpResponse from api.utils.rest import ( - get_client_ip, get_resolver_request_meta, - get_token_type, - token_is_expired_or_deleted, ) -from logs.models import KMSDBLog -from .models import ( - App, - Environment, - Lockbox, - Secret, - SecretEvent, - SecretTag, - ServerEnvironmentKey, - ServiceToken, - UserToken, -) -import jwt -import requests -from django.contrib.auth import logout -from api.models import CustomUser -from allauth.socialaccount import app_settings -from backend.api.notifier import notify_slack -from allauth.socialaccount.providers.oauth2.client import OAuth2Client, OAuth2Error -from allauth.socialaccount.providers.github.provider import GitHubProvider -from allauth.socialaccount.providers.gitlab.provider import GitLabProvider -from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter -from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter -from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter + +import json from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from rest_framework import status from django.views.decorators.csrf import csrf_exempt +from django.http import JsonResponse, HttpResponse from django.utils import timezone -import base64 -from django.shortcuts import redirect -import os -from django.db.models import Q -from djangorestframework_camel_case.parser import CamelCaseJSONParser from djangorestframework_camel_case.render import ( CamelCaseJSONRenderer, ) -CLOUD_HOSTED = settings.APP_HOST == "cloud" - - -def github_callback(request): - code = request.GET.get("code") - state = request.GET.get("state") - - client_id = os.getenv("GITHUB_INTEGRATION_CLIENT_ID") - client_secret = os.getenv("GITHUB_INTEGRATION_CLIENT_SECRET") - - state_decoded = base64.b64decode(state).decode("utf-8") - state = json.loads(state_decoded) - - original_url = state.get("returnUrl", "/") - org_id = state.get("orgId") - - # Exchange code for token - response = requests.post( - "https://github.com/login/oauth/access_token", - headers={"Accept": "application/json"}, - data={ - "client_id": client_id, - "client_secret": client_secret, - "code": code, - "redirect_uri": f"{os.getenv('ALLOWED_ORIGINS')}/service/oauth/github/callback", - }, - ) - - access_token = response.json().get("access_token") - - store_oauth_token("github", access_token, org_id) - - # Redirect back to Next.js app with token and original URL - return redirect(f"{os.getenv('ALLOWED_ORIGINS')}{original_url}") - - -# for custom gitlab adapter class -def _check_errors(response): - # 403 error's are presented as user-facing errors - if response.status_code == 403: - msg = response.content - raise OAuth2Error("Invalid data from GitLab API: %r" % (msg)) - - try: - data = response.json() - except ValueError: # JSONDecodeError on py3 - raise OAuth2Error("Invalid JSON from GitLab API: %r" % (response.text)) - - if response.status_code >= 400 or "error" in data: - # For errors, we expect the following format: - # {"error": "error_name", "error_description": "Oops!"} - # For example, if the token is not valid, we will get: - # {"message": "status_code - message"} - error = data.get("error", "") or response.status_code - desc = data.get("error_description", "") or data.get("message", "") - - raise OAuth2Error("GitLab error: %s (%s)" % (error, desc)) - - # The expected output from the API follows this format: - # {"id": 12345, ...} - if "id" not in data: - # If the id is not present, the output is not usable (no UID) - raise OAuth2Error("Invalid data from GitLab API: %r" % (data)) - - return data - - -class CustomGoogleOAuth2Adapter(GoogleOAuth2Adapter): - def complete_login(self, request, app, token, response, **kwargs): - try: - identity_data = jwt.decode( - response["id_token"], # another nested id_token was returned - options={ - "verify_signature": False, - "verify_iss": True, - "verify_aud": True, - "verify_exp": True, - }, - issuer=self.id_token_issuer, - audience=app.client_id, - ) - except jwt.PyJWTError as e: - raise OAuth2Error("Invalid id_token") from e - login = self.get_provider().sociallogin_from_response(request, identity_data) - email = login.email_addresses[0] - - if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): - try: - # Notify Slack - notify_slack(f"New user signup: {email}") - except Exception as e: - print(f"Error notifying Slack: {e}") - - try: - send_login_email(request, email, "Google") - except Exception as e: - print(f"Error sending email: {e}") - - return login - - -class CustomGitHubOAuth2Adapter(GitHubOAuth2Adapter): - provider_id = GitHubProvider.id - settings = app_settings.PROVIDERS.get(provider_id, {}) - - if "GITHUB_URL" in settings: - web_url = settings.get("GITHUB_URL").rstrip("/") - api_url = "{0}/api/v3".format(web_url) - else: - web_url = "https://github.com" - api_url = "https://api.github.com" - - access_token_url = "{0}/login/oauth/access_token".format(web_url) - authorize_url = "{0}/login/oauth/authorize".format(web_url) - profile_url = "{0}/user".format(api_url) - emails_url = "{0}/user/emails".format(api_url) - - def complete_login(self, request, app, token, **kwargs): - headers = {"Authorization": "token {}".format(token.token)} - resp = requests.get(self.profile_url, headers=headers) - resp.raise_for_status() - extra_data = resp.json() - if app_settings.QUERY_EMAIL and not extra_data.get("email"): - extra_data["email"] = self.get_email(headers) - - email = extra_data["email"] - - if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): - try: - # Notify Slack - notify_slack(f"New user signup: {email}") - except Exception as e: - print(f"Error notifying Slack: {e}") - - try: - send_login_email(request, email, "GitHub") - except Exception as e: - print(f"Error sending email: {e}") - - return self.get_provider().sociallogin_from_response(request, extra_data) - - -class CustomGitLabOAuth2Adapter(OAuth2Adapter): - provider_id = GitLabProvider.id - provider_default_url = "https://gitlab.com" - provider_api_version = "v4" - - settings = app_settings.PROVIDERS.get(provider_id, {}) - provider_base_url = settings.get("GITLAB_URL", provider_default_url) - - access_token_url = "{0}/oauth/token".format(provider_base_url) - authorize_url = "{0}/oauth/authorize".format(provider_base_url) - profile_url = "{0}/api/{1}/user".format(provider_base_url, provider_api_version) - - def complete_login(self, request, app, token, response): - response = requests.get(self.profile_url, params={"access_token": token.token}) - data = _check_errors(response) - login = self.get_provider().sociallogin_from_response(request, data) - - email = login.email_addresses[0] - - if CLOUD_HOSTED: - # Check if user exists and notify Slack for new user signup - if not CustomUser.objects.filter(email=email).exists(): - try: - notify_slack(f"New user signup: {email}") - except Exception as e: - print(f"Error notifying Slack: {e}") - - try: - send_login_email(request, email, "GitLab") - except Exception as e: - print(f"Error sending email: {e}") - - return login - - -class GoogleLoginView(SocialLoginView): - authentication_classes = [] - adapter_class = CustomGoogleOAuth2Adapter - callback_url = settings.OAUTH_REDIRECT_URI - client_class = OAuth2Client - - -class GitHubLoginView(SocialLoginView): - authentication_classes = [] - adapter_class = CustomGitHubOAuth2Adapter - callback_url = settings.OAUTH_REDIRECT_URI - client_class = OAuth2Client - - -class GitLabLoginView(SocialLoginView): - authentication_classes = [] - adapter_class = CustomGitLabOAuth2Adapter - callback_url = settings.OAUTH_REDIRECT_URI - client_class = OAuth2Client - - -def logout_view(request): - logout(request) - return JsonResponse({"message": "Logged out"}) - - -@api_view(["GET"]) -@permission_classes([AllowAny]) -def health_check(request): - return JsonResponse({"status": "alive"}) - - -@api_view(["GET"]) -@permission_classes([AllowAny]) -def kms(request, app_id): - auth_token = request.headers["authorization"] - event_type = request.headers["eventtype"] - phase_node = request.headers["phasenode"] - ph_size = request.headers["phsize"] - ip_address = get_client_ip(request) - app_token = auth_token.split("Bearer ")[1] - - if not app_token: - return HttpResponse(status=404) - try: - app = App.objects.get(app_token=app_token) - try: - timestamp = datetime.now().timestamp() * 1000 - KMSDBLog.objects.create( - app_id=app_id, - event_type=event_type, - phase_node=phase_node, - ph_size=float(ph_size), - ip_address=ip_address, - timestamp=timestamp, - ) - except: - pass - return JsonResponse({"wrappedKeyShare": app.wrapped_key_share}) - except: - return HttpResponse(status=404) - - -def user_token_kms(request): - auth_token = request.headers["authorization"] - - token = auth_token.split(" ")[2] - - user_token = UserToken.objects.get(token=token) - - serializer = UserTokenSerializer(user_token) - - return Response(serializer.data, status=status.HTTP_200_OK) - - -def service_token_kms(request): - auth_token = request.headers["authorization"] - - token = auth_token.split(" ")[2] - - service_token = ServiceToken.objects.get(token=token) - - serializer = ServiceTokenSerializer(service_token) - - return Response(serializer.data, status=status.HTTP_200_OK) - - -@api_view(["GET"]) -@permission_classes([AllowAny]) -def secrets_tokens(request): - auth_token = request.headers["authorization"] - - if token_is_expired_or_deleted(auth_token): - return HttpResponse(status=403) - - token_type = get_token_type(auth_token) - - if token_type == "Service": - return service_token_kms(request) - elif token_type == "User": - return user_token_kms(request) - else: - return HttpResponse(status=403) - - -class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): - raise_exception = True - pass - class E2EESecretsView(APIView): authentication_classes = [PhaseTokenAuthentication] @@ -643,6 +323,8 @@ def post(self, request): if check_for_duplicates_blind(secrets, env): return JsonResponse({"error": "Duplicate secret found"}, status=409) + created_secrets = [] + for secret in secrets: try: @@ -685,10 +367,16 @@ def post(self, request): user_agent, ) - return Response( - {"message": f"Created {len(secrets)} secrets"}, status=status.HTTP_200_OK + created_secrets.append(secret_obj) + + serializer = SecretSerializer( + created_secrets, + many=True, + context={"org_member": request.auth["org_member"], "sse": True}, ) + return Response(serializer.data, status=status.HTTP_200_OK) + def put(self, request): env = request.auth["environment"] @@ -718,6 +406,8 @@ def put(self, request): if check_for_duplicates_blind(secrets, env): return JsonResponse({"error": "Duplicate secret found"}, status=409) + updated_secrets = [] + for secret in secrets: secret_obj = Secret.objects.get(id=secret["id"]) @@ -781,10 +471,16 @@ def put(self, request): user_agent, ) - return Response( - {"message": f"Updated {len(secrets)} secrets"}, status=status.HTTP_200_OK + updated_secrets.append(secret_obj) + + serializer = SecretSerializer( + updated_secrets, + many=True, + context={"org_member": request.auth["org_member"], "sse": True}, ) + return Response(serializer.data, status=status.HTTP_200_OK) + def delete(self, request): env = request.auth["environment"] @@ -828,44 +524,3 @@ def delete(self, request): {"message": f"Deleted {len(secrets_to_delete)} secrets"}, status=status.HTTP_200_OK, ) - - -class LockboxView(APIView): - permission_classes = [ - AllowAny, - ] - parser_classes = [ - CamelCaseJSONParser, - ] - renderer_classes = [ - CamelCaseJSONRenderer, - ] - - @csrf_exempt - def dispatch(self, request, *args, **kwargs): - return super(LockboxView, self).dispatch(request, *args, **kwargs) - - def get(self, request, box_id): - try: - box = Lockbox.objects.get( - Q(id=box_id) - & (Q(expires_at__gte=timezone.now()) | Q(expires_at__isnull=True)) - ) - if box.allowed_views is None or box.views < box.allowed_views: - serializer = LockboxSerializer(box) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - return HttpResponse(status=status.HTTP_403_FORBIDDEN) - - except Lockbox.DoesNotExist: - return HttpResponse(status=status.HTTP_404_NOT_FOUND) - - def put(self, request, box_id): - try: - box = Lockbox.objects.get(id=box_id) - box.views += 1 - box.save() - return HttpResponse(status=status.HTTP_200_OK) - - except Lockbox.DoesNotExist: - return HttpResponse(status=status.HTTP_404_NOT_FOUND) diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 71fbb74d..6d4b470b 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -2,17 +2,11 @@ from django.urls import path, include, re_path from django.conf import settings from django.views.decorators.csrf import csrf_exempt -from api.views import ( - LockboxView, - PrivateGraphQLView, - logout_view, - health_check, - kms, - E2EESecretsView, - PublicSecretsView, - secrets_tokens, - github_callback, -) +from api.views.lockbox import LockboxView +from api.views.graphql import PrivateGraphQLView +from api.views.secrets import E2EESecretsView, PublicSecretsView +from api.views.auth import logout_view, health_check, github_callback, secrets_tokens +from api.views.kms import kms CLOUD_HOSTED = settings.APP_HOST == "cloud"