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
41 changes: 3 additions & 38 deletions controller/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@ import (
apiserverv1beta1 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1"
_ "k8s.io/client-go/plugin/pkg/client/auth"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
Expand Down Expand Up @@ -219,7 +217,7 @@ func main() {
os.Exit(1)
}

authenticator, prefix, router, option, provisioning, leasePolicy, err := config.LoadConfiguration(
authenticator, prefix, router, option, provisioning, leasePolicy, loadedConfig, err := config.LoadConfiguration(
context.Background(),
mgr.GetAPIReader(),
mgr.GetScheme(),
Expand Down Expand Up @@ -308,9 +306,8 @@ func main() {

// Setup Login Service for simplified CLI login
loginService := login.NewServiceFromEnv()
// Extract OIDC configuration from the loaded config for the login service
oidcConfigs := extractOIDCConfigs(mgr.GetAPIReader(), watchNamespace)
loginService.SetOIDCConfig(oidcConfigs)
// Use the already-parsed config to extract OIDC configuration for the login service
loginService.SetOIDCConfig(jwtAuthenticatorsToOIDCConfigs(loadedConfig.Authentication.JWT))
if err = loginService.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create service", "service", "Login")
os.Exit(1)
Expand All @@ -332,38 +329,6 @@ func main() {
}
}

// extractOIDCConfigs reads the OIDC configuration from the ConfigMap
func extractOIDCConfigs(reader client.Reader, namespace string) []login.OIDCConfig {
var configmap corev1.ConfigMap
if err := reader.Get(context.Background(), client.ObjectKey{
Namespace: namespace,
Name: "jumpstarter-controller",
}, &configmap); err != nil {
setupLog.Error(err, "unable to read configmap for OIDC config")
return nil
}

// Try new config format first
rawConfig, ok := configmap.Data["config"]
if ok {
var cfg config.Config
if err := yaml.UnmarshalStrict([]byte(rawConfig), &cfg); err == nil {
return jwtAuthenticatorsToOIDCConfigs(cfg.Authentication.JWT)
}
}

// Fall back to legacy authentication format
rawAuth, ok := configmap.Data["authentication"]
if ok {
var auth config.Authentication
if err := yaml.Unmarshal([]byte(rawAuth), &auth); err == nil {
return jwtAuthenticatorsToOIDCConfigs(auth.JWT)
}
}

return nil
}

// jwtAuthenticatorsToOIDCConfigs converts JWT authenticators to login OIDC configs
func jwtAuthenticatorsToOIDCConfigs(authenticators []apiserverv1beta1.JWTAuthenticator) []login.OIDCConfig {
var configs []login.OIDCConfig
Expand Down
157 changes: 157 additions & 0 deletions controller/cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"testing"

apiserverv1beta1 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1"
)

func TestJwtAuthenticatorsToOIDCConfigs(t *testing.T) {
tests := []struct {
name string
authenticators []apiserverv1beta1.JWTAuthenticator
wantCount int
wantNil bool
wantIssuer string
}{
{
name: "single JWT authenticator",
authenticators: []apiserverv1beta1.JWTAuthenticator{
{
Issuer: apiserverv1beta1.Issuer{
URL: "https://dex.example.com",
Audiences: []string{"jumpstarter"},
},
},
},
wantCount: 1,
},
{
name: "multiple JWT authenticators including localhost",
authenticators: []apiserverv1beta1.JWTAuthenticator{
{
Issuer: apiserverv1beta1.Issuer{
URL: "https://localhost:8085",
Audiences: []string{"jumpstarter"},
},
},
{
Issuer: apiserverv1beta1.Issuer{
URL: "https://dex.example.com",
Audiences: []string{"jumpstarter"},
},
},
},
wantCount: 1, // localhost issuer should be skipped
},
{
name: "nil authenticators returns nil",
authenticators: nil,
wantNil: true,
},
{
name: "empty authenticators returns nil",
authenticators: []apiserverv1beta1.JWTAuthenticator{},
wantNil: true,
},
{
name: "only localhost authenticator returns nil",
authenticators: []apiserverv1beta1.JWTAuthenticator{
{
Issuer: apiserverv1beta1.Issuer{
URL: "https://localhost:8085",
Audiences: []string{"jumpstarter"},
},
},
},
wantNil: true,
},
{
name: "multiple external authenticators",
authenticators: []apiserverv1beta1.JWTAuthenticator{
{
Issuer: apiserverv1beta1.Issuer{
URL: "https://dex.example.com",
Audiences: []string{"jumpstarter"},
},
},
{
Issuer: apiserverv1beta1.Issuer{
URL: "https://keycloak.example.com",
Audiences: []string{"jumpstarter", "jumpstarter-cli"},
},
},
},
wantCount: 2,
wantIssuer: "https://dex.example.com",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configs := jwtAuthenticatorsToOIDCConfigs(tt.authenticators)

if tt.wantNil {
if configs != nil {
t.Errorf("expected nil, got %v", configs)
}
return
}

if configs == nil {
t.Fatal("expected non-nil configs, got nil")
}

if len(configs) != tt.wantCount {
t.Errorf("expected %d configs, got %d", tt.wantCount, len(configs))
}

if tt.wantIssuer != "" && len(configs) > 0 {
if configs[0].Issuer != tt.wantIssuer {
t.Errorf("expected issuer %q, got %q", tt.wantIssuer, configs[0].Issuer)
}
}
})
}
}

func TestJwtAuthenticatorsToOIDCConfigsContent(t *testing.T) {
authenticators := []apiserverv1beta1.JWTAuthenticator{
{
Issuer: apiserverv1beta1.Issuer{
URL: "https://dex.example.com",
Audiences: []string{"jumpstarter", "jumpstarter-cli"},
},
},
}

configs := jwtAuthenticatorsToOIDCConfigs(authenticators)

if len(configs) != 1 {
t.Fatalf("expected 1 config, got %d", len(configs))
}

cfg := configs[0]
if cfg.Issuer != "https://dex.example.com" {
t.Errorf("expected issuer 'https://dex.example.com', got %q", cfg.Issuer)
}
if len(cfg.Audiences) != 2 {
t.Errorf("expected 2 audiences, got %d", len(cfg.Audiences))
}
}
43 changes: 9 additions & 34 deletions controller/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package config
import (
"context"
"fmt"
"time"

"github.com/jumpstarter-dev/jumpstarter-controller/internal/oidc"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
Expand Down Expand Up @@ -51,53 +49,30 @@ func LoadConfiguration(
key client.ObjectKey,
signer *oidc.Signer,
certificateAuthority string,
) (authenticator.Token, string, Router, []grpc.ServerOption, *Provisioning, *LeasePolicy, error) {
) (authenticator.Token, string, Router, []grpc.ServerOption, *Provisioning, *LeasePolicy, *Config, error) {
var configmap corev1.ConfigMap
if err := client.Get(ctx, key, &configmap); err != nil {
return nil, "", nil, nil, nil, nil, err
return nil, "", nil, nil, nil, nil, nil, err
}

rawRouter, ok := configmap.Data["router"]
if !ok {
return nil, "", nil, nil, nil, nil, fmt.Errorf("LoadConfiguration: missing router section")
return nil, "", nil, nil, nil, nil, nil, fmt.Errorf("LoadConfiguration: missing router section")
}

var router Router
if err := yaml.Unmarshal([]byte(rawRouter), &router); err != nil {
return nil, "", nil, nil, nil, nil, err
}

rawAuthenticationConfiguration, ok := configmap.Data["authentication"]
if ok {
// backwards compatibility
// TODO: remove in 0.7.0
authenticator, prefix, err := oidc.LoadAuthenticationConfiguration(
ctx,
scheme,
[]byte(rawAuthenticationConfiguration),
signer,
certificateAuthority,
)
if err != nil {
return nil, "", nil, nil, nil, nil, err
}

return authenticator, prefix, router, []grpc.ServerOption{
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 1 * time.Second,
PermitWithoutStream: true,
}),
}, &Provisioning{Enabled: false}, &LeasePolicy{MaxTags: 10}, nil
return nil, "", nil, nil, nil, nil, nil, err
}

rawConfig, ok := configmap.Data["config"]
if !ok {
return nil, "", nil, nil, nil, nil, fmt.Errorf("LoadConfiguration: missing config section")
return nil, "", nil, nil, nil, nil, nil, fmt.Errorf("LoadConfiguration: missing config section")
}

var config Config
if err := yaml.UnmarshalStrict([]byte(rawConfig), &config); err != nil {
return nil, "", nil, nil, nil, nil, err
return nil, "", nil, nil, nil, nil, nil, err
}

authenticator, prefix, err := LoadAuthenticationConfiguration(
Expand All @@ -108,13 +83,13 @@ func LoadConfiguration(
certificateAuthority,
)
if err != nil {
return nil, "", nil, nil, nil, nil, err
return nil, "", nil, nil, nil, nil, nil, err
}

serverOptions, err := LoadGrpcConfiguration(config.Grpc)
if err != nil {
return nil, "", nil, nil, nil, nil, err
return nil, "", nil, nil, nil, nil, nil, err
}

return authenticator, prefix, router, serverOptions, &config.Provisioning, &config.LeasePolicy, nil
return authenticator, prefix, router, serverOptions, &config.Provisioning, &config.LeasePolicy, &config, nil
}
Loading
Loading