Skip to content

Commit

Permalink
feat: Enterprise licensing (#3624)
Browse files Browse the repository at this point in the history
Co-authored-by: Zach Aysan <[email protected]>
  • Loading branch information
matthewelwell and zachaysan authored Dec 3, 2024
1 parent e6b0e2f commit fbd1a13
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 95 deletions.
24 changes: 24 additions & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,3 +1272,27 @@
# subscriptions created before this date full audit log and versioning
# history.
VERSIONING_RELEASE_DATE = env.date("VERSIONING_RELEASE_DATE", default=None)

SUBSCRIPTION_LICENCE_PUBLIC_KEY = env.str(
"SUBSCRIPTION_LICENCE_PUBLIC_KEY",
"""
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs1H23Xv1IlhyTbUP9Z4e
zN3t6oa97ybLufhqSRCoPxHWAY/pqqjdwiC00AnRbL/guDi1FLPEkLza2gAKfU+f
04SsNTfYL5MTPnaFtf+B+hlYmlrT1C6n05t+uQW2OQm6mWoqBssmoyR8T5FXfBls
FrT8dsZg5XG7JaWAyGbbVscHrXHXqVcLbFGO8CcO2BG2whl+7hzm4edNCsxLJqmN
uASR9KtntdulkRar0A9x+hAQUlrDKv77nMMdljNIqkcCcWrbhiDoTVCDbE99mhMq
LeC/+C54/ZiCb3r9woq/kpsbRj0Ys2b4czfjWioXooSxA0w3BE6/lV0+hVltjRO6
5QIDAQAB
-----END PUBLIC KEY-----
""",
)

# For the matching private key to the public key added above
# search for "Flagsmith licence private key" in Bitwarden.
SUBSCRIPTION_LICENCE_PRIVATE_KEY = env.str("SUBSCRIPTION_LICENCE_PRIVATE_KEY", None)

LICENSING_INSTALLED = importlib.util.find_spec("licensing") is not None

if LICENSING_INSTALLED: # pragma: no cover
INSTALLED_APPS.append("licensing")
43 changes: 43 additions & 0 deletions api/app/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,46 @@
RETRY_WEBHOOKS = True

INFLUXDB_BUCKET = "test_bucket"

SUBSCRIPTION_LICENCE_PRIVATE_KEY = """
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0o6Q+J6ArJZ2x
RyZQ5e9ue6dB4bgH7I7DYYb9t9eIb55z0vZZWVLLmIr+ngCfCxIePqCclrAen9gr
rCRhyAXD+XZYjRP0w2wlqA367HJXbti1adXnQnM4QXITNJhRnGoqiRVx7vQ/Klup
+yMBJOU4IkkSsQaAgp0eTdPlGlA+KAfCH39rsqIHNXuS1qfspI2RyaR6130NvR6D
4p07XJls1AYOs8xphdWl8b4hzbJTvC0IqRhvX+z4kEyQjprdcfwOG4qrqtIb4asm
21imOtE8CGRvHUl/cV+1l/hgv1fdbeCFzM89q16Z/KXIAWJMYfkWuOWGVEmf7yjB
9aMrfM3fAgMBAAECggEAKqGwQocBkw1GoS8kiNUrY8zFFZRa5Wvb6ZqbzEdWE7oc
EEPKph2hn7E5pIvPo7luJjsrlqktmZyp3Oy8jWMykSTP3Gg3PH3eiSiXXA/vkFj1
xiLbO8AAB1fSv1ubUy9yEuXVbNUzSbEKfxxpD30Qp+XXjxS+bxfkUuGVT62dIH3V
j251CEsCIZzwOriGP52OKK5HR24Y9/c+uGLu1CLY6qdrMgWAXTYqEoUw7ku8Sm8B
o6fuu9i0mAEJUl6qcVz3yH0QYe9pM6jDQ9oZeSkVYyspCwysTVs2jsCTMYUpK/kD
WU9sniHRgly3C9Ge3PrE4qUeMRTNk0Vd4RETtznwqQKBgQD3UiJ11FUd3g1FXL9N
iLOIayACrX37cBuc8M5iAzasNDjSVNoCrQun1091xzYK6/F7As4PtH1YUaDgXdBp
efHHl3DPTFkeztPMOKOd8tCpLai/23sbCBNc51x0LCuWWnNaAuidId1rpvFq7AMJ
jE4HPJkoL6udOzlKUHebp02ILQKBgQC6+nT2A+AZeheUiw2wBl0BRitQCxA+TN+L
vkAwLa0u/OqeNc8W50lybzHCS9nEVZ0Lp3Qk9Cl++X/k5o6k2byqJxtmMvLGqjjw
UNuZWHSoUzfdzs8yBjroLM4HsBgbEaG9E2e2zuqKBvwLqZ3fv/fXvmJDIu+aCWXC
ADtlrAvJuwKBgQDq+CW1PJ4BWk3RcGRwDUhEe0JWSO5ATCpv2Hi7tcHjqVmyutrF
YBKKy4y6oSE/DxrFe8y6LwhHOIZXo8m17B1BOyf6StcA5g9jHwyTq3WCxdZlMOis
red3hHfaB30Bw72D7u+BGgN7m4gRxVi9YYdgaLo569Bn+TRc3kZEo5aNoQKBgH7z
aJBU50ZFCFeZ5iw61dD0pJnPOTMjnLBT917+1FRP8riCzl29obep2b4TJANTIbL0
+j3Q7Y/BtV1kUTuKfreEn+zO8NmEX+6C5+cBEQvsnMTkEvfjFQHo0eaUYHmYihlH
YKbVbJdU0LLWclOmEpAQOsVcphQPB2EmKS4KF2LbAoGAOqVsQg61S1u7s4NF4JCN
EiJvBDjjwTycNCmhY7bV1R7LX+Qk/Mq9fgK3yccKV/Bl69C9Fmeopivbu20urNhn
q/sgOPDK0zJUSVh76gFon1gx7OfaHV31TrvIl0T7WnyfDvAv20F+dmmXkjnPBNNm
dXzo4kXwDOlWCJI8VhYfH/0=
-----END PRIVATE KEY-----
"""

SUBSCRIPTION_LICENCE_PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtKOkPiegKyWdsUcmUOXv
bnunQeG4B+yOw2GG/bfXiG+ec9L2WVlSy5iK/p4AnwsSHj6gnJawHp/YK6wkYcgF
w/l2WI0T9MNsJagN+uxyV27YtWnV50JzOEFyEzSYUZxqKokVce70PypbqfsjASTl
OCJJErEGgIKdHk3T5RpQPigHwh9/a7KiBzV7ktan7KSNkcmketd9Db0eg+KdO1yZ
bNQGDrPMaYXVpfG+Ic2yU7wtCKkYb1/s+JBMkI6a3XH8DhuKq6rSG+GrJttYpjrR
PAhkbx1Jf3FftZf4YL9X3W3ghczPPatemfylyAFiTGH5FrjlhlRJn+8owfWjK3zN
3wIDAQAB
-----END PUBLIC KEY-----
"""
31 changes: 22 additions & 9 deletions api/organisations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,16 +415,29 @@ def _get_subscription_metadata_for_chargebee(self) -> ChargebeeObjMetadata:
return cb_metadata

def _get_subscription_metadata_for_self_hosted(self) -> BaseSubscriptionMetadata:
if not is_enterprise():
return FREE_PLAN_SUBSCRIPTION_METADATA
if is_enterprise() and hasattr(
self.organisation, "licence"
): # pragma: no cover
licence_information = self.organisation.licence.get_licence_information()
return BaseSubscriptionMetadata(
seats=licence_information.num_seats,
projects=licence_information.num_projects,
audit_log_visibility_days=None,
feature_history_visibility_days=None,
)
# TODO: Once we've successfully rolled out licences to enterprises
# remove this branch to force them into the free plan
# if they don't have a licence.
elif is_enterprise(): # pragma: no cover
return BaseSubscriptionMetadata(
seats=self.max_seats,
api_calls=self.max_api_calls,
projects=None,
audit_log_visibility_days=None,
feature_history_visibility_days=None,
)

return BaseSubscriptionMetadata(
seats=self.max_seats,
api_calls=self.max_api_calls,
projects=None,
audit_log_visibility_days=None,
feature_history_visibility_days=None,
)
return FREE_PLAN_SUBSCRIPTION_METADATA

def add_single_seat(self):
if not self.can_auto_upgrade_seats:
Expand Down
8 changes: 4 additions & 4 deletions api/organisations/subscriptions/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ class BaseSubscriptionMetadata:
def __init__(
self,
seats: int = 0,
api_calls: int = 0,
projects: typing.Optional[int] = None,
chargebee_email: str = None,
api_calls: None | int = None,
projects: None | int = None,
chargebee_email: None | str = None,
audit_log_visibility_days: int | None = 0,
feature_history_visibility_days: int | None = DEFAULT_VERSION_LIMIT_DAYS,
**kwargs, # allows for extra unknown attrs from CB json metadata
):
) -> None:
self.seats = seats
self.api_calls = api_calls
self.projects = projects
Expand Down
14 changes: 14 additions & 0 deletions api/organisations/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,20 @@
),
]

if settings.LICENSING_INSTALLED: # pragma: no cover
from licensing.views import create_or_update_licence

urlpatterns.extend(
[
path(
"<int:organisation_id>/licence",
create_or_update_licence,
name="create-or-update-licence",
),
]
)


if settings.IS_RBAC_INSTALLED:
from rbac.views import (
GroupRoleViewSet,
Expand Down
59 changes: 29 additions & 30 deletions api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 6 additions & 8 deletions api/tests/unit/features/versioning/test_unit_versioning_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1639,8 +1639,7 @@ def test_list_versions_always_returns_current_version_even_if_outside_limit(


@pytest.mark.freeze_time(now - timedelta(days=DEFAULT_VERSION_LIMIT_DAYS + 1))
@pytest.mark.parametrize("is_saas", (True, False))
def test_list_versions_returns_all_versions_for_enterprise_plan(
def test_list_versions_returns_all_versions_for_enterprise_plan_when_saas(
feature: Feature,
environment_v2_versioning: Environment,
staff_user: FFAdminUser,
Expand All @@ -1649,10 +1648,10 @@ def test_list_versions_returns_all_versions_for_enterprise_plan(
with_project_permissions: WithProjectPermissionsCallable,
subscription: Subscription,
freezer: FrozenDateTimeFactory,
is_saas: bool,
mocker: MockerFixture,
) -> None:
# Given
is_saas = True
with_environment_permissions([VIEW_ENVIRONMENT])
with_project_permissions([VIEW_PROJECT])

Expand All @@ -1668,11 +1667,10 @@ def test_list_versions_returns_all_versions_for_enterprise_plan(
subscription.plan = "enterprise"
subscription.save()

if is_saas:
OrganisationSubscriptionInformationCache.objects.update_or_create(
organisation=subscription.organisation,
defaults={"feature_history_visibility_days": None},
)
OrganisationSubscriptionInformationCache.objects.update_or_create(
organisation=subscription.organisation,
defaults={"feature_history_visibility_days": None},
)

initial_version = EnvironmentFeatureVersion.objects.get(
feature=feature, environment=environment_v2_versioning
Expand Down
44 changes: 0 additions & 44 deletions api/tests/unit/organisations/test_unit_organisations_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,50 +336,6 @@ def test_organisation_subscription_get_subscription_metadata_returns_free_plan_m
assert subscription_metadata == FREE_PLAN_SUBSCRIPTION_METADATA


@pytest.mark.parametrize(
"subscription_id, plan, max_seats, expected_seats, expected_projects",
(
(
None,
"free",
10,
MAX_SEATS_IN_FREE_PLAN,
settings.MAX_PROJECTS_IN_FREE_PLAN,
),
("anything", "enterprise", 20, 20, None),
(TRIAL_SUBSCRIPTION_ID, "enterprise", 20, 20, None),
),
)
def test_organisation_get_subscription_metadata_for_enterprise_self_hosted_licenses(
organisation: Organisation,
subscription_id: str | None,
plan: str,
max_seats: int,
expected_seats: int,
expected_projects: int | None,
mocker: MockerFixture,
) -> None:
"""
Specific test to make sure that we can manually add subscriptions to
enterprise self-hosted deployments and the values stored in the django
database will be correctly used.
"""
# Given
Subscription.objects.filter(organisation=organisation).update(
subscription_id=subscription_id, plan=plan, max_seats=max_seats
)
organisation.subscription.refresh_from_db()
mocker.patch("organisations.models.is_saas", return_value=False)
mocker.patch("organisations.models.is_enterprise", return_value=True)

# When
subscription_metadata = organisation.subscription.get_subscription_metadata()

# Then
assert subscription_metadata.projects == expected_projects
assert subscription_metadata.seats == expected_seats


@pytest.mark.parametrize(
"subscription_id, plan, max_seats, max_api_calls, expected_seats, "
"expected_api_calls, expected_projects",
Expand Down

0 comments on commit fbd1a13

Please sign in to comment.