Skip to content

Commit

Permalink
feat: add quota logic for service accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
rohan-chaturvedi committed Nov 5, 2024
1 parent 642a002 commit f0a6d4d
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 17 deletions.
6 changes: 6 additions & 0 deletions backend/backend/graphene/mutations/service_accounts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from backend.quotas import can_add_service_account
import graphene
from graphql import GraphQLError
from api.models import (
Expand Down Expand Up @@ -53,6 +54,11 @@ 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
5 changes: 5 additions & 0 deletions backend/backend/graphene/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class OrganisationPlanType(ObjectType):
max_apps = graphene.Int()
max_envs_per_app = graphene.Int()
user_count = graphene.Int()
service_account_count = graphene.Int()
app_count = graphene.Int()


Expand Down Expand Up @@ -134,6 +135,10 @@ def resolve_plan_detail(self, info):
).count()
)

plan["service_account_count"] = ServiceAccount.objects.filter(
organisation=self, deleted_at=None
).count()

plan["app_count"] = App.objects.filter(
organisation=self, deleted_at=None
).count()
Expand Down
31 changes: 31 additions & 0 deletions backend/backend/quotas.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,37 @@ def can_add_user(organisation):
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(
organisation=organisation, deleted_at=None
).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_account_count < user_limit


def can_add_environment(app):
"""Check if a new environment can be added to the app."""

Expand Down
17 changes: 11 additions & 6 deletions frontend/apollo/gql.ts

Large diffs are not rendered by default.

36 changes: 30 additions & 6 deletions frontend/apollo/graphql.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/apollo/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type OrganisationPlanType {
maxApps: Int
maxEnvsPerApp: Int
userCount: Int
serviceAccountCount: Int
appCount: Int
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/app/[team]/access/service-accounts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function ServiceAccounts({ params }: { params: { team: string } }
<tbody className="divide-y divide-zinc-500/20">
{data?.serviceAccounts.map((account: ServiceAccountType) => (
<tr key={account.id} className="group">
<td className="flex items-center gap-2 py-4">
<td className="flex items-center gap-2 py-4 font-semibold">
<div className="rounded-full flex items-center bg-neutral-500/40 justify-center size-10">
<FaRobot className="shrink-0 text-zinc-900 dark:text-zinc-100 text-xl" />
</div>
Expand Down
43 changes: 41 additions & 2 deletions frontend/components/settings/organisation/PlanInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import { PlanLabel } from './PlanLabel'
import Spinner from '@/components/common/Spinner'
import { calculatePercentage } from '@/utils/dataUnits'
import { Button } from '@/components/common/Button'
import { FaCheckCircle, FaCube, FaCubes, FaTimesCircle, FaUser, FaUsersCog } from 'react-icons/fa'
import {
FaCheckCircle,
FaCog,
FaCube,
FaCubes,
FaTimesCircle,
FaUser,
FaUsersCog,
} from 'react-icons/fa'
import Link from 'next/link'
import { ActivatedPhaseLicenseType, ApiOrganisationPlanChoices } from '@/apollo/graphql'
import { isCloudHosted } from '@/utils/appConfig'
Expand Down Expand Up @@ -148,6 +156,13 @@ export const PlanInfo = () => {
)
: 0

const serviceAccountQuotaUsage = data
? calculatePercentage(
data.organisationPlan.serviceAccountCount,
license()?.seats || data.organisationPlan.maxUsers
)
: 0

const progressBarColor = (value: number, maxValue: number) =>
value >= maxValue ? 'bg-red-500' : value === maxValue - 1 ? 'bg-amber-500' : 'bg-emerald-500'

Expand Down Expand Up @@ -263,13 +278,37 @@ export const PlanInfo = () => {
/>
)}
<div className="flex justify-start">
<Link href={`/${activeOrganisation.name}/members`}>
<Link href={`/${activeOrganisation.name}/access/members`}>
<Button variant="secondary">
<FaUsersCog /> Manage
</Button>
</Link>
</div>
</div>

<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-lg font-medium text-black dark:text-white">Service Accounts</div>
<div className="text-neutral-500">{`${data.organisationPlan.serviceAccountCount} ${license()?.seats || data.organisationPlan.maxUsers ? `of ${license()?.seats || data.organisationPlan.maxUsers}` : ''} Seats used`}</div>
</div>
{(activeOrganisation.plan === ApiOrganisationPlanChoices.Fr || license()?.seats) && (
<ProgressBar
percentage={serviceAccountQuotaUsage}
color={progressBarColor(
data.organisationPlan.serviceAccountCount,
license()?.seats || data.organisationPlan.maxUsers
)}
size="sm"
/>
)}
<div className="flex justify-start">
<Link href={`/${activeOrganisation.name}/access/service-accounts`}>
<Button variant="secondary">
<FaCog /> Manage
</Button>
</Link>
</div>
</div>
</div>

{searchParams?.get('stripe_session_id') && (
Expand Down
1 change: 1 addition & 0 deletions frontend/graphql/queries/getOrganisations.gql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ query GetOrganisations {
maxApps
maxEnvsPerApp
userCount
serviceAccountCount
appCount
}
role {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ query GetOrganisationPlan($organisationId: ID!) {
maxApps
maxEnvsPerApp
userCount
serviceAccountCount
appCount
}
}
4 changes: 2 additions & 2 deletions frontend/utils/crypto/service-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ export const generateSAToken = async (
export const updateServiceAccountHandlers = async (orgId: string, userKeyring: OrganisationKeyring) => {
return new Promise(async (resolve, reject) => {
// Fetch service accounts
const { data: serviceAccountsData } = await graphQlClient.query({ query: GetServiceAccounts, variables: { orgId}, fetchPolicy: 'network-only' })
const { data: serviceAccountsData } = await graphQlClient.query({ query: GetServiceAccounts, variables: { orgId }, fetchPolicy: 'network-only' })
const serviceAccounts = serviceAccountsData?.serviceAccounts || []

// Fetch service account handlers
const { data: handlersData } = await graphQlClient.query({ query: GetServiceAccountHandlers, variables: { orgId}, fetchPolicy: 'network-only' })
const { data: handlersData } = await graphQlClient.query({ query: GetServiceAccountHandlers, variables: { orgId }, fetchPolicy: 'network-only' })
const handlers = handlersData.serviceAccountHandlers

// Current user kx keys
Expand Down

0 comments on commit f0a6d4d

Please sign in to comment.