Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/

# Claude Code cache
.claude/tsc-cache/
55 changes: 44 additions & 11 deletions api/core/v1beta1/package_manager_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import (
)

type PackageManagerConfig struct {
Server *PackageManagerServerConfig `json:"Server,omitempty"`
Http *PackageManagerHttpConfig `json:"Http,omitempty"`
Git *PackageManagerGitConfig `json:"Git,omitempty"`
Database *PackageManagerDatabaseConfig `json:"Database,omitempty"`
Postgres *PackageManagerPostgresConfig `json:"Postgres,omitempty"`
Storage *PackageManagerStorageConfig `json:"Storage,omitempty"`
S3Storage *PackageManagerS3StorageConfig `json:"S3Storage,omitempty"`
Metrics *PackageManagerMetricsConfig `json:"Metrics,omitempty"`
Repos *PackageManagerReposConfig `json:"Repos,omitempty"`
Cran *PackageManagerCRANConfig `json:"CRAN,omitempty"`
Debug *PackageManagerDebugConfig `json:"Debug,omitempty"`
Server *PackageManagerServerConfig `json:"Server,omitempty"`
Http *PackageManagerHttpConfig `json:"Http,omitempty"`
Git *PackageManagerGitConfig `json:"Git,omitempty"`
Database *PackageManagerDatabaseConfig `json:"Database,omitempty"`
Postgres *PackageManagerPostgresConfig `json:"Postgres,omitempty"`
Storage *PackageManagerStorageConfig `json:"Storage,omitempty"`
S3Storage *PackageManagerS3StorageConfig `json:"S3Storage,omitempty"`
Metrics *PackageManagerMetricsConfig `json:"Metrics,omitempty"`
Repos *PackageManagerReposConfig `json:"Repos,omitempty"`
Cran *PackageManagerCRANConfig `json:"CRAN,omitempty"`
Debug *PackageManagerDebugConfig `json:"Debug,omitempty"`
Authentication *PackageManagerAuthenticationConfig `json:"Authentication,omitempty"`
OpenIDConnect *PackageManagerOIDCConfig `json:"OpenIDConnect,omitempty"`

// AdditionalConfig allows appending arbitrary gcfg config content not covered by typed fields.
// The value is appended verbatim after the generated config. gcfg parsing naturally handles
Expand Down Expand Up @@ -176,6 +178,37 @@ type PackageManagerDebugConfig struct {
Log string `json:"Log,omitempty"`
}

type PackageManagerAuthenticationConfig struct {
APITokenAuth bool `json:"APITokenAuth,omitempty"`
DeviceAuthType string `json:"DeviceAuthType,omitempty"`
NewReposAuthByDefault bool `json:"NewReposAuthByDefault,omitempty"`
Lifetime string `json:"Lifetime,omitempty"`
Inactivity string `json:"Inactivity,omitempty"`
CookieSweepDuration string `json:"CookieSweepDuration,omitempty"`
}

type PackageManagerOIDCConfig struct {
ClientId string `json:"ClientId,omitempty"`
ClientSecret string `json:"ClientSecret,omitempty"`
ClientSecretFile string `json:"ClientSecretFile,omitempty"`
Issuer string `json:"Issuer,omitempty"`
RequireLogin bool `json:"RequireLogin,omitempty"`
Logging bool `json:"Logging,omitempty"`
Scope string `json:"Scope,omitempty"`
CustomScope string `json:"CustomScope,omitempty"`
NoAutoGroupsScope bool `json:"NoAutoGroupsScope,omitempty"`
GroupsClaim string `json:"GroupsClaim,omitempty"`
GroupsSeparator string `json:"GroupsSeparator,omitempty"`
RoleClaim string `json:"RoleClaim,omitempty"`
RolesSeparator string `json:"RolesSeparator,omitempty"`
UniqueIdClaim string `json:"UniqueIdClaim,omitempty"`
UsernameClaim string `json:"UsernameClaim,omitempty"`
TokenLifetime string `json:"TokenLifetime,omitempty"`
MaxAuthenticationAge string `json:"MaxAuthenticationAge,omitempty"`
DisablePKCE bool `json:"DisablePKCE,omitempty"`
EnableDevicePKCE bool `json:"EnableDevicePKCE,omitempty"`
}

// SSHKeyConfig defines SSH key configuration for Git authentication
type SSHKeyConfig struct {
// Name is a unique identifier for this SSH key configuration
Expand Down
78 changes: 78 additions & 0 deletions api/core/v1beta1/package_manager_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,81 @@ func TestPackageManagerConfig_AdditionalConfigEmpty(t *testing.T) {
require.Nil(t, err)
require.Contains(t, str, "Address = some-address.com")
}

func TestPackageManagerConfig_OpenIDConnect(t *testing.T) {
cfg := PackageManagerConfig{
OpenIDConnect: &PackageManagerOIDCConfig{
ClientId: "my-client-id",
ClientSecret: "/etc/rstudio-pm/oidc-client-secret",
Issuer: "https://login.example.com",
RequireLogin: true,
},
}
str, err := cfg.GenerateGcfg()
require.Nil(t, err)
require.Contains(t, str, "[OpenIDConnect]")
require.Contains(t, str, "ClientId = my-client-id")
require.Contains(t, str, "ClientSecret = /etc/rstudio-pm/oidc-client-secret")
require.Contains(t, str, "Issuer = https://login.example.com")
require.Contains(t, str, "RequireLogin = true")
}

func TestPackageManagerConfig_Authentication(t *testing.T) {
cfg := PackageManagerConfig{
Authentication: &PackageManagerAuthenticationConfig{
APITokenAuth: true,
DeviceAuthType: "oidc",
NewReposAuthByDefault: true,
Lifetime: "30d",
Inactivity: "12h",
CookieSweepDuration: "5m",
},
}
str, err := cfg.GenerateGcfg()
require.Nil(t, err)
require.Contains(t, str, "[Authentication]")
require.Contains(t, str, "APITokenAuth = true")
require.Contains(t, str, "DeviceAuthType = oidc")
require.Contains(t, str, "NewReposAuthByDefault = true")
require.Contains(t, str, "Lifetime = 30d")
require.Contains(t, str, "Inactivity = 12h")
require.Contains(t, str, "CookieSweepDuration = 5m")
}

func TestPackageManagerConfig_OIDCNewFields(t *testing.T) {
cfg := PackageManagerConfig{
OpenIDConnect: &PackageManagerOIDCConfig{
ClientId: "my-client",
ClientSecretFile: "/etc/rstudio-pm/oidc-secret",
Issuer: "https://auth.example.com",
Logging: true,
TokenLifetime: "1h",
DisablePKCE: true,
UniqueIdClaim: "sub",
UsernameClaim: "preferred_username",
MaxAuthenticationAge: "24h",
GroupsSeparator: ",",
RolesSeparator: ";",
CustomScope: "profile email groups",
NoAutoGroupsScope: true,
EnableDevicePKCE: true,
},
}
str, err := cfg.GenerateGcfg()
require.Nil(t, err)
require.Contains(t, str, "[OpenIDConnect]")
require.Contains(t, str, "ClientId = my-client")
require.Contains(t, str, "ClientSecretFile = /etc/rstudio-pm/oidc-secret")
require.Contains(t, str, "Issuer = https://auth.example.com")
require.Contains(t, str, "Logging = true")
require.Contains(t, str, "TokenLifetime = 1h")
require.Contains(t, str, "DisablePKCE = true")
require.Contains(t, str, "UniqueIdClaim = sub")
require.Contains(t, str, "UsernameClaim = preferred_username")
require.Contains(t, str, "MaxAuthenticationAge = 24h")
require.Contains(t, str, "GroupsSeparator = ,")
require.Contains(t, str, "RolesSeparator = ;")
require.Contains(t, str, "CustomScope = profile email groups")
require.Contains(t, str, "NoAutoGroupsScope = true")
require.Contains(t, str, "EnableDevicePKCE = true")
}
40 changes: 40 additions & 0 deletions api/core/v1beta1/packagemanager_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ type PackageManagerSpec struct {
// AzureFiles configures Azure Files integration for persistent storage
// +optional
AzureFiles *AzureFilesConfig `json:"azureFiles,omitempty"`

// OIDCClientSecretKey is the key name in the vault for the OIDC client secret.
// When set, the client secret will be mounted at /etc/rstudio-pm/oidc-client-secret
// +optional
OIDCClientSecretKey string `json:"oidcClientSecretKey,omitempty"`
}

// PackageManagerStatus defines the observed state of PackageManager
Expand Down Expand Up @@ -313,6 +318,24 @@ func (pm *PackageManager) CreateSecretVolumeFactory() *product.SecretVolumeFacto
}
}

// Add OIDC client secret volume mount if configured
if pm.Spec.OIDCClientSecretKey != "" {
vols["client-secret-volume"] = &product.VolumeDef{
Source: &v1.VolumeSource{
CSI: &v1.CSIVolumeSource{
Driver: "secrets-store.csi.k8s.io",
ReadOnly: ptr.To(true),
VolumeAttributes: map[string]string{
"secretProviderClass": pm.SecretProviderClassName(),
},
},
},
Mounts: []*product.VolumeMountDef{
{MountPath: "/etc/rstudio-pm/oidc-client-secret", SubPath: "oidc-client-secret", ReadOnly: true},
},
}
}

case product.SiteSecretKubernetes:
vols["key-volume"] = &product.VolumeDef{
Env: []v1.EnvVar{
Expand Down Expand Up @@ -350,6 +373,23 @@ func (pm *PackageManager) CreateSecretVolumeFactory() *product.SecretVolumeFacto
},
}

// Add OIDC client secret volume mount if configured
if pm.Spec.OIDCClientSecretKey != "" {
vols["client-secret-volume"] = &product.VolumeDef{
Source: &v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: pm.GetSecretVaultName(),
Items: []v1.KeyToPath{
{Key: pm.Spec.OIDCClientSecretKey, Path: "oidc-client-secret"},
},
},
},
Mounts: []*product.VolumeMountDef{
{MountPath: "/etc/rstudio-pm/oidc-client-secret", SubPath: "oidc-client-secret", ReadOnly: true},
},
}
}

default:
// uh oh... some other type of secret...?
}
Expand Down
8 changes: 8 additions & 0 deletions api/core/v1beta1/site_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ type InternalPackageManagerSpec struct {
// AdditionalConfig allows appending arbitrary gcfg config content to the generated config.
// +optional
AdditionalConfig string `json:"additionalConfig,omitempty"`

// Auth configures OIDC authentication for Package Manager's web UI
// +optional
Auth *AuthSpec `json:"auth,omitempty"`

// OIDCClientSecretKey is the key in the vault for the OIDC client secret
// +optional
OIDCClientSecretKey string `json:"oidcClientSecretKey,omitempty"`
}

type InternalConnectSpec struct {
Expand Down
45 changes: 45 additions & 0 deletions api/core/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading