Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
d898d58
Address review findings (job 692)
ian-flores Mar 4, 2026
564684c
fix: address code review findings for PPM authenticated repos
ian-flores Mar 6, 2026
edfc3cb
Merge remote-tracking branch 'origin/main' into ppm-authenticated-repos
ian-flores Mar 13, 2026
0ce8989
Address review findings (job 694)
ian-flores Mar 13, 2026
83df62a
Address review findings (job 1007)
ian-flores Mar 13, 2026
a755af5
Address review findings (job 697)
ian-flores Mar 13, 2026
37eea46
Address review findings (job 698)
ian-flores Mar 13, 2026
d68f3fb
Address review findings (job 699)
ian-flores Mar 13, 2026
cb5357a
Address review findings (job 700)
ian-flores Mar 13, 2026
be47e6c
Address review findings (job 701)
ian-flores Mar 13, 2026
e5d82c8
Address review findings (job 1013)
ian-flores Mar 13, 2026
ad2223c
Address review findings (job 702)
ian-flores Mar 13, 2026
ce41961
Address review findings (job 1015)
ian-flores Mar 13, 2026
a20e17f
Address review findings (job 736)
ian-flores Mar 13, 2026
f5a1364
Address review findings (job 1017)
ian-flores Mar 13, 2026
cf1e3ae
Address review findings (job 1009)
ian-flores Mar 13, 2026
dc2cdda
Address review findings (job 1014)
ian-flores Mar 13, 2026
b934d1d
Address review findings (job 1020)
ian-flores Mar 13, 2026
1f0e95f
Address review findings (job 1021)
ian-flores Mar 13, 2026
db41194
Address review findings (job 1022)
ian-flores Mar 13, 2026
726e091
Address review findings (job 1011)
ian-flores Mar 13, 2026
624b643
Address review findings (job 1026)
ian-flores Mar 13, 2026
dae21dd
Address review findings (job 1027)
ian-flores Mar 13, 2026
ca34fd7
Address review findings (job 1029)
ian-flores Mar 13, 2026
cfd8eac
Address review findings (job 1031)
ian-flores Mar 13, 2026
928d5b0
Address review findings (job 1033)
ian-flores Mar 13, 2026
2d21160
Address review findings (job 1035)
ian-flores Mar 13, 2026
6b716f1
Address review findings (job 1036)
ian-flores Mar 13, 2026
d8a7abb
Address review findings (job 1038)
ian-flores Mar 13, 2026
5c0f275
Address review findings (job 693)
ian-flores Mar 13, 2026
b393acb
Address review findings (job 1012)
ian-flores Mar 13, 2026
c2b6fac
chore: regenerate PackageManager CRD manifests
ian-flores Mar 13, 2026
8102b3d
Address review findings (job 1039)
ian-flores Mar 13, 2026
f3ed439
Address review findings (job 1044)
ian-flores Mar 13, 2026
a479c8f
Address review findings (job 1045)
ian-flores Mar 13, 2026
b38b3ad
Address review findings (job 1047)
ian-flores Mar 13, 2026
8a1cb5b
chore: remove manual comment from generated client-go file
ian-flores Mar 13, 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
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
101 changes: 101 additions & 0 deletions api/core/v1beta1/package_manager_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ type PackageManagerConfig struct {
Debug *PackageManagerDebugConfig `json:"Debug,omitempty"`
Authentication *PackageManagerAuthenticationConfig `json:"Authentication,omitempty"`
OpenIDConnect *PackageManagerOIDCConfig `json:"OpenIDConnect,omitempty"`
// IdentityFederation was added after v1.20.0 and has never been released with a camelCase
// JSON tag, so the PascalCase key here is safe (no existing CRs to migrate).
IdentityFederation []PackageManagerIdentityFederationConfig `json:"IdentityFederation,omitempty"`

// AdditionalConfig allows appending arbitrary gcfg config content not covered by typed fields.
// Note: the JSON key is "additionalConfig" (camelCase) for backward compatibility with v1.20.0.
// The value is appended verbatim after the generated config. gcfg parsing naturally handles
// conflicts: list values are combined, scalar values use the last occurrence.
// +optional
Expand Down Expand Up @@ -55,6 +59,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 @@ -109,6 +118,79 @@ 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\r") {
return "", fmt.Errorf("invalid IdentityFederation name %q: must not contain '\"', ']', or newlines", idf.Name)
}
// Validate all string field values reject newlines to prevent gcfg injection
for _, kv := range []struct{ key, val string }{
{"Issuer", idf.Issuer},
{"Audience", idf.Audience},
{"Subject", idf.Subject},
{"AuthorizedParty", idf.AuthorizedParty},
{"Scope", idf.Scope},
{"CustomScope", idf.CustomScope},
{"GroupsClaim", idf.GroupsClaim},
{"GroupsSeparator", idf.GroupsSeparator},
{"RoleClaim", idf.RoleClaim},
{"RolesSeparator", idf.RolesSeparator},
{"UniqueIdClaim", idf.UniqueIdClaim},
{"UsernameClaim", idf.UsernameClaim},
{"TokenLifetime", idf.TokenLifetime},
} {
if strings.ContainsAny(kv.val, "\n\r") {
return "", fmt.Errorf("invalid IdentityFederation %q field %s: must not contain newlines", idf.Name, kv.key)
}
}
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 @@ -209,6 +291,25 @@ type PackageManagerOIDCConfig struct {
EnableDevicePKCE bool `json:"EnableDevicePKCE,omitempty"`
}

type PackageManagerIdentityFederationConfig struct {
Name string `json:"Name"`
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
182 changes: 182 additions & 0 deletions api/core/v1beta1/package_manager_config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v1beta1

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -81,6 +82,65 @@ func TestPackageManagerConfig_OpenIDConnect(t *testing.T) {
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{
Expand Down Expand Up @@ -140,3 +200,125 @@ func TestPackageManagerConfig_OIDCNewFields(t *testing.T) {
require.Contains(t, str, "NoAutoGroupsScope = true")
require.Contains(t, str, "EnableDevicePKCE = true")
}

func TestPackageManagerConfig_IdentityFederationJSONRoundTrip(t *testing.T) {
original := PackageManagerConfig{
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: "connect",
Issuer: "https://issuer.example.com",
Audience: "my-audience",
Subject: "system:serviceaccount:posit-team:mysite-connect",
Scope: "repos:read:*",
},
},
}

data, err := json.Marshal(original)
require.NoError(t, err)

var roundTripped PackageManagerConfig
err = json.Unmarshal(data, &roundTripped)
require.NoError(t, err)

require.Len(t, roundTripped.IdentityFederation, 1)
require.Equal(t, "connect", roundTripped.IdentityFederation[0].Name)
require.Equal(t, "https://issuer.example.com", roundTripped.IdentityFederation[0].Issuer)
require.Equal(t, "my-audience", roundTripped.IdentityFederation[0].Audience)
require.Equal(t, "system:serviceaccount:posit-team:mysite-connect", roundTripped.IdentityFederation[0].Subject)
require.Equal(t, "repos:read:*", roundTripped.IdentityFederation[0].Scope)
}

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

func TestPackageManagerConfig_IdentityFederationRejectsQuoteInName(t *testing.T) {
cfg := PackageManagerConfig{
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: `bad"name`,
Issuer: "https://issuer.example.com",
},
},
}
_, err := cfg.GenerateGcfg()
require.Error(t, err)
require.Contains(t, err.Error(), "must not contain")
}

func TestPackageManagerConfig_IdentityFederationRejectsBracketInName(t *testing.T) {
cfg := PackageManagerConfig{
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: `name"]`,
Issuer: "https://issuer.example.com",
},
},
}
_, err := cfg.GenerateGcfg()
require.Error(t, err)
require.Contains(t, err.Error(), "must not contain")
}

func TestPackageManagerConfig_IdentityFederationRejectsCarriageReturn(t *testing.T) {
cfg := PackageManagerConfig{
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: "bad\rname",
Issuer: "https://issuer.example.com",
},
},
}
_, err := cfg.GenerateGcfg()
require.Error(t, err)
require.Contains(t, err.Error(), "must not contain")
}

func TestPackageManagerConfig_IdentityFederationRejectsNewlineInValues(t *testing.T) {
cfg := PackageManagerConfig{
IdentityFederation: []PackageManagerIdentityFederationConfig{
{
Name: "valid-name",
Issuer: "https://evil.com\nBadKey = badvalue",
},
},
}
_, err := cfg.GenerateGcfg()
require.Error(t, err)
require.Contains(t, err.Error(), "must not contain newlines")
require.Contains(t, err.Error(), "Issuer")
}
Loading
Loading