Skip to content

Commit

Permalink
Merge branch 'main' of github.com:Flagsmith/flagsmith
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle-ssg committed Nov 29, 2022
2 parents 36b0ddf + ada43d2 commit 0e3e098
Show file tree
Hide file tree
Showing 46 changed files with 1,439 additions and 267 deletions.
6 changes: 6 additions & 0 deletions .github/actions/api-deploy-ecs/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ runs:
run: echo ::set-output name=tag::${{ inputs.image_tag }}
shell: bash

- name: Write git info to Docker image
run: |
echo ${{ github.sha }} > api/CI_COMMIT_SHA
echo '${{ github.ref_name }}' > api/IMAGE_TAG
shell: bash

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1-node16
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/frontend-deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ jobs:
npm ci --only=prod
npm run env
npm run bundle
echo ${{ github.sha }} > CI_COMMIT_SHA
vercel --prod --token ${{ secrets.VERCEL_TOKEN }}
1 change: 1 addition & 0 deletions .github/workflows/frontend-deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ jobs:
npm ci --only=prod
npm run env
npm run bundle
echo ${{ github.sha }} > CI_COMMIT_SHA
vercel --prod --token ${{ secrets.VERCEL_TOKEN }}
18 changes: 16 additions & 2 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@
"CONN_MAX_AGE": DJANGO_DB_CONN_MAX_AGE,
},
}

LOGIN_THROTTLE_RATE = env("LOGIN_THROTTLE_RATE", "20/min")
SIGNUP_THROTTLE_RATE = env("SIGNUP_THROTTLE_RATE", "10000/min")
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"DEFAULT_AUTHENTICATION_CLASSES": (
Expand All @@ -192,8 +195,8 @@
"UNICODE_JSON": False,
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"DEFAULT_THROTTLE_RATES": {
"login": "20/min",
"signup": "10/min",
"login": LOGIN_THROTTLE_RATE,
"signup": SIGNUP_THROTTLE_RATE,
"mfa_code": "5/min",
"invite": "10/min",
},
Expand Down Expand Up @@ -781,3 +784,14 @@
SSE_AUTHENTICATION_TOKEN = env.str("SSE_AUTHENTICATION_TOKEN", None)

DISABLE_INVITE_LINKS = env.bool("DISABLE_INVITE_LINKS", False)

PIPEDRIVE_API_TOKEN = env.str("PIPEDRIVE_API_TOKEN", None)
PIPEDRIVE_BASE_API_URL = env.str(
"PIPEDRIVE_BASE_API_URL", "https://flagsmith.pipedrive.com/api/v1"
)
PIPEDRIVE_DOMAIN_ORGANIZATION_FIELD_KEY = env.str(
"PIPEDRIVE_DOMAIN_ORGANIZATION_FIELD_KEY", None
)
PIPEDRIVE_SIGN_UP_TYPE_DEAL_FIELD_KEY = env.str(
"PIPEDRIVE_SIGN_UP_TYPE_DEAL_FIELD_KEY", None
)
13 changes: 12 additions & 1 deletion api/custom_auth/oauth/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.exceptions import PermissionDenied

from organisations.invites.models import Invite
from users.models import SignUpType

from ..constants import USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE
from .github import GithubUser
Expand All @@ -20,6 +21,14 @@ class OAuthLoginSerializer(serializers.Serializer):
required=True,
help_text="Code or access token returned from the FE interaction with the third party login provider.",
)
sign_up_type = serializers.ChoiceField(
required=False,
allow_null=True,
allow_blank=True,
choices=SignUpType.choices,
help_text="Provide information about how the user signed up (i.e. via invite or not)",
write_only=True,
)

class Meta:
abstract = True
Expand Down Expand Up @@ -54,7 +63,9 @@ def _get_user(self, user_data: dict):
):
raise PermissionDenied(USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE)

return UserModel.objects.create(**user_data)
return UserModel.objects.create(
**user_data, sign_up_type=self.validated_data.get("sign_up_type")
)

return existing_user

Expand Down
7 changes: 5 additions & 2 deletions api/custom_auth/oauth/tests/test_unit_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def setUp(self) -> None:
def test_create(self, mock_get_user_info):
# Given
access_token = "access-token"
data = {"access_token": access_token}
sign_up_type = "NO_INVITE"
data = {"access_token": access_token, "sign_up_type": sign_up_type}
serializer = OAuthLoginSerializer(data=data, context={"request": self.request})

# monkey patch the get_user_info method to return the mock user data
Expand All @@ -46,7 +47,9 @@ def test_create(self, mock_get_user_info):
response = serializer.save()

# Then
assert UserModel.objects.filter(email=self.test_email).exists()
assert UserModel.objects.filter(
email=self.test_email, sign_up_type=sign_up_type
).exists()
assert isinstance(response, Token)
assert (timezone.now() - response.user.last_login).seconds < 5
assert response.user.email == self.test_email
Expand Down
1 change: 1 addition & 0 deletions api/custom_auth/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Meta(UserCreateSerializer.Meta):
"marketing_consent_given",
)
read_only_fields = ("is_active",)
write_only_fields = ("sign_up_type",)

def validate(self, attrs):
attrs = super().validate(attrs)
Expand Down
31 changes: 31 additions & 0 deletions api/custom_auth/tests/end_to_end/test_custom_auth_integration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import re
from collections import ChainMap

Expand Down Expand Up @@ -345,3 +346,33 @@ def test_delete_token(test_user, auth_token):
# and - if we try to delete the token again(i.e: access anything that uses is_authenticated)
# we should will get 401
assert client.delete(url).status_code == status.HTTP_401_UNAUTHORIZED


def test_register_with_sign_up_type(client, db, settings):
# Given
password = FFAdminUser.objects.make_random_password()
sign_up_type = "NO_INVITE"
email = "test@example.com"
register_data = {
"email": email,
"password": password,
"re_password": password,
"first_name": "test",
"last_name": "tester",
"sign_up_type": sign_up_type,
}

# When
response = client.post(
reverse("api-v1:custom_auth:ffadminuser-list"),
data=json.dumps(register_data),
content_type="application/json",
)

# Then
assert response.status_code == status.HTTP_201_CREATED

response_json = response.json()
assert response_json["sign_up_type"] == sign_up_type

assert FFAdminUser.objects.filter(email=email, sign_up_type=sign_up_type).exists()
56 changes: 27 additions & 29 deletions api/e2etests/tests/end_to_end/test_integration_e2e_tests.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
import os
from unittest import TestCase

import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient

from users.models import FFAdminUser


@pytest.mark.django_db
class E2eTestsIntegrationTestCase(TestCase):
def test_e2e_teardown(settings, db):
# TODO: tidy up this hack to fix throttle rates
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["signup"] = "1000/min"
token = "test-token"
e2e_user_email = "test@example.com"
register_url = "/api/v1/auth/users/"

def setUp(self) -> None:
token = "test-token"
self.e2e_user_email = "test@example.com"
os.environ["E2E_TEST_AUTH_TOKEN"] = token
os.environ["FE_E2E_TEST_USER_EMAIL"] = self.e2e_user_email
self.client = APIClient(HTTP_X_E2E_TEST_AUTH_TOKEN=token)

def test_e2e_teardown(self):
# Register a user with the e2e test user email address
test_password = FFAdminUser.objects.make_random_password()
register_data = {
"email": self.e2e_user_email,
"first_name": "test",
"last_name": "test",
"password": test_password,
"re_password": test_password,
}
register_response = self.client.post(self.register_url, data=register_data)
assert register_response.status_code == status.HTTP_201_CREATED

# then test that we can teardown that user
url = reverse("api-v1:e2etests:teardown")
teardown_response = self.client.post(url)
assert teardown_response.status_code == status.HTTP_204_NO_CONTENT
assert not FFAdminUser.objects.filter(email=self.e2e_user_email).exists()
os.environ["E2E_TEST_AUTH_TOKEN"] = token
os.environ["FE_E2E_TEST_USER_EMAIL"] = e2e_user_email

client = APIClient(HTTP_X_E2E_TEST_AUTH_TOKEN=token)

# Register a user with the e2e test user email address
test_password = FFAdminUser.objects.make_random_password()
register_data = {
"email": os.environ["FE_E2E_TEST_USER_EMAIL"],
"first_name": "test",
"last_name": "test",
"password": test_password,
"re_password": test_password,
}
register_response = client.post(register_url, data=register_data)
assert register_response.status_code == status.HTTP_201_CREATED

# then test that we can teardown that user
url = reverse("api-v1:e2etests:teardown")
teardown_response = client.post(url)
assert teardown_response.status_code == status.HTTP_204_NO_CONTENT
assert not FFAdminUser.objects.filter(email=e2e_user_email).exists()
41 changes: 39 additions & 2 deletions api/environments/permissions/permissions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import typing

from django.db.models import Model, Q
from django.http import HttpRequest
from rest_framework import exceptions
from rest_framework.permissions import BasePermission
from rest_framework.permissions import BasePermission, IsAuthenticated

from environments.models import Environment
from environments.permissions.constants import VIEW_ENVIRONMENT
Expand All @@ -24,8 +25,11 @@ def has_object_permission(self, request, view, obj):
return True


class EnvironmentPermissions(BasePermission):
class EnvironmentPermissions(IsAuthenticated):
def has_permission(self, request, view):
if not super().has_permission(request, view):
return False

if view.action == "create":
try:
project_id = request.data.get("project")
Expand All @@ -41,6 +45,9 @@ def has_permission(self, request, view):
return True

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

if view.action == "clone":
return request.user.is_project_admin(obj.project)

Expand All @@ -49,6 +56,36 @@ def has_object_permission(self, request, view, obj):
]


class MasterAPIKeyEnvironmentPermissions(BasePermission):
def has_permission(self, request: HttpRequest, view: str) -> bool:
master_api_key = getattr(request, "master_api_key", None)

if not master_api_key:
return False

if view.action == "create":
try:
project_id = request.data.get("project")
project = Project.objects.get(id=project_id)
return master_api_key.organisation_id == project.organisation.id

except Project.DoesNotExist:
return False

# return true as list will be handled by view and obj permissions will be handled later
return True

def has_object_permission(
self, request: HttpRequest, view: str, obj: Model
) -> bool:
master_api_key = getattr(request, "master_api_key", None)

if not master_api_key:
return False

return master_api_key.organisation_id == obj.project.organisation_id


class IdentityPermissions(BasePermission):
def has_permission(self, request, view):
try:
Expand Down
6 changes: 5 additions & 1 deletion api/environments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,17 @@ def _create_audit_log(self, instance, created):
ENVIRONMENT_CREATED_MESSAGE if created else ENVIRONMENT_UPDATED_MESSAGE
) % instance.name
request = self.context.get("request")

author = None if request.user.is_anonymous else request.user
master_api_key = request.master_api_key if request.user.is_anonymous else None
AuditLog.objects.create(
author=getattr(request, "user", None),
author=author,
related_object_id=instance.id,
related_object_type=RelatedObjectType.ENVIRONMENT.name,
environment=instance,
project=instance.project,
log=message,
master_api_key=master_api_key,
)


Expand Down
Loading

0 comments on commit 0e3e098

Please sign in to comment.