Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 34 additions & 30 deletions api/api/urls/v1.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import Any

from django.conf import settings
from django.urls import include, path, re_path
from drf_yasg import openapi # type: ignore[import-untyped]
from drf_yasg.views import get_schema_view # type: ignore[import-untyped]
from rest_framework import authentication, permissions, routers
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework import routers
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView

from app_analytics.views import SDKAnalyticsFlags, SelfHostedTelemetryAPIView
from environments.identities.traits.views import SDKTraits
Expand All @@ -13,30 +16,34 @@
from integrations.github.views import github_webhook
from organisations.views import chargebee_webhook

schema_view_permission_class = ( # pragma: no cover
permissions.IsAuthenticated
if settings.REQUIRE_AUTHENTICATION_FOR_API_DOCS
else permissions.AllowAny
)

schema_view = get_schema_view(
openapi.Info(
title="Flagsmith API",
default_version="v1",
description="",
license=openapi.License(name="BSD License"),
contact=openapi.Contact(email="[email protected]"),
),
public=True,
permission_classes=[schema_view_permission_class],
authentication_classes=[authentication.BasicAuthentication],
)

traits_router = routers.DefaultRouter()
traits_router.register(r"", SDKTraits, basename="sdk-traits")

app_name = "v1"


class DocsView(APIView): # pragma: no cover
permission_classes = (AllowAny,)

def get(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
# Maintain backwards-compat with /docs/?format=openapi returning raw schema
if request.GET.get("format") == "openapi":
return SpectacularAPIView.as_view()(request, *args, **kwargs)
return SpectacularSwaggerView.as_view(url_name="api-v1:schema")(
request, *args, **kwargs
)


def swagger_schema_view(
request: Any, *args: Any, **kwargs: Any
) -> Any: # pragma: no cover
# Normalize format to remove leading dot so both .json and json work
fmt = kwargs.get("format")
if isinstance(fmt, str) and fmt.startswith("."):
kwargs["format"] = fmt[1:]
return SpectacularAPIView.as_view()(request, *args, **kwargs)


urlpatterns = [
re_path(r"^organisations/", include("organisations.urls"), name="organisations"),
re_path(r"^projects/", include("projects.urls"), name="projects"),
Expand Down Expand Up @@ -81,16 +88,13 @@
),
re_path("", include("features.versioning.urls", namespace="versioning")),
# API documentation
# Keep old name for tests expecting reverse("api-v1:schema-json", ...)
re_path(
r"^swagger(?P<format>\.json|\.yaml)$",
schema_view.without_ui(cache_timeout=0),
name="schema-json",
),
re_path(
r"^docs/$",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
r"^swagger(?P<format>\.json|\.yaml)$", swagger_schema_view, name="schema-json"
),
# New endpoints
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("docs/", DocsView.as_view(), name="schema-swagger-ui"),
# Test webhook url
re_path(r"^webhooks/", include("webhooks.urls", namespace="webhooks")),
path("", include("projects.code_references.urls", namespace="code_references")),
Expand Down
34 changes: 26 additions & 8 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
"app",
"e2etests",
"simple_history",
"drf_yasg",
"drf_spectacular",
"audit",
"permissions",
"projects.code_references",
Expand Down Expand Up @@ -326,6 +326,7 @@
"util.renderers.PydanticJSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
MIDDLEWARE = [
"common.core.middleware.APIResponseVersionHeaderMiddleware",
Expand Down Expand Up @@ -538,26 +539,43 @@
EMAIL_PORT = env("EMAIL_PORT", default=587)
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=True)

SWAGGER_SETTINGS = {
"DEEP_LINKING": True,
"DEFAULT_AUTO_SCHEMA_CLASS": "api.openapi.PydanticResponseCapableSwaggerAutoSchema",
"SHOW_REQUEST_HEADERS": True,
"SECURITY_DEFINITIONS": {
SPECTACULAR_SETTINGS = {
"TITLE": "Flagsmith API",
"VERSION": "v1",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"SECURITY": [
{"Private": []},
{"Public": []},
],
"AUTHENTICATION_WHITELIST": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"SECURITY_SCHEMES": {
"Private": {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "For Private Endpoints. <a href='https://docs.flagsmith.com/clients/rest#private-api-endpoints'>Find out more</a>.", # noqa
"description": "For Private Endpoints. <a href='https://docs.flagsmith.com/clients/rest#private-api-endpoints'>Find out more</a>.",
},
"Public": {
"type": "apiKey",
"in": "header",
"name": "X-Environment-Key",
"description": "For Public Endpoints. <a href='https://docs.flagsmith.com/clients/rest#public-api-endpoints'>Find out more</a>.", # noqa
"description": "For Public Endpoints. <a href='https://docs.flagsmith.com/clients/rest#public-api-endpoints'>Find out more</a>.",
},
},
}

if env.bool("REQUIRE_AUTHENTICATION_FOR_API_DOCS", default=False):
SPECTACULAR_SETTINGS["SERVE_PERMISSIONS"] = (
"rest_framework.permissions.IsAuthenticated",
)
SPECTACULAR_SETTINGS["SERVE_AUTHENTICATION"] = (
"rest_framework.authentication.SessionAuthentication",
)


LOGIN_URL = "/admin/login/"
LOGOUT_URL = "/admin/logout/"
Expand Down
4 changes: 3 additions & 1 deletion api/app/settings/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"


SWAGGER_SETTINGS["USE_SESSION_AUTH"] = False
SPECTACULAR_SETTINGS["SERVE_AUTHENTICATION"] = (
"rest_framework.authentication.SessionAuthentication",
)

# Allow admin login with username and password
ENABLE_ADMIN_ACCESS_USER_PASS = True
Expand Down
5 changes: 3 additions & 2 deletions api/edge_api/identities/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import typing

from django.utils import timezone
from drf_spectacular.utils import extend_schema_field
from flag_engine.features.models import FeatureModel as EngineFeatureModel
from flag_engine.features.models import FeatureStateModel as EngineFeatureStateModel
from flag_engine.features.models import (
Expand Down Expand Up @@ -140,6 +141,7 @@ def to_internal_value(self, data): # type: ignore[no-untyped-def]
return FeatureStateValue(**feature_state_value_dict).value


@extend_schema_field({"oneOf": [{"type": "integer"}, {"type": "string"}]})
class EdgeFeatureField(serializers.Field): # type: ignore[type-arg]
def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def]
help_text = "ID(integer) or name(string) of the feature"
Expand All @@ -164,8 +166,7 @@ def to_internal_value(self, data: typing.Union[int, str]) -> EngineFeatureModel:
)
)

class Meta:
swagger_schema_fields = {"type": "integer/string"}
# drf-spectacular handles schema via the decorator above


class BaseEdgeIdentityFeatureStateSerializer(serializers.Serializer): # type: ignore[type-arg]
Expand Down
4 changes: 2 additions & 2 deletions api/environments/sdk/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.utils.decorators import method_decorator
from django.views.decorators.http import condition
from drf_yasg.utils import swagger_auto_schema # type: ignore[import-untyped]
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
Expand All @@ -29,7 +29,7 @@ class SDKEnvironmentAPIView(APIView):
def get_authenticators(self): # type: ignore[no-untyped-def]
return [EnvironmentKeyAuthentication(required_key_prefix="ser.")]

@swagger_auto_schema(responses={200: SDKEnvironmentDocumentModel}) # type: ignore[misc]
@extend_schema(responses={200: SDKEnvironmentDocumentModel})
@method_decorator(condition(last_modified_func=get_last_modified))
def get(self, request: Request) -> Response:
environment_document = Environment.get_environment_document(
Expand Down
2 changes: 1 addition & 1 deletion api/environments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def get_subscription(self) -> Subscription | None:
).get(id=project_id)

return getattr(project.organisation, "subscription", None)
elif view.action in ("update", "partial_update"):
elif self.instance and view.action in ("update", "partial_update"):
return getattr(self.instance.project.organisation, "subscription", None) # type: ignore[union-attr]

return None
Expand Down
Loading
Loading