Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feat/view-users-groups' into fea…
Browse files Browse the repository at this point in the history
…t/view-users-groups
  • Loading branch information
kyle-ssg committed Oct 9, 2024
2 parents ade5457 + 1da9f91 commit 1c777a2
Show file tree
Hide file tree
Showing 48 changed files with 1,351 additions and 741 deletions.
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.143.0"
".": "2.144.0"
}
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## [2.144.0](https://github.com/Flagsmith/flagsmith/compare/v2.143.0...v2.144.0) (2024-10-03)


### Features

* Identity alias ([#4620](https://github.com/Flagsmith/flagsmith/issues/4620)) ([d18049b](https://github.com/Flagsmith/flagsmith/commit/d18049bc3abd6ac49334fb041bc8fca4778ed198))
* Improve segment override UI ([#4633](https://github.com/Flagsmith/flagsmith/issues/4633)) ([a265d74](https://github.com/Flagsmith/flagsmith/commit/a265d7475aa80d00b10c7a939918741f0c64040e))
* Introduce the SDK Evaluation Context schema ([#4414](https://github.com/Flagsmith/flagsmith/issues/4414)) ([d6c6004](https://github.com/Flagsmith/flagsmith/commit/d6c6004f0f514ba997c4c46797c58900dfc5a4e1))
* Manage tags permission ([#4615](https://github.com/Flagsmith/flagsmith/issues/4615)) ([b3da659](https://github.com/Flagsmith/flagsmith/commit/b3da6594e11d453d9dde4a80b443f9c23fe226ab))
* Support cookie authentication ([#4662](https://github.com/Flagsmith/flagsmith/issues/4662)) ([e65c8da](https://github.com/Flagsmith/flagsmith/commit/e65c8da425dd241d445361482ab3c12c4b97b0a6))


### Bug Fixes

* always store and search dashboard alias in lower case ([#4676](https://github.com/Flagsmith/flagsmith/issues/4676)) ([22a3083](https://github.com/Flagsmith/flagsmith/commit/22a30831d0452f94dfd02f1c90e1e22a8c3249f3))
* **ci:** Rate-limited Trivy database pulls ([#4677](https://github.com/Flagsmith/flagsmith/issues/4677)) ([4bca509](https://github.com/Flagsmith/flagsmith/commit/4bca50909892b5f22b0042d4defa914581c65f02))
* Clear project org search on close ([#4690](https://github.com/Flagsmith/flagsmith/issues/4690)) ([b4b48b7](https://github.com/Flagsmith/flagsmith/commit/b4b48b75c0006b667edd65422392151689bd3758))
* encode identity search ([#4691](https://github.com/Flagsmith/flagsmith/issues/4691)) ([0485601](https://github.com/Flagsmith/flagsmith/commit/0485601f5a86359d4cc8c3cb765fce3e76184199))
* encode search when querying features ([#4689](https://github.com/Flagsmith/flagsmith/issues/4689)) ([7c746f4](https://github.com/Flagsmith/flagsmith/commit/7c746f42f23b6b0cc2013282c05bd2b46ef85b75))
* ensure MANAGE_TAGS permission allows create tag ([#4678](https://github.com/Flagsmith/flagsmith/issues/4678)) ([58eb9ed](https://github.com/Flagsmith/flagsmith/commit/58eb9ed8140d1a73a4d36c4f297d1f8d3162808a))
* feature specific segment ([#4682](https://github.com/Flagsmith/flagsmith/issues/4682)) ([a867ed1](https://github.com/Flagsmith/flagsmith/commit/a867ed15204ea75029b65474c8aef41b320ec49d))
* Handle cancellation date for api usage ([#4672](https://github.com/Flagsmith/flagsmith/issues/4672)) ([17be366](https://github.com/Flagsmith/flagsmith/commit/17be366a02f5f8ac4379f1936e4ab2af7284720c))
* remove trait ([#4686](https://github.com/Flagsmith/flagsmith/issues/4686)) ([6dc8b7b](https://github.com/Flagsmith/flagsmith/commit/6dc8b7b4a52a32fd7f9de63ab21a129050829f19))
* update permissions on classes with missing / unclear permissions ([#4667](https://github.com/Flagsmith/flagsmith/issues/4667)) ([19026e4](https://github.com/Flagsmith/flagsmith/commit/19026e4ced349b02f6863f7669bdf29f162141fa))

## [2.143.0](https://github.com/Flagsmith/flagsmith/compare/v2.142.0...v2.143.0) (2024-09-27)


Expand Down
2 changes: 1 addition & 1 deletion api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ POETRY_VERSION ?= 1.8.3

GUNICORN_LOGGER_CLASS ?= util.logging.GunicornJsonCapableLogger

SAML_REVISION ?= v1.6.3
SAML_REVISION ?= v1.6.4
RBAC_REVISION ?= v0.8.0

-include .env-local
Expand Down
45 changes: 30 additions & 15 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"rest_framework.authtoken",
# Used for managing api keys
"rest_framework_api_key",
"rest_framework_simplejwt.token_blacklist",
"djoser",
"django.contrib.sites",
"custom_auth",
Expand Down Expand Up @@ -254,6 +255,7 @@
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"DEFAULT_AUTHENTICATION_CLASSES": (
"custom_auth.jwt_cookie.authentication.JWTCookieAuthentication",
"rest_framework.authentication.TokenAuthentication",
"api_keys.authentication.MasterAPIKeyAuthentication",
),
Expand Down Expand Up @@ -416,19 +418,6 @@

MEDIA_URL = "/media/" # unused but needs to be different from STATIC_URL in django 3

# CORS settings

CORS_ORIGIN_ALLOW_ALL = True
FLAGSMITH_CORS_EXTRA_ALLOW_HEADERS = env.list(
"FLAGSMITH_CORS_EXTRA_ALLOW_HEADERS", default=["sentry-trace"]
)
CORS_ALLOW_HEADERS = [
*default_headers,
*FLAGSMITH_CORS_EXTRA_ALLOW_HEADERS,
"X-Environment-Key",
"X-E2E-Test-Auth-Token",
]

DEFAULT_FROM_EMAIL = env("SENDER_EMAIL", default="noreply@flagsmith.com")
EMAIL_CONFIGURATION = {
# Invitations with name is anticipated to take two arguments. The persons name and the
Expand Down Expand Up @@ -826,6 +815,16 @@
"user_create": USER_CREATE_PERMISSIONS,
},
}
SIMPLE_JWT = {
"AUTH_TOKEN_CLASSES": ["rest_framework_simplejwt.tokens.SlidingToken"],
"SLIDING_TOKEN_LIFETIME": timedelta(
minutes=env.int(
"COOKIE_AUTH_JWT_ACCESS_TOKEN_LIFETIME_MINUTES",
default=10 * 60,
)
),
"SIGNING_KEY": env.str("COOKIE_AUTH_JWT_SIGNING_KEY", default=SECRET_KEY),
}

# Github OAuth credentials
GITHUB_CLIENT_ID = env.str("GITHUB_CLIENT_ID", default="")
Expand Down Expand Up @@ -907,8 +906,6 @@
SENTRY_API_KEY = env("SENTRY_API_KEY", default=None)
AMPLITUDE_API_KEY = env("AMPLITUDE_API_KEY", default=None)
ENABLE_FLAGSMITH_REALTIME = env.bool("ENABLE_FLAGSMITH_REALTIME", default=False)
USE_SECURE_COOKIES = env.bool("USE_SECURE_COOKIES", default=True)
COOKIE_SAME_SITE = env.str("COOKIE_SAME_SITE", default="none")

# Set this to enable create organisation for only superusers
RESTRICT_ORG_CREATE_TO_SUPERUSERS = env.bool("RESTRICT_ORG_CREATE_TO_SUPERUSERS", False)
Expand Down Expand Up @@ -1038,6 +1035,24 @@

DISABLE_INVITE_LINKS = env.bool("DISABLE_INVITE_LINKS", False)
PREVENT_SIGNUP = env.bool("PREVENT_SIGNUP", default=False)
COOKIE_AUTH_ENABLED = env.bool("COOKIE_AUTH_ENABLED", default=False)
USE_SECURE_COOKIES = env.bool("USE_SECURE_COOKIES", default=True)
COOKIE_SAME_SITE = env.str("COOKIE_SAME_SITE", default="none")

# CORS settings

CORS_ORIGIN_ALLOW_ALL = env.bool("CORS_ORIGIN_ALLOW_ALL", not COOKIE_AUTH_ENABLED)
CORS_ALLOW_CREDENTIALS = env.bool("CORS_ALLOW_CREDENTIALS", COOKIE_AUTH_ENABLED)
FLAGSMITH_CORS_EXTRA_ALLOW_HEADERS = env.list(
"FLAGSMITH_CORS_EXTRA_ALLOW_HEADERS", default=["sentry-trace"]
)
CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS", default=[])
CORS_ALLOW_HEADERS = [
*default_headers,
*FLAGSMITH_CORS_EXTRA_ALLOW_HEADERS,
"X-Environment-Key",
"X-E2E-Test-Auth-Token",
]

# use a separate boolean setting so that we add it to the API containers in environments
# where we're running the task processor, so we avoid creating unnecessary tasks
Expand Down
9 changes: 5 additions & 4 deletions api/app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@ def project_overrides(request):
"amplitude": "AMPLITUDE_API_KEY",
"api": "API_URL",
"assetURL": "ASSET_URL",
"cookieAuthEnabled": "COOKIE_AUTH_ENABLED",
"cookieSameSite": "COOKIE_SAME_SITE",
"crispChat": "CRISP_CHAT_API_KEY",
"disableAnalytics": "DISABLE_ANALYTICS_FEATURES",
"flagsmith": "FLAGSMITH_ON_FLAGSMITH_API_KEY",
"flagsmithAnalytics": "FLAGSMITH_ANALYTICS",
"flagsmithRealtime": "ENABLE_FLAGSMITH_REALTIME",
"flagsmithClientAPI": "FLAGSMITH_ON_FLAGSMITH_API_URL",
"ga": "GOOGLE_ANALYTICS_API_KEY",
"flagsmithRealtime": "ENABLE_FLAGSMITH_REALTIME",
"fpr": "FIRST_PROMOTER_ID",
"ga": "GOOGLE_ANALYTICS_API_KEY",
"githubAppURL": "GITHUB_APP_URL",
"headway": "HEADWAY_API_KEY",
"hideInviteLinks": "DISABLE_INVITE_LINKS",
"linkedinPartnerTracking": "LINKEDIN_PARTNER_TRACKING",
Expand All @@ -54,8 +57,6 @@ def project_overrides(request):
"preventSignup": "PREVENT_SIGNUP",
"sentry": "SENTRY_API_KEY",
"useSecureCookies": "USE_SECURE_COOKIES",
"cookieSameSite": "COOKIE_SAME_SITE",
"githubAppURL": "GITHUB_APP_URL",
}

override_data = {
Expand Down
20 changes: 13 additions & 7 deletions api/core/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re

from django.conf import settings
from django.contrib.sites.models import Site
from django.contrib.sites import models as sites_models
from django.http import HttpRequest
from rest_framework.request import Request

Expand All @@ -11,12 +11,18 @@


def get_current_site_url(request: HttpRequest | Request | None = None) -> str:
if settings.DOMAIN_OVERRIDE:
domain = settings.DOMAIN_OVERRIDE
elif current_site := Site.objects.filter(id=settings.SITE_ID).first():
domain = current_site.domain
else:
domain = settings.DEFAULT_DOMAIN
if not (domain := settings.DOMAIN_OVERRIDE):
try:
domain = sites_models.Site.objects.get_current(request).domain
except sites_models.Site.DoesNotExist:
# For the rare case when `DOMAIN_OVERRIDE` was not set and no `Site` object present,
# store a default domain `Site` in the sites cache
# so it's correctly invalidated should the user decide to create own `Site` object.
domain = settings.DEFAULT_DOMAIN
sites_models.SITE_CACHE[settings.SITE_ID] = sites_models.Site(
name="Flagsmith",
domain=domain,
)

if request:
scheme = request.scheme
Expand Down
1 change: 1 addition & 0 deletions api/custom_auth/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ class CustomAuthAppConfig(AppConfig):

def ready(self) -> None:
from custom_auth import tasks # noqa F401
from custom_auth.jwt_cookie import signals # noqa F401
Empty file.
17 changes: 17 additions & 0 deletions api/custom_auth/jwt_cookie/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from rest_framework.request import Request
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.tokens import Token

from custom_auth.jwt_cookie.constants import JWT_SLIDING_COOKIE_KEY
from users.models import FFAdminUser


class JWTCookieAuthentication(JWTAuthentication):
def authenticate_header(self, request: Request) -> str:
return f'Cookie realm="{self.www_authenticate_realm}"'

def authenticate(self, request: Request) -> tuple[FFAdminUser, Token] | None:
if raw_token := request.COOKIES.get(JWT_SLIDING_COOKIE_KEY):
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
return None
1 change: 1 addition & 0 deletions api/custom_auth/jwt_cookie/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
JWT_SLIDING_COOKIE_KEY = "jwt"
18 changes: 18 additions & 0 deletions api/custom_auth/jwt_cookie/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.conf import settings
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import SlidingToken

from custom_auth.jwt_cookie.constants import JWT_SLIDING_COOKIE_KEY
from users.models import FFAdminUser


def authorise_response(user: FFAdminUser, response: Response) -> Response:
sliding_token = SlidingToken.for_user(user)
response.set_cookie(
JWT_SLIDING_COOKIE_KEY,
str(sliding_token),
httponly=True,
secure=settings.USE_SECURE_COOKIES,
samesite=settings.COOKIE_SAME_SITE,
)
return response
20 changes: 20 additions & 0 deletions api/custom_auth/jwt_cookie/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Any
from urllib.parse import urlparse

from core.helpers import get_current_site_url
from corsheaders.signals import check_request_enabled
from django.dispatch import receiver
from django.http import HttpRequest


@receiver(check_request_enabled)
def cors_allow_current_site(request: HttpRequest, **kwargs: Any) -> bool:
# The signal is expected to only be dispatched:
# - When `settings.CORS_ORIGIN_ALLOW_ALL` is set to `False`.
# - For requests with `HTTP_ORIGIN` set.
origin_url = urlparse(request.META["HTTP_ORIGIN"])
current_site_url = urlparse(get_current_site_url(request))
return (
origin_url.scheme == current_site_url.scheme
and origin_url.netloc == current_site_url.netloc
)
15 changes: 15 additions & 0 deletions api/custom_auth/jwt_cookie/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from djoser.views import TokenDestroyView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import SlidingToken

from custom_auth.jwt_cookie.constants import JWT_SLIDING_COOKIE_KEY


class JWTSlidingTokenLogoutView(TokenDestroyView):
def post(self, request: Request) -> Response:
response = super().post(request)
if isinstance(jwt_token := request.auth, SlidingToken):
jwt_token.blacklist()
response.delete_cookie(JWT_SLIDING_COOKIE_KEY)
return response
16 changes: 13 additions & 3 deletions api/custom_auth/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from django.conf import settings
from djoser.conf import settings as djoser_settings
from djoser.serializers import TokenCreateSerializer, UserCreateSerializer
Expand Down Expand Up @@ -73,13 +75,15 @@ def _validate_registration_invite(self, email: str, sign_up_type: str) -> None:


class CustomUserCreateSerializer(UserCreateSerializer, InviteLinkValidationMixin):
key = serializers.SerializerMethodField()
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
if not settings.COOKIE_AUTH_ENABLED:
self.fields["key"] = serializers.SerializerMethodField()

class Meta(UserCreateSerializer.Meta):
fields = UserCreateSerializer.Meta.fields + (
"is_active",
"marketing_consent_given",
"key",
"uuid",
)
read_only_fields = ("is_active", "uuid")
Expand Down Expand Up @@ -115,8 +119,14 @@ def validate(self, attrs):
attrs["email"] = email.lower()
return attrs

def save(self) -> FFAdminUser:
instance = super().save()
if "view" in self.context:
self.context["view"].user = instance
return instance

@staticmethod
def get_key(instance):
def get_key(instance) -> str:
token, _ = Token.objects.get_or_create(user=instance)
return token.key

Expand Down
8 changes: 6 additions & 2 deletions api/custom_auth/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import include, path
from djoser.views import TokenDestroyView
from rest_framework.routers import DefaultRouter

from custom_auth.jwt_cookie.views import JWTSlidingTokenLogoutView
from custom_auth.views import (
CustomAuthTokenLoginOrRequestMFACode,
CustomAuthTokenLoginWithMFACode,
Expand All @@ -26,7 +26,11 @@
CustomAuthTokenLoginWithMFACode.as_view(),
name="mfa-authtoken-login-code",
),
path("logout/", TokenDestroyView.as_view(), name="authtoken-logout"),
path(
"logout/",
JWTSlidingTokenLogoutView.as_view(),
name="jwt-logout",
),
path("", include(ffadmin_user_router.urls)),
path("token/", delete_token, name="delete-token"),
# NOTE: endpoints provided by `djoser.urls`
Expand Down
Loading

0 comments on commit 1c777a2

Please sign in to comment.