diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 348fc6dc..032ddf32 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -407,7 +407,7 @@ "System integrity protection disabled": "System integrity protection disabled", "Prevents this workload from modifying critical host operating system files. We recommend keeping this enabled to maintain system integrity.": "Prevents this workload from modifying critical host operating system files. We recommend keeping this enabled to maintain system integrity.", "Rootless user identity": "Rootless user identity", - "By default, workloads run as the '{{ runAsUser }}' user. To specify a custom user identity, edit the application configuration via YAML or CLI.": "By default, workloads run as the '{{ runAsUser }}' user. To specify a custom user identity, edit the application configuration via YAML or CLI.", + "The recommended user identity is '{{ runAsUser }}'. To specify a custom user identity, edit the application configuration via YAML or CLI.": "The recommended user identity is '{{ runAsUser }}'. To specify a custom user identity, edit the application configuration via YAML or CLI.", "Application {{ appNum }}": "Application {{ appNum }}", "Application type": "Application type", "Select an application type": "Select an application type", diff --git a/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx b/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx index 2bc43d1c..6fd11de6 100644 --- a/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx +++ b/libs/ui-components/src/components/DetailsPage/Tables/ApplicationsTable.tsx @@ -6,7 +6,7 @@ import { DeviceApplicationStatus } from '@flightctl/types'; import { useTranslation } from '../../../hooks/useTranslation'; import ApplicationStatus from '../../Status/ApplicationStatus'; import { getAppTypeLabel } from '../../../utils/apps'; -import { RUN_AS_DEFAULT_USER } from '../../../types/deviceSpec'; +import { RUN_AS_ROOT_USER } from '../../../types/deviceSpec'; type ApplicationsTableProps = { appsStatus: DeviceApplicationStatus[]; @@ -45,7 +45,7 @@ const ApplicationsTable = ({ appsStatus }: ApplicationsTableProps) => { {app.appType ? : '-'} - {app.runAs || RUN_AS_DEFAULT_USER} + {app.runAs || RUN_AS_ROOT_USER} {app.embedded ? t('Yes') : t('No')} ); diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts b/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts index 79f3cd3a..039dccd7 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts @@ -38,7 +38,8 @@ import { InlineFileForm, KubeSecretTemplate, QuadletAppForm, - RUN_AS_DEFAULT_USER, + RUN_AS_FLIGHTCTL_USER, + RUN_AS_ROOT_USER, SingleContainerAppForm, SpecConfigTemplate, SystemdUnitFormValue, @@ -292,6 +293,15 @@ const haveValuesFilesChanged = (current: string[], updated: string[]): boolean = const haveHelmValuesChanged = (current: Record, updated: Record): boolean => JSON.stringify(current) !== JSON.stringify(updated); +const hasRunAsChanged = (current: string | undefined, updated: string | undefined): boolean => { + if (!current) { + // For empty "runAs", we mark the app as changed. + // This means that we'll set the field explicitly to the default user (currently "root"). + return true; + } + return current !== updated; +}; + // Single container apps always have an image, and it doesn't have an inline variant const hasContainerAppChanged = (current: ContainerApplication, updated: ContainerApplication): boolean => hasStringChanged(current.name, updated.name) || @@ -299,7 +309,7 @@ const hasContainerAppChanged = (current: ContainerApplication, updated: Containe havePortsChanged(current.ports || [], updated.ports || []) || haveResourceLimitsChanged(current.resources?.limits, updated.resources?.limits) || haveEnvVarsChanged(current.envVars || {}, updated.envVars || {}) || - hasStringChanged(current.runAs, updated.runAs, RUN_AS_DEFAULT_USER) || + hasRunAsChanged(current.runAs, updated.runAs) || haveVolumesChanged(current.volumes || [], updated.volumes || []); // Helm apps always have an image (chart), and it doesn't have an inline variant @@ -346,7 +356,7 @@ const hasQuadletAppChanged = ( if (baseChanged) { return true; } - return hasStringChanged(current.runAs, updated.runAs, RUN_AS_DEFAULT_USER); + return hasRunAsChanged(current.runAs, updated.runAs); }; const hasApplicationChanged = (current: ApplicationProviderSpec, updated: ApplicationProviderSpec): boolean => { @@ -452,7 +462,7 @@ const toApiContainerApp = (app: SingleContainerAppForm): ContainerApplication => name: app.name, image: app.image, appType: app.appType, - runAs: app.runAs || RUN_AS_DEFAULT_USER, + runAs: app.runAs || RUN_AS_ROOT_USER, envVars: variablesToEnvVars(app.variables || []), volumes: formVolumesToApi(app.volumes || [], AppType.AppTypeContainer), }; @@ -494,7 +504,7 @@ const toApiComposeApp = (app: ComposeAppForm): ComposeApplication => { // Quadlet apps are currently the same as Compose apps, plus an optional "runAs" field. const toApiQuadletApp = (app: QuadletAppForm): QuadletApplication => { const baseApp = toApiComposeApp(app); - return { ...baseApp, appType: AppType.AppTypeQuadlet, runAs: app.runAs || RUN_AS_DEFAULT_USER }; + return { ...baseApp, appType: AppType.AppTypeQuadlet, runAs: app.runAs || RUN_AS_ROOT_USER }; }; export const toApiApplication = (app: AppForm): ApplicationProviderSpec => { @@ -626,6 +636,15 @@ export const getApiConfig = (ct: SpecConfigTemplate): ConfigSourceProvider => { }; }; +const getRunAsUser = (app: ContainerApplication | QuadletApplication | undefined): string => { + if (app) { + // Existing apps that don't have a "runAs" user are actually running as the "root" user. + return app.runAs || RUN_AS_ROOT_USER; + } + // For new applications, we want to promote "flightctl" as the default user. + return RUN_AS_FLIGHTCTL_USER; +}; + const toContainerAppForm = (containerApp: ContainerApplication | undefined): SingleContainerAppForm => { const ports = containerApp?.ports?.map((portString) => { @@ -645,7 +664,7 @@ const toContainerAppForm = (containerApp: ContainerApplication | undefined): Sin ports, cpuLimit: limits?.cpu || '', memoryLimit: limits?.memory || '', - runAs: containerApp?.runAs || RUN_AS_DEFAULT_USER, + runAs: getRunAsUser(containerApp), }; }; @@ -690,10 +709,11 @@ const toComposeAppForm = (app: ComposeApplication | undefined): ComposeAppForm = const toQuadletAppForm = (app: QuadletApplication | undefined): QuadletAppForm => { const baseApp = toComposeAppForm(app); + return { ...baseApp, appType: AppType.AppTypeQuadlet, - runAs: app?.runAs || RUN_AS_DEFAULT_USER, + runAs: getRunAsUser(app), }; }; diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx index 2b61e1c6..f081908b 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationIntegritySettings.tsx @@ -5,7 +5,7 @@ import { Content, FormGroup, FormSection, Switch } from '@patternfly/react-core' import { useTranslation } from '../../../../hooks/useTranslation'; import TextField from '../../../form/TextField'; import { DefaultHelperText } from '../../../form/FieldHelperText'; -import { RUN_AS_DEFAULT_USER, RUN_AS_ROOT_USER } from '../../../../types/deviceSpec'; +import { RUN_AS_FLIGHTCTL_USER, RUN_AS_ROOT_USER } from '../../../../types/deviceSpec'; type ApplicationIntegritySettingsProps = { index: number; @@ -27,7 +27,7 @@ const ApplicationIntegritySettings = ({ index, isReadOnly }: ApplicationIntegrit label={isRootless ? t('System integrity protection enabled') : t('System integrity protection disabled')} isChecked={isRootless} onChange={async (_, checked) => { - await setRunAs(checked ? RUN_AS_DEFAULT_USER : RUN_AS_ROOT_USER); + await setRunAs(checked ? RUN_AS_FLIGHTCTL_USER : RUN_AS_ROOT_USER); }} isDisabled={isReadOnly} /> @@ -46,13 +46,13 @@ const ApplicationIntegritySettings = ({ index, isReadOnly }: ApplicationIntegrit diff --git a/libs/ui-components/src/types/deviceSpec.ts b/libs/ui-components/src/types/deviceSpec.ts index b85bb868..14bf48c5 100644 --- a/libs/ui-components/src/types/deviceSpec.ts +++ b/libs/ui-components/src/types/deviceSpec.ts @@ -17,8 +17,9 @@ import { import { FlightCtlLabel } from './extraTypes'; import { UpdateScheduleMode } from '../utils/time'; -export const RUN_AS_DEFAULT_USER = 'flightctl'; +// At the moment the "root" user is the default user when no user is specified. export const RUN_AS_ROOT_USER = 'root'; +export const RUN_AS_FLIGHTCTL_USER = 'flightctl'; export enum ConfigType { GIT = 'git',