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
36 changes: 36 additions & 0 deletions .github/workflows/operator-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,22 @@ jobs:
affinity: {}
EOF

# Create Auth Secret
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: test-gateway-auth
namespace: default
type: Opaque
stringData:
users.yaml: |
- username: "admin"
password: "admin"
password_hashed: false
roles: ["admin"]
EOF

# Create Gateway CR
cat <<EOF | kubectl apply -f -
apiVersion: gateway.api-platform.wso2.com/v1alpha1
Expand All @@ -846,6 +862,8 @@ jobs:
scope: Cluster
configRef:
name: test-gateway-config
authSecretRef:
name: test-gateway-auth
EOF

- name: Wait for Gateway to be ready
Expand Down Expand Up @@ -1245,6 +1263,22 @@ jobs:
# Configure Gateway to use test images (same configmap as before but in new namespace)
kubectl create configmap test-gateway-config --from-file=values.yaml=<(kubectl get cm test-gateway-config -n default -o jsonpath='{.data.values\.yaml}') -n scoped-test

# Create Auth Secret in scoped-test
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: test-gateway-auth
namespace: scoped-test
type: Opaque
stringData:
users.yaml: |
- username: "admin"
password: "admin"
password_hashed: false
roles: ["admin"]
EOF

cat <<EOF | kubectl apply -f -
apiVersion: gateway.api-platform.wso2.com/v1alpha1
kind: Gateway
Expand All @@ -1257,6 +1291,8 @@ jobs:
scope: Cluster
configRef:
name: test-gateway-config
authSecretRef:
name: test-gateway-auth
EOF

echo "Waiting for scoped-gateway to be Ready..."
Expand Down
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use (
./gateway/gateway-controller
./gateway/it
./gateway/policy-engine
./kubernetes/gateway-operator
./platform-api/src
./sdk
)
94 changes: 42 additions & 52 deletions go.work.sum

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions kubernetes/gateway-operator/api/v1alpha1/gateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ type GatewaySpec struct {
// If not specified, the operator will use the default mounted configuration.
// +optional
ConfigRef *corev1.LocalObjectReference `json:"configRef,omitempty"`

// AuthSecretRef references a Secret containing the user list in a 'users.yaml' key.
// The Secret data should contain a key "users.yaml" with a list of users.
// This takes precedence over auth configuration in ConfigRef.
// +optional
AuthSecretRef *corev1.LocalObjectReference `json:"authSecretRef,omitempty"`
}

// GatewayControlPlane defines control plane connection settings
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ spec:
required:
- scope
type: object
authSecretRef:
description: |-
AuthSecretRef references a Secret containing the user list in a 'users.yaml' key.
The Secret data should contain a key "users.yaml" with a list of users.
This takes precedence over auth configuration in ConfigRef.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
configRef:
description: |-
ConfigRef references a ConfigMap containing custom Helm values configuration.
Expand Down
44 changes: 44 additions & 0 deletions kubernetes/gateway-operator/internal/auth/auth_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,50 @@ func GetDeploymentConfigFromGateway(ctx context.Context, k8sClient client.Client
return &deploymentConfig.Gateway.Config.GatewayController.Auth, nil
}

// GetAuthConfigFromSecret retrieves authentication configuration from a Gateway's AuthSecretRef.
// It parses the 'users.yaml' key from the Secret into a list of AuthUser.
// Returns nil if no AuthSecretRef is specified, or if the Secret/key is missing.
func GetAuthConfigFromSecret(ctx context.Context, k8sClient client.Client, gateway *apiv1.Gateway) (*AuthSettings, error) {
// If no AuthSecretRef, return nil
if gateway.Spec.AuthSecretRef == nil {
return nil, nil
}

// Get the Secret
secret := &corev1.Secret{}
if err := k8sClient.Get(ctx, client.ObjectKey{
Name: gateway.Spec.AuthSecretRef.Name,
Namespace: gateway.Namespace,
}, secret); err != nil {
return nil, fmt.Errorf("failed to get Auth Secret %s/%s: %w", gateway.Namespace, gateway.Spec.AuthSecretRef.Name, err)
}

// Look for users.yaml key
usersYAML, ok := secret.Data["users.yaml"]
if !ok {
// Fallback to stringData if not in Data (though client normally consolidates them)
// But in controller-runtime Struct Data contains byte slices
return nil, fmt.Errorf("secret %s/%s does not contain 'users.yaml' key", gateway.Namespace, gateway.Spec.AuthSecretRef.Name)
}

// Parse the YAML
var users []AuthUser
if err := yaml.Unmarshal(usersYAML, &users); err != nil {
return nil, fmt.Errorf("failed to parse users from Secret: %w", err)
}

// Construct AuthSettings
// We assume basic auth is enabled if users are provided via secret
authSettings := &AuthSettings{
Basic: BasicAuthConfig{
Enabled: true,
Users: users,
},
}

return authSettings, nil
}

// GetBasicAuthCredentials extracts basic auth credentials from the auth config
// Returns username, password, and ok=true if basic auth is enabled and has at least one user
// Returns empty strings and ok=false if basic auth is not configured or disabled
Expand Down
57 changes: 57 additions & 0 deletions kubernetes/gateway-operator/internal/auth/auth_helper_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package auth

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
apiv1 "github.com/wso2/api-platform/kubernetes/gateway-operator/api/v1alpha1"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestAuthConfigParsing(t *testing.T) {
Expand Down Expand Up @@ -70,3 +76,54 @@ func TestCalculateConfigHash(t *testing.T) {
assert.NotEqual(t, hash1, hash3, "Different content should produce different hash")
assert.NotEmpty(t, hash1)
}

func TestGetAuthConfigFromSecret(t *testing.T) {
// Setup scheme
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = apiv1.AddToScheme(scheme)

// Define test data
secretName := "auth-secret"
namespace := "default"

usersYaml := `
- username: "secretadmin"
password: "secretpassword"
roles: ["admin"]
`

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Data: map[string][]byte{
"users.yaml": []byte(usersYaml),
},
}

gateway := &apiv1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gateway",
Namespace: namespace,
},
Spec: apiv1.GatewaySpec{
AuthSecretRef: &corev1.LocalObjectReference{
Name: secretName,
},
},
}

// Create fake client
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build()

// Test
authSettings, err := GetAuthConfigFromSecret(context.Background(), client, gateway)
assert.NoError(t, err)
assert.NotNil(t, authSettings)
assert.True(t, authSettings.Basic.Enabled)
assert.Len(t, authSettings.Basic.Users, 1)
assert.Equal(t, "secretadmin", authSettings.Basic.Users[0].Username)
assert.Equal(t, "secretpassword", authSettings.Basic.Users[0].Password)
}
Original file line number Diff line number Diff line change
Expand Up @@ -874,14 +874,28 @@ func (r *RestApiReconciler) addAuthToRequest(ctx context.Context, req *http.Requ
return fmt.Errorf("failed to get Gateway CR: %w", err)
}

// Try to get auth config from the Gateway's ConfigMap
authConfig, err := auth.GetDeploymentConfigFromGateway(ctx, r.Client, gateway)
if err != nil {
log.Warn("Failed to retrieve auth config from Gateway ConfigMap, using default credentials",
zap.Error(err),
// Try to get auth config from the Gateway's AuthSecretRef first
var authConfig *auth.AuthSettings

// 1. Try Secret
authConfig, errSecret := auth.GetAuthConfigFromSecret(ctx, r.Client, gateway)
if errSecret != nil {
log.Warn("Failed to retrieve auth config from Secret referenced in Gateway, trying ConfigMap",
zap.Error(errSecret),
zap.String("gateway", gatewayInfo.Name))
}

// 2. Try ConfigMap if Secret failed or not present
if authConfig == nil {
var errConfigMap error
authConfig, errConfigMap = auth.GetDeploymentConfigFromGateway(ctx, r.Client, gateway)
if errConfigMap != nil {
log.Warn("Failed to retrieve auth config from Gateway ConfigMap, using default credentials",
zap.Error(errConfigMap),
zap.String("gateway", gatewayInfo.Name))
}
}

var username, password string
if authConfig != nil {
// Try to get credentials from the auth config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ spec:
required:
- scope
type: object
authSecretRef:
description: |-
AuthSecretRef references a Secret containing the user list in a 'users.yaml' key.
The Secret data should contain a key "users.yaml" with a list of users.
This takes precedence over auth configuration in ConfigRef.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
configRef:
description: |-
ConfigRef references a ConfigMap containing custom Helm values configuration.
Expand Down
Loading