Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
63a4ebb
feat: added idp resource creation
OlegErshov Dec 17, 2025
efb720e
fixed tests
OlegErshov Dec 17, 2025
255dc4f
feat: added workspace type patching
OlegErshov Dec 17, 2025
a2f013a
feat: removed secret waiting functionality
OlegErshov Dec 17, 2025
17ca9c5
feat: removed realm subroutine
OlegErshov Dec 17, 2025
5c9a8dd
feat: updated client id fetching
OlegErshov Dec 17, 2025
6ac8c7b
fix: improved timout and secret error handling
OlegErshov Dec 17, 2025
48d1840
fix: added waiting for finalizers functionality in integration tests
OlegErshov Dec 17, 2025
9e7ee49
chore: refactored idp resource creation
OlegErshov Dec 17, 2025
38baad0
chore: refactored client id fetching
OlegErshov Dec 17, 2025
d94f9be
fix: replaces http schema with https for logout uris
OlegErshov Dec 17, 2025
4e3e757
chore: refactored idp subroutine initialization
OlegErshov Dec 18, 2025
11d478c
chore: removed unused fields in config
OlegErshov Dec 18, 2025
e82eb27
chore: updated secret's naming in tests
OlegErshov Dec 18, 2025
00c6096
chore: addressed other coderabbit comments
OlegErshov Dec 18, 2025
a4e02dd
chore: increased timeout
OlegErshov Dec 18, 2025
8690c3b
Update internal/subroutine/idp.go
OlegErshov Dec 18, 2025
5755207
fix: used patch call instead of createOrUpdate
OlegErshov Dec 18, 2025
908dd14
fix: added equality check before patching workspace type
OlegErshov Dec 19, 2025
da02ebd
feat: simplified client id look up
OlegErshov Dec 22, 2025
2318d08
fix: updated tests
OlegErshov Dec 22, 2025
a8c5291
chore: added apiexport endpoint slice name config variable back
OlegErshov Dec 22, 2025
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
26 changes: 11 additions & 15 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ type Config struct {
KCP struct {
Kubeconfig string `mapstructure:"kcp-kubeconfig" default:"/api-kubeconfig/kubeconfig"`
} `mapstructure:",squash"`
APIExportEndpointSliceName string `mapstructure:"api-export-endpoint-slice-name"`
CoreModulePath string `mapstructure:"core-module-path"`
WorkspaceDir string `mapstructure:"workspace-dir" default:"/operator/"`
BaseDomain string `mapstructure:"base-domain" default:"portal.dev.local:8443"`
GroupClaim string `mapstructure:"group-claim" default:"groups"`
UserClaim string `mapstructure:"user-claim" default:"email"`
InitializerName string `mapstructure:"initializer-name" default:"root:security"`
DomainCALookup bool `mapstructure:"domain-ca-lookup" default:"false"`
SecretWaitingTimeoutInSeconds int `mapstructure:"secret-waiting-timeout-seconds" default:"60"`
IDP struct {
APIExportEndpointSliceName string `mapstructure:"api-export-endpoint-slice-name"`
CoreModulePath string `mapstructure:"core-module-path"`
BaseDomain string `mapstructure:"base-domain" default:"portal.dev.local:8443"`
GroupClaim string `mapstructure:"group-claim" default:"groups"`
UserClaim string `mapstructure:"user-claim" default:"email"`
InitializerName string `mapstructure:"initializer-name" default:"root:security"`
DomainCALookup bool `mapstructure:"domain-ca-lookup" default:"false"`
HttpClientTimeoutSeconds int `mapstructure:"http-client-timeout-seconds" default:"30"`
IDP struct {
// SMTP settings
SMTPServer string `mapstructure:"idp-smtp-server"`
SMTPPort int `mapstructure:"idp-smtp-port"`
Expand All @@ -34,11 +33,8 @@ type Config struct {
StartTLS bool `mapstructure:"idp-smtp-starttls" default:"false"`

// Auth settings
SMTPUser string `mapstructure:"idp-smtp-user"`
// secret name and key will be removed with idp creation subroutine
SMTPPasswordSecretName string `mapstructure:"idp-smtp-password-secret-name"`
SMTPPasswordSecretKey string `mapstructure:"idp-smtp-password-secret-key" default:"password"`
SMTPPassword string `mapstructure:"idp-smtp-password"`
SMTPUser string `mapstructure:"idp-smtp-user"`
SMTPPassword string `mapstructure:"idp-smtp-password"`

AdditionalRedirectURLs []string `mapstructure:"idp-additional-redirect-urls"`
} `mapstructure:",squash"`
Expand Down
7 changes: 4 additions & 3 deletions internal/controller/initializer_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ func NewLogicalClusterReconciler(log *logger.Logger, orgClient client.Client, cf
log: log,
mclifecycle: builder.NewBuilder("logicalcluster", "LogicalClusterReconciler", []lifecyclesubroutine.Subroutine{
subroutine.NewWorkspaceInitializer(orgClient, cfg, mgr),
subroutine.NewWorkspaceAuthConfigurationSubroutine(orgClient, inClusterClient, cfg),
subroutine.NewRealmSubroutine(inClusterClient, &cfg, cfg.BaseDomain),
subroutine.NewIDPSubroutine(orgClient, mgr, cfg),
subroutine.NewInviteSubroutine(orgClient, mgr),
subroutine.NewRemoveInitializer(mgr, cfg, inClusterClient),
subroutine.NewWorkspaceAuthConfigurationSubroutine(orgClient, inClusterClient, cfg),
subroutine.NewRemoveInitializer(mgr, cfg),
}, log).
WithReadOnly().
WithStaticThenExponentialRateLimiter().
BuildMultiCluster(mgr),
}
}
Expand Down
132 changes: 132 additions & 0 deletions internal/subroutine/idp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package subroutine

import (
"context"
"fmt"
"slices"
"strings"

kcpv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
accountv1alpha1 "github.com/platform-mesh/account-operator/api/v1alpha1"
"github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject"
lifecyclesubroutine "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine"
"github.com/rs/zerolog/log"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

"github.com/platform-mesh/golang-commons/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"

"github.com/platform-mesh/security-operator/api/v1alpha1"
"github.com/platform-mesh/security-operator/internal/config"
)

func NewIDPSubroutine(orgsClient client.Client, mgr mcmanager.Manager, cfg config.Config) *IDPSubroutine {
return &IDPSubroutine{
orgsClient: orgsClient,
mgr: mgr,
additionalRedirectURLs: cfg.IDP.AdditionalRedirectURLs,
baseDomain: cfg.BaseDomain,
}
}

var _ lifecyclesubroutine.Subroutine = &IDPSubroutine{}

type IDPSubroutine struct {
orgsClient client.Client
mgr mcmanager.Manager
additionalRedirectURLs []string
baseDomain string
}

func (w *IDPSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) {
return ctrl.Result{}, nil
}

func (w *IDPSubroutine) Finalizers(_ runtimeobject.RuntimeObject) []string {
return nil
}

func (w *IDPSubroutine) GetName() string { return "IDPSubroutine" }

func (w *IDPSubroutine) Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) {
lc := instance.(*kcpv1alpha1.LogicalCluster)

workspaceName := getWorkspaceName(lc)
if workspaceName == "" {
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get workspace name"), true, false)
}

cl, err := w.mgr.ClusterFromContext(ctx)
if err != nil {
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get cluster from context %w", err), true, true)
}

var account accountv1alpha1.Account
err = w.orgsClient.Get(ctx, types.NamespacedName{Name: workspaceName}, &account)
if err != nil {
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get account resource %w", err), true, true)
}

if account.Spec.Type != accountv1alpha1.AccountTypeOrg {
log.Debug().Str("workspace", workspaceName).Msg("account is not of type organization, skipping idp creation")
return ctrl.Result{}, nil
}

clientConfig := v1alpha1.IdentityProviderClientConfig{
ClientName: workspaceName,
ClientType: v1alpha1.IdentityProviderClientTypeConfidential,
RedirectURIs: append(w.additionalRedirectURLs, fmt.Sprintf("https://%s.%s/*", workspaceName, w.baseDomain)),
PostLogoutRedirectURIs: []string{fmt.Sprintf("https://%s.%s/logout*", workspaceName, w.baseDomain)},
SecretRef: corev1.SecretReference{
Name: fmt.Sprintf("portal-client-secret-%s-%s", workspaceName, workspaceName),
Namespace: "default",
},
}

idp := &v1alpha1.IdentityProviderConfiguration{ObjectMeta: metav1.ObjectMeta{Name: workspaceName}}
_, err = controllerutil.CreateOrUpdate(ctx, cl.GetClient(), idp, func() error {
clientIdx := slices.IndexFunc(idp.Spec.Clients, func(c v1alpha1.IdentityProviderClientConfig) bool {
return c.ClientName == clientConfig.ClientName
})
if clientIdx != -1 {
idp.Spec.Clients[clientIdx].RedirectURIs = clientConfig.RedirectURIs
idp.Spec.Clients[clientIdx].ClientType = clientConfig.ClientType
idp.Spec.Clients[clientIdx].SecretRef = clientConfig.SecretRef
idp.Spec.Clients[clientIdx].PostLogoutRedirectURIs = clientConfig.PostLogoutRedirectURIs
return nil
}

idp.Spec.Clients = append(idp.Spec.Clients, clientConfig)
return nil
})
if err != nil {
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to create idp resource %w", err), true, true)
}

log.Info().Str("workspace", workspaceName).Msg("idp configuration resource is created")

if err := cl.GetClient().Get(ctx, types.NamespacedName{Name: workspaceName}, idp); err != nil {
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get idp resource %w", err), true, true)
}
if !meta.IsStatusConditionTrue(idp.GetConditions(), "Ready") {
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("idp resource is not ready yet"), true, false)
}

log.Info().Str("workspace", workspaceName).Msg("idp resource is ready")
return ctrl.Result{}, nil
}

func getWorkspaceName(lc *kcpv1alpha1.LogicalCluster) string {
if path, ok := lc.Annotations["kcp.io/path"]; ok {
pathElements := strings.Split(path, ":")
return pathElements[len(pathElements)-1]
}
return ""
}
5 changes: 3 additions & 2 deletions internal/subroutine/idp/subroutine.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func New(ctx context.Context, cfg *config.Config, orgsClient client.Client, mgr
httpClient := cCfg.Client(ctx)

oidcClient := &http.Client{
Timeout: 30 * time.Second,
Timeout: time.Duration(cfg.HttpClientTimeoutSeconds) * time.Second,
}

return &subroutine{
Expand Down Expand Up @@ -165,6 +165,7 @@ func (s *subroutine) Process(ctx context.Context, instance runtimeobject.Runtime
log.Info().Str("secretName", managedClient.SecretRef.Name).Msg("Secret not found, client was likely deleted")
continue
}
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get registration access token from secret: %w", err), true, true)
}

if err := s.deleteClient(ctx, realmName, managedClient.ClientID, managedClient.RegistrationClientURI, registrationAccessToken); err != nil {
Expand Down Expand Up @@ -194,7 +195,7 @@ func (s *subroutine) Process(ctx context.Context, instance runtimeobject.Runtime
var clientInfo clientInfo
if clientID != "" {
registrationAccessToken, err := s.readRegistrationAccessTokenFromSecret(ctx, clientConfig.SecretRef)
if err != nil {
if err != nil && !kerrors.IsNotFound(err) {
return ctrl.Result{}, errors.NewOperatorError(fmt.Errorf("failed to get registration access token from secret: %w", err), true, true)
}

Expand Down
Loading