diff --git a/src/app/api/app-status/route.ts b/src/app/api/app-status/route.ts index fd8596f..8bea7ec 100644 --- a/src/app/api/app-status/route.ts +++ b/src/app/api/app-status/route.ts @@ -1,7 +1,7 @@ import k3s from "@/server/adapter/kubernetes-api.adapter"; import appService from "@/server/services/app.service"; import deploymentService from "@/server/services/deployment.service"; -import { getAuthUserSession, simpleRoute } from "@/server/utils/action-wrapper.utils"; +import { getAuthUserSession, isAuthorizedReadForApp, simpleRoute } from "@/server/utils/action-wrapper.utils"; import { Informer, V1Pod } from "@kubernetes/client-node"; import { z } from "zod"; import * as k8s from '@kubernetes/client-node'; @@ -18,6 +18,7 @@ export async function POST(request: Request) { const input = await request.json(); const podInfo = zodInputModel.parse(input); let { appId } = podInfo; + await isAuthorizedReadForApp(appId); const app = await appService.getById(appId); const namespace = app.projectId; diff --git a/src/app/api/logs-download/route.ts b/src/app/api/logs-download/route.ts index 11e13f7..5458816 100644 --- a/src/app/api/logs-download/route.ts +++ b/src/app/api/logs-download/route.ts @@ -3,7 +3,7 @@ import { FsUtils } from "@/server/utils/fs.utils"; import { PathUtils } from "@/server/utils/path.utils"; import { NextRequest, NextResponse } from "next/server"; import fs from 'fs/promises'; -import { getAuthUserSession } from "@/server/utils/action-wrapper.utils"; +import { getAuthUserSession, isAuthorizedReadForApp } from "@/server/utils/action-wrapper.utils"; import { ServiceException } from "@/shared/model/service.exception.model"; import { z } from "zod"; import { stringToDate } from "@/shared/utils/zod.utils"; @@ -25,6 +25,8 @@ export async function GET(request: NextRequest) { const date = requestUrl.searchParams.get('date'); const validatedData = zodInputModel.parse({ appId, date }); + await isAuthorizedReadForApp(validatedData.appId); + const logsPath = PathUtils.appLogsFile(validatedData.appId, validatedData.date); if (!await FsUtils.fileExists(logsPath)) { throw new ServiceException(`Could not find logs for ${appId}.`); diff --git a/src/app/project/app/[appId]/actions.ts b/src/app/project/app/[appId]/actions.ts index b2bd8a2..9fba344 100644 --- a/src/app/project/app/[appId]/actions.ts +++ b/src/app/project/app/[appId]/actions.ts @@ -3,20 +3,20 @@ import { SuccessActionResult } from "@/shared/model/server-action-error-return.model"; import appService from "@/server/services/app.service"; import deploymentService from "@/server/services/deployment.service"; -import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils"; +import { isAuthorizedReadForApp, isAuthorizedWriteForApp, simpleAction } from "@/server/utils/action-wrapper.utils"; import eventService from "@/server/services/event.service"; export const deploy = async (appId: string, forceBuild = false) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); await appService.buildAndDeploy(appId, forceBuild); return new SuccessActionResult(undefined, 'Successfully started deployment.'); }); export const stopApp = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); const app = await appService.getExtendedById(appId); await deploymentService.setReplicasForDeployment(app.projectId, app.id, 0); return new SuccessActionResult(undefined, 'Successfully stopped app.'); @@ -24,7 +24,7 @@ export const stopApp = async (appId: string) => export const startApp = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); const app = await appService.getExtendedById(appId); await deploymentService.setReplicasForDeployment(app.projectId, app.id, app.replicas); return new SuccessActionResult(undefined, 'Successfully started app.'); @@ -32,7 +32,7 @@ export const startApp = async (appId: string) => export const getLatestAppEvents = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedReadForApp(appId); const app = await appService.getById(appId); return await eventService.getEventsForApp(app.projectId, app.id); }); diff --git a/src/app/project/app/[appId]/advanced/actions.ts b/src/app/project/app/[appId]/advanced/actions.ts index f8becab..3faa242 100644 --- a/src/app/project/app/[appId]/advanced/actions.ts +++ b/src/app/project/app/[appId]/advanced/actions.ts @@ -1,24 +1,14 @@ 'use server' -import { appVolumeEditZodModel } from "@/shared/model/volume-edit.model"; -import { ServerActionResult, SuccessActionResult } from "@/shared/model/server-action-error-return.model"; +import { SuccessActionResult } from "@/shared/model/server-action-error-return.model"; import appService from "@/server/services/app.service"; -import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils"; -import { z } from "zod"; -import { ServiceException } from "@/shared/model/service.exception.model"; -import pvcService from "@/server/services/pvc.service"; -import { fileMountEditZodModel } from "@/shared/model/file-mount-edit.model"; -import { VolumeBackupEditModel, volumeBackupEditZodModel } from "@/shared/model/backup-volume-edit.model"; -import volumeBackupService from "@/server/services/volume-backup.service"; -import backupService from "@/server/services/standalone-services/backup.service"; -import { volumeUploadZodModel } from "@/shared/model/volume-upload.model"; -import restoreService from "@/server/services/restore.service"; +import { isAuthorizedWriteForApp, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils"; import { BasicAuthEditModel, basicAuthEditZodModel } from "@/shared/model/basic-auth-edit.model"; export const saveBasicAuth = async (prevState: any, inputData: BasicAuthEditModel) => saveFormAction(inputData, basicAuthEditZodModel, async (validatedData) => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(validatedData.appId); await appService.saveBasicAuth({ ...validatedData, @@ -30,7 +20,7 @@ export const saveBasicAuth = async (prevState: any, inputData: BasicAuthEditMode export const deleteBasicAuth = async (basicAuthId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(await appService.getBasicAuthById(basicAuthId).then(b => b.appId)); await appService.deleteBasicAuthById(basicAuthId); return new SuccessActionResult(undefined, 'Successfully deleted item'); }); diff --git a/src/app/project/app/[appId]/advanced/basic-auth.tsx b/src/app/project/app/[appId]/advanced/basic-auth.tsx index a8dad44..ad909b1 100644 --- a/src/app/project/app/[appId]/advanced/basic-auth.tsx +++ b/src/app/project/app/[appId]/advanced/basic-auth.tsx @@ -13,8 +13,9 @@ import BasicAuthEditDialog from "./basic-auth-edit-dialog"; import { deleteBasicAuth } from "./actions"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -export default function BasicAuth({ app }: { - app: AppExtendedModel +export default function BasicAuth({ app, readonly }: { + app: AppExtendedModel; + readonly: boolean; }) { const { openConfirmDialog: openDialog } = useConfirmDialog(); @@ -64,24 +65,24 @@ export default function BasicAuth({ app }: { - + {!readonly && - + } ))} - + {!readonly && - + } ; } \ No newline at end of file diff --git a/src/app/project/app/[appId]/app-action-buttons.tsx b/src/app/project/app/[appId]/app-action-buttons.tsx index f183e3d..fc5b58b 100644 --- a/src/app/project/app/[appId]/app-action-buttons.tsx +++ b/src/app/project/app/[appId]/app-action-buttons.tsx @@ -7,24 +7,29 @@ import { AppExtendedModel } from "@/shared/model/app-extended.model"; import { Toast } from "@/frontend/utils/toast.utils"; import AppStatus from "./app-status"; import { ExternalLink, Hammer, Pause, Play, Rocket } from "lucide-react"; -import { toast } from "sonner"; import { AppEventsDialog } from "./app-events-dialog"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { UserSession } from "@/shared/model/sim-session.model"; +import { RoleUtils } from "@/server/utils/role.utils"; export default function AppActionButtons({ - app + app, + session }: { app: AppExtendedModel; + session: UserSession; }) { + const hasWriteAccess = RoleUtils.sessionHasWriteAccessForApp(session, app.id); return
- - - - + {hasWriteAccess && <> + + + + } {app.appDomains.length > 0 && - + } ))} - + {!readonly && - + } ; diff --git a/src/app/project/app/[appId]/domains/ports-and-internal-hostnames.tsx b/src/app/project/app/[appId]/domains/ports-and-internal-hostnames.tsx index e144920..e843198 100644 --- a/src/app/project/app/[appId]/domains/ports-and-internal-hostnames.tsx +++ b/src/app/project/app/[appId]/domains/ports-and-internal-hostnames.tsx @@ -15,8 +15,9 @@ import { EditIcon, Plus, TrashIcon } from "lucide-react"; import { Toast } from "@/frontend/utils/toast.utils"; import { useConfirmDialog } from "@/frontend/states/zustand.states"; -export default function InternalHostnames({ app }: { - app: AppExtendedModel +export default function InternalHostnames({ app, readonly }: { + app: AppExtendedModel; + readonly: boolean; }) { const { openConfirmDialog: openDialog } = useConfirmDialog(); @@ -34,7 +35,6 @@ export default function InternalHostnames({ app }: { const internalUrl = KubeObjectNameUtils.toServiceName(app.id); - return <> @@ -42,38 +42,38 @@ export default function InternalHostnames({ app }: { If you want to connect other apps to this app, you have to configure the internal ports below. - - {app.appPorts.length} Ports - - - Port - Action - - - - {app.appPorts.map(port => ( - - - {port.port} - - - - - - - +
+ {app.appPorts.length} Ports + + + Port + Action - ))} - -
+ + + {app.appPorts.map(port => ( + + + {port.port} + + {!readonly && + + + + + } + + ))} + +
- + {!readonly && - + }
diff --git a/src/app/project/app/[appId]/environment/actions.ts b/src/app/project/app/[appId]/environment/actions.ts index 232f821..d3cfb31 100644 --- a/src/app/project/app/[appId]/environment/actions.ts +++ b/src/app/project/app/[appId]/environment/actions.ts @@ -2,12 +2,12 @@ import { AppEnvVariablesModel, appEnvVariablesZodModel } from "@/shared/model/env-edit.model"; import appService from "@/server/services/app.service"; -import { getAuthUserSession, saveFormAction } from "@/server/utils/action-wrapper.utils"; +import { getAuthUserSession, isAuthorizedWriteForApp, saveFormAction } from "@/server/utils/action-wrapper.utils"; export const saveEnvVariables = async (prevState: any, inputData: AppEnvVariablesModel, appId: string) => saveFormAction(inputData, appEnvVariablesZodModel, async (validatedData) => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); const existingApp = await appService.getById(appId); await appService.save({ ...existingApp, diff --git a/src/app/project/app/[appId]/environment/env-edit.tsx b/src/app/project/app/[appId]/environment/env-edit.tsx index 9c9b860..4210b27 100644 --- a/src/app/project/app/[appId]/environment/env-edit.tsx +++ b/src/app/project/app/[appId]/environment/env-edit.tsx @@ -16,12 +16,14 @@ import { Textarea } from "@/components/ui/textarea"; import { AppExtendedModel } from "@/shared/model/app-extended.model"; -export default function EnvEdit({ app }: { - app: AppExtendedModel +export default function EnvEdit({ app, readonly }: { + app: AppExtendedModel; + readonly: boolean; }) { const form = useForm({ resolver: zodResolver(appEnvVariablesZodModel), - defaultValues: app + defaultValues: app, + disabled: readonly, }); const [state, formAction] = useFormState((state: ServerActionResult, payload: AppEnvVariablesModel) => saveEnvVariables(state, payload, app.id), FormUtils.getInitialFormState()); @@ -63,9 +65,9 @@ export default function EnvEdit({ app }: { )} /> - + {!readonly && Save - + } diff --git a/src/app/project/app/[appId]/general/actions.ts b/src/app/project/app/[appId]/general/actions.ts index ca06c2b..ac8ea24 100644 --- a/src/app/project/app/[appId]/general/actions.ts +++ b/src/app/project/app/[appId]/general/actions.ts @@ -7,13 +7,13 @@ import { ErrorActionResult, ServerActionResult, SuccessActionResult } from "@/sh import { ServiceException } from "@/shared/model/service.exception.model"; import appService from "@/server/services/app.service"; import userService from "@/server/services/user.service"; -import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils"; +import { getAuthUserSession, isAuthorizedWriteForApp, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils"; export const saveGeneralAppSourceInfo = async (prevState: any, inputData: AppSourceInfoInputModel, appId: string) => { if (inputData.sourceType === 'GIT') { return saveFormAction(inputData, appSourceInfoGitZodModel, async (validatedData) => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); const existingApp = await appService.getById(appId); await appService.save({ ...existingApp, @@ -24,7 +24,7 @@ export const saveGeneralAppSourceInfo = async (prevState: any, inputData: AppSou }); } else if (inputData.sourceType === 'CONTAINER') { return saveFormAction(inputData, appSourceInfoContainerZodModel, async (validatedData) => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); const existingApp = await appService.getById(appId); await appService.save({ ...existingApp, @@ -43,7 +43,7 @@ export const saveGeneralAppRateLimits = async (prevState: any, inputData: AppRat if (validatedData.replicas < 1) { throw new ServiceException('Replica Count must be at least 1'); } - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); const extendedApp = await appService.getExtendedById(appId); if (extendedApp.appVolumes.some(v => v.accessMode === 'ReadWriteOnce') && validatedData.replicas > 1) { diff --git a/src/app/project/app/[appId]/general/app-rate-limits.tsx b/src/app/project/app/[appId]/general/app-rate-limits.tsx index 71ca0c1..3faccbf 100644 --- a/src/app/project/app/[appId]/general/app-rate-limits.tsx +++ b/src/app/project/app/[appId]/general/app-rate-limits.tsx @@ -21,12 +21,14 @@ import { AppExtendedModel } from "@/shared/model/app-extended.model"; import { cn } from "@/frontend/utils/utils"; -export default function GeneralAppRateLimits({ app }: { - app: AppExtendedModel +export default function GeneralAppRateLimits({ app, readonly }: { + app: AppExtendedModel; + readonly: boolean; }) { const form = useForm({ resolver: zodResolver(appRateLimitsZodModel), - defaultValues: app + defaultValues: app, + disabled: readonly }); const [state, formAction] = useFormState((state: ServerActionResult, payload: AppRateLimitsModel) => saveGeneralAppRateLimits(state, payload, app.id), FormUtils.getInitialFormState()); @@ -125,10 +127,10 @@ export default function GeneralAppRateLimits({ app }: { />
- + {!readonly && Save

{state?.message}

-
+
}
diff --git a/src/app/project/app/[appId]/general/app-source.tsx b/src/app/project/app/[appId]/general/app-source.tsx index ffe8ade..327d025 100644 --- a/src/app/project/app/[appId]/general/app-source.tsx +++ b/src/app/project/app/[appId]/general/app-source.tsx @@ -18,15 +18,17 @@ import { App } from "@prisma/client"; import { toast } from "sonner"; import { AppExtendedModel } from "@/shared/model/app-extended.model"; -export default function GeneralAppSource({ app }: { - app: AppExtendedModel +export default function GeneralAppSource({ app, readonly }: { + app: AppExtendedModel; + readonly: boolean; }) { const form = useForm({ resolver: zodResolver(appSourceInfoInputZodModel), defaultValues: { ...app, sourceType: app.sourceType as 'GIT' | 'CONTAINER' - } + }, + disabled: readonly, }); const [state, formAction] = useFormState((state: ServerActionResult, payload: AppSourceInfoInputModel) => saveGeneralAppSourceInfo(state, payload, app.id), FormUtils.getInitialFormState()); @@ -197,10 +199,10 @@ export default function GeneralAppSource({ app }: { - + {!readonly && Save

{state?.message}

-
+
} diff --git a/src/app/project/app/[appId]/layout.tsx b/src/app/project/app/[appId]/layout.tsx index 7698e4e..95c4ef5 100644 --- a/src/app/project/app/[appId]/layout.tsx +++ b/src/app/project/app/[appId]/layout.tsx @@ -1,5 +1,5 @@ import { Inter } from "next/font/google"; -import { getAuthUserSession } from "@/server/utils/action-wrapper.utils"; +import { isAuthorizedReadForApp } from "@/server/utils/action-wrapper.utils"; import appService from "@/server/services/app.service"; import { Breadcrumb, @@ -19,11 +19,11 @@ export default async function RootLayout({ children: React.ReactNode; }>) { - await getAuthUserSession(); const appId = params?.appId; if (!appId) { return

Could not find app with id {appId}

} + const session = await isAuthorizedReadForApp(appId); const app = await appService.getExtendedById(appId); return ( @@ -32,7 +32,7 @@ export default async function RootLayout({ title={app.name} subtitle={`App ID: ${app.id}`}> - + {children} ); diff --git a/src/app/project/app/[appId]/overview/actions.ts b/src/app/project/app/[appId]/overview/actions.ts index 5049e3c..5371c20 100644 --- a/src/app/project/app/[appId]/overview/actions.ts +++ b/src/app/project/app/[appId]/overview/actions.ts @@ -8,55 +8,54 @@ import buildService from "@/server/services/build.service"; import deploymentService from "@/server/services/deployment.service"; import monitoringService from "@/server/services/monitoring.service"; import podService from "@/server/services/pod.service"; -import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils"; +import { isAuthorizedReadForApp, isAuthorizedWriteForApp, simpleAction } from "@/server/utils/action-wrapper.utils"; import { PodsResourceInfoModel } from "@/shared/model/pods-resource-info.model"; import appLogsService from "@/server/services/standalone-services/app-logs.service"; import { DownloadableAppLogsModel } from "@/shared/model/downloadable-app-logs.model"; import { ServiceException } from "@/shared/model/service.exception.model"; - export const getDeploymentsAndBuildsForApp = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedReadForApp(appId); const app = await appService.getExtendedById(appId); return await deploymentService.getDeploymentHistory(app.projectId, appId); }) as Promise>; export const deleteBuild = async (buildName: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(await buildService.getAppIdByBuildName(buildName)); await buildService.deleteBuild(buildName); return new SuccessActionResult(undefined, 'Successfully stopped and deleted build.'); }) as Promise>; export const getPodsForApp = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedReadForApp(appId); const app = await appService.getExtendedById(appId); return await podService.getPodsForApp(app.projectId, appId); }) as Promise>; export const getRessourceDataApp = async (projectId: string, appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedReadForApp(appId); return await monitoringService.getMonitoringForApp(projectId, appId); }) as Promise>; export const createNewWebhookUrl = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(appId); await appService.regenerateWebhookId(appId); }); export const getDownloadableLogs = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedReadForApp(appId); return new SuccessActionResult(await appLogsService.getAvailableLogsForApp(appId)); }) as Promise>; export const exportLogsToFileForToday = async (appId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedReadForApp(appId); const result = await appLogsService.writeAppLogsToDiskForApp(appId); if (!result) { throw new ServiceException('There are no logs available for today.'); diff --git a/src/app/project/app/[appId]/overview/deployments.tsx b/src/app/project/app/[appId]/overview/deployments.tsx index 2fad0c2..f412a4e 100644 --- a/src/app/project/app/[appId]/overview/deployments.tsx +++ b/src/app/project/app/[appId]/overview/deployments.tsx @@ -12,11 +12,14 @@ import { DeploymentInfoModel } from "@/shared/model/deployment-info.model"; import DeploymentStatusBadge from "./deployment-status-badge"; import { BuildLogsDialog } from "./build-logs-overlay"; import ShortCommitHash from "@/components/custom/short-commit-hash"; +import { RolePermissionEnum } from "@/shared/model/role-extended.model.ts"; export default function BuildsTab({ - app + app, + role }: { app: AppExtendedModel; + role: RolePermissionEnum; }) { const { openConfirmDialog: openDialog } = useConfirmDialog(); @@ -89,7 +92,7 @@ export default function BuildsTab({
{item.deploymentId && } - {item.buildJobName && item.status === 'BUILDING' && } + {role === RolePermissionEnum.READWRITE && item.buildJobName && item.status === 'BUILDING' && }
}} diff --git a/src/app/project/app/[appId]/overview/logs.tsx b/src/app/project/app/[appId]/overview/logs.tsx index 09dbdbc..159e110 100644 --- a/src/app/project/app/[appId]/overview/logs.tsx +++ b/src/app/project/app/[appId]/overview/logs.tsx @@ -13,11 +13,14 @@ import { Download, Expand, Terminal } from "lucide-react"; import { TerminalDialog } from "./terminal-overlay"; import { LogsDownloadOverlay } from "./logs-download-overlay"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { RolePermissionEnum } from "@/shared/model/role-extended.model.ts"; export default function Logs({ - app + app, + role }: { app: AppExtendedModel; + role: RolePermissionEnum; }) { const [selectedPod, setSelectedPod] = useState(undefined); const [appPods, setAppPods] = useState(undefined); @@ -76,7 +79,7 @@ export default function Logs({ -
+ {role === RolePermissionEnum.READWRITE &&
Terminal -
+
}
diff --git a/src/app/project/app/[appId]/overview/webhook-deployment.tsx b/src/app/project/app/[appId]/overview/webhook-deployment.tsx index 26dd583..5169881 100644 --- a/src/app/project/app/[appId]/overview/webhook-deployment.tsx +++ b/src/app/project/app/[appId]/overview/webhook-deployment.tsx @@ -7,11 +7,14 @@ import { useConfirmDialog } from "@/frontend/states/zustand.states"; import { Toast } from "@/frontend/utils/toast.utils"; import { ClipboardCopy } from "lucide-react"; import { toast } from "sonner"; +import { RolePermissionEnum } from "@/shared/model/role-extended.model.ts"; export default function WebhookDeploymentInfo({ - app + app, + role }: { app: AppExtendedModel; + role: RolePermissionEnum; }) { const { openConfirmDialog } = useConfirmDialog(); const [webhookUrl, setWebhookUrl] = useState(undefined); @@ -52,7 +55,7 @@ export default function WebhookDeploymentInfo({ {webhookUrl && } - + {role === RolePermissionEnum.READWRITE && }
diff --git a/src/app/project/app/[appId]/page.tsx b/src/app/project/app/[appId]/page.tsx index a25ea76..e226bdf 100644 --- a/src/app/project/app/[appId]/page.tsx +++ b/src/app/project/app/[appId]/page.tsx @@ -1,9 +1,10 @@ -import { getAuthUserSession } from "@/server/utils/action-wrapper.utils"; +import { getAuthUserSession, isAuthorizedReadForApp } from "@/server/utils/action-wrapper.utils"; import appService from "@/server/services/app.service"; import AppTabs from "./app-tabs"; import AppBreadcrumbs from "./app-breadcrumbs"; import s3TargetService from "@/server/services/s3-target.service"; import volumeBackupService from "@/server/services/volume-backup.service"; +import { RoleUtils } from "@/server/utils/role.utils"; export default async function AppPage({ searchParams, @@ -12,11 +13,12 @@ export default async function AppPage({ searchParams?: { [key: string]: string | undefined }; params: { appId: string } }) { - await getAuthUserSession(); const appId = params?.appId; if (!appId) { return

Could not find app with id {appId}

} + const session = await isAuthorizedReadForApp(appId); + const role = RoleUtils.getRolePermissionForApp(session, appId); const [app, s3Targets, volumeBackups] = await Promise.all([ appService.getExtendedById(appId), s3TargetService.getAll(), @@ -25,6 +27,7 @@ export default async function AppPage({ return (<> simpleAction(async () => { - await getAuthUserSession(); + await validateVolumeWriteAuthorization(volumeId); const validatedData = volumeUploadZodModel.parse({ volumeId, file: '' @@ -39,7 +40,7 @@ export const restoreVolumeFromZip = async (prevState: any, inputData: FormData, export const saveVolume = async (prevState: any, inputData: z.infer) => saveFormAction(inputData, actionAppVolumeEditZodModel, async (validatedData) => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(validatedData.appId); const existingApp = await appService.getExtendedById(validatedData.appId); const existingVolume = validatedData.id ? await appService.getVolumeById(validatedData.id) : undefined; if (existingVolume && existingVolume.size > validatedData.size) { @@ -57,20 +58,20 @@ export const saveVolume = async (prevState: any, inputData: z.infer simpleAction(async () => { - await getAuthUserSession(); + await validateVolumeWriteAuthorization(volumeId); await appService.deleteVolumeById(volumeId); return new SuccessActionResult(undefined, 'Successfully deleted volume'); }); export const getPvcUsage = async (appId: string, projectId: string) => simpleAction(async () => { - await getAuthUserSession(); + await isAuthorizedReadForApp(appId); return monitoringService.getPvcUsageFromApp(appId, projectId); }) as Promise>; export const downloadPvcData = async (volumeId: string) => simpleAction(async () => { - await getAuthUserSession(); + await validateVolumeReadAuthorization(volumeId); const fileNameOfDownloadedFile = await pvcService.downloadPvcData(volumeId); return new SuccessActionResult(fileNameOfDownloadedFile, 'Successfully zipped volume data'); // returns the download path on the server }) as Promise>; @@ -82,7 +83,7 @@ const actionAppFileMountEditZodModel = fileMountEditZodModel.merge(z.object({ export const saveFileMount = async (prevState: any, inputData: z.infer) => saveFormAction(inputData, actionAppFileMountEditZodModel, async (validatedData) => { - await getAuthUserSession(); + await isAuthorizedWriteForApp(validatedData.appId); await appService.saveFileMount({ ...validatedData, id: validatedData.id ?? undefined, @@ -91,14 +92,14 @@ export const saveFileMount = async (prevState: any, inputData: z.infer simpleAction(async () => { - await getAuthUserSession(); + await validateFileMountWriteAuthorization(fileMountId); await appService.deleteFileMountById(fileMountId); return new SuccessActionResult(undefined, 'Successfully deleted volume'); }); export const saveBackupVolume = async (prevState: any, inputData: VolumeBackupEditModel) => saveFormAction(inputData, volumeBackupEditZodModel, async (validatedData) => { - await getAuthUserSession(); + await validateVolumeWriteAuthorization(validatedData.volumeId); if (validatedData.retention < 1) { throw new ServiceException('Retention must be at least 1'); } @@ -112,7 +113,7 @@ export const saveBackupVolume = async (prevState: any, inputData: VolumeBackupEd export const deleteBackupVolume = async (backupVolumeId: string) => simpleAction(async () => { - await getAuthUserSession(); + await validateBackupVolumeWriteAuthorization(backupVolumeId); await volumeBackupService.deleteById(backupVolumeId); await backupService.registerAllBackups(); return new SuccessActionResult(undefined, 'Successfully deleted backup schedule'); @@ -120,17 +121,69 @@ export const deleteBackupVolume = async (backupVolumeId: string) => export const runBackupVolumeSchedule = async (backupVolumeId: string) => simpleAction(async () => { - await getAuthUserSession(); + await validateBackupVolumeWriteAuthorization(backupVolumeId); await backupService.runBackupForVolume(backupVolumeId); return new SuccessActionResult(undefined, 'Backup created and uploaded successfully'); }); export const openFileBrowserForVolume = async (volumeId: string) => simpleAction(async () => { - await getAuthUserSession(); + await validateVolumeWriteAuthorization(volumeId); const fileBrowserDomain = await fileBrowserService.deployFileBrowserForVolume(volumeId); return new SuccessActionResult(fileBrowserDomain, 'File browser started successfully'); }) as Promise>; \ No newline at end of file + }>>; + +async function validateVolumeWriteAuthorization(volumeId: string) { + const volumeAppId = await dataAccess.client.appVolume.findFirstOrThrow({ + where: { + id: volumeId, + }, + select: { + appId: true, + } + }); + await isAuthorizedWriteForApp(volumeAppId?.appId); +} + +async function validateVolumeReadAuthorization(volumeId: string) { + const volumeAppId = await dataAccess.client.appVolume.findFirstOrThrow({ + where: { + id: volumeId, + }, + select: { + appId: true, + } + }); + await isAuthorizedReadForApp(volumeAppId?.appId); +} + +async function validateFileMountWriteAuthorization(fileMountId: string) { + const fileMountAppId = await dataAccess.client.appFileMount.findFirstOrThrow({ + where: { + id: fileMountId, + }, + select: { + appId: true, + } + }); + await isAuthorizedWriteForApp(fileMountAppId?.appId); +} + +async function validateBackupVolumeWriteAuthorization(backupVolumeId: string) { + const volumeAppId = await dataAccess.client.volumeBackup.findFirstOrThrow({ + where: { + id: backupVolumeId, + }, + select: { + volume: { + select: { + appId: true, + } + } + } + }); + await isAuthorizedWriteForApp(volumeAppId?.volume.appId); +} \ No newline at end of file diff --git a/src/app/project/app/[appId]/volumes/file-mount.tsx b/src/app/project/app/[appId]/volumes/file-mount.tsx index 734c9a2..1bd820f 100644 --- a/src/app/project/app/[appId]/volumes/file-mount.tsx +++ b/src/app/project/app/[appId]/volumes/file-mount.tsx @@ -14,8 +14,9 @@ import FileMountEditDialog from "./file-mount-edit-dialog"; type AppVolumeWithCapacity = (AppVolume & { capacity?: string }); -export default function FileMount({ app }: { - app: AppExtendedModel +export default function FileMount({ app, readonly }: { + app: AppExtendedModel; + readonly: boolean; }) { const { openConfirmDialog: openDialog } = useConfirmDialog(); @@ -50,24 +51,25 @@ export default function FileMount({ app }: { {app.appFileMounts.map(fileMount => ( {fileMount.containerMountPath} - + {!readonly && - + } ))} - + {!readonly && + } ; } \ No newline at end of file diff --git a/src/app/project/app/[appId]/volumes/storages.tsx b/src/app/project/app/[appId]/volumes/storages.tsx index 54f30c8..96de9e1 100644 --- a/src/app/project/app/[appId]/volumes/storages.tsx +++ b/src/app/project/app/[appId]/volumes/storages.tsx @@ -25,8 +25,9 @@ import { Progress } from "@/components/ui/progress"; type AppVolumeWithCapacity = (AppVolume & { usedBytes?: number; capacityBytes?: number; usedPercentage?: number }); -export default function StorageList({ app }: { - app: AppExtendedModel +export default function StorageList({ app, readonly }: { + app: AppExtendedModel; + readonly: boolean; }) { const [volumesWithStorage, setVolumesWithStorage] = React.useState(app.appVolumes); @@ -181,7 +182,7 @@ export default function StorageList({ app }: { - + {!readonly && + + +

Edit volume settings

+
+
+
+ - + -

Edit volume settings

+

Delete volume

- - - - - - - -

Delete volume

-
-
-
+ } ))} - + {!readonly && - + } ; } \ No newline at end of file diff --git a/src/app/project/app/[appId]/volumes/volume-backup.tsx b/src/app/project/app/[appId]/volumes/volume-backup.tsx index ba5fd1d..4985ab5 100644 --- a/src/app/project/app/[appId]/volumes/volume-backup.tsx +++ b/src/app/project/app/[appId]/volumes/volume-backup.tsx @@ -17,11 +17,13 @@ import { VolumeBackupExtendedModel } from "@/shared/model/volume-backup-extended export default function VolumeBackupList({ app, volumeBackups, - s3Targets + s3Targets, + readonly }: { app: AppExtendedModel, s3Targets: S3Target[], - volumeBackups: VolumeBackupExtendedModel[] + volumeBackups: VolumeBackupExtendedModel[]; + readonly: boolean; }) { const { openConfirmDialog: openDialog } = useConfirmDialog(); @@ -79,7 +81,7 @@ export default function VolumeBackupList({ {volumeBackup.retention} {volumeBackup.target.name} {formatDateTime(volumeBackup.createdAt)} - + {!readonly && @@ -90,17 +92,17 @@ export default function VolumeBackupList({ - + } ))} - + {!readonly && - + } ; } \ No newline at end of file diff --git a/src/app/projects/project-page.tsx b/src/app/projects/project-page.tsx index bba9c2a..9f3abd4 100644 --- a/src/app/projects/project-page.tsx +++ b/src/app/projects/project-page.tsx @@ -27,8 +27,6 @@ export default async function ProjectPage() { const relevantProjectsForUser = data.filter((project) => project.apps.some((app) => RoleUtils.sessionHasReadAccessForApp(session, app.id))); - - return (
diff --git a/src/server/services/app.service.ts b/src/server/services/app.service.ts index 303f0f4..351dad9 100644 --- a/src/server/services/app.service.ts +++ b/src/server/services/app.service.ts @@ -208,6 +208,14 @@ class AppService { return savedItem; } + async getDomainById(id: string) { + return await dataAccess.client.appDomain.findFirstOrThrow({ + where: { + id + } + }); + } + async deleteDomainById(id: string) { const existingDomain = await dataAccess.client.appDomain.findFirst({ where: { @@ -399,6 +407,14 @@ class AppService { return savedItem; } + async getPortById(portId: string) { + return await dataAccess.client.appPort.findFirstOrThrow({ + where: { + id: portId + } + }); + } + async deletePortById(id: string) { const existingPort = await dataAccess.client.appPort.findFirst({ where: { @@ -469,6 +485,14 @@ class AppService { } } + async getBasicAuthById(id: string) { + return await dataAccess.client.appBasicAuth.findFirstOrThrow({ + where: { + id + } + }); + } + async getAll() { const apps = await dataAccess.client.app.findMany({ orderBy: { diff --git a/src/server/services/build.service.ts b/src/server/services/build.service.ts index f2ef198..d7e0798 100644 --- a/src/server/services/build.service.ts +++ b/src/server/services/build.service.ts @@ -191,6 +191,23 @@ class BuildService { } } + async getBuildByName(buildName: string) { + const jobs = await k3s.batch.listNamespacedJob(BUILD_NAMESPACE); + return jobs.body.items.find((job) => job.metadata?.name === buildName); + } + + async getAppIdByBuildName(buildName: string) { + const job = await this.getBuildByName(buildName); + if (!job) { + throw new ServiceException(`No build found with name ${buildName}`); + } + const appId = job.metadata?.annotations?.[Constants.QS_ANNOTATION_APP_ID]; + if (!appId) { + throw new ServiceException(`No appId found for build ${buildName}`); + } + return appId; + } + async deleteBuild(buildName: string) { await k3s.batch.deleteNamespacedJob(buildName, BUILD_NAMESPACE); console.log(`Deleted build job ${buildName}`); diff --git a/src/server/utils/role.utils.ts b/src/server/utils/role.utils.ts index 4e27c00..14c1a2c 100644 --- a/src/server/utils/role.utils.ts +++ b/src/server/utils/role.utils.ts @@ -6,6 +6,16 @@ export class RoleUtils { return (session.permissions?.find(app => app.appId === appId)?.permission ?? null) as RolePermissionEnum | null; } + static sessionIsReadOnlyForApp(session: UserSession, appId: string) { + if (this.isAdmin(session)) { + return false; + } + const rolePermission = this.getRolePermissionForApp(session, appId); + const roleHasReadAccessForApp = rolePermission === RolePermissionEnum.READ; + const roleHasWriteAccessForApp = rolePermission === RolePermissionEnum.READWRITE; + return !!roleHasReadAccessForApp && !roleHasWriteAccessForApp; + } + static sessionHasReadAccessForApp(session: UserSession, appId: string) { if (this.isAdmin(session)) { return true;