diff --git a/go.mod b/go.mod index 508637a0..c29f3ffe 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( require ( github.com/fatih/color v1.14.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/rs/xid v1.4.0 // indirect diff --git a/go.sum b/go.sum index 67ddba40..0fadf17e 100644 --- a/go.sum +++ b/go.sum @@ -108,6 +108,8 @@ github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXK github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/pkg/handlers/update_test.go b/pkg/handlers/update_test.go index 636caa9a..034b3fed 100644 --- a/pkg/handlers/update_test.go +++ b/pkg/handlers/update_test.go @@ -96,7 +96,7 @@ func TestMakeUpdateHandler(t *testing.T) { } } }, - "allowed_users": ["user1", "user2"] + "allowed_users": ["somelonguid1@egi.eu", "somelonguid2@egi.eu"] } `) req, _ := http.NewRequest("PUT", "/system/services", body) diff --git a/pkg/types/expose_test.go b/pkg/types/expose_test.go index e0185f09..69bab5b3 100644 --- a/pkg/types/expose_test.go +++ b/pkg/types/expose_test.go @@ -22,6 +22,7 @@ import ( appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" testclient "k8s.io/client-go/kubernetes/fake" @@ -229,3 +230,142 @@ func TestHortizontalAutoScaleSpec(t *testing.T) { t.Errorf("Expected target cpu 40 but got %d", res.Spec.TargetCPUUtilizationPercentage) } } + +func TestListIngress(t *testing.T) { + + K8sObjects := []runtime.Object{ + &netv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-ing", + Namespace: "namespace", + }, + }, + } + + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + cfg := &Config{ServicesNamespace: "namespace"} + + _, err := listIngress(kubeClientset, cfg) + + if err != nil { + t.Errorf("Error listing ingresses: %v", err) + } +} + +func TestUpdateIngress(t *testing.T) { + + K8sObjects := []runtime.Object{ + &netv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-ing", + Namespace: "namespace", + }, + }, + } + + service := Service{ + Name: "service", + } + + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + cfg := &Config{ServicesNamespace: "namespace"} + + err := updateIngress(service, kubeClientset, cfg) + + if err != nil { + t.Errorf("Error updating ingress: %v", err) + } +} + +func TestDeleteIngress(t *testing.T) { + + K8sObjects := []runtime.Object{ + &netv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-ing", + Namespace: "namespace", + }, + }, + } + + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + cfg := &Config{ServicesNamespace: "namespace"} + + err := deleteIngress("service-ing", kubeClientset, cfg) + + if err != nil { + t.Errorf("Error deleting ingress: %v", err) + } +} + +func TestUpdateSecret(t *testing.T) { + + K8sObjects := []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-auth-expose", + Namespace: "namespace", + }, + }, + } + service := Service{ + Name: "service", + } + + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + cfg := &Config{ServicesNamespace: "namespace"} + + err := updateSecret(service, kubeClientset, cfg) + + if err != nil { + t.Errorf("Error updating secret: %v", err) + } +} + +func TestDeleteSecret(t *testing.T) { + + K8sObjects := []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-auth-expose", + Namespace: "namespace", + }, + }, + } + + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + cfg := &Config{ServicesNamespace: "namespace"} + + err := deleteSecret("service", kubeClientset, cfg) + + if err != nil { + t.Errorf("Error deleting secret: %v", err) + } +} + +func TestExistsSecret(t *testing.T) { + + K8sObjects := []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-auth-expose", + Namespace: "namespace", + }, + }, + } + + kubeClientset := testclient.NewSimpleClientset(K8sObjects...) + cfg := &Config{ServicesNamespace: "namespace"} + + exists := existsSecret("service", kubeClientset, cfg) + + if exists != true { + t.Errorf("Expected secret to exist but got %v", exists) + } + + notexists := existsSecret("service1", kubeClientset, cfg) + + if notexists != false { + t.Errorf("Expected secret not to exist but got %v", notexists) + } +} diff --git a/pkg/utils/auth/auth.go b/pkg/utils/auth/auth.go index 559c6aad..57795a48 100644 --- a/pkg/utils/auth/auth.go +++ b/pkg/utils/auth/auth.go @@ -29,7 +29,7 @@ import ( ) // GetAuthMiddleware returns the appropriate gin auth middleware -func GetAuthMiddleware(cfg *types.Config, kubeClientset *kubernetes.Clientset) gin.HandlerFunc { +func GetAuthMiddleware(cfg *types.Config, kubeClientset kubernetes.Interface) gin.HandlerFunc { if !cfg.OIDCEnable { return gin.BasicAuth(gin.Accounts{ // Use the config's username and password for basic auth @@ -40,7 +40,7 @@ func GetAuthMiddleware(cfg *types.Config, kubeClientset *kubernetes.Clientset) g } // CustomAuth returns a custom auth handler (gin middleware) -func CustomAuth(cfg *types.Config, kubeClientset *kubernetes.Clientset) gin.HandlerFunc { +func CustomAuth(cfg *types.Config, kubeClientset kubernetes.Interface) gin.HandlerFunc { basicAuthHandler := gin.BasicAuth(gin.Accounts{ // Use the config's username and password for basic auth cfg.Username: cfg.Password, @@ -53,7 +53,7 @@ func CustomAuth(cfg *types.Config, kubeClientset *kubernetes.Clientset) gin.Hand minIOAdminClient.CreateAllUsersGroup() minIOAdminClient.UpdateUsersInGroup(oscarUser, "all_users_group", false) - oidcHandler := getOIDCMiddleware(kubeClientset, minIOAdminClient, cfg.OIDCIssuer, cfg.OIDCSubject, cfg.OIDCGroups) + oidcHandler := getOIDCMiddleware(kubeClientset, minIOAdminClient, cfg.OIDCIssuer, cfg.OIDCSubject, cfg.OIDCGroups, nil) return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if strings.HasPrefix(authHeader, "Bearer ") { diff --git a/pkg/utils/auth/auth_test.go b/pkg/utils/auth/auth_test.go new file mode 100644 index 00000000..1a9c50b5 --- /dev/null +++ b/pkg/utils/auth/auth_test.go @@ -0,0 +1,145 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 auth + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/grycap/oscar/v3/pkg/types" + "k8s.io/client-go/kubernetes/fake" +) + +func TestGetAuthMiddleware(t *testing.T) { + cfg := &types.Config{ + OIDCEnable: false, + Username: "testuser", + Password: "testpass", + } + kubeClientset := fake.NewSimpleClientset() + + router := gin.New() + router.Use(GetAuthMiddleware(cfg, kubeClientset)) + router.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, "") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + req.SetBasicAuth("testuser", "testpass") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %v, got %v", http.StatusOK, w.Code) + } + + we := httptest.NewRecorder() + reqe, _ := http.NewRequest("GET", "/", nil) + reqe.SetBasicAuth("testuser", "otherpass") + router.ServeHTTP(we, reqe) + + if we.Code != http.StatusUnauthorized { + t.Errorf("expected status %v, got %v", http.StatusUnauthorized, we.Code) + } +} + +func TestGetLoggerMiddleware(t *testing.T) { + router := gin.New() + router.Use(GetLoggerMiddleware()) + router.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, "") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %v, got %v", http.StatusOK, w.Code) + } +} + +func TestGetUIDFromContext(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Set("uidOrigin", "testuid") + + uid, err := GetUIDFromContext(c) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if uid != "testuid" { + t.Errorf("expected uid %v, got %v", "testuid", uid) + } +} + +func TestGetMultitenancyConfigFromContext(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + mc := &MultitenancyConfig{} + c.Set("multitenancyConfig", mc) + + mcFromContext, err := GetMultitenancyConfigFromContext(c) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if mcFromContext != mc { + t.Errorf("expected multitenancyConfig %v, got %v", mc, mcFromContext) + } +} + +func TestCustomAuth(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if !strings.HasPrefix(hreq.URL.Path, "/minio/admin/v3/") { + t.Errorf("Unexpected path in request, got: %s", hreq.URL.Path) + } + if hreq.URL.Path == "/minio/admin/v3/info" { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"Mode": "local", "Region": "us-east-1"}`)) + } else { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"status": "success"}`)) + } + })) + + cfg := &types.Config{ + OIDCEnable: false, + Username: "testuser", + Password: "testpass", + MinIOProvider: &types.MinIOProvider{ + Endpoint: server.URL, + AccessKey: "minio", + SecretKey: "minio123", + }, + } + kubeClientset := fake.NewSimpleClientset() + + router := gin.New() + router.Use(CustomAuth(cfg, kubeClientset)) + router.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, "") + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + req.SetBasicAuth("testuser", "testpass") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected status %v, got %v", http.StatusOK, w.Code) + } +} diff --git a/pkg/utils/auth/multitenancy_test.go b/pkg/utils/auth/multitenancy_test.go new file mode 100644 index 00000000..2f93a055 --- /dev/null +++ b/pkg/utils/auth/multitenancy_test.go @@ -0,0 +1,149 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 auth + +import ( + "context" + "encoding/base64" + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNewMultitenancyConfig(t *testing.T) { + clientset := fake.NewSimpleClientset() + uid := "test-uid" + mc := NewMultitenancyConfig(clientset, uid) + + if mc.owner_uid != uid { + t.Errorf("expected owner_uid to be %s, got %s", uid, mc.owner_uid) + } +} + +func TestUpdateCache(t *testing.T) { + clientset := fake.NewSimpleClientset() + mc := NewMultitenancyConfig(clientset, "test-uid") + + mc.UpdateCache("user1") + if len(mc.usersCache) != 1 { + t.Errorf("expected usersCache length to be 1, got %d", len(mc.usersCache)) + } +} + +func TestClearCache(t *testing.T) { + clientset := fake.NewSimpleClientset() + mc := NewMultitenancyConfig(clientset, "test-uid") + + mc.UpdateCache("user1") + mc.ClearCache() + if len(mc.usersCache) != 0 { + t.Errorf("expected usersCache length to be 0, got %d", len(mc.usersCache)) + } +} + +func TestUserExists(t *testing.T) { + clientset := fake.NewSimpleClientset() + mc := NewMultitenancyConfig(clientset, "test-uid") + + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: ServicesNamespace, + }, + } + clientset.CoreV1().Secrets(ServicesNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) + + exists := mc.UserExists("user1@egi.eu") + if !exists { + t.Errorf("expected user1 to exist") + } +} + +func TestCreateSecretForOIDC(t *testing.T) { + clientset := fake.NewSimpleClientset() + mc := NewMultitenancyConfig(clientset, "test-uid") + + err := mc.CreateSecretForOIDC("user1@egi.eu", "secret-key") + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + secret, err := clientset.CoreV1().Secrets(ServicesNamespace).Get(context.TODO(), "user1", metav1.GetOptions{}) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + if secret.StringData["secretKey"] != "secret-key" { + t.Errorf("expected secretKey to be 'secret-key', got %s", secret.StringData["secretKey"]) + } +} + +func TestGetUserCredentials(t *testing.T) { + clientset := fake.NewSimpleClientset() + mc := NewMultitenancyConfig(clientset, "test-uid") + + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: ServicesNamespace, + }, + Data: map[string][]byte{ + "accessKey": []byte("access-key"), + "secretKey": []byte("secret-key"), + }, + } + clientset.CoreV1().Secrets(ServicesNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) + + accessKey, secretKey, err := mc.GetUserCredentials("user1@egi.eu") + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + if accessKey != "access-key" { + t.Errorf("expected accessKey to be 'access-key', got %s", accessKey) + } + + if secretKey != "secret-key" { + t.Errorf("expected secretKey to be 'secret-key', got %s", secretKey) + } +} + +func TestGenerateRandomKey(t *testing.T) { + key, err := GenerateRandomKey(32) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + dkey, _ := base64.RawURLEncoding.DecodeString(key) + if len(dkey) != 32 { + t.Errorf("expected key length to be 32, got %d", len(key)) + } +} + +func TestCheckUsersInCache(t *testing.T) { + clientset := fake.NewSimpleClientset() + mc := NewMultitenancyConfig(clientset, "test-uid") + + mc.UpdateCache("user1") + mc.UpdateCache("user2") + + notFoundUsers := mc.CheckUsersInCache([]string{"user1", "user3"}) + if len(notFoundUsers) != 1 { + t.Errorf("expected notFoundUsers length to be 1, got %d", len(notFoundUsers)) + } +} diff --git a/pkg/utils/auth/oidc.go b/pkg/utils/auth/oidc.go index fb60575b..40dbd54d 100644 --- a/pkg/utils/auth/oidc.go +++ b/pkg/utils/auth/oidc.go @@ -76,8 +76,11 @@ func NewOIDCManager(issuer string, subject string, groups []string) (*oidcManage } // getIODCMiddleware returns the Gin's handler middleware to validate OIDC-based auth -func getOIDCMiddleware(kubeClientset *kubernetes.Clientset, minIOAdminClient *utils.MinIOAdminClient, issuer string, subject string, groups []string) gin.HandlerFunc { +func getOIDCMiddleware(kubeClientset kubernetes.Interface, minIOAdminClient *utils.MinIOAdminClient, issuer string, subject string, groups []string, oidcConfig *oidc.Config) gin.HandlerFunc { oidcManager, err := NewOIDCManager(issuer, subject, groups) + if oidcConfig != nil { + oidcManager.config = oidcConfig + } if err != nil { return func(c *gin.Context) { c.AbortWithStatus(http.StatusUnauthorized) diff --git a/pkg/utils/auth/oidc_test.go b/pkg/utils/auth/oidc_test.go new file mode 100644 index 00000000..142e520f --- /dev/null +++ b/pkg/utils/auth/oidc_test.go @@ -0,0 +1,219 @@ +/* +Copyright (C) GRyCAP - I3M - UPV + +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 auth + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "github.com/grycap/oscar/v3/pkg/types" + "github.com/grycap/oscar/v3/pkg/utils" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNewOIDCManager(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if hreq.URL.Path == "/.well-known/openid-configuration" { + rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `"}`)) + } + })) + + issuer := server.URL + subject := "test-subject" + groups := []string{"group1", "group2"} + + oidcManager, err := NewOIDCManager(issuer, subject, groups) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if oidcManager == nil { + t.Errorf("expected oidcManager to be non-nil") + } +} + +func TestGetUserInfo(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + fmt.Println(hreq.URL.Path) + rw.Header().Set("Content-Type", "application/json") + if hreq.URL.Path == "/.well-known/openid-configuration" { + rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `", "userinfo_endpoint": "http://` + hreq.Host + `/userinfo"}`)) + } else if hreq.URL.Path == "/userinfo" { + rw.Write([]byte(`{"sub": "test-subject", "eduperson_entitlement": ["urn:mace:egi.eu:group:group1"]}`)) + } + })) + + issuer := server.URL + subject := "test-subject" + groups := []string{"group1", "group2"} + + oidcManager, err := NewOIDCManager(issuer, subject, groups) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + rawToken := "test-token" + ui, err := oidcManager.GetUserInfo(rawToken) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if ui.Subject != "test-subject" { + t.Errorf("expected subject to be %v, got %v", "test-subject", ui.Subject) + } + if len(ui.Groups) != 1 || ui.Groups[0] != "group1" { + t.Errorf("expected groups to be %v, got %v", []string{"group1"}, ui.Groups) + } +} + +func TestIsAuthorised(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + rw.Header().Set("Content-Type", "application/json") + if hreq.URL.Path == "/.well-known/openid-configuration" { + rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `", "userinfo_endpoint": "http://` + hreq.Host + `/userinfo"}`)) + } else if hreq.URL.Path == "/userinfo" { + rw.Write([]byte(`{"sub": "user1@egi.eu", "eduperson_entitlement": ["urn:mace:egi.eu:group:group1"]}`)) + } + })) + + issuer := server.URL + subject := "user1@egi.eu" + groups := []string{"group1", "group2"} + + oidcManager, err := NewOIDCManager(issuer, subject, groups) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + rawToken := GetToken(issuer, subject) + oidcManager.config.InsecureSkipSignatureCheck = true + + if !oidcManager.IsAuthorised(rawToken) { + t.Errorf("expected token to be authorised") + } + + resg1, err2 := oidcManager.UserHasVO(rawToken, "group1") + if err2 != nil { + t.Errorf("expected no error, got %v", err) + } + if !resg1 { + t.Errorf("expected user to have VO") + } + resg2, err3 := oidcManager.UserHasVO(rawToken, "group2") + if err3 != nil { + t.Errorf("expected no error, got %v", err) + } + if resg2 { + t.Errorf("expected user not to have VO") + } + + uid, _ := oidcManager.GetUID(rawToken) + if uid != subject { + t.Errorf("expected uid to be %v, got %v", subject, uid) + } +} + +func TestGetOIDCMiddleware(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, hreq *http.Request) { + if hreq.URL.Path == "/.well-known/openid-configuration" { + rw.Write([]byte(`{"issuer": "http://` + hreq.Host + `", "userinfo_endpoint": "http://` + hreq.Host + `/userinfo"}`)) + } else if hreq.URL.Path == "/userinfo" { + rw.Write([]byte(`{"sub": "user@egi.eu", "eduperson_entitlement": ["urn:mace:egi.eu:group:group1"]}`)) + } else if hreq.URL.Path == "/minio/admin/v3/info" { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"Mode": "local", "Region": "us-east-1"}`)) + } else { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"status": "success"}`)) + } + })) + + kubeClientset := fake.NewSimpleClientset() + cfg := types.Config{ + MinIOProvider: &types.MinIOProvider{ + Endpoint: server.URL, + Verify: false, + }, + } + minIOAdminClient, _ := utils.MakeMinIOAdminClient(&cfg) + issuer := server.URL + subject := "user@egi.eu" + groups := []string{"group1", "group2"} + + oidcConfig := &oidc.Config{ + InsecureSkipSignatureCheck: true, + SkipClientIDCheck: true, + } + middleware := getOIDCMiddleware(kubeClientset, minIOAdminClient, issuer, subject, groups, oidcConfig) + if middleware == nil { + t.Errorf("expected middleware to be non-nil") + } + + scenarios := []struct { + token string + code int + name string + }{ + { + name: "invalid-token", + token: "invalid-token", + code: http.StatusUnauthorized, + }, + { + name: "valid-token", + token: GetToken(issuer, subject), + code: http.StatusOK, + }, + } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + // Create a new Gin context + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Test the middleware with an invalid token + c.Request = &http.Request{ + Header: http.Header{ + "Authorization": []string{"Bearer " + s.token}, + }, + } + middleware(c) + if c.Writer.Status() != s.code { + t.Errorf("expected status to be %v, got %v", s.code, c.Writer.Status()) + } + }) + } +} + +func GetToken(issuer string, subject string) string { + claims := jwt.MapClaims{ + "iss": issuer, + "sub": subject, + "exp": time.Now().Add(1 * time.Hour).Unix(), + "iat": time.Now().Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + privateKey, _ := rsa.GenerateKey(rand.Reader, 1024) + rawToken, _ := token.SignedString(privateKey) + return rawToken +}