Skip to content

Commit

Permalink
Merge pull request #898 from intuitem/CA-471-implement-license-expira…
Browse files Browse the repository at this point in the history
…tion

Ca 471 implement license expiration
  • Loading branch information
Mohamed-Hacene authored Oct 21, 2024
2 parents 3ce0819 + 4e2493c commit 36fb28a
Show file tree
Hide file tree
Showing 18 changed files with 180 additions and 11 deletions.
38 changes: 38 additions & 0 deletions enterprise/backend/enterprise_core/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
from datetime import datetime

import structlog
from rest_framework import permissions

logger = structlog.get_logger(__name__)


class LicensePermission(permissions.BasePermission):
def has_permission(self, request, view):
expiration_date_str = os.environ.get("LICENSE_EXPIRATION")

if not expiration_date_str:
# Handle the case where no expiration date is set
logger.warning("License expiration date is not set.")
return True

try:
expiration_date = datetime.fromisoformat(expiration_date_str)
except ValueError:
logger.error(
"Invalid expiration date format. Expiration date should follow ISO 8601 format.",
expiration_date=expiration_date_str,
)
return False

if expiration_date < datetime.now():
# License has expired, only allow read operations
if request.method not in permissions.SAFE_METHODS:
logger.warning(
"License has expired, only read operations are allowed.",
expiration_date=expiration_date,
)
return False

# License is valid, allow all operations
return True
7 changes: 6 additions & 1 deletion enterprise/backend/enterprise_core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def set_ciso_assistant_url(_, __, event_dict):
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
"core.permissions.RBACPermissions",
"enterprise_core.permissions.LicensePermission",
],
"DEFAULT_FILTER_CLASSES": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
Expand Down Expand Up @@ -407,10 +408,14 @@ def set_ciso_assistant_url(_, __, event_dict):
}

logger.info(
"Enterprise startup info", feature_flags=FEATURE_FLAGS, module_paths=MODULE_PATHS
"Enterprise startup information",
feature_flags=FEATURE_FLAGS,
module_paths=MODULE_PATHS,
)

LICENSE_SEATS = int(os.environ.get("LICENSE_SEATS", 1))
LICENSE_EXPIRATION = os.environ.get("LICENSE_EXPIRATION", "unset")

logger.info("License information", seats=LICENSE_SEATS, expiration=LICENSE_EXPIRATION)

INSTALLED_APPS.append("enterprise_core")
3 changes: 2 additions & 1 deletion enterprise/backend/enterprise_core/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.urls import include, path


from .views import get_build
from .views import LicenseStatusView, get_build

urlpatterns = [
path("build/", get_build, name="get_build"),
path("license-status/", LicenseStatusView.as_view(), name="license-status"),
]
47 changes: 44 additions & 3 deletions enterprise/backend/enterprise_core/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import magic
from datetime import datetime
from django.utils.formats import date_format

import magic
import structlog
from core.views import BaseModelViewSet
from django.conf import settings
from iam.models import User
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.decorators import (
action,
api_view,
permission_classes,
)
from rest_framework.parsers import FileUploadParser
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView

from django.conf import settings

Expand Down Expand Up @@ -158,6 +164,35 @@ def upload_favicon(self, request, pk):
return self.handle_file_upload(request, pk, "favicon")


class LicenseStatusView(APIView):
def get(self, request):
expiry_date_str = settings.LICENSE_EXPIRATION

if not expiry_date_str:
return Response(
{"status": "unknown", "message": "No expiry date set"},
status=status.HTTP_400_BAD_REQUEST,
)

try:
expiry_date = datetime.fromisoformat(expiry_date_str)
except ValueError as e:
logger.error("Invalid expiry date format", exc_info=e)
return Response(
{"status": "error", "message": "Invalid expiry date format"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

now = datetime.now()

if expiry_date > now:
days_left = (expiry_date - now).days
return Response({"status": "active", "days_left": days_left})
else:
days_expired = (now - expiry_date).days
return Response({"status": "expired", "days_expired": days_expired})


@api_view(["GET"])
def get_build(request):
"""
Expand All @@ -167,12 +202,18 @@ def get_build(request):
BUILD = settings.BUILD
LICENSE_SEATS = settings.LICENSE_SEATS
LICENSE_EXPIRATION = settings.LICENSE_EXPIRATION
try:
expiration_iso = datetime.fromisoformat(LICENSE_EXPIRATION)
license_expiration = date_format(expiration_iso, use_l10n=True)
except ValueError:
logger.error("Invalid expiry date format", exc_info=True)
license_expiration = LICENSE_EXPIRATION
return Response(
{
"version": VERSION,
"build": BUILD,
"license_seats": LICENSE_SEATS,
"available_seats": LICENSE_SEATS - len(User.get_editors()),
"license_expiration": LICENSE_EXPIRATION,
"license_expiration": license_expiration,
}
)
70 changes: 70 additions & 0 deletions enterprise/frontend/src/routes/(app)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script lang="ts">
// Most of your app wide CSS should be put in this file
import { safeTranslate } from '$lib/utils/i18n';
import { AppBar, AppShell } from '@skeletonlabs/skeleton';
import '../../app.postcss';
import { browser } from '$app/environment';
import Breadcrumbs from '$lib/components/Breadcrumbs/Breadcrumbs.svelte';
import SideBar from '$lib/components/SideBar/SideBar.svelte';
import { deleteCookie, getCookie } from '$lib/utils/cookies';
import { clientSideToast, pageTitle } from '$lib/utils/stores';
import * as m from '$paraglide/messages';
import type { LayoutData } from './$types';
export let data: LayoutData;
let sidebarOpen = true;
$: classesSidebarOpen = (open: boolean) => (open ? 'ml-64' : 'ml-7');
$: if (browser) {
const fromLogin = getCookie('from_login');
if (fromLogin === 'true') {
deleteCookie('from_login');
fetch('/api/waiting-risk-acceptances').then(async (res) => {
const data = await res.json();
const number = data.count ?? 0;
if (number <= 0) return;
clientSideToast.set({
message: m.waitingRiskAcceptances({
number: number,
s: number > 1 ? 's' : '',
itPlural: number > 1 ? 'i' : 'e'
}),
type: 'info'
});
});
}
}
</script>

<!-- App Shell -->
<AppShell
slotPageContent="p-8 bg-gradient-to-br from-violet-100 to-slate-200"
regionPage="transition-all duration-300 {classesSidebarOpen(sidebarOpen)}"
>
<svelte:fragment slot="sidebarLeft">
<SideBar bind:open={sidebarOpen} />
</svelte:fragment>
<svelte:fragment slot="pageHeader">
{#if data.licenseStatus.status === 'expired'}
<aside class="variant-soft-warning text-center w-full items-center py-2">
{m.licenseExpiredMessage()}
</aside>
{/if}
<AppBar background="bg-white" padding="py-2 px-4">
<span
class="text-2xl font-bold pb-1 bg-gradient-to-r from-pink-500 to-violet-600 bg-clip-text text-transparent"
id="page-title"
>
{safeTranslate($pageTitle)}
</span>
<hr class="w-screen my-1" />
<Breadcrumbs />
</AppBar>
</svelte:fragment>
<!-- Router Slot -->
<slot />
<!-- ---- / ---- -->
</AppShell>
4 changes: 3 additions & 1 deletion enterprise/frontend/src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import type { LayoutServerLoad } from './$types';
import type { GlobalSettings } from '$lib/utils/types';
import { BASE_API_URL } from '$lib/utils/constants';

export const load: LayoutServerLoad = async ({ fetch, locals }) => {
let clientSettings: GlobalSettings;
if (!locals.globalSettings) {
const _settings = await fetch('/settings/client-settings').then((res) => res.json());
clientSettings = { name: 'clientSettings', settings: _settings };
} else clientSettings = locals.globalSettings;
const licenseStatus = await fetch(`${BASE_API_URL}/license-status/`).then(res => res.json())
if (!locals.user && clientSettings.settings.show_images_unauthenticated !== true) {
clientSettings.settings.logo = '';
clientSettings.settings.favicon = '';
clientSettings.settings.logo_hash = '';
clientSettings.settings.favicon_hash = '';
}
return { featureFlags: locals.featureFlags, clientSettings };
return { featureFlags: locals.featureFlags, clientSettings, licenseStatus };
};
1 change: 1 addition & 0 deletions frontend/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "مرحبًا! لديك حاليًا {number} مخاطرة معلقة .يمكنك العثور عليها في علامة التبويب المخاطر.",
"licenseSeats": "مقاعد الترخيص",
"licenseExpiration": "انتهاء صلاحية الترخيص",
"licenseExpiredMessage": "انتهت صلاحية ترخيصك. عمليات الكتابة معطلة. يرجى الاتصال بالمسؤول لتجديد الترخيص.",
"answer": "إجابة",
"questionnaireMode": "وضع الاستبيان",
"assessmentMode": "وضع التقييم",
Expand Down
3 changes: 2 additions & 1 deletion frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,8 @@
"waitingRiskAcceptances": "Hallo! Sie haben derzeit {number} Risiko{s} zur Annahme ausstehend. Sie finden sie auf der Registerkarte „Risiken“.",
"licenseSeats": "Lizenzplätze",
"licenseExpiration": "Ablauf der Lizenz",
"licenseExpiredMessage": "Ihre Lizenz ist abgelaufen. Schreibvorgänge sind deaktiviert. Wenden Sie sich zur Verlängerung an Ihren Administrator.",
"backupGreaterVersionError": "Das Backup kann nicht geladen werden, die Version des Backups ist höher als die aktuelle Version Ihrer Anwendung.",
"answer": "Antwort",
"questionnaireMode": "Fragebogenmodus",
"assessmentMode": "Bewertungsmodus",
Expand Down Expand Up @@ -765,7 +767,6 @@
"startDate": "Startdatum",
"startDateHelpText": "Startdatum (nützlich für die Zeitleiste)",
"backupLoadingError": "Beim Laden der Sicherung ist ein Fehler aufgetreten.",
"backupGreaterVersionError": "Das Backup kann nicht geladen werden, die Version des Backups ist höher als die aktuelle Version Ihrer Anwendung.",
"backupLowerVersionError": "Ein Fehler ist aufgetreten. Die Sicherungsversion ist möglicherweise zu alt. Wenn dies der Fall ist, muss sie vor einem erneuten Versuch aktualisiert werden.",
"questionSingular": "Frage",
"questionPlural": "Fragen",
Expand Down
5 changes: 3 additions & 2 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,8 @@
"theFollowingControlsWillBeAddedColon": "The following controls will be added:",
"ShowAllNodesMessage": "Show all",
"ShowOnlyAssessable": "Only assessable",
"NoPreviewMessage": "No preview available.",
"licenseExpiredMessage": "Your license has expired. Write operations are disabled. Please contact your administrator to renew it.",
"experimental": "Experimental",
"timeline": "Timeline",
"graph": "Graph",
Expand All @@ -767,11 +769,10 @@
"backupLoadingError": "An error occurred while loading the backup.",
"backupGreaterVersionError": "Can't load the backup, the version of the backup is higher than the current version of your application.",
"backupLowerVersionError": "An error occurred, the backup version may be too old, if so it must be updated before retrying.",
"NoPreviewMessage": "No preview available.",
"entityAssessmentEvidenceHelpText": "An external questionnaire",
"associatedEntities": "Associated entities",
"noMailerConfigured": "No mailer configured",
"errorLicenseSeatsExceeded": "The number of license seats is exceeded, you will not be able to grant editing rights to this user. Please contact your administrator.",
"errorLicenseSeatsExceeded": "The number of editor seats allowed by your license is exceeded, you will not be able to grant editing rights to this user. Please contact your administrator.",
"availableSeats": "Available seats",
"showImagesUnauthenticated": "Show logo and favicon to unauthenticated users",
"showImagesUnauthenticatedHelpText": "If disabled, the regular CISO Assistant logo and favicon will be displayed on the login screen",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "Hola! Actualmente tienes {number} riesgo{s} aceptación pendiente. Puedes encontrarlos en la pestaña de riesgo.",
"licenseSeats": "Asientos de licencia",
"licenseExpiration": "Caducidad de la licencia",
"licenseExpiredMessage": "Su licencia ha expirado. Las operaciones de escritura están deshabilitadas. Comuníquese con su administrador para renovarla.",
"answer": "Respuesta",
"questionnaireMode": "Modo cuestionario",
"assessmentMode": "Modo de evaluación",
Expand Down
3 changes: 2 additions & 1 deletion frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,7 @@
"tableMode": "Mode tableau",
"owner": "Propriétaire",
"waitingRiskAcceptances": "Bonjour ! Vous avez actuellement {number} risque{s} en attente d'acceptation. Vous pouvez les retrouver dans l'onglet risque.",
"suggestControls": "Mesures recommendées",
"licenseSeats": "Sièges de licence",
"licenseExpiration": "Expiration de la licence",
"answer": "Répondre",
Expand All @@ -751,14 +752,14 @@
"questionOrQuestions": "Question(s)",
"successfullyUpdatedClientSettings": "Paramètres du client mis à jour avec succès. Vous pouvez rafrachir la page.",
"xRaysEmptyMessage": "Vous devez créer au moins un projet pour utiliser X-rays.",
"suggestControls": "Mesures recommandées",
"createAppliedControlsFromSuggestionsHelpText": "Créer des mesures appliquées à partir des mesures de référence suggérées",
"createAppliedControlsFromSuggestionsSuccess": "Mesures appliquées créées avec succès à partir des suggestions",
"createAppliedControlsFromSuggestionsError": "Une erreur s'est produite lors de la création des mesures appliquées depuis les suggestions",
"createAppliedControlsFromSuggestionsConfirmMessage": "{count} mesures appliquées seront créées à partir des suggestions. Voulez-vous continuer ?",
"theFollowingControlsWillBeAddedColon": "Les mesures suivantes seront appliquées :",
"ShowAllNodesMessage": "Tout afficher",
"ShowOnlyAssessable": "Uniquement évaluables",
"licenseExpiredMessage": "Votre licence a expiré. Les opérations d'écriture sont désactivées. Veuillez contacter votre administrateur pour la renouveler.",
"experimental": "Expérimental",
"timeline": "Chronologie",
"graph": "Graphique",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "नमस्ते! आपके पास वर्तमान में {number} जोखिम स्वीकृति{s} लंबित हैं। आप उन्हें जोखिम टैब में पा सकते हैं।",
"licenseSeats": "लाइसेंस सीटें",
"licenseExpiration": "लाइसेंस समाप्ति",
"licenseExpiredMessage": "आपका लाइसेंस समाप्त हो गया है। लिखने के कार्य अक्षम हैं। कृपया इसे नवीनीकृत करने के लिए अपने व्यवस्थापक से संपर्क करें।",
"answer": "उत्तर",
"questionnaireMode": "प्रश्नावली मोड",
"assessmentMode": "मूल्यांकन मोड",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "Ciao! Al momento hai {number} rischi{s} in attesa di accettazione. Puoi trovarli nella scheda rischi.",
"licenseSeats": "Posti di licenza",
"licenseExpiration": "Scadenza della licenza",
"licenseExpiredMessage": "La tua licenza è scaduta. Le operazioni di scrittura sono disabilitate. Contatta il tuo amministratore per rinnovarla.",
"answer": "Risposta",
"questionnaireMode": "Modalità questionario",
"assessmentMode": "Modalità di valutazione",
Expand Down
3 changes: 2 additions & 1 deletion frontend/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,9 @@
"tableMode": "Tabelmodus",
"owner": "Eigenaar",
"waitingRiskAcceptances": "Hallo! U hebt momenteel {number} risico{s} acceptatie in behandeling. U kunt ze vinden in het tabblad risico.",
"licenseSeats": "Licentie stoelen",
"licenseSeats": "Licentieplaatsen",
"licenseExpiration": "Licentie verloopt",
"licenseExpiredMessage": "Uw licentie is verlopen. Schrijfbewerkingen zijn uitgeschakeld. Neem contact op met uw beheerder om deze te verlengen.",
"answer": "Antwoord",
"questionnaireMode": "Vragenlijstmodus",
"assessmentMode": "Beoordelingsmodus",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "Cześć! Obecnie masz {number} ryzyko{s} oczekujące na akceptację. Możesz je znaleźć w zakładce ryzyko.",
"licenseSeats": "Miejsca na licencję",
"licenseExpiration": "Wygaśnięcie licencji",
"licenseExpiredMessage": "Twoja licencja wygasła. Operacje zapisu są wyłączone. Skontaktuj się z administratorem, aby ją odnowić.",
"answer": "Odpowiedź",
"questionnaireMode": "Tryb ankiety",
"assessmentMode": "Tryb oceny",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "Olá! No momento, você tem {number} aceitação de risco{s} pendente. Você pode encontrá-los na aba de risco.",
"licenseSeats": "Assentos de licença",
"licenseExpiration": "Expiração da licença",
"licenseExpiredMessage": "Sua licença expirou. Operações de gravação estão desabilitadas. Entre em contato com seu administrador para renová-la.",
"answer": "Responder",
"questionnaireMode": "Modo questionário",
"assessmentMode": "Modo de avaliação",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "Buna ziua! În prezent, aveți {number} riscul{s} în așteptare. Le puteți găsi în fila de riscuri.",
"licenseSeats": "Scaune de licență",
"licenseExpiration": "Expirarea licenței",
"licenseExpiredMessage": "Licența dvs. a expirat. Operațiunile de scriere sunt dezactivate. Vă rugăm să contactați administratorul pentru a-l reînnoi.",
"answer": "Răspuns",
"questionnaireMode": "Modul chestionar",
"assessmentMode": "Modul de evaluare",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/ur.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"waitingRiskAcceptances": "ہیلو! آپ کے پاس فی الحال {number} خطرے کی منظوری {s} زیر التواء ہیں۔ آپ انہیں خطرے والے ٹیب میں پا سکتے ہیں۔",
"licenseSeats": "لائسنس نشستیں",
"licenseExpiration": "لائسنس کی میعاد ختم",
"licenseExpiredMessage": "آپ کے لائسنس کی میعاد ختم ہو گئی ہے۔ تحریری کارروائیاں غیر فعال ہیں۔ اس کی تجدید کے لیے براہ کرم اپنے منتظم سے رابطہ کریں۔",
"answer": "جواب دیں۔",
"questionnaireMode": "سوالنامہ موڈ",
"assessmentMode": "تشخیص موڈ",
Expand Down

0 comments on commit 36fb28a

Please sign in to comment.