Skip to content

Commit

Permalink
feat: update seat limits and usage (#393)
Browse files Browse the repository at this point in the history
* feat: consolidate seat quota logic

* feat: misc updates to plan info display

* refactor: org seat usage types

* fix: misc copy and ui updates

* feat: remove feature list on self-hosted
  • Loading branch information
rohan-chaturvedi authored Nov 23, 2024
1 parent 1b46ab2 commit 94b15b6
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 146 deletions.
13 changes: 11 additions & 2 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
from api.services import Providers, ServiceConfig
from api.tasks import trigger_sync_tasks
from backend.quotas import (
can_add_account,
can_add_app,
can_add_environment,
can_add_service_token,
can_add_user,
)


Expand Down Expand Up @@ -215,6 +215,14 @@ def delete(self, *args, **kwargs):
self.save()


class ServiceAccountManager(models.Manager):
def create(self, *args, **kwargs):
organisation = kwargs.get("organisation")
if not can_add_account(organisation):
raise ValueError("Cannot add more accounts to this organisation's plan.")
return super().create(*args, **kwargs)


class ServiceAccount(models.Model):
id = models.TextField(default=uuid4, primary_key=True, editable=False)
name = models.CharField(max_length=255)
Expand All @@ -234,6 +242,7 @@ class ServiceAccount(models.Model):
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(null=True, blank=True)
objects = ServiceAccountManager()


class ServiceAccountHandler(models.Model):
Expand All @@ -251,7 +260,7 @@ class ServiceAccountHandler(models.Model):
class OrganisationMemberInviteManager(models.Manager):
def create(self, *args, **kwargs):
organisation = kwargs.get("organisation")
if not can_add_user(organisation):
if not can_add_account(organisation):
raise ValueError("Cannot add more users to this organisation's plan.")
return super().create(*args, **kwargs)

Expand Down
6 changes: 0 additions & 6 deletions backend/backend/graphene/mutations/service_accounts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from backend.quotas import can_add_service_account
import graphene
from graphql import GraphQLError
from api.models import (
Expand Down Expand Up @@ -55,11 +54,6 @@ def mutate(
"You don't have the permissions required to create Service Accounts in this organisation"
)

if not can_add_service_account(org):
raise GraphQLError(
"You cannot add any more service accounts to this organisation"
)

if handlers is None or len(handlers) == 0:
raise GraphQLError("At least one service account handler must be provided")

Expand Down
34 changes: 22 additions & 12 deletions backend/backend/graphene/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@
from api.utils.access.roles import default_roles


class SeatsUsed(ObjectType):
users = graphene.Int()
service_accounts = graphene.Int()
total = graphene.Int()


class OrganisationPlanType(ObjectType):
name = graphene.String()
max_users = graphene.Int()
max_apps = graphene.Int()
max_envs_per_app = graphene.Int()
user_count = graphene.Int()
service_account_count = graphene.Int()
seats_used = graphene.Field(SeatsUsed)
app_count = graphene.Int()


Expand Down Expand Up @@ -126,18 +131,23 @@ def resolve_plan_detail(self, info):

plan = PLAN_CONFIG[self.plan]

plan["user_count"] = (
OrganisationMember.objects.filter(
plan["seats_used"] = {
"users": (
OrganisationMember.objects.filter(
organisation=self, deleted_at=None
).count()
+ OrganisationMemberInvite.objects.filter(
organisation=self, valid=True, expires_at__gte=timezone.now()
).count()
),
"service_accounts": ServiceAccount.objects.filter(
organisation=self, deleted_at=None
).count()
+ OrganisationMemberInvite.objects.filter(
organisation=self, valid=True, expires_at__gte=timezone.now()
).count()
)
).count(),
}

plan["service_account_count"] = ServiceAccount.objects.filter(
organisation=self, deleted_at=None
).count()
plan["seats_used"]["total"] = (
plan["seats_used"]["users"] + plan["seats_used"]["service_accounts"]
)

plan["app_count"] = App.objects.filter(
organisation=self, deleted_at=None
Expand Down
50 changes: 14 additions & 36 deletions backend/backend/quotas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
PLAN_CONFIG = {
"FR": {
"name": "Free",
"max_users": 5 if CLOUD_HOSTED else 20,
"max_apps": 3 if CLOUD_HOSTED else 20,
"max_users": 5 if CLOUD_HOSTED else None,
"max_apps": 3 if CLOUD_HOSTED else None,
"max_envs_per_app": 3,
"max_tokens_per_app": 3,
},
Expand Down Expand Up @@ -54,72 +54,50 @@ def can_add_app(organisation):
return current_app_count < plan_limits["max_apps"]


def can_add_user(organisation):
"""Check if a new user can be added to the organisation."""
def can_add_account(organisation):
"""Check if a new human or service account can be added to the organisation."""

OrganisationMember = apps.get_model("api", "OrganisationMember")
OrganisationMemberInvite = apps.get_model("api", "OrganisationMemberInvite")
ServiceAccount = apps.get_model("api", "ServiceAccount")
ActivatedPhaseLicense = apps.get_model("api", "ActivatedPhaseLicense")

plan_limits = PLAN_CONFIG[organisation.plan]
license_exists = ActivatedPhaseLicense.objects.filter(
organisation=organisation
).exists()

current_user_count = (
# Calculate the current count of users and service accounts
current_human_user_count = (
OrganisationMember.objects.filter(
organisation=organisation, deleted_at=None
).count()
+ OrganisationMemberInvite.objects.filter(
organisation=organisation, valid=True, expires_at__gte=timezone.now()
).count()
)

if license_exists:
license = (
ActivatedPhaseLicense.objects.filter(organisation=organisation)
.order_by("-activated_at")
.first()
)
user_limit = license.seats

else:
user_limit = plan_limits["max_users"]

if user_limit is None:
return True
return current_user_count < user_limit


def can_add_service_account(organisation):
"""Check if a new service account can be added to the organisation."""

ServiceAccount = apps.get_model("api", "ServiceAccount")
ActivatedPhaseLicense = apps.get_model("api", "ActivatedPhaseLicense")

plan_limits = PLAN_CONFIG[organisation.plan]
license_exists = ActivatedPhaseLicense.objects.filter(
organisation=organisation
).exists()

current_account_count = ServiceAccount.objects.filter(
current_service_account_count = ServiceAccount.objects.filter(
organisation=organisation, deleted_at=None
).count()
total_account_count = current_human_user_count + current_service_account_count

# Determine the user limit
if license_exists:
license = (
ActivatedPhaseLicense.objects.filter(organisation=organisation)
.order_by("-activated_at")
.first()
)
user_limit = license.seats

else:
user_limit = plan_limits["max_users"]

# If there's no limit, allow unlimited additions
if user_limit is None:
return True
return current_account_count < user_limit

# Check if the total account count is below the limit
return total_account_count < user_limit


def can_add_environment(app):
Expand Down
8 changes: 4 additions & 4 deletions frontend/apollo/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ const documents = {
"query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n kms {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n }\n kmsLogsCount(appId: $appId)\n}": types.GetAppKmsLogsDocument,
"query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}": types.GetAppsDocument,
"query GetDashboard($organisationId: ID!) {\n apps(organisationId: $organisationId) {\n id\n sseEnabled\n }\n userTokens(organisationId: $organisationId) {\n id\n }\n organisationInvites(orgId: $organisationId) {\n id\n }\n organisationMembers(organisationId: $organisationId, role: null) {\n id\n }\n savedCredentials(orgId: $organisationId) {\n id\n }\n syncs(orgId: $organisationId) {\n id\n }\n}": types.GetDashboardDocument,
"query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n serviceAccountCount\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}": types.GetOrganisationsDocument,
"query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}": types.GetOrganisationsDocument,
"query CheckOrganisationNameAvailability($name: String!) {\n organisationNameAvailable(name: $name)\n}": types.CheckOrganisationNameAvailabilityDocument,
"query GetGlobalAccessUsers($organisationId: ID!) {\n organisationGlobalAccessUsers(organisationId: $organisationId) {\n id\n role {\n name\n permissions\n }\n identityKey\n self\n }\n}": types.GetGlobalAccessUsersDocument,
"query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n self\n }\n inviteeEmail\n }\n}": types.GetInvitesDocument,
"query GetLicenseData {\n license {\n id\n customerName\n organisationName\n expiresAt\n plan\n seats\n isActivated\n organisationOwner {\n fullName\n email\n }\n }\n}": types.GetLicenseDataDocument,
"query GetOrgLicense($organisationId: ID!) {\n organisationLicense(organisationId: $organisationId) {\n id\n customerName\n issuedAt\n expiresAt\n activatedAt\n plan\n seats\n tokens\n }\n}": types.GetOrgLicenseDocument,
"query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role {\n id\n name\n description\n permissions\n color\n }\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n self\n }\n}": types.GetOrganisationMembersDocument,
"query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n serviceAccountCount\n appCount\n }\n}": types.GetOrganisationPlanDocument,
"query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n}": types.GetOrganisationPlanDocument,
"query GetRoles($orgId: ID!) {\n roles(orgId: $orgId) {\n id\n name\n description\n color\n permissions\n isDefault\n }\n}": types.GetRolesDocument,
"query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n email\n }\n apps {\n id\n name\n }\n }\n}": types.VerifyInviteDocument,
"query GetAppEnvironments($appId: ID!, $memberId: ID, $memberType: MemberType) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}": types.GetAppEnvironmentsDocument,
Expand Down Expand Up @@ -413,7 +413,7 @@ export function graphql(source: "query GetDashboard($organisationId: ID!) {\n a
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n serviceAccountCount\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"): (typeof documents)["query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n serviceAccountCount\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"];
export function graphql(source: "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"): (typeof documents)["query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -441,7 +441,7 @@ export function graphql(source: "query GetOrganisationMembers($organisationId: I
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n serviceAccountCount\n appCount\n }\n}"): (typeof documents)["query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n serviceAccountCount\n appCount\n }\n}"];
export function graphql(source: "query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n}"): (typeof documents)["query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit 94b15b6

Please sign in to comment.