Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/
20 changes: 0 additions & 20 deletions api/core/v1beta1/_assets/usr/local/bin/pwb-psql

This file was deleted.

16 changes: 16 additions & 0 deletions api/core/v1beta1/connect_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ type ConnectSpec struct {
// ChronicleSidecarProductApiKeyEnabled assumes the api key for this product has been added to a secret and
// injects the secret as an environment variable to the sidecar. **EXPERIMENTAL**
ChronicleSidecarProductApiKeyEnabled bool `json:"chronicleSidecarProductApiKeyEnabled,omitempty"`

// AuthenticatedRepos enables PPM authenticated repository access for Connect
// +optional
AuthenticatedRepos bool `json:"authenticatedRepos,omitempty"`

// PPMAuthImage specifies the container image for PPM auth init/sidecar containers
// +optional
PPMAuthImage string `json:"ppmAuthImage,omitempty"`

// PPMUrl specifies the PPM URL for authenticated repository access
// +optional
PPMUrl string `json:"ppmUrl,omitempty"`

// PPMAuthAudience is the audience claim for the projected SA token used in PPM Identity Federation
// +optional
PPMAuthAudience string `json:"ppmAuthAudience,omitempty"`
}

// TODO: Validation should require Volume definition for off-host-execution...
Expand Down
133 changes: 122 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,20 @@ 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"`
IdentityFederation []PackageManagerIdentityFederationConfig `json:"identityFederation,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 @@ -53,6 +56,11 @@ func (configStruct *PackageManagerConfig) GenerateGcfg() (string, error) {
continue
}

// Skip IdentityFederation - handled specially after the main loop
if fieldName == "IdentityFederation" {
continue
}

if fieldValue.IsNil() {
continue
}
Expand Down Expand Up @@ -107,6 +115,59 @@ func (configStruct *PackageManagerConfig) GenerateGcfg() (string, error) {
}
}

// Render named IdentityFederation sections (these use the gcfg named subsection syntax)
for _, idf := range configStruct.IdentityFederation {
if strings.ContainsAny(idf.Name, "\"]\n") {
return "", fmt.Errorf("invalid IdentityFederation name %q: must not contain '\"', ']', or newlines", idf.Name)
}
builder.WriteString(fmt.Sprintf("\n[IdentityFederation \"%s\"]\n", idf.Name))
if idf.Issuer != "" {
builder.WriteString("Issuer = " + idf.Issuer + "\n")
}
if idf.Logging {
builder.WriteString("Logging = true\n")
}
if idf.Audience != "" {
builder.WriteString("Audience = " + idf.Audience + "\n")
}
if idf.Subject != "" {
builder.WriteString("Subject = " + idf.Subject + "\n")
}
if idf.AuthorizedParty != "" {
builder.WriteString("AuthorizedParty = " + idf.AuthorizedParty + "\n")
}
if idf.Scope != "" {
builder.WriteString("Scope = " + idf.Scope + "\n")
}
if idf.CustomScope != "" {
builder.WriteString("CustomScope = " + idf.CustomScope + "\n")
}
if idf.NoAutoGroupsScope {
builder.WriteString("NoAutoGroupsScope = true\n")
}
if idf.GroupsClaim != "" {
builder.WriteString("GroupsClaim = " + idf.GroupsClaim + "\n")
}
if idf.GroupsSeparator != "" {
builder.WriteString("GroupsSeparator = " + idf.GroupsSeparator + "\n")
}
if idf.RoleClaim != "" {
builder.WriteString("RoleClaim = " + idf.RoleClaim + "\n")
}
if idf.RolesSeparator != "" {
builder.WriteString("RolesSeparator = " + idf.RolesSeparator + "\n")
}
if idf.UniqueIdClaim != "" {
builder.WriteString("UniqueIdClaim = " + idf.UniqueIdClaim + "\n")
}
if idf.UsernameClaim != "" {
builder.WriteString("UsernameClaim = " + idf.UsernameClaim + "\n")
}
if idf.TokenLifetime != "" {
builder.WriteString("TokenLifetime = " + idf.TokenLifetime + "\n")
}
}

if configStruct.AdditionalConfig != "" {
builder.WriteString(configStruct.AdditionalConfig)
}
Expand Down Expand Up @@ -176,6 +237,56 @@ 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"`
}

type PackageManagerIdentityFederationConfig struct {
Name string `json:"name,omitempty"`
Issuer string `json:"Issuer,omitempty"`
Logging bool `json:"Logging,omitempty"`
Audience string `json:"Audience,omitempty"`
Subject string `json:"Subject,omitempty"`
AuthorizedParty string `json:"AuthorizedParty,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"`
}

// SSHKeyConfig defines SSH key configuration for Git authentication
type SSHKeyConfig struct {
// Name is a unique identifier for this SSH key configuration
Expand Down
174 changes: 174 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,177 @@ 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_IdentityFederation(t *testing.T) {
cfg := PackageManagerConfig{
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: "connect",
Issuer: "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE",
Audience: "sts.amazonaws.com",
Subject: "system:serviceaccount:posit-team:mysite-connect",
Scope: "repos:read:*",
},
{
Name: "workbench",
Issuer: "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE",
Audience: "sts.amazonaws.com",
Subject: "system:serviceaccount:posit-team:mysite-workbench",
Scope: "repos:read:*",
},
},
}
str, err := cfg.GenerateGcfg()
require.Nil(t, err)
require.Contains(t, str, `[IdentityFederation "connect"]`)
require.Contains(t, str, `[IdentityFederation "workbench"]`)
require.Contains(t, str, "Issuer = https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE")
require.Contains(t, str, "Audience = sts.amazonaws.com")
require.Contains(t, str, "Subject = system:serviceaccount:posit-team:mysite-connect")
require.Contains(t, str, "Scope = repos:read:*")
}

func TestPackageManagerConfig_OpenIDConnectAndIdentityFederation(t *testing.T) {
cfg := PackageManagerConfig{
Server: &PackageManagerServerConfig{
Address: "https://packagemanager.example.com",
},
OpenIDConnect: &PackageManagerOIDCConfig{
ClientId: "ppm-client",
ClientSecret: "/etc/rstudio-pm/oidc-client-secret",
Issuer: "https://login.example.com",
RequireLogin: true,
},
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: "connect",
Issuer: "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE",
Audience: "sts.amazonaws.com",
Subject: "system:serviceaccount:posit-team:.*-connect",
Scope: "repos:read:*",
TokenLifetime: "1h",
},
},
}
str, err := cfg.GenerateGcfg()
require.Nil(t, err)
require.Contains(t, str, "[Server]")
require.Contains(t, str, "[OpenIDConnect]")
require.Contains(t, str, `[IdentityFederation "connect"]`)
require.Contains(t, str, "TokenLifetime = 1h")
}

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")
}

func TestPackageManagerConfig_IdentityFederationNewFields(t *testing.T) {
cfg := PackageManagerConfig{
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: "my-idp",
Issuer: "https://issuer.example.com",
Logging: true,
Audience: "my-audience",
CustomScope: "read write",
NoAutoGroupsScope: true,
GroupsClaim: "groups",
GroupsSeparator: ",",
RoleClaim: "roles",
RolesSeparator: ";",
UniqueIdClaim: "sub",
UsernameClaim: "preferred_username",
TokenLifetime: "2h",
},
},
}
str, err := cfg.GenerateGcfg()
require.Nil(t, err)
require.Contains(t, str, `[IdentityFederation "my-idp"]`)
require.Contains(t, str, "Issuer = https://issuer.example.com")
require.Contains(t, str, "Logging = true")
require.Contains(t, str, "Audience = my-audience")
require.Contains(t, str, "CustomScope = read write")
require.Contains(t, str, "NoAutoGroupsScope = true")
require.Contains(t, str, "GroupsClaim = groups")
require.Contains(t, str, "GroupsSeparator = ,")
require.Contains(t, str, "RoleClaim = roles")
require.Contains(t, str, "RolesSeparator = ;")
require.Contains(t, str, "UniqueIdClaim = sub")
require.Contains(t, str, "UsernameClaim = preferred_username")
require.Contains(t, str, "TokenLifetime = 2h")
}
Loading