diff --git a/api/applications/applications_handler_factory.go b/api/applications/applications_handler_factory.go index d54a24a9..711e61f9 100644 --- a/api/applications/applications_handler_factory.go +++ b/api/applications/applications_handler_factory.go @@ -2,6 +2,7 @@ package applications import ( "context" + "github.com/equinor/radix-api/api/utils/access" "github.com/equinor/radix-api/models" authorizationapi "k8s.io/api/authorization/v1" diff --git a/api/environments/component_handler.go b/api/environments/component_handler.go index a2a8e56c..97d60fb2 100644 --- a/api/environments/component_handler.go +++ b/api/environments/component_handler.go @@ -108,6 +108,11 @@ func (eh EnvironmentHandler) RestartComponent(ctx context.Context, appName, envN // RestartComponentAuxiliaryResource Restarts a component's auxiliary resource func (eh EnvironmentHandler) RestartComponentAuxiliaryResource(ctx context.Context, appName, envName, componentName, auxType string) error { log.Ctx(ctx).Info().Msgf("Restarting auxiliary resource %s for component %s, %s", auxType, componentName, appName) + if isAdmin, err := kubequery.IsRadixApplicationAdmin(ctx, eh.accounts.UserAccount.Client, appName); err != nil { + return err + } else if !isAdmin { + return http.ForbiddenError("you must be administrator to restart the Oauth2 Proxy service") + } radixDeployment, err := kubequery.GetLatestRadixDeployment(ctx, eh.accounts.UserAccount.RadixClient, appName, envName) if err != nil { @@ -163,7 +168,7 @@ func canDeploymentBeRestarted(deployment *appsv1.Deployment) bool { } func (eh EnvironmentHandler) patchDeploymentForRestart(ctx context.Context, deployment *appsv1.Deployment) error { - deployClient := eh.accounts.UserAccount.Client.AppsV1().Deployments(deployment.GetNamespace()) + deployClient := eh.accounts.ServiceAccount.Client.AppsV1().Deployments(deployment.GetNamespace()) return retry.RetryOnConflict(retry.DefaultRetry, func() error { deployToPatch, err := deployClient.Get(ctx, deployment.GetName(), metav1.GetOptions{}) diff --git a/api/environments/environment_controller_test.go b/api/environments/environment_controller_test.go index 8b978eba..4f520e45 100644 --- a/api/environments/environment_controller_test.go +++ b/api/environments/environment_controller_test.go @@ -34,7 +34,7 @@ import ( operatorutils "github.com/equinor/radix-operator/pkg/apis/utils" "github.com/equinor/radix-operator/pkg/apis/utils/labels" radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" - "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" + radixfake "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" "github.com/golang/mock/gomock" kedav2 "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned" kedafake "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/fake" @@ -43,10 +43,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" + authorizationapiv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" kubefake "k8s.io/client-go/kubernetes/fake" + testing2 "k8s.io/client-go/testing" secretsstorevclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" secretproviderfake "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/fake" ) @@ -64,10 +67,10 @@ const ( subscriptionId = "12347718-c8f8-4995-bfbb-02655ff1f89c" ) -func setupTest(t *testing.T, envHandlerOpts []EnvironmentHandlerOptions) (*commontest.Utils, *controllertest.Utils, *controllertest.Utils, kubernetes.Interface, radixclient.Interface, kedav2.Interface, prometheusclient.Interface, secretsstorevclient.Interface, *certclientfake.Clientset) { +func setupTest(t *testing.T, envHandlerOpts []EnvironmentHandlerOptions) (*commontest.Utils, *controllertest.Utils, *controllertest.Utils, *kubefake.Clientset, radixclient.Interface, kedav2.Interface, prometheusclient.Interface, secretsstorevclient.Interface, *certclientfake.Clientset) { // Setup - kubeclient := kubefake.NewSimpleClientset() - radixClient := fake.NewSimpleClientset() + kubeclient := kubefake.NewClientset() + radixClient := radixfake.NewSimpleClientset() kedaClient := kedafake.NewSimpleClientset() prometheusclient := prometheusfake.NewSimpleClientset() secretproviderclient := secretproviderfake.NewSimpleClientset() @@ -1005,13 +1008,42 @@ func Test_GetEnvironmentEvents_Handler(t *testing.T) { func TestRestartAuxiliaryResource(t *testing.T) { auxType := "oauth" + called := 0 // Setup commonTestUtils, environmentControllerTestUtils, _, kubeClient, _, _, _, _, _ := setupTest(t, nil) + kubeClient.Fake.PrependReactor("create", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.DeepCopy().(testing2.CreateAction) + if !ok { + return false, nil, nil + } + + review, ok := createAction.GetObject().(*authorizationapiv1.SelfSubjectAccessReview) + if !ok { + return false, nil, nil + } + + called++ + + if review.Spec.ResourceAttributes.Name != anyAppName { + return true, review, nil + } + + assert.Equal(t, review.Spec.ResourceAttributes.Name, anyAppName) + assert.Equal(t, review.Spec.ResourceAttributes.Resource, v1.ResourceRadixRegistrations) + assert.Equal(t, review.Spec.ResourceAttributes.Verb, "patch") + + review.Status.Allowed = true + return true, review, nil + }) _, err := commonTestUtils.ApplyRegistration(operatorutils. NewRegistrationBuilder(). WithName(anyAppName)) require.NoError(t, err) + _, err = commonTestUtils.ApplyRegistration(operatorutils. + NewRegistrationBuilder(). + WithName("forbidden")) + require.NoError(t, err) _, err = commonTestUtils.ApplyApplication(operatorutils. NewRadixApplicationBuilder(). WithAppName(anyAppName). @@ -1045,9 +1077,17 @@ func TestRestartAuxiliaryResource(t *testing.T) { responseChannel := environmentControllerTestUtils.ExecuteRequest("POST", fmt.Sprintf("/api/v1/applications/%s/environments/%s/components/%s/aux/%s/restart", anyAppName, anyEnvironment, anyComponentName, auxType)) response := <-responseChannel assert.Equal(t, http.StatusOK, response.Code) + assert.Equal(t, 1, called) kubeDeploy, _ := kubeClient.AppsV1().Deployments(envNs).Get(context.Background(), "comp1-aux-resource", metav1.GetOptions{}) assert.NotEmpty(t, kubeDeploy.Spec.Template.Annotations[restartedAtAnnotation]) + + // Test Forbidden for other app names + + responseChannel = environmentControllerTestUtils.ExecuteRequest("POST", fmt.Sprintf("/api/v1/applications/%s/environments/%s/components/%s/aux/%s/restart", "forbidden", anyEnvironment, anyComponentName, auxType)) + response = <-responseChannel + assert.Equal(t, http.StatusForbidden, response.Code) + assert.Equal(t, 2, called) } func Test_GetJobs(t *testing.T) { diff --git a/api/environments/environment_handler.go b/api/environments/environment_handler.go index 696e8726..edd09999 100644 --- a/api/environments/environment_handler.go +++ b/api/environments/environment_handler.go @@ -23,7 +23,7 @@ import ( "github.com/equinor/radix-common/utils/slice" deployUtils "github.com/equinor/radix-operator/pkg/apis/deployment" "github.com/equinor/radix-operator/pkg/apis/kube" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" k8sObjectUtils "github.com/equinor/radix-operator/pkg/apis/utils" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/api/errors" @@ -126,7 +126,7 @@ func (eh EnvironmentHandler) GetEnvironmentSummary(ctx context.Context, appName if err != nil { return nil, err } - envNames := slice.Map(reList, func(re v1.RadixEnvironment) string { return re.Spec.EnvName }) + envNames := slice.Map(reList, func(re radixv1.RadixEnvironment) string { return re.Spec.EnvName }) rdList, err := kubequery.GetRadixDeploymentsForEnvironments(ctx, eh.accounts.UserAccount.RadixClient, appName, envNames, 10) if err != nil { return nil, err @@ -208,7 +208,7 @@ func (eh EnvironmentHandler) GetEnvironment(ctx context.Context, appName, envNam } // CreateEnvironment Handler for CreateEnvironment. Creates an environment if it does not exist -func (eh EnvironmentHandler) CreateEnvironment(ctx context.Context, appName, envName string) (*v1.RadixEnvironment, error) { +func (eh EnvironmentHandler) CreateEnvironment(ctx context.Context, appName, envName string) (*radixv1.RadixEnvironment, error) { // ensure application exists rr, err := eh.accounts.UserAccount.RadixClient.RadixV1().RadixRegistrations().Get(ctx, appName, metav1.GetOptions{}) if err != nil { @@ -286,7 +286,7 @@ func (eh EnvironmentHandler) getNotOrphanedEnvNames(ctx context.Context, appName } return slice.Map( slice.FindAll(reList, predicate.IsNotOrphanEnvironment), - func(re v1.RadixEnvironment) string { return re.Spec.EnvName }, + func(re radixv1.RadixEnvironment) string { return re.Spec.EnvName }, ), nil } @@ -315,7 +315,7 @@ func (eh EnvironmentHandler) StopEnvironment(ctx context.Context, appName, envNa return err } if radixDeployment == nil { - return http.ValidationError(v1.KindRadixDeployment, "no radix deployments found") + return http.ValidationError(radixv1.KindRadixDeployment, "no radix deployments found") } log.Ctx(ctx).Info().Msgf("Stopping components in environment %s, %s", envName, appName) @@ -335,7 +335,7 @@ func (eh EnvironmentHandler) ResetManuallyStoppedComponentsInEnvironment(ctx con return err } if radixDeployment == nil { - return http.ValidationError(v1.KindRadixDeployment, "no radix deployments found") + return http.ValidationError(radixv1.KindRadixDeployment, "no radix deployments found") } log.Ctx(ctx).Info().Msgf("Starting components in environment %s, %s", envName, appName) @@ -356,7 +356,7 @@ func (eh EnvironmentHandler) RestartEnvironment(ctx context.Context, appName, en return err } if radixDeployment == nil { - return http.ValidationError(v1.KindRadixDeployment, "no radix deployments found") + return http.ValidationError(radixv1.KindRadixDeployment, "no radix deployments found") } log.Ctx(ctx).Info().Msgf("Restarting components in environment %s, %s", envName, appName) @@ -423,7 +423,7 @@ func (eh EnvironmentHandler) getRadixCommonComponentUpdater(ctx context.Context, return nil, err } if rd == nil { - return nil, http.ValidationError(v1.KindRadixDeployment, "no radix deployments found") + return nil, http.ValidationError(radixv1.KindRadixDeployment, "no radix deployments found") } baseUpdater := &baseComponentUpdater{ appName: appName, @@ -432,7 +432,7 @@ func (eh EnvironmentHandler) getRadixCommonComponentUpdater(ctx context.Context, radixDeployment: rd, } var updater radixDeployCommonComponentUpdater - var componentToPatch v1.RadixCommonDeployComponent + var componentToPatch radixv1.RadixCommonDeployComponent componentIndex, componentToPatch := deployUtils.GetDeploymentComponent(rd, componentName) if !radixutils.IsNil(componentToPatch) { updater = &radixDeployComponentUpdater{base: baseUpdater} diff --git a/api/kubequery/radixapplication.go b/api/kubequery/radixapplication.go index de574eaa..082f8929 100644 --- a/api/kubequery/radixapplication.go +++ b/api/kubequery/radixapplication.go @@ -3,10 +3,13 @@ package kubequery import ( "context" + "github.com/equinor/radix-api/api/utils/access" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" operatorUtils "github.com/equinor/radix-operator/pkg/apis/utils" radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" + authorizationapi "k8s.io/api/authorization/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ) // GetRadixApplication returns the RadixApplication for the specified application name. @@ -14,3 +17,13 @@ func GetRadixApplication(ctx context.Context, client radixclient.Interface, appN ns := operatorUtils.GetAppNamespace(appName) return client.RadixV1().RadixApplications(ns).Get(ctx, appName, metav1.GetOptions{}) } + +func IsRadixApplicationAdmin(ctx context.Context, kubeClient kubernetes.Interface, appName string) (bool, error) { + return access.HasAccess(ctx, kubeClient, &authorizationapi.ResourceAttributes{ + Verb: "patch", + Group: radixv1.GroupName, + Resource: radixv1.ResourceRadixRegistrations, + Version: "*", + Name: appName, + }) +} diff --git a/api/kubequery/radixapplication_test.go b/api/kubequery/radixapplication_test.go index 321aea3a..1f1acb14 100644 --- a/api/kubequery/radixapplication_test.go +++ b/api/kubequery/radixapplication_test.go @@ -8,8 +8,12 @@ import ( radixfake "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + authorizationapiv1 "k8s.io/api/authorization/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + testing2 "k8s.io/client-go/testing" ) func Test_GetRadixApplication(t *testing.T) { @@ -26,3 +30,34 @@ func Test_GetRadixApplication(t *testing.T) { _, err = GetRadixApplication(context.Background(), client, "app2") assert.True(t, errors.IsNotFound(err)) } + +func Test_IsRadixApplicationAdmin(t *testing.T) { + called := 0 + client := fake.NewClientset() + client.Fake.PrependReactor("create", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) { + createAction, ok := action.DeepCopy().(testing2.CreateAction) + if !ok { + return false, nil, nil + } + + review, ok := createAction.GetObject().(*authorizationapiv1.SelfSubjectAccessReview) + if !ok { + return false, nil, nil + } + + called++ + assert.Equal(t, review.Spec.ResourceAttributes, &authorizationapiv1.ResourceAttributes{ + Verb: "patch", + Group: radixv1.GroupName, + Resource: radixv1.ResourceRadixRegistrations, + Version: "*", + Name: "any-app-name", + }) + return true, review, nil + }) + + actual, err := IsRadixApplicationAdmin(context.Background(), client, "any-app-name") + require.NoError(t, err) + assert.False(t, actual, "Should be false since we dont set it to true in our reactor") + assert.Equal(t, 1, called) +} diff --git a/go.mod b/go.mod index 563f096e..2f2b8898 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ toolchain go1.22.5 require ( github.com/cert-manager/cert-manager v1.15.0 - github.com/equinor/radix-common v1.9.4 + github.com/equinor/radix-common v1.9.5 github.com/equinor/radix-job-scheduler v1.11.0 - github.com/equinor/radix-operator v1.58.3 + github.com/equinor/radix-operator v1.59.1 github.com/evanphx/json-patch/v5 v5.9.0 github.com/felixge/httpsnoop v1.0.4 github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/go.sum b/go.sum index a34541a0..a34ae571 100644 --- a/go.sum +++ b/go.sum @@ -83,12 +83,12 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/equinor/radix-common v1.9.4 h1:ErSnB2tqlRwaQuQdaA0qzsReDtHDgubcvqRO098ncEw= -github.com/equinor/radix-common v1.9.4/go.mod h1:+g0Wj0D40zz29DjNkYKVmCVeYy4OsFWKI7Qi9rA6kpY= +github.com/equinor/radix-common v1.9.5 h1:p1xldkYUoavwIMguoxxOyVkOXLPA6K8qMsgzeztQtQw= +github.com/equinor/radix-common v1.9.5/go.mod h1:+g0Wj0D40zz29DjNkYKVmCVeYy4OsFWKI7Qi9rA6kpY= github.com/equinor/radix-job-scheduler v1.11.0 h1:8wCmXOVl/1cto8q2WJQEE06Cw68/QmfoifYVR49vzkY= github.com/equinor/radix-job-scheduler v1.11.0/go.mod h1:yPXn3kDcMY0Z3kBkosjuefsdY1x2g0NlBeybMmHz5hc= -github.com/equinor/radix-operator v1.58.3 h1:F4YhNkQ4uRONP125OTfG8hdy9PiyKlOWVO8/p2NIi70= -github.com/equinor/radix-operator v1.58.3/go.mod h1:DTPXOxU3uHPvji7qBGSK1b03iXROpX3l94kYjcOHkPM= +github.com/equinor/radix-operator v1.59.1 h1:wzb2tF4MWtGDWmyYuIv3oh17G5Bx8ztW9O+WgnI3QFc= +github.com/equinor/radix-operator v1.59.1/go.mod h1:uRW9SgVZ94hkpq87npVv2YVviRuXNJ1zgCleya1uvr8= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=