Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and configure these applications.
- [Configuration](#configuration)
- [Log mode values](#log-mode-values)
- [Use custom application namespace](#use-custom-application-namespace)
- [OIDC Authentication Configuration](#oidc-authentication-configuration)
- [Developer Guide](#developer-guide)
- [Pre-requisites](#pre-requisites)
- [Download manifests](#download-manifests)
Expand Down Expand Up @@ -117,6 +118,51 @@ To enable it:
- For cases in which ODH is already running in the cluster:
- WARNING: Be aware that switching to a different application namespace can cause issues that require manual intervention to be fixed, therefore we suggest this to be done for new clusters only.

#### OIDC Authentication Configuration

When using OIDC authentication mode (e.g., with external identity providers like Keycloak or Auth0), the operator creates a default Auth CR with placeholder values that **must be configured** by cluster administrators.

**Default Auth CR in OIDC mode:**
```yaml
apiVersion: services.platform.opendatahub.io/v1alpha1
kind: Auth
metadata:
name: auth
spec:
adminGroups:
- "REPLACE-WITH-OIDC-ADMIN-GROUP" # MUST be replaced with your OIDC admin group
allowedGroups:
- "system:authenticated"
```

**To configure Auth for OIDC:**

Replace the placeholder with your OIDC provider's group names:
```yaml
apiVersion: services.platform.opendatahub.io/v1alpha1
kind: Auth
metadata:
name: auth
spec:
adminGroups:
- "my-oidc-admin-group" # Your actual OIDC admin group from your provider
allowedGroups:
- "system:authenticated"
- "my-oidc-users-group" # Optional: add specific OIDC user groups
```

**Important Notes:**
- The groups must match the group names/claims from your OIDC provider (check your OIDC provider's configuration)
- `adminGroups` grants full administrative access to ODH resources via:
- ClusterRoleBinding: `admingroupcluster-rolebinding` and ClusterRole: `admingroupcluster-role`
- RoleBinding: `admingroup-rolebinding` and Role: `admingroup-role`
- `allowedGroups` grants read-only access to ODH resources via:
- ClusterRoleBinding: `allowedgroupcluster-rolebinding` and ClusterRole: `allowedgroupcluster-role`
- OpenShift Groups are NOT needed in OIDC mode (RBAC is managed via Kubernetes RoleBindings/ClusterRoleBindings)
- The groups are mapped from OIDC JWT tokens to Kubernetes RBAC automatically

For OAuth mode (OpenShift's integrated authentication), the Auth CR is created with platform-specific defaults and typically does not require modification.

## Developer Guide

#### Pre-requisites
Expand Down
28 changes: 21 additions & 7 deletions internal/controller/dscinitialization/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,38 @@ func (r *DSCInitializationReconciler) CreateAuth(ctx context.Context, platform c
return err
}
// Auth CR not found, create default Auth CR
if err := r.Client.Create(ctx, BuildDefaultAuth(platform)); err != nil && !k8serr.IsAlreadyExists(err) {
if err := r.Client.Create(ctx, BuildDefaultAuth(ctx, r.Client, platform)); err != nil && !k8serr.IsAlreadyExists(err) {
return err
}
return nil
}

// BuildDefaultAuth creates a default Auth custom resource with platform-specific configuration.
// For OAuth mode, uses platform-specific admin groups (odh-admins, rhods-admins, etc.).
// For OIDC mode, uses a placeholder that cluster admin must replace with actual OIDC group names.
//
// Parameters:
// - ctx: Context for the operation
// - cli: Kubernetes client to detect authentication mode
// - platform: The target platform type (OpenDataHub, SelfManagedRhoai, or ManagedRhoai)
//
// Returns:
// - client.Object: A serviceApi.Auth resource with platform-specific admin group and system:authenticated in allowed groups
func BuildDefaultAuth(platform common.Platform) client.Object {
// Get admin group for the platform, with fallback to OpenDataHub admin group
adminGroup := adminGroups[platform]
if adminGroup == "" {
adminGroup = adminGroups[cluster.OpenDataHub]
// - client.Object: A serviceApi.Auth resource with appropriate admin group and system:authenticated in allowed groups
func BuildDefaultAuth(ctx context.Context, cli client.Client, platform common.Platform) client.Object {
var adminGroup string

// Detect authentication mode
isOAuth, err := cluster.IsIntegratedOAuth(ctx, cli)

if err == nil && !isOAuth {
// OIDC mode: set an non-exist group that cluster admin should replace with actual OIDC group.
adminGroup = "REPLACE-WITH-OIDC-ADMIN-GROUP"
} else {
// OAuth mode: Use platform-specific admin group
adminGroup = adminGroups[platform]
if adminGroup == "" { // fallback to OpenDataHub admin group.
adminGroup = adminGroups[cluster.OpenDataHub]
}
}

return &serviceApi.Auth{
Expand Down
20 changes: 19 additions & 1 deletion internal/controller/dscinitialization/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ func TestBuildDefaultAuth(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
ctx := t.Context()

// Create fake client (will be Oauth mode by default)
cli, err := fakeclient.New()
g.Expect(err).ShouldNot(HaveOccurred())

authObj := dscinitialization.BuildDefaultAuth(tt.platform)
authObj := dscinitialization.BuildDefaultAuth(ctx, cli, tt.platform)
g.Expect(authObj).ShouldNot(BeNil(), "BuildDefaultAuth should not return nil")

auth, ok := authObj.(*serviceApi.Auth)
Expand All @@ -85,6 +90,19 @@ func TestBuildDefaultAuth(t *testing.T) {
g.Expect(auth.Spec.AllowedGroups[0]).Should(Equal("system:authenticated"))
})
}
t.Run("OIDC mode uses placeholder group", func(t *testing.T) {
g := NewWithT(t)
ctx := t.Context()
cli, err := fakeclient.New(fakeclient.WithClusterAuthType(cluster.AuthModeOIDC))
g.Expect(err).ShouldNot(HaveOccurred())
obj := dscinitialization.BuildDefaultAuth(ctx, cli, cluster.OpenDataHub)
auth, ok := obj.(*serviceApi.Auth)
g.Expect(ok).To(BeTrue())
g.Expect(auth.Spec.AdminGroups).To(HaveLen(1))
g.Expect(auth.Spec.AdminGroups[0]).To(Equal("REPLACE-WITH-OIDC-ADMIN-GROUP"))
// AllowedGroups should remain unchanged
g.Expect(auth.Spec.AllowedGroups).To(Equal([]string{"system:authenticated"}))
})
}

func TestCreateAuth(t *testing.T) {
Expand Down
27 changes: 0 additions & 27 deletions internal/controller/services/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,9 @@
package auth

import (
"context"

configv1 "github.com/openshift/api/config/v1"
k8serr "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"

serviceApi "github.com/opendatahub-io/opendatahub-operator/v2/api/services/v1alpha1"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk"
)

const (
ServiceName = serviceApi.AuthServiceName
)

// IsDefaultAuthMethod returns true if the default authentication method is IntegratedOAuth or empty.
// This will give indication that Operator should create userGroups or not in the cluster.
func IsDefaultAuthMethod(ctx context.Context, cli client.Client) (bool, error) {
authenticationobj := &configv1.Authentication{}
if err := cli.Get(ctx, client.ObjectKey{Name: cluster.ClusterAuthenticationObj, Namespace: ""}, authenticationobj); err != nil {
if meta.IsNoMatchError(err) { // when CRD is missing, convert error type
return false, k8serr.NewNotFound(schema.GroupResource{Group: gvk.Auth.Group}, cluster.ClusterAuthenticationObj)
}
return false, err
}

// for now, HPC support "" "None" "IntegratedOAuth"(default) "OIDC"
// other offering support "" "None" "IntegratedOAuth"(default)
// we only create userGroups for "IntegratedOAuth" or "" and leave other or new supported type value in the future
return authenticationobj.Spec.Type == configv1.AuthenticationTypeIntegratedOAuth || authenticationobj.Spec.Type == "", nil
}
4 changes: 2 additions & 2 deletions internal/controller/services/auth/auth_controller_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,12 @@ func addUserGroup(ctx context.Context, rr *odhtypes.ReconciliationRequest, userG
}

func createDefaultGroup(ctx context.Context, rr *odhtypes.ReconciliationRequest) error {
ok, err := IsDefaultAuthMethod(ctx, rr.Client)
ok, err := cluster.IsIntegratedOAuth(ctx, rr.Client)
if err != nil {
return err
}
if !ok {
logf.Log.Info("default auth method is not enabled")
logf.Log.Info("Integrated OAuth not detected; skipping default group creation")
return nil
}

Expand Down
125 changes: 0 additions & 125 deletions internal/controller/services/auth/auth_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,13 @@ package auth_test
import (
"testing"

configv1 "github.com/openshift/api/config/v1"
operatorv1 "github.com/openshift/api/operator/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/opendatahub-io/opendatahub-operator/v2/api/common"
dsciv2 "github.com/opendatahub-io/opendatahub-operator/v2/api/dscinitialization/v2"
serviceApi "github.com/opendatahub-io/opendatahub-operator/v2/api/services/v1alpha1"
"github.com/opendatahub-io/opendatahub-operator/v2/internal/controller/services/auth"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/utils/test/fakeclient"

. "github.com/onsi/gomega"
)
Expand Down Expand Up @@ -104,124 +100,3 @@ func TestServiceHandler_Init(t *testing.T) {
err := handler.Init(cluster.OpenDataHub)
g.Expect(err).ShouldNot(HaveOccurred())
}

// TestIsDefaultAuthMethod validates the OpenShift authentication method detection logic.
// This function determines whether the operator should create default admin groups
// based on the cluster's authentication configuration. The test ensures:
//
// 1. IntegratedOAuth is correctly identified as default (should create groups)
// 2. Empty auth type is correctly identified as default (should create groups)
// 3. Custom auth types are correctly identified as non-default (should not create groups)
// 4. Missing authentication objects are handled with proper errors
//
// Authentication Types and Behavior:
// - IntegratedOAuth (default): Create default admin groups
// - "" (empty, default): Create default admin groups
// - "None": Do not create default admin groups
// - Custom types: Do not create default admin groups
// - Missing object: Return error (cluster configuration issue)
//
// This is critical for security because it determines whether the operator will
// automatically create admin groups that could grant elevated access.
func TestIsDefaultAuthMethod(t *testing.T) {
ctx := t.Context()

tests := []struct {
name string
authObject *configv1.Authentication
expectError bool
expectedResult bool
description string
}{
{
name: "should return true for IntegratedOAuth",
authObject: &configv1.Authentication{
ObjectMeta: metav1.ObjectMeta{
Name: cluster.ClusterAuthenticationObj,
},
Spec: configv1.AuthenticationSpec{
Type: configv1.AuthenticationTypeIntegratedOAuth,
},
},
expectError: false,
expectedResult: true,
description: "IntegratedOAuth should be considered default auth method",
},
{
name: "should return true for empty type",
authObject: &configv1.Authentication{
ObjectMeta: metav1.ObjectMeta{
Name: cluster.ClusterAuthenticationObj,
},
Spec: configv1.AuthenticationSpec{
Type: "",
},
},
expectError: false,
expectedResult: true,
description: "Empty type should be considered default auth method",
},
{
name: "should return false for other auth types",
authObject: &configv1.Authentication{
ObjectMeta: metav1.ObjectMeta{
Name: cluster.ClusterAuthenticationObj,
},
Spec: configv1.AuthenticationSpec{
Type: "CustomAuth",
},
},
expectError: false,
expectedResult: false,
description: "Other auth types should not be considered default auth method",
},
{
name: "should return false for None",
authObject: &configv1.Authentication{
ObjectMeta: metav1.ObjectMeta{
Name: cluster.ClusterAuthenticationObj,
},
Spec: configv1.AuthenticationSpec{
Type: configv1.AuthenticationTypeNone,
},
},
expectError: false,
expectedResult: false,
description: "None should not be considered default auth method",
},
{
name: "should handle missing authentication object",
authObject: nil,
expectError: true,
expectedResult: false,
description: "Should return error when authentication object doesn't exist",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

var cli client.Client
var err error

if tt.authObject != nil {
cli, err = fakeclient.New(
fakeclient.WithObjects(tt.authObject),
)
} else {
cli, err = fakeclient.New()
}
g.Expect(err).ShouldNot(HaveOccurred())

result, err := auth.IsDefaultAuthMethod(ctx, cli)

if tt.expectError {
g.Expect(err).Should(HaveOccurred(), tt.description)
} else {
g.Expect(err).ShouldNot(HaveOccurred(), tt.description)
g.Expect(result).Should(Equal(tt.expectedResult), tt.description)
}
})
}
}
8 changes: 5 additions & 3 deletions internal/controller/services/gateway/gateway_auth_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"

serviceApi "github.com/opendatahub-io/opendatahub-operator/v2/api/services/v1alpha1"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster"
odhtypes "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/resources"
)
Expand All @@ -29,7 +30,8 @@ func createKubeAuthProxyInfrastructure(ctx context.Context, rr *odhtypes.Reconci
return fmt.Errorf("failed to resolve domain: %w", err)
}

authMode, err := detectClusterAuthMode(ctx, rr)
// Use cluster package function instead of local detectClusterAuthMode
authMode, err := cluster.GetClusterAuthenticationMode(ctx, rr.Client)
if err != nil {
return fmt.Errorf("failed to detect cluster authentication mode: %w", err)
}
Expand All @@ -43,7 +45,7 @@ func createKubeAuthProxyInfrastructure(ctx context.Context, rr *odhtypes.Reconci
}

var oidcConfig *serviceApi.OIDCConfig
if authMode == AuthModeOIDC {
if authMode == cluster.AuthModeOIDC {
oidcConfig = gatewayConfig.Spec.OIDC
}

Expand All @@ -57,7 +59,7 @@ func createKubeAuthProxyInfrastructure(ctx context.Context, rr *odhtypes.Reconci
return fmt.Errorf("failed to deploy auth proxy: %w", err)
}

if authMode == AuthModeIntegratedOAuth {
if authMode == cluster.AuthModeIntegratedOAuth {
if err := createOAuthClient(ctx, rr, clientSecret); err != nil {
return fmt.Errorf("failed to create OAuth client: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/controller/services/gateway/gateway_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import (
"github.com/opendatahub-io/opendatahub-operator/v2/api/common"
dsciv2 "github.com/opendatahub-io/opendatahub-operator/v2/api/dscinitialization/v2"
serviceApi "github.com/opendatahub-io/opendatahub-operator/v2/api/services/v1alpha1"
"github.com/opendatahub-io/opendatahub-operator/v2/internal/controller/services/auth"
sr "github.com/opendatahub-io/opendatahub-operator/v2/internal/controller/services/registry"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions/deploy"
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions/gc"
Expand Down Expand Up @@ -89,7 +89,7 @@ func (h *ServiceHandler) NewReconciler(ctx context.Context, mgr ctrl.Manager) er

// Only watch OAuthClient if cluster uses IntegratedOAuth (not OIDC or None)
// This prevents errors in ROSA environments where OAuthClient CRD doesn't exist
if isIntegratedOAuth, err := auth.IsDefaultAuthMethod(ctx, mgr.GetClient()); err == nil && isIntegratedOAuth {
if isIntegratedOAuth, err := cluster.IsIntegratedOAuth(ctx, mgr.GetClient()); err == nil && isIntegratedOAuth {
reconcilerBuilder = reconcilerBuilder.OwnsGVK(gvk.OAuthClient) // OpenShift OAuth integration
}

Expand Down
Loading