Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e7f4882
feat: add PPM authenticated repos via Identity Federation
ian-flores Mar 2, 2026
6421971
Address review findings (job 691)
ian-flores Mar 2, 2026
4a1e85d
fix: regenerate Helm chart after OIDCAudience field addition
ian-flores Mar 2, 2026
6fa8672
fix: address PR review findings for PPM auth
ian-flores Mar 3, 2026
46d71de
feat: add full PPM authentication config field coverage
ian-flores Mar 3, 2026
7a13ad1
fix: regenerate deepcopy and client-go after config field additions
ian-flores Mar 3, 2026
0e6428f
fix: set RunAsUser for PPM auth containers and add default OIDC scope
ian-flores Mar 4, 2026
4736a69
fix: use wget+sed instead of curl+jq in token exchange script
ian-flores Mar 4, 2026
662afca
fix: serialize IdentityFederation to JSON for K8s round-trip
ian-flores Mar 4, 2026
9815e5d
fix: regenerate Helm chart after IdentityFederation field change
ian-flores Mar 4, 2026
538d07f
fix: serialize IdentityFederation Name field for K8s round-trip
ian-flores Mar 4, 2026
052e816
fix: use ClientSecretFile and set UsernameClaim for Identity Federation
ian-flores Mar 4, 2026
76e03dd
style: fix gofmt alignment in package manager OIDC config
ian-flores Mar 4, 2026
c1f4050
Address review findings (job 692)
ian-flores Mar 4, 2026
b7f2803
refactor: separate auth repos code into its own PR
ian-flores Mar 5, 2026
0e61489
style: fix gofmt alignment after removing IdentityFederation field
ian-flores Mar 6, 2026
a2aa3b8
Merge remote-tracking branch 'origin/main' into ppm-auth-repos
ian-flores Mar 9, 2026
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 @@ -84,6 +84,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 @@ -318,6 +323,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 @@ -355,6 +378,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 @@ -240,6 +240,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