diff --git a/Makefile b/Makefile index 185ea828..c52e4c0a 100644 --- a/Makefile +++ b/Makefile @@ -181,6 +181,10 @@ helm-generate: manifests kubebuilder ## Regenerate Helm chart from kustomize $(SED) -i 's/team-operator-controller-manager-metrics-service/{{ .Values.controllerManager.serviceAccountName }}-metrics-service/g' dist/chart/templates/metrics/metrics-service.yaml # Fix RoleBinding namespace to use watchNamespace value $(SED) -i '/kind: RoleBinding/,/roleRef:/{s/namespace: posit-team/namespace: {{ .Values.watchNamespace }}/}' dist/chart/templates/rbac/role_binding.yaml + # Remove duplicate metrics service that kubebuilder generates - we already have one in dist/chart/templates/metrics/ + # This was causing "services 'team-operator-controller-manager-metrics-service' already exists" errors + # The correct metrics service is gated on .Values.metrics.enable, not .Values.rbac.enable + rm -f dist/chart/templates/rbac/auth_proxy_service.yaml # Remove kubebuilder-generated test workflow - we use our own CI workflows rm -f .github/workflows/test-chart.yml diff --git a/api/core/v1beta1/site_types.go b/api/core/v1beta1/site_types.go index c14e1cda..8f6e716b 100644 --- a/api/core/v1beta1/site_types.go +++ b/api/core/v1beta1/site_types.go @@ -389,6 +389,13 @@ type InternalWorkbenchSpec struct { // Workbench Auth/Login Landing Page Customization HTML AuthLoginPageHtml string `json:"authLoginPageHtml,omitempty"` + // AuditedJobs configures Workbench Audited Jobs for tracking execution details + // alongside job output, including digital signatures and environment data. + // Requires the Advanced product tier. + // See: https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html + // +optional + AuditedJobs *AuditedJobsConfig `json:"auditedJobs,omitempty"` + // JupyterConfig contains Jupyter configuration for Workbench JupyterConfig *WorkbenchJupyterConfig `json:"jupyterConfig,omitempty"` } diff --git a/api/core/v1beta1/workbench_config.go b/api/core/v1beta1/workbench_config.go index 9c794d02..90ada883 100644 --- a/api/core/v1beta1/workbench_config.go +++ b/api/core/v1beta1/workbench_config.go @@ -901,6 +901,63 @@ type WorkbenchJupyterConfig struct { DefaultSessionContainerImage string `json:"default-session-container-image,omitempty"` } +// AuditedJobsConfig contains configuration for Workbench Audited Jobs. +// See: https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html +type AuditedJobsConfig struct { + // Enabled enables audited jobs support (0=disabled, 1=enabled) + // Maps to rserver.conf: audited-jobs + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + // +optional + Enabled *int `json:"enabled,omitempty"` + + // StoragePath sets the directory for audited job data + // Maps to rserver.conf: audited-jobs-storage-path + // +optional + StoragePath string `json:"storagePath,omitempty"` + + // PrivateKeyPath sets the path to the RSA private key for digital signatures + // Maps to rserver.conf: audited-jobs-private-key-path + // +optional + PrivateKeyPath string `json:"privateKeyPath,omitempty"` + + // PublicKeyPaths sets the path(s) to RSA public keys for signature verification + // Maps to rserver.conf: audited-jobs-public-key-paths + // +optional + PublicKeyPaths string `json:"publicKeyPaths,omitempty"` + + // LogLimit sets the maximum number of audit log entries + // Maps to rserver.conf: audited-jobs-log-limit + // +optional + LogLimit *int `json:"logLimit,omitempty"` + + // DeletionExpiry sets the number of days before completed audited jobs are deleted + // Maps to rserver.conf: audited-jobs-deletion-expiry + // +optional + DeletionExpiry *int `json:"deletionExpiry,omitempty"` + + // VanillaRequired requires --vanilla flag for R jobs (0=disabled, 1=enabled) + // Maps to rserver.conf: audited-jobs-vanilla-required + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + // +optional + VanillaRequired *int `json:"vanillaRequired,omitempty"` + + // DetailsEnvironment enables capturing environment information (0=disabled, 1=enabled) + // Maps to rserver.conf: audited-jobs-details-environment + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + // +optional + DetailsEnvironment *int `json:"detailsEnvironment,omitempty"` + + // DetailsUserDefined enables capturing user-defined data (0=disabled, 1=enabled) + // Maps to rserver.conf: audited-jobs-details-user-defined + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1 + // +optional + DetailsUserDefined *int `json:"detailsUserDefined,omitempty"` +} + type WorkbenchLoggingConfig struct { All *WorkbenchLoggingSection `json:"*,omitempty"` } @@ -974,6 +1031,17 @@ type WorkbenchRServerConfig struct { WorkbenchApiAdminEnabled int `json:"workbench-api-admin-enabled,omitempty"` WorkbenchApiSuperAdminEnabled int `json:"workbench-api-super-admin-enabled,omitempty"` ForceAdminUiEnabled int `json:"force-admin-ui-enabled,omitempty"` + // Audited Jobs Configuration + // See: https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html + AuditedJobs int `json:"audited-jobs,omitempty"` + AuditedJobsStoragePath string `json:"audited-jobs-storage-path,omitempty"` + AuditedJobsPrivateKeyPath string `json:"audited-jobs-private-key-path,omitempty"` + AuditedJobsPublicKeyPaths string `json:"audited-jobs-public-key-paths,omitempty"` + AuditedJobsLogLimit int `json:"audited-jobs-log-limit,omitempty"` + AuditedJobsDeletionExpiry int `json:"audited-jobs-deletion-expiry,omitempty"` + AuditedJobsVanillaRequired int `json:"audited-jobs-vanilla-required,omitempty"` + AuditedJobsDetailsEnvironment int `json:"audited-jobs-details-environment,omitempty"` + AuditedJobsDetailsUserDefined int `json:"audited-jobs-details-user-defined,omitempty"` } type WorkbenchLauncherConfig struct { diff --git a/api/core/v1beta1/workbench_config_test.go b/api/core/v1beta1/workbench_config_test.go index 3d3b801f..4fe83598 100644 --- a/api/core/v1beta1/workbench_config_test.go +++ b/api/core/v1beta1/workbench_config_test.go @@ -177,6 +177,61 @@ func TestWorkbenchConfig_GenerateConfigmap(t *testing.T) { require.Contains(t, res["logging.conf"], "[*]\nlog-level") } +func TestWorkbenchConfig_AuditedJobs(t *testing.T) { + wb := WorkbenchConfig{ + WorkbenchIniConfig: WorkbenchIniConfig{ + RServer: &WorkbenchRServerConfig{ + AuditedJobs: 1, + AuditedJobsStoragePath: "/mnt/shared-storage/audited-jobs", + AuditedJobsPrivateKeyPath: "/etc/rstudio/audited-jobs-private-key.pem", + AuditedJobsPublicKeyPaths: "/etc/rstudio/audited-jobs-public-key.pem", + AuditedJobsLogLimit: 5000, + AuditedJobsDeletionExpiry: 60, + AuditedJobsVanillaRequired: 1, + AuditedJobsDetailsEnvironment: 1, + AuditedJobsDetailsUserDefined: 0, + }, + }, + } + + res, err := wb.GenerateConfigmap() + require.Nil(t, err) + require.Contains(t, res["rserver.conf"], "audited-jobs=1\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-storage-path=/mnt/shared-storage/audited-jobs\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-private-key-path=/etc/rstudio/audited-jobs-private-key.pem\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-public-key-paths=/etc/rstudio/audited-jobs-public-key.pem\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-log-limit=5000\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-deletion-expiry=60\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-vanilla-required=1\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-details-environment=1\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-details-user-defined=0\n") +} + +func TestWorkbenchConfig_AuditedJobsPartial(t *testing.T) { + wb := WorkbenchConfig{ + WorkbenchIniConfig: WorkbenchIniConfig{ + RServer: &WorkbenchRServerConfig{ + AuditedJobs: 1, + AuditedJobsStoragePath: "/mnt/shared-storage/audited-jobs", + }, + }, + } + + res, err := wb.GenerateConfigmap() + require.Nil(t, err) + require.Contains(t, res["rserver.conf"], "audited-jobs=1\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-storage-path=/mnt/shared-storage/audited-jobs\n") + // Unset int fields render as zero (consistent with all other WorkbenchRServerConfig int fields) + require.Contains(t, res["rserver.conf"], "audited-jobs-log-limit=0\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-deletion-expiry=0\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-vanilla-required=0\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-details-environment=0\n") + require.Contains(t, res["rserver.conf"], "audited-jobs-details-user-defined=0\n") + // Unset string fields should be omitted + require.NotContains(t, res["rserver.conf"], "audited-jobs-private-key-path=") + require.NotContains(t, res["rserver.conf"], "audited-jobs-public-key-paths=") +} + func TestWorkbenchConfig_GenerateSupervisorConfigmap(t *testing.T) { wbc := WorkbenchConfig{ SupervisordIniConfig: SupervisordIniConfig{ diff --git a/api/core/v1beta1/zz_generated.deepcopy.go b/api/core/v1beta1/zz_generated.deepcopy.go index b9831e4b..34e00eb6 100644 --- a/api/core/v1beta1/zz_generated.deepcopy.go +++ b/api/core/v1beta1/zz_generated.deepcopy.go @@ -29,6 +29,51 @@ func (in *ApiSettingsConfig) DeepCopy() *ApiSettingsConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuditedJobsConfig) DeepCopyInto(out *AuditedJobsConfig) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(int) + **out = **in + } + if in.LogLimit != nil { + in, out := &in.LogLimit, &out.LogLimit + *out = new(int) + **out = **in + } + if in.DeletionExpiry != nil { + in, out := &in.DeletionExpiry, &out.DeletionExpiry + *out = new(int) + **out = **in + } + if in.VanillaRequired != nil { + in, out := &in.VanillaRequired, &out.VanillaRequired + *out = new(int) + **out = **in + } + if in.DetailsEnvironment != nil { + in, out := &in.DetailsEnvironment, &out.DetailsEnvironment + *out = new(int) + **out = **in + } + if in.DetailsUserDefined != nil { + in, out := &in.DetailsUserDefined, &out.DetailsUserDefined + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuditedJobsConfig. +func (in *AuditedJobsConfig) DeepCopy() *AuditedJobsConfig { + if in == nil { + return nil + } + out := new(AuditedJobsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthSpec) DeepCopyInto(out *AuthSpec) { *out = *in @@ -1384,6 +1429,11 @@ func (in *InternalWorkbenchSpec) DeepCopyInto(out *InternalWorkbenchSpec) { in.PositronSettings.DeepCopyInto(&out.PositronSettings) out.VSCodeSettings = in.VSCodeSettings out.ApiSettings = in.ApiSettings + if in.AuditedJobs != nil { + in, out := &in.AuditedJobs, &out.AuditedJobs + *out = new(AuditedJobsConfig) + (*in).DeepCopyInto(*out) + } if in.JupyterConfig != nil { in, out := &in.JupyterConfig, &out.JupyterConfig *out = new(WorkbenchJupyterConfig) diff --git a/client-go/applyconfiguration/core/v1beta1/auditedjobsconfig.go b/client-go/applyconfiguration/core/v1beta1/auditedjobsconfig.go new file mode 100644 index 00000000..cc101dc8 --- /dev/null +++ b/client-go/applyconfiguration/core/v1beta1/auditedjobsconfig.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2023-2026 Posit Software, PBC + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// AuditedJobsConfigApplyConfiguration represents a declarative configuration of the AuditedJobsConfig type for use +// with apply. +type AuditedJobsConfigApplyConfiguration struct { + Enabled *int `json:"enabled,omitempty"` + StoragePath *string `json:"storagePath,omitempty"` + PrivateKeyPath *string `json:"privateKeyPath,omitempty"` + PublicKeyPaths *string `json:"publicKeyPaths,omitempty"` + LogLimit *int `json:"logLimit,omitempty"` + DeletionExpiry *int `json:"deletionExpiry,omitempty"` + VanillaRequired *int `json:"vanillaRequired,omitempty"` + DetailsEnvironment *int `json:"detailsEnvironment,omitempty"` + DetailsUserDefined *int `json:"detailsUserDefined,omitempty"` +} + +// AuditedJobsConfigApplyConfiguration constructs a declarative configuration of the AuditedJobsConfig type for use with +// apply. +func AuditedJobsConfig() *AuditedJobsConfigApplyConfiguration { + return &AuditedJobsConfigApplyConfiguration{} +} + +// WithEnabled sets the Enabled field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Enabled field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithEnabled(value int) *AuditedJobsConfigApplyConfiguration { + b.Enabled = &value + return b +} + +// WithStoragePath sets the StoragePath field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the StoragePath field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithStoragePath(value string) *AuditedJobsConfigApplyConfiguration { + b.StoragePath = &value + return b +} + +// WithPrivateKeyPath sets the PrivateKeyPath field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PrivateKeyPath field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithPrivateKeyPath(value string) *AuditedJobsConfigApplyConfiguration { + b.PrivateKeyPath = &value + return b +} + +// WithPublicKeyPaths sets the PublicKeyPaths field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PublicKeyPaths field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithPublicKeyPaths(value string) *AuditedJobsConfigApplyConfiguration { + b.PublicKeyPaths = &value + return b +} + +// WithLogLimit sets the LogLimit field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LogLimit field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithLogLimit(value int) *AuditedJobsConfigApplyConfiguration { + b.LogLimit = &value + return b +} + +// WithDeletionExpiry sets the DeletionExpiry field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionExpiry field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithDeletionExpiry(value int) *AuditedJobsConfigApplyConfiguration { + b.DeletionExpiry = &value + return b +} + +// WithVanillaRequired sets the VanillaRequired field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the VanillaRequired field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithVanillaRequired(value int) *AuditedJobsConfigApplyConfiguration { + b.VanillaRequired = &value + return b +} + +// WithDetailsEnvironment sets the DetailsEnvironment field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DetailsEnvironment field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithDetailsEnvironment(value int) *AuditedJobsConfigApplyConfiguration { + b.DetailsEnvironment = &value + return b +} + +// WithDetailsUserDefined sets the DetailsUserDefined field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DetailsUserDefined field is set to the value of the last call. +func (b *AuditedJobsConfigApplyConfiguration) WithDetailsUserDefined(value int) *AuditedJobsConfigApplyConfiguration { + b.DetailsUserDefined = &value + return b +} diff --git a/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go b/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go index f6b305d8..027c94c9 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalworkbenchspec.go @@ -43,6 +43,7 @@ type InternalWorkbenchSpecApplyConfiguration struct { DomainPrefix *string `json:"domainPrefix,omitempty"` BaseDomain *string `json:"baseDomain,omitempty"` AuthLoginPageHtml *string `json:"authLoginPageHtml,omitempty"` + AuditedJobs *AuditedJobsConfigApplyConfiguration `json:"auditedJobs,omitempty"` JupyterConfig *WorkbenchJupyterConfigApplyConfiguration `json:"jupyterConfig,omitempty"` } @@ -322,6 +323,14 @@ func (b *InternalWorkbenchSpecApplyConfiguration) WithAuthLoginPageHtml(value st return b } +// WithAuditedJobs sets the AuditedJobs field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobs field is set to the value of the last call. +func (b *InternalWorkbenchSpecApplyConfiguration) WithAuditedJobs(value *AuditedJobsConfigApplyConfiguration) *InternalWorkbenchSpecApplyConfiguration { + b.AuditedJobs = value + return b +} + // WithJupyterConfig sets the JupyterConfig field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the JupyterConfig field is set to the value of the last call. diff --git a/client-go/applyconfiguration/core/v1beta1/workbenchrserverconfig.go b/client-go/applyconfiguration/core/v1beta1/workbenchrserverconfig.go index fb6ecef5..15878fe1 100644 --- a/client-go/applyconfiguration/core/v1beta1/workbenchrserverconfig.go +++ b/client-go/applyconfiguration/core/v1beta1/workbenchrserverconfig.go @@ -45,6 +45,15 @@ type WorkbenchRServerConfigApplyConfiguration struct { WorkbenchApiAdminEnabled *int `json:"workbench-api-admin-enabled,omitempty"` WorkbenchApiSuperAdminEnabled *int `json:"workbench-api-super-admin-enabled,omitempty"` ForceAdminUiEnabled *int `json:"force-admin-ui-enabled,omitempty"` + AuditedJobs *int `json:"audited-jobs,omitempty"` + AuditedJobsStoragePath *string `json:"audited-jobs-storage-path,omitempty"` + AuditedJobsPrivateKeyPath *string `json:"audited-jobs-private-key-path,omitempty"` + AuditedJobsPublicKeyPaths *string `json:"audited-jobs-public-key-paths,omitempty"` + AuditedJobsLogLimit *int `json:"audited-jobs-log-limit,omitempty"` + AuditedJobsDeletionExpiry *int `json:"audited-jobs-deletion-expiry,omitempty"` + AuditedJobsVanillaRequired *int `json:"audited-jobs-vanilla-required,omitempty"` + AuditedJobsDetailsEnvironment *int `json:"audited-jobs-details-environment,omitempty"` + AuditedJobsDetailsUserDefined *int `json:"audited-jobs-details-user-defined,omitempty"` } // WorkbenchRServerConfigApplyConfiguration constructs a declarative configuration of the WorkbenchRServerConfig type for use with @@ -350,3 +359,75 @@ func (b *WorkbenchRServerConfigApplyConfiguration) WithForceAdminUiEnabled(value b.ForceAdminUiEnabled = &value return b } + +// WithAuditedJobs sets the AuditedJobs field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobs field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobs(value int) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobs = &value + return b +} + +// WithAuditedJobsStoragePath sets the AuditedJobsStoragePath field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsStoragePath field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsStoragePath(value string) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsStoragePath = &value + return b +} + +// WithAuditedJobsPrivateKeyPath sets the AuditedJobsPrivateKeyPath field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsPrivateKeyPath field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsPrivateKeyPath(value string) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsPrivateKeyPath = &value + return b +} + +// WithAuditedJobsPublicKeyPaths sets the AuditedJobsPublicKeyPaths field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsPublicKeyPaths field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsPublicKeyPaths(value string) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsPublicKeyPaths = &value + return b +} + +// WithAuditedJobsLogLimit sets the AuditedJobsLogLimit field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsLogLimit field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsLogLimit(value int) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsLogLimit = &value + return b +} + +// WithAuditedJobsDeletionExpiry sets the AuditedJobsDeletionExpiry field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsDeletionExpiry field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsDeletionExpiry(value int) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsDeletionExpiry = &value + return b +} + +// WithAuditedJobsVanillaRequired sets the AuditedJobsVanillaRequired field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsVanillaRequired field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsVanillaRequired(value int) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsVanillaRequired = &value + return b +} + +// WithAuditedJobsDetailsEnvironment sets the AuditedJobsDetailsEnvironment field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsDetailsEnvironment field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsDetailsEnvironment(value int) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsDetailsEnvironment = &value + return b +} + +// WithAuditedJobsDetailsUserDefined sets the AuditedJobsDetailsUserDefined field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AuditedJobsDetailsUserDefined field is set to the value of the last call. +func (b *WorkbenchRServerConfigApplyConfiguration) WithAuditedJobsDetailsUserDefined(value int) *WorkbenchRServerConfigApplyConfiguration { + b.AuditedJobsDetailsUserDefined = &value + return b +} diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go index 22670ecf..9f3d735d 100644 --- a/client-go/applyconfiguration/utils.go +++ b/client-go/applyconfiguration/utils.go @@ -23,6 +23,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { // Group=core, Version=v1beta1 case v1beta1.SchemeGroupVersion.WithKind("ApiSettingsConfig"): return &corev1beta1.ApiSettingsConfigApplyConfiguration{} + case v1beta1.SchemeGroupVersion.WithKind("AuditedJobsConfig"): + return &corev1beta1.AuditedJobsConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("AuthSpec"): return &corev1beta1.AuthSpecApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("AzureFilesConfig"): diff --git a/config/crd/bases/core.posit.team_sites.yaml b/config/crd/bases/core.posit.team_sites.yaml index 701b84c9..75c9d775 100644 --- a/config/crd/bases/core.posit.team_sites.yaml +++ b/config/crd/bases/core.posit.team_sites.yaml @@ -899,6 +899,67 @@ spec: workbenchApiSuperAdminEnabled: type: integer type: object + auditedJobs: + description: |- + AuditedJobs configures Workbench Audited Jobs for tracking execution details + alongside job output, including digital signatures and environment data. + Requires the Advanced product tier. + See: https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html + properties: + deletionExpiry: + description: |- + DeletionExpiry sets the number of days before completed audited jobs are deleted + Maps to rserver.conf: audited-jobs-deletion-expiry + type: integer + detailsEnvironment: + description: |- + DetailsEnvironment enables capturing environment information (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs-details-environment + maximum: 1 + minimum: 0 + type: integer + detailsUserDefined: + description: |- + DetailsUserDefined enables capturing user-defined data (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs-details-user-defined + maximum: 1 + minimum: 0 + type: integer + enabled: + description: |- + Enabled enables audited jobs support (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs + maximum: 1 + minimum: 0 + type: integer + logLimit: + description: |- + LogLimit sets the maximum number of audit log entries + Maps to rserver.conf: audited-jobs-log-limit + type: integer + privateKeyPath: + description: |- + PrivateKeyPath sets the path to the RSA private key for digital signatures + Maps to rserver.conf: audited-jobs-private-key-path + type: string + publicKeyPaths: + description: |- + PublicKeyPaths sets the path(s) to RSA public keys for signature verification + Maps to rserver.conf: audited-jobs-public-key-paths + type: string + storagePath: + description: |- + StoragePath sets the directory for audited job data + Maps to rserver.conf: audited-jobs-storage-path + type: string + vanillaRequired: + description: |- + VanillaRequired requires --vanilla flag for R jobs (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs-vanilla-required + maximum: 1 + minimum: 0 + type: integer + type: object auth: properties: administratorRoleMapping: diff --git a/config/crd/bases/core.posit.team_workbenches.yaml b/config/crd/bases/core.posit.team_workbenches.yaml index 2d66295f..086f374b 100644 --- a/config/crd/bases/core.posit.team_workbenches.yaml +++ b/config/crd/bases/core.posit.team_workbenches.yaml @@ -367,6 +367,27 @@ spec: type: string admin-superuser-group: type: string + audited-jobs: + description: |- + Audited Jobs Configuration + See: https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html + type: integer + audited-jobs-deletion-expiry: + type: integer + audited-jobs-details-environment: + type: integer + audited-jobs-details-user-defined: + type: integer + audited-jobs-log-limit: + type: integer + audited-jobs-private-key-path: + type: string + audited-jobs-public-key-paths: + type: string + audited-jobs-storage-path: + type: string + audited-jobs-vanilla-required: + type: integer auth-openid: type: integer auth-openid-issuer: diff --git a/dist/chart/templates/crd/core.posit.team_sites.yaml b/dist/chart/templates/crd/core.posit.team_sites.yaml index d40af799..155291c7 100755 --- a/dist/chart/templates/crd/core.posit.team_sites.yaml +++ b/dist/chart/templates/crd/core.posit.team_sites.yaml @@ -920,6 +920,67 @@ spec: workbenchApiSuperAdminEnabled: type: integer type: object + auditedJobs: + description: |- + AuditedJobs configures Workbench Audited Jobs for tracking execution details + alongside job output, including digital signatures and environment data. + Requires the Advanced product tier. + See: https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html + properties: + deletionExpiry: + description: |- + DeletionExpiry sets the number of days before completed audited jobs are deleted + Maps to rserver.conf: audited-jobs-deletion-expiry + type: integer + detailsEnvironment: + description: |- + DetailsEnvironment enables capturing environment information (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs-details-environment + maximum: 1 + minimum: 0 + type: integer + detailsUserDefined: + description: |- + DetailsUserDefined enables capturing user-defined data (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs-details-user-defined + maximum: 1 + minimum: 0 + type: integer + enabled: + description: |- + Enabled enables audited jobs support (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs + maximum: 1 + minimum: 0 + type: integer + logLimit: + description: |- + LogLimit sets the maximum number of audit log entries + Maps to rserver.conf: audited-jobs-log-limit + type: integer + privateKeyPath: + description: |- + PrivateKeyPath sets the path to the RSA private key for digital signatures + Maps to rserver.conf: audited-jobs-private-key-path + type: string + publicKeyPaths: + description: |- + PublicKeyPaths sets the path(s) to RSA public keys for signature verification + Maps to rserver.conf: audited-jobs-public-key-paths + type: string + storagePath: + description: |- + StoragePath sets the directory for audited job data + Maps to rserver.conf: audited-jobs-storage-path + type: string + vanillaRequired: + description: |- + VanillaRequired requires --vanilla flag for R jobs (0=disabled, 1=enabled) + Maps to rserver.conf: audited-jobs-vanilla-required + maximum: 1 + minimum: 0 + type: integer + type: object auth: properties: administratorRoleMapping: diff --git a/dist/chart/templates/crd/core.posit.team_workbenches.yaml b/dist/chart/templates/crd/core.posit.team_workbenches.yaml index f2177734..b6151e34 100755 --- a/dist/chart/templates/crd/core.posit.team_workbenches.yaml +++ b/dist/chart/templates/crd/core.posit.team_workbenches.yaml @@ -388,6 +388,27 @@ spec: type: string admin-superuser-group: type: string + audited-jobs: + description: |- + Audited Jobs Configuration + See: https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html + type: integer + audited-jobs-deletion-expiry: + type: integer + audited-jobs-details-environment: + type: integer + audited-jobs-details-user-defined: + type: integer + audited-jobs-log-limit: + type: integer + audited-jobs-private-key-path: + type: string + audited-jobs-public-key-paths: + type: string + audited-jobs-storage-path: + type: string + audited-jobs-vanilla-required: + type: integer auth-openid: type: integer auth-openid-issuer: diff --git a/dist/chart/templates/rbac/auth_proxy_service.yaml b/dist/chart/templates/rbac/auth_proxy_service.yaml deleted file mode 100755 index b665f0dc..00000000 --- a/dist/chart/templates/rbac/auth_proxy_service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{- if .Values.rbac.enable }} -apiVersion: v1 -kind: Service -metadata: - labels: - {{- include "chart.labels" . | nindent 4 }} - name: {{ .Values.controllerManager.serviceAccountName }}-metrics-service - namespace: {{ .Release.Namespace }} -spec: - ports: - - name: https - port: 8443 - protocol: TCP - targetPort: https - selector: - control-plane: controller-manager -{{- end -}} diff --git a/docs/guides/workbench-configuration.md b/docs/guides/workbench-configuration.md index 9915e0e9..d9fab78c 100644 --- a/docs/guides/workbench-configuration.md +++ b/docs/guides/workbench-configuration.md @@ -20,11 +20,12 @@ When configured via a Site resource, Workbench: 3. [Off-Host Execution / Kubernetes Launcher](#off-host-execution--kubernetes-launcher) 4. [IDE Configuration](#ide-configuration) 5. [Data Integrations](#data-integrations) -6. [Session Customization](#session-customization) -7. [Non-Root Execution Mode](#non-root-execution-mode) -8. [Experimental Features](#experimental-features) -9. [Example Configurations](#example-configurations) -10. [Troubleshooting](#troubleshooting) +6. [Audited Jobs](#audited-jobs) +7. [Session Customization](#session-customization) +8. [Non-Root Execution Mode](#non-root-execution-mode) +9. [Experimental Features](#experimental-features) +10. [Example Configurations](#example-configurations) +11. [Troubleshooting](#troubleshooting) --- @@ -517,6 +518,88 @@ Schema = PUBLIC --- +## Audited Jobs + +Audited Jobs is an advanced feature in Posit Workbench that tracks execution details alongside job output, including digital signatures and environment data. This feature requires the Advanced product tier and the `pwb-supervisor` binary to be available in sessions. + +### Configuration + +Enable and configure Audited Jobs via the Site resource: + +```yaml +apiVersion: core.posit.team/v1beta1 +kind: Site +metadata: + name: my-site + namespace: posit-team +spec: + workbench: + auditedJobs: + # Enable audited jobs (0=disabled, 1=enabled) + enabled: 1 + + # Directory for storing audited job data + storagePath: "/mnt/shared-storage/audited-jobs" + + # RSA key paths for digital signatures + privateKeyPath: "/etc/rstudio/audited-jobs-private-key.pem" + publicKeyPaths: "/etc/rstudio/audited-jobs-public-key.pem" + + # Maximum number of audit log entries (default: unlimited) + logLimit: 5000 + + # Days before completed audited jobs are deleted (default: 30) + deletionExpiry: 60 + + # Require --vanilla flag for R jobs (0=disabled, 1=enabled) + vanillaRequired: 0 + + # Capture environment information (0=disabled, 1=enabled) + detailsEnvironment: 1 + + # Capture user-defined data (0=disabled, 1=enabled) + detailsUserDefined: 1 +``` + +### Key Management + +For production deployments, you'll need to manage RSA keys for digital signatures: + +1. **Generate RSA key pair** (if not already provided): + ```bash + openssl genrsa -out audited-jobs-private-key.pem 2048 + openssl rsa -in audited-jobs-private-key.pem -pubout -out audited-jobs-public-key.pem + ``` + +2. **Create Kubernetes Secret** with the keys: + ```bash + kubectl create secret generic audited-jobs-keys \ + --from-file=private-key.pem=audited-jobs-private-key.pem \ + --from-file=public-key.pem=audited-jobs-public-key.pem \ + -n posit-team + ``` + +3. **Mount keys into Workbench** via additional volumes: + ```yaml + spec: + workbench: + additionalVolumes: + - secretName: audited-jobs-keys + mountPath: /etc/rstudio + readOnly: true + ``` + +### Requirements + +- **Product Tier**: Advanced tier required for Audited Jobs feature +- **Binary**: `pwb-supervisor` must be available in the session image +- **Storage**: Shared storage recommended when `storagePath` is configured +- **Keys**: RSA keys required for digital signature functionality + +For more details, see the [Posit Workbench documentation on Audited Jobs](https://docs.posit.co/ide/server-pro/admin/auditing_and_monitoring/audited_workbench_jobs.html). + +--- + ## Session Customization ### Session Tolerations diff --git a/internal/controller/core/site_controller_workbench.go b/internal/controller/core/site_controller_workbench.go index e5ad5b3f..936c9111 100644 --- a/internal/controller/core/site_controller_workbench.go +++ b/internal/controller/core/site_controller_workbench.go @@ -397,6 +397,38 @@ func (r *SiteReconciler) reconcileWorkbench( targetWorkbench.Spec.Config.WorkbenchIniConfig.Jupyter = site.Spec.Workbench.JupyterConfig } + // Propagate audited jobs configuration + if site.Spec.Workbench.AuditedJobs != nil { + aj := site.Spec.Workbench.AuditedJobs + if aj.Enabled != nil { + targetWorkbench.Spec.Config.RServer.AuditedJobs = *aj.Enabled + } + if aj.StoragePath != "" { + targetWorkbench.Spec.Config.RServer.AuditedJobsStoragePath = aj.StoragePath + } + if aj.PrivateKeyPath != "" { + targetWorkbench.Spec.Config.RServer.AuditedJobsPrivateKeyPath = aj.PrivateKeyPath + } + if aj.PublicKeyPaths != "" { + targetWorkbench.Spec.Config.RServer.AuditedJobsPublicKeyPaths = aj.PublicKeyPaths + } + if aj.LogLimit != nil { + targetWorkbench.Spec.Config.RServer.AuditedJobsLogLimit = *aj.LogLimit + } + if aj.DeletionExpiry != nil { + targetWorkbench.Spec.Config.RServer.AuditedJobsDeletionExpiry = *aj.DeletionExpiry + } + if aj.VanillaRequired != nil { + targetWorkbench.Spec.Config.RServer.AuditedJobsVanillaRequired = *aj.VanillaRequired + } + if aj.DetailsEnvironment != nil { + targetWorkbench.Spec.Config.RServer.AuditedJobsDetailsEnvironment = *aj.DetailsEnvironment + } + if aj.DetailsUserDefined != nil { + targetWorkbench.Spec.Config.RServer.AuditedJobsDetailsUserDefined = *aj.DetailsUserDefined + } + } + // if landing/auth page is customized if site.Spec.Workbench.AuthLoginPageHtml != "" { targetWorkbench.Spec.AuthLoginPageHtml = site.Spec.Workbench.AuthLoginPageHtml diff --git a/internal/controller/core/site_test.go b/internal/controller/core/site_test.go index 4db8baf5..f95095fb 100644 --- a/internal/controller/core/site_test.go +++ b/internal/controller/core/site_test.go @@ -402,6 +402,79 @@ func TestSiteReconcileWithSharedDirectory(t *testing.T) { assert.Equal(t, resource.MustParse("10Gi"), pvc.Spec.Resources.Requests[corev1.ResourceStorage]) } +func TestSiteAuditedJobsConfiguration(t *testing.T) { + siteName := "audited-jobs-config-site" + siteNamespace := "posit-team" + + // Helper function to create int pointers + intPtr := func(i int) *int { + return &i + } + + site := defaultSite(siteName) + site.Spec.Workbench.AuditedJobs = &v1beta1.AuditedJobsConfig{ + Enabled: intPtr(1), + StoragePath: "/mnt/shared-storage/audited-jobs", + PrivateKeyPath: "/etc/rstudio/audited-jobs-private-key.pem", + PublicKeyPaths: "/etc/rstudio/audited-jobs-public-key.pem", + LogLimit: intPtr(5000), + DeletionExpiry: intPtr(60), + VanillaRequired: intPtr(1), + DetailsEnvironment: intPtr(1), + DetailsUserDefined: intPtr(0), + } + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.Nil(t, err) + + testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) + + // Verify that the Audited Jobs configuration was applied to the RServer config + assert.NotNil(t, testWorkbench.Spec.Config.RServer) + assert.Equal(t, 1, testWorkbench.Spec.Config.RServer.AuditedJobs) + assert.Equal(t, "/mnt/shared-storage/audited-jobs", testWorkbench.Spec.Config.RServer.AuditedJobsStoragePath) + assert.Equal(t, "/etc/rstudio/audited-jobs-private-key.pem", testWorkbench.Spec.Config.RServer.AuditedJobsPrivateKeyPath) + assert.Equal(t, "/etc/rstudio/audited-jobs-public-key.pem", testWorkbench.Spec.Config.RServer.AuditedJobsPublicKeyPaths) + assert.Equal(t, 5000, testWorkbench.Spec.Config.RServer.AuditedJobsLogLimit) + assert.Equal(t, 60, testWorkbench.Spec.Config.RServer.AuditedJobsDeletionExpiry) + assert.Equal(t, 1, testWorkbench.Spec.Config.RServer.AuditedJobsVanillaRequired) + assert.Equal(t, 1, testWorkbench.Spec.Config.RServer.AuditedJobsDetailsEnvironment) + assert.Equal(t, 0, testWorkbench.Spec.Config.RServer.AuditedJobsDetailsUserDefined) +} + +func TestSiteAuditedJobsPartialConfiguration(t *testing.T) { + siteName := "audited-jobs-partial-site" + siteNamespace := "posit-team" + + intPtr := func(i int) *int { + return &i + } + + site := defaultSite(siteName) + site.Spec.Workbench.AuditedJobs = &v1beta1.AuditedJobsConfig{ + Enabled: intPtr(1), + StoragePath: "/mnt/shared-storage/audited-jobs", + } + + cli, _, err := runFakeSiteReconciler(t, siteNamespace, siteName, site) + assert.Nil(t, err) + + testWorkbench := getWorkbench(t, cli, siteNamespace, siteName) + + assert.NotNil(t, testWorkbench.Spec.Config.RServer) + // Set fields should be propagated + assert.Equal(t, 1, testWorkbench.Spec.Config.RServer.AuditedJobs) + assert.Equal(t, "/mnt/shared-storage/audited-jobs", testWorkbench.Spec.Config.RServer.AuditedJobsStoragePath) + // Unset fields should remain at zero values + assert.Equal(t, "", testWorkbench.Spec.Config.RServer.AuditedJobsPrivateKeyPath) + assert.Equal(t, "", testWorkbench.Spec.Config.RServer.AuditedJobsPublicKeyPaths) + assert.Equal(t, 0, testWorkbench.Spec.Config.RServer.AuditedJobsLogLimit) + assert.Equal(t, 0, testWorkbench.Spec.Config.RServer.AuditedJobsDeletionExpiry) + assert.Equal(t, 0, testWorkbench.Spec.Config.RServer.AuditedJobsVanillaRequired) + assert.Equal(t, 0, testWorkbench.Spec.Config.RServer.AuditedJobsDetailsEnvironment) + assert.Equal(t, 0, testWorkbench.Spec.Config.RServer.AuditedJobsDetailsUserDefined) +} + func TestSiteJupyterConfiguration(t *testing.T) { siteName := "jupyter-config-site" siteNamespace := "posit-team"