Skip to content

Commit

Permalink
Show license info to all users
Browse files Browse the repository at this point in the history
  • Loading branch information
MWedl committed Jan 14, 2025
1 parent 0a87f39 commit 3185811
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 69 deletions.
4 changes: 2 additions & 2 deletions api/src/reportcreator_api/api_utils/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from reportcreator_api.api_utils import backup_utils
from reportcreator_api.api_utils.healthchecks import run_healthchecks
from reportcreator_api.api_utils.models import BackupLog
from reportcreator_api.api_utils.permissions import IsAdminOrSystem, IsUserManagerOrSuperuserOrSystem
from reportcreator_api.api_utils.permissions import IsAdminOrSystem
from reportcreator_api.api_utils.serializers import (
BackupLogSerializer,
BackupSerializer,
Expand Down Expand Up @@ -115,7 +115,7 @@ def backuplogs(self, request, *args, **kwargs):
return self.get_paginated_response(serializer.data)

@extend_schema(responses=OpenApiTypes.OBJECT)
@action(detail=False, url_name='license', url_path='license', methods=['get'], permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES + [IsUserManagerOrSuperuserOrSystem])
@action(detail=False, url_name='license', url_path='license', methods=['get'])
async def license_info(self, request, *args, **kwargs):
return Response(data=await license.aget_license_info())

Expand Down
2 changes: 1 addition & 1 deletion api/src/reportcreator_api/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ def remove_empty_items(lst=None):
'img-src': [SELF, 'data:'],
'font-src': [SELF],
'worker-src': [SELF],
'connect-src': [SELF, 'data:'],
'connect-src': [SELF, 'data:', 'https://portal.sysreptor.com'],
'frame-src': [SELF],
'frame-ancestors': [SELF],
'form-action': [SELF],
Expand Down
2 changes: 1 addition & 1 deletion api/src/reportcreator_api/tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def clear_sessions(task_info):
async def activate_license_request(license_info):
async with httpx.AsyncClient(timeout=10) as client:
res = await client.post(
url='https://panel.sysreptor.com/api/v1/licenses/activate/',
url='https://portal.sysreptor.com/api/v1/licenses/activate/',
headers={'Content-Type': 'application/json'},
data=json.dumps(license_info, cls=DjangoJSONEncoder),
)
Expand Down
2 changes: 1 addition & 1 deletion api/src/reportcreator_api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ def guest_urls():
return [
('utils list', lambda s, c: c.get(reverse('utils-list'))),
('utils cwes', lambda s, c: c.get(reverse('utils-cwes'))),
('utils-license', lambda s, c: c.get(reverse('utils-license'))),

*viewset_urls('pentestuser', get_kwargs=lambda s, detail: {'pk': 'self'}, retrieve=True, update=True, update_partial=True),
*viewset_urls('pentestuser', get_kwargs=lambda s, detail: {}, list=True),
Expand Down Expand Up @@ -304,7 +305,6 @@ def user_manager_urls():
*viewset_urls('authidentity', get_kwargs=lambda s, detail: {'pentestuser_pk': s.user_other.pk} | ({'pk': s.user_other.auth_identities.first().pk} if detail else {}), list=True, retrieve=True, create=True, create_data={'identifier': 'other.identifier'}, update=True, update_partial=True, destroy=True),
*viewset_urls('apitoken', get_kwargs=lambda s, detail: {'pentestuser_pk': s.user_other.pk} | ({'pk': s.user_other.api_tokens.first().pk} if detail else {}), list=True, retrieve=True, destroy=True),
*viewset_urls('userpublickey', get_kwargs=lambda s, detail: {'pentestuser_pk': s.user_other.pk} | ({'pk': s.user_other.public_keys.first().pk} if detail else {}), list=True, retrieve=True),
('utils-license', lambda s, c: c.get(reverse('utils-license'))),
]


Expand Down
19 changes: 8 additions & 11 deletions packages/frontend/src/components/LicenseInfoMenuItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<v-list-item to="/license/" title="License" :disabled="!auth.permissions.value.view_license">
<v-list-item to="/license/" title="License">
<template #prepend>
<v-badge :color="licenseError ? 'error' : licenseWarning ? 'warning' : 'transparent'" dot>
<v-icon icon="mdi-check-decagram" />
Expand All @@ -9,24 +9,21 @@
</template>

<script setup lang="ts">
const auth = useAuth();
const apiSettings = useApiSettings();
const licenseError = computed(() => apiSettings.settings!.license.error !== null);
const { data: licenseInfo } = useLazyAsyncData(async () => {
if (auth.permissions.value.view_license && licenseError.value) {
await nextTick();
return await $fetch<LicenseInfoDetails>('/api/v1/utils/license/', { method: 'GET' });
} else {
return null;
useLazyAsyncData(async () => {
if (apiSettings.isProfessionalLicense) {
await apiSettings.getLicenseInfo();
}
});
const licenseError = computed(() => apiSettings.settings!.license.error !== null);
const licenseWarning = computed(() => {
if (!licenseInfo.value || !licenseInfo.value.valid_until) {
if (!apiSettings.licenseInfo || !apiSettings.licenseInfo.valid_until) {
return false;
}
const warnThresholdDate = new Date();
warnThresholdDate.setDate(new Date().getDate() + 2 * 30);
return new Date(licenseInfo.value.valid_until) < warnThresholdDate;
return new Date(apiSettings.licenseInfo.valid_until) < warnThresholdDate;
});
</script>
2 changes: 1 addition & 1 deletion packages/frontend/src/components/LoginForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ async function loginStep(fn: () => Promise<LoginResponse|null>) {
if (res.status === LoginResponseStatus.SUCCESS) {
// trigger login in nuxt-auth
await auth.fetchUser();
await auth.finishLogin(res);
emit('login', res);
} else if (res.status === LoginResponseStatus.MFA_REQUIRED) {
mfaMethods.value = res.mfa!;
Expand Down
84 changes: 40 additions & 44 deletions packages/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,51 +62,47 @@
/>
</template>

<template v-if="auth.permissions.value.superuser || auth.permissions.value.user_manager || auth.permissions.value.view_license">
<v-list-item class="mt-4 pa-0" min-height="0">
<v-list-subheader title="Administration" />
<template #append>
<template v-if="apiSettings.isProfessionalLicense">
<s-btn-icon
v-if="auth.permissions.value.admin"
:to="{path: '/users/self/admin/disable/', query: { next: route.fullPath }}"
density="comfortable"
>
<v-icon size="small" color="primary" icon="mdi-account-arrow-down" />
<s-tooltip activator="parent" text="Disable Superuser Permissions" />
</s-btn-icon>
<s-btn-icon
v-else
:to="{path: '/users/self/admin/enable/', query: { next: route.fullPath }}"
:disabled="!auth.user.value!.is_superuser"
density="comfortable"
data-testid="enable-superuser"
>
<v-icon size="small" icon="mdi-account-arrow-up" />
<s-tooltip activator="parent" text="Enable Superuser Permissions" />
</s-btn-icon>
</template>
</template>
</v-list-item>
<v-list-item class="mt-4 pa-0" min-height="0">
<v-list-subheader title="Administration" />
<template #append v-if="apiSettings.isProfessionalLicense && auth.permissions.value.superuser">
<s-btn-icon
v-if="auth.permissions.value.admin"
:to="{path: '/users/self/admin/disable/', query: { next: route.fullPath }}"
density="comfortable"
>
<v-icon size="small" color="primary" icon="mdi-account-arrow-down" />
<s-tooltip activator="parent" text="Disable Superuser Permissions" />
</s-btn-icon>
<s-btn-icon
v-else
:to="{path: '/users/self/admin/enable/', query: { next: route.fullPath }}"
:disabled="!auth.user.value!.is_superuser"
density="comfortable"
data-testid="enable-superuser"
>
<v-icon size="small" icon="mdi-account-arrow-up" />
<s-tooltip activator="parent" text="Enable Superuser Permissions" />
</s-btn-icon>
</template>
</v-list-item>

<v-list-item
to="/users/"
data-testid="users-tab"
prepend-icon="mdi-account-multiple"
:active="route.path.startsWith('/users') && !route.path.startsWith('/users/self')"
:disabled="!auth.permissions.value.user_manager"
>
<template #title><permission-info :value="auth.permissions.value.user_manager" permission-name="User Manager">Users</permission-info></template>
</v-list-item>
<v-list-item
to="/backups/"
prepend-icon="mdi-tools"
:disabled="!auth.permissions.value.view_backup"
>
<template #title><pro-info><permission-info :value="auth.permissions.value.view_backup || !apiSettings.isProfessionalLicense" permission-name="Superuser">Backups</permission-info></pro-info></template>
</v-list-item>
<license-info-menu-item />
</template>
<v-list-item
to="/users/"
data-testid="users-tab"
prepend-icon="mdi-account-multiple"
:active="route.path.startsWith('/users') && !route.path.startsWith('/users/self')"
:disabled="!auth.permissions.value.user_manager"
>
<template #title><permission-info :value="auth.permissions.value.user_manager" permission-name="User Manager">Users</permission-info></template>
</v-list-item>
<v-list-item
to="/backups/"
prepend-icon="mdi-tools"
:disabled="!auth.permissions.value.view_backup"
>
<template #title><pro-info><permission-info :value="auth.permissions.value.view_backup || !apiSettings.isProfessionalLicense" permission-name="Superuser">Backups</permission-info></pro-info></template>
</v-list-item>
<license-info-menu-item />
</v-list>
</v-navigation-drawer>

Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/pages/login/auto.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const { error } = useAsyncData(async () => {
} else if (res.status !== LoginResponseStatus.SUCCESS) {
throw new Error(`Login failed: ${res.status}`);
}
await auth.fetchUser();
await auth.finishLogin(res);
await auth.redirect();
} catch (error: any) {
if (error?.data?.detail) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ const route = useRoute();
const auth = useAuth();
const { error } = useAsyncData(async () => {
try {
await $fetch(`/api/v1/auth/login/oidc/${route.params.authProviderId}/complete/`, {
const res = await $fetch<LoginResponse>(`/api/v1/auth/login/oidc/${route.params.authProviderId}/complete/`, {
method: 'GET',
params: route.query,
});
await auth.fetchUser();
await auth.finishLogin(res);
await auth.redirect();
} catch (error: any) {
if (error?.data?.detail) {
Expand Down
23 changes: 22 additions & 1 deletion packages/nuxt-base-layer/src/composables/apisettings.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { type ApiSettings, type AuthProvider, type CWE, AuthProviderType } from '#imports';
import { type ApiSettings, type AuthProvider, type CWE, type LicenseInfoDetails, AuthProviderType } from '#imports';

export const useApiSettings = defineStore('apisettings', {
state: () => ({
settings: null as ApiSettings | null,
getSettingsSync: null as Promise<ApiSettings> | null,
cwes: null as CWE[]|null,
getCwesSync: null as Promise<CWE[]>|null,
licenseInfo: null as LicenseInfoDetails|null,
getLicenseInfoSync: null as Promise<LicenseInfoDetails>|null,
}),
actions: {
async fetchSettings() : Promise<ApiSettings> {
Expand Down Expand Up @@ -47,6 +49,25 @@ export const useApiSettings = defineStore('apisettings', {
this.getCwesSync = null;
}
}
},
async fetchLicenseInfo(): Promise<LicenseInfoDetails> {
this.licenseInfo = await $fetch<LicenseInfoDetails>('/api/v1/utils/license/', { method: 'GET' });
return this.licenseInfo!;
},
async getLicenseInfo(): Promise<LicenseInfoDetails> {
if (this.licenseInfo) {
return this.licenseInfo;
} else if (this.getLicenseInfoSync) {
return await this.getLicenseInfoSync;
} else {
try {
this.getLicenseInfoSync = this.fetchLicenseInfo();
return await this.getLicenseInfoSync;
} finally {
this.getLicenseInfoSync = null;
}
}

}
},
getters: {
Expand Down
18 changes: 14 additions & 4 deletions packages/nuxt-base-layer/src/composables/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NavigateToOptions } from '#app/composables/router'
import type { LocationQueryValue } from "#vue-router";
import { AuthProviderType, type AuthProvider, type User, useApiSettings } from "#imports";
import { AuthProviderType, type AuthProvider, type User, useApiSettings, type LoginResponse, LoginResponseStatus } from "#imports";

export const useAuthStore = defineStore('auth', {
state: () => ({
Expand All @@ -24,7 +24,6 @@ export const useAuthStore = defineStore('auth', {
edit_projects: (state.user && (!state.user.is_guest || state.user.scope.includes('project_admin') || (state.user.is_guest && apiSettings.settings?.guest_permissions.edit_projects))) || false,
share_notes: (apiSettings.settings?.features.sharing && state.user && (!state.user.is_guest || state.user.scope.includes('project_admin') || (state.user.is_guest && apiSettings.settings?.guest_permissions.share_notes))) || false,
archive_projects: (apiSettings.settings?.features.archiving && state.user && (!state.user.is_guest || state.user.scope.includes('admin') || (state.user.is_guest && apiSettings.settings?.guest_permissions.update_project_settings))) || false,
view_license: state.user?.is_superuser || state.user?.is_user_manager || state.user?.is_system_user || false,
view_backup: (apiSettings.isProfessionalLicense && state.user?.scope.includes('admin')) || false,
};
},
Expand Down Expand Up @@ -100,16 +99,26 @@ export function useAuth() {
store.user = null;
}

async function finishLogin(response: LoginResponse) {
if (response.status !== LoginResponseStatus.SUCCESS) {
throw new Error('Login failed');
}
const apiSettings = useApiSettings();
apiSettings.licenseInfo = response.license!;

return await fetchUser();
}

async function authProviderLoginBegin(authProvider: AuthProvider, options = { reauth: false }) {
if (authProvider.type === AuthProviderType.LOCAL) {
await navigateTo('/login/local/');
} else if (authProvider.type === AuthProviderType.REMOTEUSER) {
try {
await $fetch('/api/v1/auth/login/remoteuser/', {
const res = await $fetch<LoginResponse>('/api/v1/auth/login/remoteuser/', {
method: 'POST',
body: {}
});
await fetchUser();
await finishLogin(res);
await redirect();
} catch (error) {
requestErrorToast({ error, message: 'Login failed' });
Expand All @@ -133,5 +142,6 @@ export function useAuth() {
redirectToReAuth,
fetchUser,
authProviderLoginBegin,
finishLogin,
};
}
1 change: 1 addition & 0 deletions packages/nuxt-base-layer/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export type LoginResponse = {
status: LoginResponseStatus,
first_login?: boolean,
mfa?: MfaMethod[],
license?: LicenseInfoDetails,
}

export type UserNotification = BaseModel & {
Expand Down

0 comments on commit 3185811

Please sign in to comment.