Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: seed data for permission and roles e2e tests #4173

Merged
merged 21 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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
16 changes: 16 additions & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,10 @@
LOGOUT_URL = "/admin/logout/"

# Enable E2E tests
E2E_TEST_AUTH_TOKEN = env.str("E2E_TEST_AUTH_TOKEN", default=None)
if E2E_TEST_AUTH_TOKEN is not None:
MIDDLEWARE.append("e2etests.middleware.E2ETestMiddleware")

ENABLE_FE_E2E = env.bool("ENABLE_FE_E2E", default=False)
# Email associated with user that is used by front end for end to end testing purposes
E2E_TEST_EMAIL_DOMAIN = "flagsmithe2etestdomain.io"
Expand All @@ -518,6 +522,18 @@
E2E_CHANGE_EMAIL_USER = f"e2e_change_email@{E2E_TEST_EMAIL_DOMAIN}"
# User email address used for the rest of the E2E tests
E2E_USER = f"e2e_user@{E2E_TEST_EMAIL_DOMAIN}"
E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS = (
f"e2e_non_admin_user_with_org_permissions@{E2E_TEST_EMAIL_DOMAIN}"
)
E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS = (
f"e2e_non_admin_user_with_project_permissions@{E2E_TEST_EMAIL_DOMAIN}"
)
E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS = (
f"e2e_non_admin_user_with_env_permissions@{E2E_TEST_EMAIL_DOMAIN}"
)
E2E_NON_ADMIN_USER_WITH_A_ROLE = (
f"e2e_non_admin_user_with_a_role@{E2E_TEST_EMAIL_DOMAIN}"
)
# Identity for E2E segment tests
E2E_IDENTITY = "test-identity"

Expand Down
132 changes: 124 additions & 8 deletions api/e2etests/e2e_seed_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,34 @@
from edge_api.identities.models import EdgeIdentity
from environments.identities.models import Identity
from environments.models import Environment
from environments.permissions.constants import (
UPDATE_FEATURE_STATE,
VIEW_ENVIRONMENT,
VIEW_IDENTITIES,
)
from environments.permissions.models import UserEnvironmentPermission
from organisations.models import Organisation, OrganisationRole, Subscription
from projects.models import Project
from users.models import FFAdminUser
from organisations.permissions.models import UserOrganisationPermission
from organisations.permissions.permissions import (
CREATE_PROJECT,
MANAGE_USER_GROUPS,
)
from organisations.subscriptions.constants import SCALE_UP
from projects.models import Project, UserProjectPermission
from projects.permissions import (
CREATE_ENVIRONMENT,
CREATE_FEATURE,
VIEW_AUDIT_LOG,
VIEW_PROJECT,
)
from users.models import FFAdminUser, UserPermissionGroup

# Password used by all the test users
PASSWORD = "str0ngp4ssw0rd!"

PROJECT_PERMISSION_PROJECT = "My Test Project 5 Project Permission"
ENV_PERMISSION_PROJECT = "My Test Project 6 Env Permission"


def delete_user_and_its_organisations(user_email: str) -> None:
user: FFAdminUser | None = FFAdminUser.objects.filter(email=user_email).first()
Expand All @@ -25,6 +46,18 @@ def teardown() -> None:
delete_user_and_its_organisations(user_email=settings.E2E_SIGNUP_USER)
delete_user_and_its_organisations(user_email=settings.E2E_USER)
delete_user_and_its_organisations(user_email=settings.E2E_CHANGE_EMAIL_USER)
delete_user_and_its_organisations(
user_email=settings.E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS
)
delete_user_and_its_organisations(
user_email=settings.E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS
)
delete_user_and_its_organisations(
user_email=settings.E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS
)
delete_user_and_its_organisations(
user_email=settings.E2E_NON_ADMIN_USER_WITH_A_ROLE
)


def seed_data() -> None:
Expand All @@ -36,6 +69,44 @@ def seed_data() -> None:
username=settings.E2E_USER,
)
org_admin.add_organisation(organisation, OrganisationRole.ADMIN)
non_admin_user_with_org_permissions: FFAdminUser = FFAdminUser.objects.create_user(
email=settings.E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS,
password=PASSWORD,
)
non_admin_user_with_project_permissions: FFAdminUser = (
FFAdminUser.objects.create_user(
email=settings.E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS,
password=PASSWORD,
)
)
non_admin_user_with_env_permissions: FFAdminUser = FFAdminUser.objects.create_user(
email=settings.E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS,
password=PASSWORD,
)
non_admin_user_with_a_role: FFAdminUser = FFAdminUser.objects.create_user(
email=settings.E2E_NON_ADMIN_USER_WITH_A_ROLE,
password=PASSWORD,
)
non_admin_user_with_org_permissions.add_organisation(
organisation,
)
non_admin_user_with_project_permissions.add_organisation(
organisation,
)
non_admin_user_with_env_permissions.add_organisation(
organisation,
)
non_admin_user_with_a_role.add_organisation(
organisation,
)

# Add permissions to the non-admin user with org permissions
user_org_permission = UserOrganisationPermission.objects.create(
user=non_admin_user_with_org_permissions, organisation=organisation
)
user_org_permission.add_permission(CREATE_PROJECT)
user_org_permission.add_permission(MANAGE_USER_GROUPS)
UserPermissionGroup.objects.create(name="TestGroup", organisation=organisation)

# We add different projects and environments to give each e2e test its own isolated context.
project_test_data = [
Expand All @@ -49,7 +120,17 @@ def seed_data() -> None:
{"name": "My Test Project 2", "environments": ["Development"]},
{"name": "My Test Project 3", "environments": ["Development"]},
{"name": "My Test Project 4", "environments": ["Development"]},
{
"name": PROJECT_PERMISSION_PROJECT,
"environments": ["Development"],
},
{"name": ENV_PERMISSION_PROJECT, "environments": ["Development"]},
{"name": "My Test Project 7 Role", "environments": ["Development"]},
]
# Upgrade organisation seats
Subscription.objects.filter(organisation__in=org_admin.organisations.all()).update(
max_seats=8, plan=SCALE_UP, subscription_id="test_subscription_id"
)

# Create projects and environments
projects = []
Expand All @@ -58,19 +139,59 @@ def seed_data() -> None:
project = Project.objects.create(
name=project_info["name"], organisation=organisation
)
if project_info["name"] == PROJECT_PERMISSION_PROJECT:
# Add permissions to the non-admin user with project permissions
user_proj_permission: UserProjectPermission = (
UserProjectPermission.objects.create(
user=non_admin_user_with_project_permissions, project=project
)
)
[
user_proj_permission.add_permission(permission_key)
for permission_key in [
VIEW_PROJECT,
CREATE_ENVIRONMENT,
CREATE_FEATURE,
VIEW_AUDIT_LOG,
]
]
projects.append(project)

for env_name in project_info["environments"]:
environment = Environment.objects.create(name=env_name, project=project)

if project_info["name"] == ENV_PERMISSION_PROJECT:
# Add permissions to the non-admin user with env permissions
user_env_permission = UserEnvironmentPermission.objects.create(
user=non_admin_user_with_env_permissions, environment=environment
)
user_env_proj_permission: UserProjectPermission = (
UserProjectPermission.objects.create(
user=non_admin_user_with_env_permissions, project=project
)
)
user_env_proj_permission.add_permission(VIEW_PROJECT)
user_env_proj_permission.add_permission(CREATE_FEATURE)
[
user_env_permission.add_permission(permission_key)
for permission_key in [
VIEW_ENVIRONMENT,
UPDATE_FEATURE_STATE,
VIEW_IDENTITIES,
]
]
environments.append(environment)

# We're only creating identities for 3 of the 5 environments because
# We're only creating identities for 6 of the 7 environments because
# they are necessary for the environments created above and to keep
# the e2e tests isolated."
identities_test_data = [
{"identifier": settings.E2E_IDENTITY, "environment": environments[2]},
{"identifier": settings.E2E_IDENTITY, "environment": environments[3]},
{"identifier": settings.E2E_IDENTITY, "environment": environments[4]},
{"identifier": settings.E2E_IDENTITY, "environment": environments[5]},
{"identifier": settings.E2E_IDENTITY, "environment": environments[6]},
{"identifier": settings.E2E_IDENTITY, "environment": environments[7]},
]

for identity_info in identities_test_data:
Expand All @@ -82,8 +203,3 @@ def seed_data() -> None:
EdgeIdentity(engine_identity).save()
else:
Identity.objects.create(**identity_info)

# Upgrade organisation seats
Subscription.objects.filter(organisation__in=org_admin.organisations.all()).update(
max_seats=2
)
16 changes: 16 additions & 0 deletions api/e2etests/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.conf import settings


class E2ETestMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
request.is_e2e = False
if (
request.META.get("HTTP_X_E2E_TEST_AUTH_TOKEN")
== settings.E2E_TEST_AUTH_TOKEN
):
request.is_e2e = True

return self.get_response(request)
20 changes: 11 additions & 9 deletions api/e2etests/permissions.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import os

from django.views import View
from rest_framework.permissions import BasePermission
from rest_framework.request import Request


class E2ETestPermission(BasePermission):
def has_permission(self, request, view):
if "E2E_TEST_AUTH_TOKEN" not in os.environ:
return False
return (
request.META.get("HTTP_X_E2E_TEST_AUTH_TOKEN")
== os.environ["E2E_TEST_AUTH_TOKEN"]
)
def has_permission(self, request: Request, view: View) -> bool:
return getattr(request, "is_e2e", False) is True


class E2ETestPermissionMixin:
def has_permission(self, request: Request, view: View) -> bool:
if getattr(request, "is_e2e", False) is True:
return True
return super().has_permission(request, view)
9 changes: 8 additions & 1 deletion api/projects/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework.permissions import BasePermission, IsAuthenticated

from e2etests.permissions import E2ETestPermissionMixin
from organisations.models import Organisation
from organisations.permissions.permissions import CREATE_PROJECT
from projects.models import Project
Expand Down Expand Up @@ -31,11 +32,16 @@
]


class ProjectPermissions(IsAuthenticated):
class ProjectPermissions(E2ETestPermissionMixin, IsAuthenticated):
def has_permission(self, request, view):
"""Check if user has permission to list / create project"""
if not super().has_permission(request, view):
return False
elif (
super().has_permission(request, view)
and getattr(request, "is_e2e", False) is True
):
return True
Copy link
Contributor

@matthewelwell matthewelwell Aug 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@novakzaballa even if this was necessary, the call to super() is quite clearly unnecessary as we're early returning on the line above if it's not true.

In general though, this is exactly the job that the E2ETestPermissionMixin is trying to achieve. Can you have another look at this please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right, the call to super() was no longer necessary and has been corrected.

There was an issue with this part of the code that was validating the number of projects and returning False, I moved part of the logic exclusively to this validation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I don't think the mixin is actually doing anything? I would rather see the mixin used, or removed if we can't get it working.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it since it doesn't do what we expect.


if view.action == "create" and request.user.belongs_to(
int(request.data.get("organisation"))
Expand All @@ -48,6 +54,7 @@ def has_permission(self, request, view):
subscription_metadata = (
organisation.subscription.get_subscription_metadata()
)

total_projects_created = Project.objects.filter(
organisation=organisation
).count()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework.test import APIClient

from organisations.models import Subscription
from organisations.subscriptions.constants import SCALE_UP
from users.models import FFAdminUser


Expand All @@ -14,8 +15,8 @@ def test_e2e_teardown(settings, db) -> None:
token = "test-token"
register_url = "/api/v1/auth/users/"
settings.ENABLE_FE_E2E = True

os.environ["E2E_TEST_AUTH_TOKEN"] = token
settings.E2E_TEST_AUTH_TOKEN = token
settings.MIDDLEWARE.append("e2etests.middleware.E2ETestMiddleware")

client = APIClient(HTTP_X_E2E_TEST_AUTH_TOKEN=token)

Expand All @@ -41,7 +42,9 @@ def test_e2e_teardown(settings, db) -> None:
for subscription in Subscription.objects.filter(
organisation__in=e2e_user.organisations.all()
):
assert subscription.max_seats == 2
assert subscription.max_seats == 8
assert subscription.plan == SCALE_UP
assert subscription.subscription_id == "test_subscription_id"


def test_e2e_teardown_with_incorrect_token(settings, db):
Expand Down
26 changes: 26 additions & 0 deletions api/tests/unit/projects/test_unit_projects_permissions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from unittest import mock

import pytest
Expand Down Expand Up @@ -58,6 +59,31 @@ def test_create_project_has_permission(
assert response is True


def test_create_project_has_permission_with_e2e_test_auth_token(
staff_user: FFAdminUser,
organisation: Organisation,
with_organisation_permissions: WithOrganisationPermissionsCallable,
) -> None:
# Given
with_organisation_permissions([CREATE_PROJECT])
mock_request = mock.MagicMock(
user=staff_user, data={"name": "Test", "organisation": organisation.id}
)
token = "test-token"
settings.ENABLE_FE_E2E = True
os.environ["E2E_TEST_AUTH_TOKEN"] = token

mock_request.META = {"E2E_TEST_AUTH_TOKEN": token}
mock_view = mock.MagicMock(action="create", detail=False)
project_permissions = ProjectPermissions()

# When
response = project_permissions.has_permission(mock_request, mock_view)

# Then
assert response is True


def test_admin_can_update_project_has_permission(
organisation: Organisation,
staff_user: FFAdminUser,
Expand Down
Loading