diff --git a/workspaces/backend/api/app.go b/workspaces/backend/api/app.go index 92f02b77..efb11b3b 100644 --- a/workspaces/backend/api/app.go +++ b/workspaces/backend/api/app.go @@ -22,6 +22,8 @@ import ( "github.com/julienschmidt/httprouter" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authorization/authorizer" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kubeflow/notebooks/workspaces/backend/internal/config" @@ -52,20 +54,26 @@ const ( ) type App struct { - Config config.EnvConfig + Config *config.EnvConfig logger *slog.Logger repositories *repositories.Repositories Scheme *runtime.Scheme + RequestAuthN authenticator.Request + RequestAuthZ authorizer.Authorizer } // NewApp creates a new instance of the app -func NewApp(cfg config.EnvConfig, logger *slog.Logger, cl client.Client, scheme *runtime.Scheme) (*App, error) { +func NewApp(cfg *config.EnvConfig, logger *slog.Logger, cl client.Client, scheme *runtime.Scheme, reqAuthN authenticator.Request, reqAuthZ authorizer.Authorizer) (*App, error) { + + // TODO: log the configuration on startup app := &App{ Config: cfg, logger: logger, repositories: repositories.NewRepositories(cl), Scheme: scheme, + RequestAuthN: reqAuthN, + RequestAuthZ: reqAuthZ, } return app, nil } diff --git a/workspaces/backend/api/auth.go b/workspaces/backend/api/auth.go new file mode 100644 index 00000000..4e905199 --- /dev/null +++ b/workspaces/backend/api/auth.go @@ -0,0 +1,73 @@ +/* +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 api + +import ( + "fmt" + "net/http" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/kubeflow/notebooks/workspaces/backend/internal/auth" +) + +// requireAuth verifies that the request is authenticated and authorized to take the actions specified by the given policies. +// If this method returns false, the request has been handled and the caller should return immediately. +// If this method returns true, the request is authenticated and authorized to proceed. +// This method should only be called once per request. +func (a *App) requireAuth(w http.ResponseWriter, r *http.Request, policies []*auth.ResourcePolicy) bool { + ctx := r.Context() + + // if auth is disabled, allow the request to proceed + if a.Config.DisableAuth { + return true + } + + // authenticate the request (extract user and groups from the request headers) + res, ok, err := a.RequestAuthN.AuthenticateRequest(r) + if err != nil { + err = fmt.Errorf("failed to authenticate request: %w", err) + a.serverErrorResponse(w, r, err) + return false + } + if !ok { + a.unauthorizedResponse(w, r) + return false + } + + // for each policy, check if the user is authorized to take the requested action + for _, policy := range policies { + attributes := policy.AttributesFor(res.User) + authorized, reason, err := a.RequestAuthZ.Authorize(ctx, attributes) + if err != nil { + err = fmt.Errorf("failed to authorize request for user %q: %w", res.User.GetName(), err) + a.serverErrorResponse(w, r, err) + return false + } + + if authorized != authorizer.DecisionAllow { + msg := fmt.Sprintf("authorization was denied for user %q", res.User.GetName()) + if reason != "" { + msg = fmt.Sprintf("%s: %s", msg, reason) + } + a.forbiddenResponse(w, r, msg) + return false + } + } + + return true +} diff --git a/workspaces/backend/api/errors.go b/workspaces/backend/api/errors.go index e8ec75c6..3c807fcd 100644 --- a/workspaces/backend/api/errors.go +++ b/workspaces/backend/api/errors.go @@ -46,6 +46,15 @@ func (a *App) LogError(r *http.Request, err error) { a.logger.Error(err.Error(), "method", method, "uri", uri) } +func (a *App) LogWarn(r *http.Request, message string) { + var ( + method = r.Method + uri = r.URL.RequestURI() + ) + + a.logger.Warn(message, "method", method, "uri", uri) +} + //nolint:unused func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { httpError := &HTTPError{ @@ -103,6 +112,30 @@ func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) { a.errorResponse(w, r, httpError) } +func (a *App) unauthorizedResponse(w http.ResponseWriter, r *http.Request) { + httpError := &HTTPError{ + StatusCode: http.StatusUnauthorized, + ErrorResponse: ErrorResponse{ + Code: strconv.Itoa(http.StatusUnauthorized), + Message: "authentication is required to access this resource", + }, + } + a.errorResponse(w, r, httpError) +} + +func (a *App) forbiddenResponse(w http.ResponseWriter, r *http.Request, msg string) { + a.LogWarn(r, msg) + + httpError := &HTTPError{ + StatusCode: http.StatusForbidden, + ErrorResponse: ErrorResponse{ + Code: strconv.Itoa(http.StatusForbidden), + Message: "you are not authorized to access this resource", + }, + } + a.errorResponse(w, r, httpError) +} + //nolint:unused func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) { message, err := json.Marshal(errors) diff --git a/workspaces/backend/api/healthcheck_handler_test.go b/workspaces/backend/api/healthcheck_handler_test.go index 3df32e5f..ec1fe340 100644 --- a/workspaces/backend/api/healthcheck_handler_test.go +++ b/workspaces/backend/api/healthcheck_handler_test.go @@ -26,28 +26,13 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/kubeflow/notebooks/workspaces/backend/internal/config" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/health_check" - "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" ) var _ = Describe("HealthCheck Handler", func() { - var ( - a App - ) Context("when backend is healthy", func() { - BeforeEach(func() { - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - }) - It("should return a health check response", func() { By("creating the HTTP request") req, err := http.NewRequest(http.MethodGet, HealthCheckPath, http.NoBody) diff --git a/workspaces/backend/api/namespaces_handler.go b/workspaces/backend/api/namespaces_handler.go index d178e927..dc41380e 100644 --- a/workspaces/backend/api/namespaces_handler.go +++ b/workspaces/backend/api/namespaces_handler.go @@ -20,7 +20,9 @@ import ( "net/http" "github.com/julienschmidt/httprouter" + corev1 "k8s.io/api/core/v1" + "github.com/kubeflow/notebooks/workspaces/backend/internal/auth" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces" ) @@ -28,6 +30,18 @@ type NamespacesEnvelope Envelope[[]models.Namespace] func (a *App) GetNamespacesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbList, + &corev1.Namespace{}, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + namespaces, err := a.repositories.Namespace.GetNamespaces(r.Context()) if err != nil { a.serverErrorResponse(w, r, err) diff --git a/workspaces/backend/api/namespaces_handler_test.go b/workspaces/backend/api/namespaces_handler_test.go index 3d414d39..5f12911e 100644 --- a/workspaces/backend/api/namespaces_handler_test.go +++ b/workspaces/backend/api/namespaces_handler_test.go @@ -29,15 +29,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/kubeflow/notebooks/workspaces/backend/internal/config" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces" - "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" ) var _ = Describe("Namespaces Handler", func() { - var ( - a App - ) // NOTE: these tests assume a specific state of the cluster, so cannot be run in parallel with other tests. // therefore, we run them using the `Serial` Ginkgo decorators. @@ -47,14 +42,6 @@ var _ = Describe("Namespaces Handler", func() { const namespaceName2 = "get-ns-test-ns2" BeforeEach(func() { - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - By("creating Namespace 1") namespace1 := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -95,6 +82,9 @@ var _ = Describe("Namespaces Handler", func() { req, err := http.NewRequest(http.MethodGet, AllNamespacesPath, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetNamespacesHandler") ps := httprouter.Params{} rr := httptest.NewRecorder() diff --git a/workspaces/backend/api/suite_test.go b/workspaces/backend/api/suite_test.go index 814c5c98..2e947859 100644 --- a/workspaces/backend/api/suite_test.go +++ b/workspaces/backend/api/suite_test.go @@ -19,39 +19,52 @@ package api import ( "context" "fmt" + "log/slog" "path/filepath" "runtime" "testing" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + "github.com/kubeflow/notebooks/workspaces/backend/internal/auth" + "github.com/kubeflow/notebooks/workspaces/backend/internal/config" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +const ( + userIdHeader = "userid-header" + userIdPrefix = "" + groupsHeader = "groups-header" + + adminUser = "notebooks-admin" +) + var ( testEnv *envtest.Environment cfg *rest.Config k8sClient client.Client + a *App + ctx context.Context cancel context.CancelFunc ) @@ -95,6 +108,30 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + By("creating the notebooks-admin ClusterRoleBinding") + Expect(k8sClient.Create(ctx, &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "notebooks-admin", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "User", + Name: adminUser, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "cluster-admin", + }, + })).To(Succeed()) + + By("listing the clusterRoles") + clusterRoles := &rbacv1.ClusterRoleList{} + Expect(k8sClient.List(ctx, clusterRoles)).To(Succeed()) + for _, clusterRole := range clusterRoles.Items { + fmt.Printf("ClusterRole: %s\n", clusterRole.Name) + } + By("setting up the controller manager") k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, @@ -104,6 +141,21 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) + By("initializing the application logger") + appLogger := slog.New(slog.NewTextHandler(GinkgoWriter, nil)) + + By("creating the request authenticator") + reqAuthN, err := auth.NewRequestAuthenticator(userIdHeader, userIdPrefix, groupsHeader) + Expect(err).NotTo(HaveOccurred()) + + By("creating the request authorizer") + reqAuthZ, err := auth.NewRequestAuthorizer(k8sManager.GetConfig(), k8sManager.GetHTTPClient()) + Expect(err).NotTo(HaveOccurred()) + + By("creating the application") + // NOTE: we use the `k8sClient` rather than `k8sManager.GetClient()` to avoid race conditions with the cached client + a, err = NewApp(&config.EnvConfig{}, appLogger, k8sClient, k8sManager.GetScheme(), reqAuthN, reqAuthZ) + go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) diff --git a/workspaces/backend/api/workspacekinds_handler.go b/workspaces/backend/api/workspacekinds_handler.go index c5119440..81439167 100644 --- a/workspaces/backend/api/workspacekinds_handler.go +++ b/workspaces/backend/api/workspacekinds_handler.go @@ -22,7 +22,10 @@ import ( "net/http" "github.com/julienschmidt/httprouter" + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/kubeflow/notebooks/workspaces/backend/internal/auth" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds" repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspacekinds" ) @@ -33,12 +36,25 @@ type WorkspaceKindEnvelope Envelope[models.WorkspaceKind] func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { name := ps.ByName("name") - if name == "" { a.serverErrorResponse(w, r, fmt.Errorf("workspace kind name is missing")) return } + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbGet, + &kubefloworgv1beta1.WorkspaceKind{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + }, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + workspaceKind, err := a.repositories.WorkspaceKind.GetWorkspaceKind(r.Context(), name) if err != nil { if errors.Is(err, repository.ErrWorkspaceKindNotFound) { @@ -60,6 +76,18 @@ func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps } func (a *App) GetWorkspaceKindsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbList, + &kubefloworgv1beta1.WorkspaceKind{}, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + workspaceKinds, err := a.repositories.WorkspaceKind.GetWorkspaceKinds(r.Context()) if err != nil { a.serverErrorResponse(w, r, err) diff --git a/workspaces/backend/api/workspacekinds_handler_test.go b/workspaces/backend/api/workspacekinds_handler_test.go index 8d19f261..03683a62 100644 --- a/workspaces/backend/api/workspacekinds_handler_test.go +++ b/workspaces/backend/api/workspacekinds_handler_test.go @@ -32,9 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/kubeflow/notebooks/workspaces/backend/internal/config" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds" - "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" ) var _ = Describe("WorkspaceKinds Handler", func() { @@ -47,8 +45,6 @@ var _ = Describe("WorkspaceKinds Handler", func() { const namespaceName1 = "wsk-exist-test-ns1" var ( - a App - workspaceKind1Name string workspaceKind1Key types.NamespacedName workspaceKind2Name string @@ -62,14 +58,6 @@ var _ = Describe("WorkspaceKinds Handler", func() { workspaceKind2Name = fmt.Sprintf("workspacekind-2-%s", uniqueName) workspaceKind2Key = types.NamespacedName{Name: workspaceKind2Name} - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - By("creating Namespace 1") namespace1 := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -111,7 +99,6 @@ var _ = Describe("WorkspaceKinds Handler", func() { }, } Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed()) - }) It("should retrieve the all WorkspaceKinds successfully", func() { @@ -119,6 +106,9 @@ var _ = Describe("WorkspaceKinds Handler", func() { req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspaceKindsHandler") ps := httprouter.Params{} rr := httptest.NewRecorder() @@ -164,6 +154,9 @@ var _ = Describe("WorkspaceKinds Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspaceKindHandler") ps := httprouter.Params{ httprouter.Param{Key: WorkspaceKindNamePathParam, Value: workspaceKind1Name}, @@ -206,23 +199,14 @@ var _ = Describe("WorkspaceKinds Handler", func() { // therefore, we run them using the `Serial` Ginkgo decorators. Context("with no existing WorkspaceKinds", Serial, func() { - var a App - - BeforeEach(func() { - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - }) - It("should return an empty list of WorkspaceKinds", func() { By("creating the HTTP request") req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspacesHandler") ps := httprouter.Params{} rr := httptest.NewRecorder() @@ -254,6 +238,9 @@ var _ = Describe("WorkspaceKinds Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspaceKindHandler") ps := httprouter.Params{ httprouter.Param{Key: WorkspaceNamePathParam, Value: missingWorkspaceKindName}, diff --git a/workspaces/backend/api/workspaces_handler.go b/workspaces/backend/api/workspaces_handler.go index aa970f77..726395ee 100644 --- a/workspaces/backend/api/workspaces_handler.go +++ b/workspaces/backend/api/workspaces_handler.go @@ -23,7 +23,10 @@ import ( "net/http" "github.com/julienschmidt/httprouter" + kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/kubeflow/notebooks/workspaces/backend/internal/auth" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces" repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces" ) @@ -34,20 +37,35 @@ type WorkspaceEnvelope Envelope[models.Workspace] func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { namespace := ps.ByName(NamespacePathParam) - workspaceName := ps.ByName(WorkspaceNamePathParam) - - var workspace models.Workspace - var err error if namespace == "" { a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil")) return } + + workspaceName := ps.ByName(WorkspaceNamePathParam) if workspaceName == "" { a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil")) return } - workspace, err = a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName) + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbGet, + &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: workspaceName, + }, + }, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + + workspace, err := a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName) if err != nil { if errors.Is(err, repository.ErrWorkspaceNotFound) { a.notFoundResponse(w, r) @@ -71,6 +89,22 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { namespace := ps.ByName(NamespacePathParam) + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbList, + &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + }, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + var workspaces []models.Workspace var err error if namespace == "" { @@ -95,7 +129,6 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { namespace := ps.ByName("namespace") - if namespace == "" { a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing")) return @@ -109,6 +142,23 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps workspaceModel.Namespace = namespace + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbCreate, + &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: workspaceModel.Name, + }, + }, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + createdWorkspace, err := a.repositories.Workspace.CreateWorkspace(r.Context(), workspaceModel) if err != nil { a.serverErrorResponse(w, r, fmt.Errorf("error creating workspace: %w", err)) @@ -129,18 +179,34 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps func (a *App) DeleteWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { namespace := ps.ByName("namespace") - workspaceName := ps.ByName("name") - if namespace == "" { a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing")) return } + workspaceName := ps.ByName("name") if workspaceName == "" { a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing")) return } + // =========================== AUTH =========================== + authPolicies := []*auth.ResourcePolicy{ + auth.NewResourcePolicy( + auth.ResourceVerbDelete, + &kubefloworgv1beta1.Workspace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: workspaceName, + }, + }, + ), + } + if success := a.requireAuth(w, r, authPolicies); !success { + return + } + // ============================================================ + err := a.repositories.Workspace.DeleteWorkspace(r.Context(), namespace, workspaceName) if err != nil { if errors.Is(err, repository.ErrWorkspaceNotFound) { diff --git a/workspaces/backend/api/workspaces_handler_test.go b/workspaces/backend/api/workspaces_handler_test.go index 0f367988..fbc80e82 100644 --- a/workspaces/backend/api/workspaces_handler_test.go +++ b/workspaces/backend/api/workspaces_handler_test.go @@ -34,9 +34,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" - "github.com/kubeflow/notebooks/workspaces/backend/internal/config" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces" - "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories" ) var _ = Describe("Workspaces Handler", func() { @@ -50,8 +48,6 @@ var _ = Describe("Workspaces Handler", func() { const namespaceName2 = "ws-exist-ns2" var ( - a App - workspaceName1 string workspaceKey1 types.NamespacedName workspaceName2 string @@ -74,14 +70,6 @@ var _ = Describe("Workspaces Handler", func() { workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) workspaceKindKey = types.NamespacedName{Name: workspaceKindName} - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - By("creating Namespace 1") namespace1 := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -173,6 +161,9 @@ var _ = Describe("Workspaces Handler", func() { req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspacesHandler") ps := httprouter.Params{} rr := httptest.NewRecorder() @@ -225,6 +216,9 @@ var _ = Describe("Workspaces Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspacesHandler") ps := httprouter.Params{ httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, @@ -277,6 +271,9 @@ var _ = Describe("Workspaces Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspaceHandler") ps := httprouter.Params{ httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, @@ -327,8 +324,6 @@ var _ = Describe("Workspaces Handler", func() { const namespaceName1 = "ws-invalid-ns1" var ( - a App - workspaceMissingWskName string workspaceMissingWskKey types.NamespacedName @@ -353,14 +348,6 @@ var _ = Describe("Workspaces Handler", func() { workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) workspaceKindKey = types.NamespacedName{Name: workspaceKindName} - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - By("creating Namespace 1") namespace1 := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -439,6 +426,9 @@ var _ = Describe("Workspaces Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspacesHandler") ps := httprouter.Params{ httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, @@ -506,6 +496,9 @@ var _ = Describe("Workspaces Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspaceHandler") ps := httprouter.Params{ httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, @@ -542,23 +535,14 @@ var _ = Describe("Workspaces Handler", func() { // therefore, we run them using the `Serial` Ginkgo decorators. Context("with no existing Workspaces", Serial, func() { - var a App - - BeforeEach(func() { - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - }) - It("should return an empty list of Workspaces for all namespaces", func() { By("creating the HTTP request") req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspacesHandler") ps := httprouter.Params{} rr := httptest.NewRecorder() @@ -590,6 +574,9 @@ var _ = Describe("Workspaces Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspacesHandler") ps := httprouter.Params{ httprouter.Param{Key: NamespacePathParam, Value: missingNamespace}, @@ -625,6 +612,9 @@ var _ = Describe("Workspaces Handler", func() { req, err := http.NewRequest(http.MethodGet, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing GetWorkspaceHandler") ps := httprouter.Params{ httprouter.Param{Key: NamespacePathParam, Value: missingNamespace}, @@ -647,7 +637,6 @@ var _ = Describe("Workspaces Handler", func() { const namespaceNameCrud = "ws-crud-ns" var ( - a App workspaceName string workspaceKey types.NamespacedName workspaceKindName string @@ -661,14 +650,6 @@ var _ = Describe("Workspaces Handler", func() { workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) workspaceKindKey = types.NamespacedName{Name: workspaceKindName} - repos := repositories.NewRepositories(k8sClient) - a = App{ - Config: config.EnvConfig{ - Port: 4000, - }, - repositories: repos, - } - By("creating the Namespace") namespaceA := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -755,6 +736,9 @@ var _ = Describe("Workspaces Handler", func() { Expect(err).NotTo(HaveOccurred()) req.Header.Set("Content-Type", "application/json") + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing CreateWorkspaceHandler") rr := httptest.NewRecorder() ps := httprouter.Params{ @@ -783,6 +767,9 @@ var _ = Describe("Workspaces Handler", func() { req, err = http.NewRequest(http.MethodDelete, path, http.NoBody) Expect(err).NotTo(HaveOccurred()) + By("setting the auth headers") + req.Header.Set(userIdHeader, adminUser) + By("executing DeleteWorkspaceHandler") rr = httptest.NewRecorder() ps = httprouter.Params{ diff --git a/workspaces/backend/cmd/main.go b/workspaces/backend/cmd/main.go index c26a0bb2..9b07c306 100644 --- a/workspaces/backend/cmd/main.go +++ b/workspaces/backend/cmd/main.go @@ -26,27 +26,78 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" application "github.com/kubeflow/notebooks/workspaces/backend/api" + "github.com/kubeflow/notebooks/workspaces/backend/internal/auth" "github.com/kubeflow/notebooks/workspaces/backend/internal/config" "github.com/kubeflow/notebooks/workspaces/backend/internal/helper" "github.com/kubeflow/notebooks/workspaces/backend/internal/server" ) func main() { - var cfg config.EnvConfig - flag.IntVar(&cfg.Port, "port", getEnvAsInt("PORT", 4000), "API server port") + // Define command line flags + cfg := &config.EnvConfig{} + flag.IntVar(&cfg.Port, + "port", + getEnvAsInt("PORT", 4000), + "API server port", + ) + flag.Float64Var( + &cfg.ClientQPS, + "client-qps", + getEnvAsFloat64("CLIENT_QPS", 50), + "QPS configuration passed to rest.Client", + ) + flag.IntVar( + &cfg.ClientBurst, + "client-burst", + getEnvAsInt("CLIENT_BURST", 100), + "Maximum Burst configuration passed to rest.Client", + ) + flag.BoolVar( + // TODO: remove before GA + &cfg.DisableAuth, + "disable-auth", + getEnvAsBool("DISABLE_AUTH", true), + "Disable authentication and authorization", + ) + flag.StringVar( + &cfg.UserIdHeader, + "userid-header", + getEnvAsStr("USERID_HEADER", "kubeflow-userid"), + "Key of request header containing user id", + ) + flag.StringVar( + &cfg.UserIdPrefix, + "userid-prefix", + getEnvAsStr("USERID_PREFIX", ":"), + "Request header user id common prefix", + ) + flag.StringVar( + &cfg.GroupsHeader, + "groups-header", + getEnvAsStr("GROUPS_HEADER", "kubeflow-groups"), + "Key of request header containing user groups", + ) + // Initialize the logger logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - kubeconfig, err := helper.GetKubeconfig() + // Build the Kubernetes client configuration + kubeconfig, err := ctrl.GetConfig() if err != nil { - logger.Error("failed to get kubeconfig", "error", err) + logger.Error("failed to get Kubernetes config", "error", err) os.Exit(1) } + kubeconfig.QPS = float32(cfg.ClientQPS) + kubeconfig.Burst = cfg.ClientBurst + + // Build the Kubernetes scheme scheme, err := helper.BuildScheme() if err != nil { logger.Error("failed to build Kubernetes scheme", "error", err) os.Exit(1) } + + // Create the controller manager mgr, err := ctrl.NewManager(kubeconfig, ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ @@ -60,7 +111,21 @@ func main() { os.Exit(1) } - app, err := application.NewApp(cfg, logger, mgr.GetClient(), mgr.GetScheme()) + // Create the request authenticator + reqAuthN, err := auth.NewRequestAuthenticator(cfg.UserIdHeader, cfg.UserIdPrefix, cfg.GroupsHeader) + if err != nil { + logger.Error("failed to create request authenticator", "error", err) + os.Exit(1) + } + + // Create the request authorizer + reqAuthZ, err := auth.NewRequestAuthorizer(mgr.GetConfig(), mgr.GetHTTPClient()) + if err != nil { + logger.Error("failed to create request authorizer", "error", err) + } + + // Create the application and server + app, err := application.NewApp(cfg, logger, mgr.GetClient(), mgr.GetScheme(), reqAuthN, reqAuthZ) if err != nil { logger.Error("failed to create app", "error", err) os.Exit(1) @@ -75,6 +140,7 @@ func main() { os.Exit(1) } + // Start the controller manager logger.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { logger.Error("problem running manager", "error", err) @@ -90,3 +156,28 @@ func getEnvAsInt(name string, defaultVal int) int { } return defaultVal } + +func getEnvAsFloat64(name string, defaultVal float64) float64 { + if value, exists := os.LookupEnv(name); exists { + if floatValue, err := strconv.ParseFloat(value, 64); err == nil { + return floatValue + } + } + return defaultVal +} + +func getEnvAsStr(name string, defaultVal string) string { + if value, exists := os.LookupEnv(name); exists { + return value + } + return defaultVal +} + +func getEnvAsBool(name string, defaultVal bool) bool { + if value, exists := os.LookupEnv(name); exists { + if boolValue, err := strconv.ParseBool(value); err == nil { + return boolValue + } + } + return defaultVal +} diff --git a/workspaces/backend/go.mod b/workspaces/backend/go.mod index 11966de1..b459b779 100644 --- a/workspaces/backend/go.mod +++ b/workspaces/backend/go.mod @@ -9,23 +9,29 @@ require ( github.com/kubeflow/notebooks/workspaces/controller v0.0.0 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 - github.com/stretchr/testify v1.9.0 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 + k8s.io/apiserver v0.31.0 k8s.io/client-go v0.31.0 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/controller-runtime v0.19.1 ) require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -34,12 +40,15 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/cel-go v0.20.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/imdario/mergo v0.3.6 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -47,31 +56,46 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.31.0 // indirect + k8s.io/component-base v0.31.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/workspaces/backend/go.sum b/workspaces/backend/go.sum index b8c5755a..e2470d9c 100644 --- a/workspaces/backend/go.sum +++ b/workspaces/backend/go.sum @@ -1,7 +1,16 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,12 +22,17 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -36,6 +50,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -48,8 +64,12 @@ github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2 github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -93,12 +113,18 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -108,6 +134,22 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -132,6 +174,8 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -157,6 +201,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -166,6 +216,7 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -178,14 +229,20 @@ k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24 k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY= +k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk= k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= +k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/workspaces/backend/internal/auth/authentication.go b/workspaces/backend/internal/auth/authentication.go new file mode 100644 index 00000000..025b1233 --- /dev/null +++ b/workspaces/backend/internal/auth/authentication.go @@ -0,0 +1,70 @@ +/* +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 auth + +import ( + "fmt" + "net/http" + "strings" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/request/headerrequest" + "k8s.io/apiserver/pkg/authentication/user" +) + +// NewRequestAuthenticator returns a new request authenticator based on the provided configuration. +func NewRequestAuthenticator(useridHeader string, useridPrefix string, groupsHeader string) (authenticator.Request, error) { + + // create an upstream `requestHeaderAuthRequestHandler` to extract user and groups from the request headers + requestHeaderAuthenticator, err := headerrequest.New( + []string{useridHeader}, + []string{groupsHeader}, + nil, + ) + if err != nil { + return nil, fmt.Errorf("failed to create request header authenticator: %w", err) + } + + // if the user id prefix is empty, return the upstream authenticator as is + if useridPrefix == "" { + return requestHeaderAuthenticator, nil + } + + // wrap the upstream authenticator to trim the user prefix from the user id + requestAuthenticator := authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) { + response, ok, err := requestHeaderAuthenticator.AuthenticateRequest(req) + if err != nil { + return nil, false, err + } + + // if the request was not authenticated, return the response as is + if !ok { + return response, ok, nil + } + + // trim the user id prefix from the username + return &authenticator.Response{ + User: &user.DefaultInfo{ + Name: strings.TrimPrefix(response.User.GetName(), useridPrefix), + Groups: response.User.GetGroups(), + Extra: response.User.GetExtra(), + }, + }, true, nil + }) + + return requestAuthenticator, nil +} diff --git a/workspaces/backend/internal/auth/authorization.go b/workspaces/backend/internal/auth/authorization.go new file mode 100644 index 00000000..28aeec5f --- /dev/null +++ b/workspaces/backend/internal/auth/authorization.go @@ -0,0 +1,128 @@ +/* +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 auth + +import ( + "fmt" + "net/http" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/authorization/authorizerfactory" + authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + allowCacheTTL = 10 * time.Second + denyCacheTTL = 10 * time.Second +) + +// NewRequestAuthorizer returns a new request authorizer based on the provided configuration. +// loosely based on `WithAuthenticationAndAuthorization` from: https://github.com/kubernetes-sigs/controller-runtime/blob/v0.20.1/pkg/metrics/filters/filters.go#L36-L122 +func NewRequestAuthorizer(restConfig *rest.Config, httpClient *http.Client) (authorizer.Authorizer, error) { + authorizationV1Client, err := authorizationv1.NewForConfigAndClient(restConfig, httpClient) + if err != nil { + return nil, err + } + + authorizerConfig := authorizerfactory.DelegatingAuthorizerConfig{ + SubjectAccessReviewClient: authorizationV1Client, + + // AllowCacheTTL is the length of time that a successful authorization response will be cached + AllowCacheTTL: allowCacheTTL, + + // DenyCacheTTL is the length of time that a denied authorization response will be cached + DenyCacheTTL: denyCacheTTL, + + // wait.Backoff is copied from: https://github.com/kubernetes/apiserver/blob/v0.29.0/pkg/server/options/authentication.go#L43-L50 + // options.DefaultAuthWebhookRetryBackoff is not used to avoid a dependency on "k8s.io/apiserver/pkg/server/options". + WebhookRetryBackoff: &wait.Backoff{ + Duration: 500 * time.Millisecond, + Factor: 1.5, + Jitter: 0.2, + Steps: 5, + }, + } + + delegatingAuthorizer, err := authorizerConfig.New() + if err != nil { + return nil, fmt.Errorf("failed to create authorizer: %w", err) + } + + return delegatingAuthorizer, nil +} + +type ResourcePolicy struct { + Verb ResourceVerb + + Group string + Version string + Kind string + + Namespace string + Name string +} + +// NewResourcePolicy returns a new resource policy based on the provided verb and resource object. +func NewResourcePolicy(verb ResourceVerb, object client.Object) *ResourcePolicy { + policy := &ResourcePolicy{ + Verb: verb, + Group: object.GetObjectKind().GroupVersionKind().Group, + Version: object.GetObjectKind().GroupVersionKind().Version, + Kind: object.GetObjectKind().GroupVersionKind().Kind, + } + + if object.GetNamespace() != "" { + policy.Namespace = object.GetNamespace() + } + + if object.GetName() != "" { + policy.Name = object.GetName() + } + + return policy +} + +// AttributesFor returns an authorizer.Attributes which could be used with an authorizer.Authorizer to authorize the user for the resource policy. +func (p *ResourcePolicy) AttributesFor(u user.Info) authorizer.Attributes { + return authorizer.AttributesRecord{ + User: u, + Verb: string(p.Verb), + Namespace: p.Namespace, + APIGroup: p.Group, + APIVersion: p.Version, + Resource: p.Kind, + Name: p.Name, + ResourceRequest: true, + } +} + +// ResourceVerb represents a verb for an action on a resource. +type ResourceVerb string + +const ( + ResourceVerbCreate ResourceVerb = "create" + ResourceVerbGet ResourceVerb = "get" + ResourceVerbList ResourceVerb = "list" + ResourceVerbUpdate ResourceVerb = "update" + ResourceVerbPatch ResourceVerb = "patch" + ResourceVerbDelete ResourceVerb = "delete" +) diff --git a/workspaces/backend/internal/config/environment.go b/workspaces/backend/internal/config/environment.go index 88a4c315..70dc3f37 100644 --- a/workspaces/backend/internal/config/environment.go +++ b/workspaces/backend/internal/config/environment.go @@ -18,4 +18,13 @@ package config type EnvConfig struct { Port int + + ClientQPS float64 + ClientBurst int + + DisableAuth bool + + UserIdHeader string + UserIdPrefix string + GroupsHeader string } diff --git a/workspaces/backend/internal/helper/k8s.go b/workspaces/backend/internal/helper/k8s.go index 2b8661c1..74532f8b 100644 --- a/workspaces/backend/internal/helper/k8s.go +++ b/workspaces/backend/internal/helper/k8s.go @@ -19,22 +19,11 @@ package helper import ( "fmt" - "k8s.io/apimachinery/pkg/runtime" - clientRest "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" + "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ) -// GetKubeconfig returns the current KUBECONFIG configuration based on the default loading rules. -func GetKubeconfig() (*clientRest.Config, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - configOverrides := &clientcmd.ConfigOverrides{} - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) - return kubeConfig.ClientConfig() -} - // BuildScheme returns builds a new runtime scheme with all the necessary types registered. func BuildScheme() (*runtime.Scheme, error) { scheme := runtime.NewScheme()