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

feat: Enterprise licensing #3624

Merged
merged 47 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c6274ce
WIP: licensing
matthewelwell Mar 15, 2024
a3262f4
Tidying up method
matthewelwell May 16, 2024
2bc0bf4
Fix typing
zachaysan Oct 30, 2024
6239687
Fix conflicts and merge branch 'main' into feat/licence-keys
zachaysan Oct 30, 2024
8ce9abe
Remove unnecessary mock
zachaysan Oct 30, 2024
c6885fe
Remove unnecessary code
zachaysan Oct 30, 2024
694e466
Add related name to one to one relation
zachaysan Oct 30, 2024
32f75b3
Add licence to test to load up the results
zachaysan Oct 31, 2024
40f0e44
Add licence content to test
zachaysan Oct 31, 2024
6c7bf8d
Create licensing private key command
zachaysan Nov 1, 2024
8a02971
Create licensing public key command
zachaysan Nov 1, 2024
766432e
Create licensing cryptographic helpers
zachaysan Nov 1, 2024
dcc573b
Test signatures
zachaysan Nov 1, 2024
d762817
Update licence upload with signatures and test for missing files
zachaysan Nov 1, 2024
7028bf7
Add cryptography to pyproject.toml explicitly
zachaysan Nov 1, 2024
220d59a
Remove TODO
zachaysan Nov 1, 2024
3789647
Add signatures to endpoint
zachaysan Nov 1, 2024
fbc5e90
TODO finished
zachaysan Nov 1, 2024
280307c
Remove TODO as it's odd but ok
zachaysan Nov 1, 2024
d7597a9
Add key settings and remove TODO
zachaysan Nov 1, 2024
e7788d6
Add test keys to test settings
zachaysan Nov 1, 2024
ecab02c
Remove nested object TODO
zachaysan Nov 1, 2024
c5dd2a2
Add __init__.py
zachaysan Nov 4, 2024
6754529
Create signed licence management command
zachaysan Nov 4, 2024
3d75ba1
Add test for failed signature
zachaysan Nov 4, 2024
5fcd0d3
Add signature tests for test helpers
zachaysan Nov 4, 2024
7baa333
Merge branch 'main' into feat/licence-keys
zachaysan Nov 4, 2024
462646a
Remove TODO
zachaysan Nov 4, 2024
67e8738
Add a default public key for digital signature checking
zachaysan Nov 4, 2024
f4fba4e
Move second migration into 0001_initial.py
zachaysan Nov 5, 2024
8904e7e
Switch to Django stdout and add exception handling for a missing priv…
zachaysan Nov 5, 2024
47c374f
Create test for missing private key
zachaysan Nov 5, 2024
449dace
Split parameterized test into two separate tests
zachaysan Nov 5, 2024
cfc3e28
Raise an exception if private key is missing
zachaysan Nov 5, 2024
67adf66
Merge branch 'main' into feat/licence-keys
zachaysan Nov 5, 2024
f90a45a
Remove managment commands from organisations
zachaysan Nov 11, 2024
2697602
Remove licensing module
zachaysan Nov 11, 2024
6cedd7f
Optionally add licensing to installed apps
zachaysan Nov 11, 2024
cc880bf
Remove cryptography from dependencies
zachaysan Nov 11, 2024
91f1423
Add check if licensing isn't present
zachaysan Nov 11, 2024
2408dc8
Make licensing path optional
zachaysan Nov 11, 2024
b8a31e9
Remove licensing tests into the new repo
zachaysan Nov 11, 2024
1f027b0
Merge branch 'main' into feat/licence-keys
zachaysan Nov 11, 2024
7d87608
Remove licensing tests
zachaysan Nov 11, 2024
66791ae
Add pragma no covers
zachaysan Nov 13, 2024
c3e15c6
Merge branch 'main' into feat/licence-keys
zachaysan Nov 13, 2024
0a06ed9
Add comment to elif branch
zachaysan Nov 28, 2024
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
24 changes: 24 additions & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1271,3 +1271,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,
)
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved

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.

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(
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved
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
Loading