diff --git a/.vscode/launch.json b/.vscode/launch.json index 92558aa9..40125ec4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,6 @@ "RADIX_CONTAINER_REGISTRY":"radixdev.azurecr.io", "PIPELINE_IMG_TAG": "master-latest", "TEKTON_IMG_TAG": "main-latest", - "K8S_API_HOST": "https://weekly-24-clusters-dev-16ede4-uk527vqt.hcp.northeurope.azmk8s.io:443", "RADIX_CLUSTER_TYPE": "development", "RADIX_DNS_ZONE": "dev.radix.equinor.com", "RADIX_CLUSTERNAME": "weekly-24", @@ -25,11 +24,10 @@ "PROMETHEUS_URL":"http://localhost:9091", "RADIX_APP":"radix-api", "LOG_LEVEL":"info", - "LOG_PRETTY":"true" - }, - "args": [ - "--useOutClusterClient=false" - ] + "LOG_PRETTY":"true", + "OIDC_AUDIENCE": "6dae42f8-4368-4678-94ff-3960e28e3630", + "OIDC_ISSUER": "https://sts.windows.net/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/" + } } ] } diff --git a/Makefile b/Makefile index ae72015e..99fc75b1 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ mocks: bootstrap mockgen -source ./api/events/event_handler.go -destination ./api/events/mock/event_handler_mock.go -package mock mockgen -source ./api/environmentvariables/env_vars_handler.go -destination ./api/environmentvariables/env_vars_handler_mock.go -package environmentvariables mockgen -source ./api/environmentvariables/env_vars_handler_factory.go -destination ./api/environmentvariables/env_vars_handler_factory_mock.go -package environmentvariables + mockgen -source ./api/utils/token/validator.go -destination ./api/utils/token/mock/validator_mock.go -package mock .PHONY: test test: diff --git a/README.md b/README.md index 5e41a250..5ce47962 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,6 @@ The following env vars are needed. Useful default values in brackets. kubectl -n monitor port-forward svc/prometheus-operator-prometheus 9091:9090 ``` -You also probably want to start with the argument `--useOutClusterClient=false`. When `useOutClusterClient` is `false`, several debugging settings are enabled: -* a service principal with superpowers is used to authorize the requests, and the client's `Authorization` bearer token is ignored. -* the Radix API will connect to the currently-configured `kubectl` context and ignore `K8S_API_HOST`. -* the server CORS settings are modified to accept the `X-Requested-With` header in incoming requests. This is necessary to allow direct requests from web browser while e.g. debugging [radix-web-console](https://github.com/equinor/radix-web-console). -* verbose debugging output from CORS rule evaluation is logged to console. - - If you are using VSCode, there is a convenient launch configuration in `.vscode`. #### Validate code diff --git a/api/alerting/handler_test.go b/api/alerting/handler_test.go index 2d6c5cea..032d7720 100644 --- a/api/alerting/handler_test.go +++ b/api/alerting/handler_test.go @@ -8,7 +8,6 @@ import ( certclientfake "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/fake" alertModels "github.com/equinor/radix-api/api/alerting/models" "github.com/equinor/radix-api/models" - radixmodels "github.com/equinor/radix-common/models" operatoralert "github.com/equinor/radix-operator/pkg/apis/alert" "github.com/equinor/radix-operator/pkg/apis/kube" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" @@ -34,7 +33,7 @@ func (s *HandlerTestSuite) SetupTest() { kedaClient := kedafake.NewSimpleClientset() secretProviderClient := secretproviderfake.NewSimpleClientset() certClient := certclientfake.NewSimpleClientset() - s.accounts = models.NewAccounts(kubeClient, radixClient, kedaClient, secretProviderClient, nil, certClient, kubeClient, radixClient, kedaClient, secretProviderClient, nil, certClient, "", radixmodels.Impersonation{}) + s.accounts = models.NewAccounts(kubeClient, radixClient, kedaClient, secretProviderClient, nil, certClient, kubeClient, radixClient, kedaClient, secretProviderClient, nil, certClient) } func TestHandlerTestSuite(t *testing.T) { diff --git a/api/applications/applications_controller_test.go b/api/applications/applications_controller_test.go index b09a864d..80ed311c 100644 --- a/api/applications/applications_controller_test.go +++ b/api/applications/applications_controller_test.go @@ -20,6 +20,8 @@ import ( metricsMock "github.com/equinor/radix-api/api/metrics/mock" controllertest "github.com/equinor/radix-api/api/test" "github.com/equinor/radix-api/api/utils" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" + "github.com/equinor/radix-api/internal/config" "github.com/equinor/radix-api/models" radixhttp "github.com/equinor/radix-common/net/http" radixutils "github.com/equinor/radix-common/utils" @@ -55,7 +57,7 @@ const ( func setupTest(t *testing.T, requireAppConfigurationItem, requireAppADGroups bool) (*commontest.Utils, *controllertest.Utils, *kubefake.Clientset, *radixfake.Clientset, *kedafake.Clientset, *prometheusfake.Clientset, *secretproviderfake.Clientset, *certfake.Clientset) { return setupTestWithFactory(t, newTestApplicationHandlerFactory( - ApplicationHandlerConfig{RequireAppConfigurationItem: requireAppConfigurationItem, RequireAppADGroups: requireAppADGroups}, + config.Config{RequireAppConfigurationItem: requireAppConfigurationItem, RequireAppADGroups: requireAppADGroups}, func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { return true, nil }, @@ -79,15 +81,19 @@ func setupTestWithFactory(t *testing.T, handlerFactory ApplicationHandlerFactory prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) // controllerTestUtils is used for issuing HTTP request and processing responses + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) controllerTestUtils := controllertest.NewTestUtils( kubeclient, radixclient, kedaClient, secretproviderclient, certClient, - NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return true, nil - }, handlerFactory, prometheusHandlerMock), + mockValidator, + NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { + return true, nil + }, handlerFactory, prometheusHandlerMock), ) return &commonTestUtils, &controllerTestUtils, kubeclient, radixclient, kedaClient, prometheusclient, secretproviderclient, certClient @@ -116,18 +122,22 @@ func TestGetApplications_HasAccessToSomeRR(t *testing.T) { t.Run("no access", func(t *testing.T) { prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) controllerTestUtils := controllertest.NewTestUtils( kubeclient, radixclient, kedaClient, secretproviderclient, certClient, - NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return false, nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { - return true, nil - }), prometheusHandlerMock)) + mockValidator, + NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { + return false, nil + }, newTestApplicationHandlerFactory(config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }), prometheusHandlerMock)) responseChannel := controllerTestUtils.ExecuteRequest("GET", "/api/v1/applications") response := <-responseChannel @@ -139,12 +149,15 @@ func TestGetApplications_HasAccessToSomeRR(t *testing.T) { t.Run("access to single app", func(t *testing.T) { prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewApplicationController(func(_ context.Context, _ kubernetes.Interface, rr v1.RadixRegistration) (bool, error) { - return rr.GetName() == "my-second-app", nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { - return true, nil - }), prometheusHandlerMock)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, rr v1.RadixRegistration) (bool, error) { + return rr.GetName() == "my-second-app", nil + }, newTestApplicationHandlerFactory(config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }), prometheusHandlerMock)) responseChannel := controllerTestUtils.ExecuteRequest("GET", "/api/v1/applications") response := <-responseChannel @@ -156,12 +169,15 @@ func TestGetApplications_HasAccessToSomeRR(t *testing.T) { t.Run("access to all app", func(t *testing.T) { prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return true, nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { return true, nil - }), prometheusHandlerMock)) + }, newTestApplicationHandlerFactory(config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }), prometheusHandlerMock)) responseChannel := controllerTestUtils.ExecuteRequest("GET", "/api/v1/applications") response := <-responseChannel @@ -237,12 +253,15 @@ func TestSearchApplicationsPost(t *testing.T) { require.NoError(t, err) prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return true, nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { return true, nil - }), prometheusHandlerMock)) + }, newTestApplicationHandlerFactory(config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }), prometheusHandlerMock)) // Tests t.Run("search for "+appNames[0], func(t *testing.T) { @@ -319,12 +338,15 @@ func TestSearchApplicationsPost(t *testing.T) { t.Run("search for "+appNames[0]+" - no access", func(t *testing.T) { prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return false, nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { - return true, nil - }), prometheusHandlerMock)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { + return false, nil + }, newTestApplicationHandlerFactory(config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }), prometheusHandlerMock)) params := applicationModels.ApplicationsSearchRequest{Names: []string{appNames[0]}} responseChannel := controllerTestUtils.ExecuteRequestWithParameters("POST", "/api/v1/applications/_search", ¶ms) response := <-responseChannel @@ -415,12 +437,15 @@ func TestSearchApplicationsGet(t *testing.T) { require.NoError(t, err) prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return true, nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { return true, nil - }), prometheusHandlerMock)) + }, newTestApplicationHandlerFactory(config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }), prometheusHandlerMock)) // Tests t.Run("search for "+appNames[0], func(t *testing.T) { @@ -487,12 +512,15 @@ func TestSearchApplicationsGet(t *testing.T) { t.Run("search for "+appNames[0]+" - no access", func(t *testing.T) { prometheusHandlerMock := createPrometheusHandlerMock(t, radixclient, nil) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return false, nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { - return true, nil - }), prometheusHandlerMock)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { + return false, nil + }, newTestApplicationHandlerFactory(config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }), prometheusHandlerMock)) params := "apps=" + appNames[0] responseChannel := controllerTestUtils.ExecuteRequest("GET", "/api/v1/applications/_search?"+params) response := <-responseChannel @@ -857,7 +885,7 @@ func TestGetApplication_AllFieldsAreSet(t *testing.T) { assert.Equal(t, adUsers, application.Registration.AdUsers) assert.Equal(t, readerAdGroups, application.Registration.ReaderAdGroups) assert.Equal(t, readerAdUsers, application.Registration.ReaderAdUsers) - assert.Equal(t, "not-existing-test-radix-email@equinor.com", application.Registration.Creator) + assert.Equal(t, "test-principal", application.Registration.Creator) assert.Equal(t, "abranch", application.Registration.ConfigBranch) assert.Equal(t, "a/custom-radixconfig.yaml", application.Registration.RadixConfigFullName) assert.Equal(t, "ci", application.Registration.ConfigurationItem) @@ -1492,7 +1520,7 @@ func TestModifyApplication_UpdateADGroupValidation(t *testing.T) { for _, ts := range scenarios { t.Run(ts.name, func(t *testing.T) { _, controllerTestUtils, _, radixClient, _, _, _, _ := setupTestWithFactory(t, newTestApplicationHandlerFactory( - ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: ts.requireAppADGroups}, + config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: ts.requireAppADGroups}, func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { return ts.hasAccessToAdGroups, nil }, @@ -1993,13 +2021,23 @@ func Test_GetUsedResources(t *testing.T) { Times(1). Return(ts.expectedUsedResources, ts.expectedUsedResourcesError) } + validator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + validator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).Times(1).Return(controllertest.NewTestPrincipal(true), nil) prometheusHandlerMock := createPrometheusHandlerMock(t, radixClient, &mockHandlerModifier) - controllerTestUtils := controllertest.NewTestUtils(kubeClient, radixClient, kedaClient, secretProviderClient, certClient, NewApplicationController(func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { - return true, nil - }, newTestApplicationHandlerFactory(ApplicationHandlerConfig{RequireAppConfigurationItem: true, RequireAppADGroups: true}, - func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { - return true, nil - }), prometheusHandlerMock)) + controllerTestUtils := controllertest.NewTestUtils(kubeClient, radixClient, kedaClient, secretProviderClient, certClient, validator, + NewApplicationController( + func(_ context.Context, _ kubernetes.Interface, _ v1.RadixRegistration) (bool, error) { + return true, nil + }, + newTestApplicationHandlerFactory( + config.Config{RequireAppConfigurationItem: true, RequireAppADGroups: true}, + func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) { + return true, nil + }, + ), + prometheusHandlerMock, + ), + ) responseChannel := controllerTestUtils.ExecuteRequest("GET", fmt.Sprintf("/api/v1/applications/%s/resources%s", appName1, ts.queryString)) response := <-responseChannel @@ -2085,11 +2123,11 @@ func buildApplicationRegistrationRequest(applicationRegistration applicationMode } type testApplicationHandlerFactory struct { - config ApplicationHandlerConfig + config config.Config hasAccessToGetConfigMap hasAccessToGetConfigMapFunc } -func newTestApplicationHandlerFactory(config ApplicationHandlerConfig, hasAccessToGetConfigMap hasAccessToGetConfigMapFunc) *testApplicationHandlerFactory { +func newTestApplicationHandlerFactory(config config.Config, hasAccessToGetConfigMap hasAccessToGetConfigMapFunc) *testApplicationHandlerFactory { return &testApplicationHandlerFactory{ config: config, hasAccessToGetConfigMap: hasAccessToGetConfigMap, diff --git a/api/applications/applications_handler.go b/api/applications/applications_handler.go index a11a439b..eeab8f2b 100644 --- a/api/applications/applications_handler.go +++ b/api/applications/applications_handler.go @@ -11,12 +11,12 @@ import ( "time" applicationModels "github.com/equinor/radix-api/api/applications/models" - "github.com/equinor/radix-api/api/deployments" "github.com/equinor/radix-api/api/environments" - job "github.com/equinor/radix-api/api/jobs" jobModels "github.com/equinor/radix-api/api/jobs/models" "github.com/equinor/radix-api/api/kubequery" + "github.com/equinor/radix-api/api/middleware/auth" apimodels "github.com/equinor/radix-api/api/models" + "github.com/equinor/radix-api/internal/config" "github.com/equinor/radix-api/models" radixhttp "github.com/equinor/radix-common/net/http" radixutils "github.com/equinor/radix-common/utils" @@ -51,33 +51,26 @@ type hasAccessToGetConfigMapFunc func(ctx context.Context, kubeClient kubernetes // ApplicationHandler Instance variables type ApplicationHandler struct { - jobHandler job.JobHandler environmentHandler environments.EnvironmentHandler accounts models.Accounts - config ApplicationHandlerConfig - namespace string - hasAccessToGetConfigMap func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, configMapName string) (bool, error) + config config.Config + hasAccessToGetConfigMap hasAccessToGetConfigMapFunc + tektonImageTag string + pipelineImageTag string } // NewApplicationHandler Constructor -func NewApplicationHandler(accounts models.Accounts, config ApplicationHandlerConfig, hasAccessToGetConfigMap hasAccessToGetConfigMapFunc) ApplicationHandler { +func NewApplicationHandler(accounts models.Accounts, config config.Config, hasAccessToGetConfigMap hasAccessToGetConfigMapFunc) ApplicationHandler { return ApplicationHandler{ - accounts: accounts, - jobHandler: job.Init(accounts, deployments.Init(accounts)), environmentHandler: environments.Init(environments.WithAccounts(accounts)), + accounts: accounts, config: config, - namespace: getApiNamespace(config), hasAccessToGetConfigMap: hasAccessToGetConfigMap, + tektonImageTag: config.TektonImageTag, + pipelineImageTag: config.PipelineImageTag, } } -func getApiNamespace(config ApplicationHandlerConfig) string { - if namespace := operatorUtils.GetEnvironmentNamespace(config.AppName, config.EnvironmentName); len(namespace) > 0 { - return namespace - } - panic("missing RADIX_APP or RADIX_ENVIRONMENT environment variables") -} - func (ah *ApplicationHandler) getUserAccount() models.Account { return ah.accounts.UserAccount } @@ -129,11 +122,7 @@ func (ah *ApplicationHandler) RegisterApplication(ctx context.Context, applicati var err error application := applicationRegistrationRequest.ApplicationRegistration - - creator, err := ah.accounts.GetOriginator() - if err != nil { - return nil, err - } + creator := auth.GetOriginator(ctx) application.RadixConfigFullName = cleanFileFullName(application.RadixConfigFullName) if len(application.RadixConfigFullName) > 0 { @@ -442,7 +431,7 @@ func (ah *ApplicationHandler) TriggerPipelinePromote(ctx context.Context, appNam return nil, radixhttp.ValidationError("Radix Application Pipeline", "Deployment name, from environment and to environment are required for \"promote\" pipeline") } - log.Ctx(ctx).Info().Msgf("Creating promote pipeline job for %s using deployment %s from environment %s into environment %s", appName, deploymentName, fromEnvironment, toEnvironment) + log.Ctx(ctx).Info().Msgf("Creating promote pipeline jobController for %s using deployment %s from environment %s into environment %s", appName, deploymentName, fromEnvironment, toEnvironment) pipeline, err := jobPipeline.GetPipelineFromName("promote") if err != nil { @@ -457,7 +446,7 @@ func (ah *ApplicationHandler) TriggerPipelinePromote(ctx context.Context, appNam jobParameters := pipelineParameters.MapPipelineParametersPromoteToJobParameter() jobParameters.CommitID = radixDeployment.GetLabels()[kube.RadixCommitLabel] - jobSummary, err := ah.jobHandler.HandleStartPipelineJob(ctx, appName, pipeline, jobParameters) + jobSummary, err := HandleStartPipelineJob(ctx, ah.accounts.UserAccount.RadixClient, appName, ah.pipelineImageTag, ah.tektonImageTag, pipeline, jobParameters) if err != nil { return nil, err } @@ -497,7 +486,7 @@ func (ah *ApplicationHandler) TriggerPipelineDeploy(ctx context.Context, appName return nil, radixhttp.ValidationError("Radix Application Pipeline", "To environment is required for \"deploy\" pipeline") } - log.Ctx(ctx).Info().Msgf("Creating deploy pipeline job for %s into environment %s", appName, toEnvironment) + log.Ctx(ctx).Info().Msgf("Creating deploy pipeline jobController for %s into environment %s", appName, toEnvironment) pipeline, err := jobPipeline.GetPipelineFromName("deploy") if err != nil { @@ -506,7 +495,7 @@ func (ah *ApplicationHandler) TriggerPipelineDeploy(ctx context.Context, appName jobParameters := pipelineParameters.MapPipelineParametersDeployToJobParameter() - jobSummary, err := ah.jobHandler.HandleStartPipelineJob(ctx, appName, pipeline, jobParameters) + jobSummary, err := HandleStartPipelineJob(ctx, ah.accounts.UserAccount.RadixClient, appName, ah.pipelineImageTag, ah.tektonImageTag, pipeline, jobParameters) if err != nil { return nil, err } @@ -521,7 +510,7 @@ func (ah *ApplicationHandler) TriggerPipelineApplyConfig(ctx context.Context, ap return nil, err } - log.Ctx(ctx).Info().Msgf("Creating apply config pipeline job for %s", appName) + log.Ctx(ctx).Info().Msgf("Creating apply config pipeline jobController for %s", appName) pipeline, err := jobPipeline.GetPipelineFromName("apply-config") if err != nil { @@ -530,7 +519,7 @@ func (ah *ApplicationHandler) TriggerPipelineApplyConfig(ctx context.Context, ap jobParameters := pipelineParameters.MapPipelineParametersApplyConfigToJobParameter() - jobSummary, err := ah.jobHandler.HandleStartPipelineJob(ctx, appName, pipeline, jobParameters) + jobSummary, err := HandleStartPipelineJob(ctx, ah.accounts.UserAccount.RadixClient, appName, ah.pipelineImageTag, ah.tektonImageTag, pipeline, jobParameters) if err != nil { return nil, err } @@ -553,7 +542,7 @@ func (ah *ApplicationHandler) triggerPipelineBuildOrBuildDeploy(ctx context.Cont return nil, applicationModels.AppNameAndBranchAreRequiredForStartingPipeline() } - log.Ctx(ctx).Info().Msgf("Creating build pipeline job for %s on branch %s for commit %s", appName, branch, commitID) + log.Ctx(ctx).Info().Msgf("Creating build pipeline jobController for %s on branch %s for commit %s", appName, branch, commitID) radixRegistration, err := ah.getUserAccount().RadixClient.RadixV1().RadixRegistrations().Get(ctx, appName, metav1.GetOptions{}) if err != nil { @@ -581,7 +570,7 @@ func (ah *ApplicationHandler) triggerPipelineBuildOrBuildDeploy(ctx context.Cont log.Ctx(ctx).Info().Msgf("Creating build pipeline job for %s on branch %s for commit %s", appName, branch, commitID) - jobSummary, err := ah.jobHandler.HandleStartPipelineJob(ctx, appName, pipeline, jobParameters) + jobSummary, err := HandleStartPipelineJob(ctx, ah.accounts.UserAccount.RadixClient, appName, ah.pipelineImageTag, ah.tektonImageTag, pipeline, jobParameters) if err != nil { return nil, err } @@ -749,31 +738,32 @@ func (ah *ApplicationHandler) validateUserIsMemberOfAdGroups(ctx context.Context } return nil } + radixApiAppNamespace := operatorUtils.GetEnvironmentNamespace(ah.config.AppName, ah.config.EnvironmentName) name := fmt.Sprintf("access-validation-%s", appName) labels := map[string]string{"radix-access-validation": "true"} configMapName := fmt.Sprintf("%s-%s", name, strings.ToLower(operatorUtils.RandString(6))) - role, err := createRoleToGetConfigMap(ctx, ah.accounts.ServiceAccount.Client, ah.namespace, name, labels, configMapName) + role, err := createRoleToGetConfigMap(ctx, ah.accounts.ServiceAccount.Client, radixApiAppNamespace, name, labels, configMapName) if err != nil { return err } defer func() { - err = deleteRole(context.Background(), ah.accounts.ServiceAccount.Client, ah.namespace, role.GetName()) + err = deleteRole(context.Background(), ah.accounts.ServiceAccount.Client, radixApiAppNamespace, role.GetName()) if err != nil { log.Ctx(ctx).Warn().Msgf("Failed to delete role %s: %v", role.GetName(), err) } }() - roleBinding, err := createRoleBindingForRole(ctx, ah.accounts.ServiceAccount.Client, ah.namespace, role, name, adGroups, labels) + roleBinding, err := createRoleBindingForRole(ctx, ah.accounts.ServiceAccount.Client, radixApiAppNamespace, role, name, adGroups, labels) if err != nil { return err } defer func() { - err = deleteRoleBinding(context.Background(), ah.accounts.ServiceAccount.Client, ah.namespace, roleBinding.GetName()) + err = deleteRoleBinding(context.Background(), ah.accounts.ServiceAccount.Client, radixApiAppNamespace, roleBinding.GetName()) if err != nil { log.Ctx(ctx).Warn().Msgf("Failed to delete role binding %s: %v", roleBinding.GetName(), err) } }() - valid, err := ah.hasAccessToGetConfigMap(ctx, ah.accounts.UserAccount.Client, ah.namespace, configMapName) + valid, err := ah.hasAccessToGetConfigMap(ctx, ah.accounts.UserAccount.Client, radixApiAppNamespace, configMapName) if err != nil { return err } diff --git a/api/applications/applications_handler_config.go b/api/applications/applications_handler_config.go deleted file mode 100644 index ae190e54..00000000 --- a/api/applications/applications_handler_config.go +++ /dev/null @@ -1,48 +0,0 @@ -package applications - -import ( - "github.com/equinor/radix-api/internal/flags" - "github.com/mitchellh/mapstructure" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -func LoadApplicationHandlerConfig(args []string) (ApplicationHandlerConfig, error) { - var cfg ApplicationHandlerConfig - - flagset := ApplicationHandlerConfigFlagSet() - if err := flagset.Parse(args); err != nil { - return cfg, err - } - - v := viper.New() - v.AutomaticEnv() - - if err := flags.Register(v, "", flagset, &cfg); err != nil { - return cfg, err - } - - err := v.UnmarshalExact(&cfg, func(dc *mapstructure.DecoderConfig) { dc.TagName = "cfg" }) - return cfg, err -} - -type ApplicationHandlerConfig struct { - RequireAppConfigurationItem bool `cfg:"require_app_configuration_item" flag:"require-app-configuration-item"` - RequireAppADGroups bool `cfg:"require_app_ad_groups" flag:"require-app-ad-groups"` - AppName string `cfg:"radix_app" flag:"radix-app"` - EnvironmentName string `cfg:"radix_environment" flag:"radix-environment"` - DNSZone string `cfg:"radix_dns_zone" flag:"radix-dns-zone"` - PrometheusUrl string `cfg:"prometheus_url" flag:"prometheus-url"` -} - -func ApplicationHandlerConfigFlagSet() *pflag.FlagSet { - flagset := pflag.NewFlagSet("config", pflag.ExitOnError) - flagset.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} - flagset.Bool("require-app-configuration-item", true, "Require configuration item for application") - flagset.Bool("require-app-ad-groups", true, "Require AD groups for application") - flagset.String("radix-app", "", "Application name") - flagset.String("radix-environment", "", "Environment name") - flagset.String("radix-dns-zone", "", "Radix DNS zone") - flagset.String("prometheus-url", "", "Prometheus URL") - return flagset -} diff --git a/api/applications/applications_handler_factory.go b/api/applications/applications_handler_factory.go index 711e61f9..74f398b0 100644 --- a/api/applications/applications_handler_factory.go +++ b/api/applications/applications_handler_factory.go @@ -4,6 +4,7 @@ import ( "context" "github.com/equinor/radix-api/api/utils/access" + "github.com/equinor/radix-api/internal/config" "github.com/equinor/radix-api/models" authorizationapi "k8s.io/api/authorization/v1" "k8s.io/client-go/kubernetes" @@ -15,11 +16,11 @@ type ApplicationHandlerFactory interface { } type applicationHandlerFactory struct { - config ApplicationHandlerConfig + config config.Config } // NewApplicationHandlerFactory creates a new ApplicationHandlerFactory -func NewApplicationHandlerFactory(config ApplicationHandlerConfig) ApplicationHandlerFactory { +func NewApplicationHandlerFactory(config config.Config) ApplicationHandlerFactory { return &applicationHandlerFactory{ config: config, } diff --git a/api/applications/get_applications_handler.go b/api/applications/get_applications_handler.go index 04afd6ce..7f2aca21 100644 --- a/api/applications/get_applications_handler.go +++ b/api/applications/get_applications_handler.go @@ -4,13 +4,16 @@ import ( "context" "sort" "strings" + "sync" applicationModels "github.com/equinor/radix-api/api/applications/models" deployment "github.com/equinor/radix-api/api/deployments" deploymentModels "github.com/equinor/radix-api/api/deployments/models" environmentModels "github.com/equinor/radix-api/api/environments/models" jobModels "github.com/equinor/radix-api/api/jobs/models" + "github.com/equinor/radix-api/api/kubequery" "github.com/equinor/radix-api/api/utils/access" + "github.com/equinor/radix-operator/pkg/client/clientset/versioned" "golang.org/x/sync/errgroup" authorizationapi "k8s.io/api/authorization/v1" @@ -48,7 +51,7 @@ func (ah *ApplicationHandler) GetApplications(ctx context.Context, matcher appli var latestApplicationJobs map[string]*jobModels.JobSummary if options.IncludeLatestJobSummary { - if latestApplicationJobs, err = ah.getJobsForApplication(ctx, radixRegistrations); err != nil { + if latestApplicationJobs, err = getLatestJobPerApplication(ctx, ah.accounts.UserAccount.RadixClient, radixRegistrations); err != nil { return nil, err } } @@ -154,19 +157,6 @@ func getComponentsForActiveDeploymentsInEnvironments(ctx context.Context, deploy return components, nil } -func (ah *ApplicationHandler) getJobsForApplication(ctx context.Context, radixRegistations []v1.RadixRegistration) (map[string]*jobModels.JobSummary, error) { - forApplications := map[string]bool{} - for _, app := range radixRegistations { - forApplications[app.GetName()] = true - } - - applicationJobs, err := ah.jobHandler.GetLatestJobPerApplication(ctx, forApplications) - if err != nil { - return nil, err - } - return applicationJobs, nil -} - func (ah *ApplicationHandler) filterRadixRegByAccess(ctx context.Context, radixregs []v1.RadixRegistration, hasAccess hasAccessToRR) ([]v1.RadixRegistration, error) { result := []v1.RadixRegistration{} limit := 25 @@ -221,3 +211,45 @@ func hasAccess(ctx context.Context, client kubernetes.Interface, rr v1.RadixRegi Name: rr.GetName(), }) } + +func getLatestJobPerApplication(ctx context.Context, radixClient versioned.Interface, radixRegistations []v1.RadixRegistration) (map[string]*jobModels.JobSummary, error) { + g, ctx := errgroup.WithContext(ctx) + g.SetLimit(25) + jobSummaries := sync.Map{} + + for _, rr := range radixRegistations { + g.Go(func() error { + jobs, err := kubequery.GetRadixJobs(ctx, radixClient, rr.GetName()) + if err != nil { + return err + } + + var latestJob *v1.RadixJob + for _, job := range jobs { + if latestJob == nil || job.Status.Started.After(latestJob.Status.Started.Time) { + latestJob = &job + } + } + + if latestJob != nil { + jobSummaries.Store(rr.GetName(), jobModels.GetSummaryFromRadixJob(latestJob)) + } + return nil + }) + } + + err := g.Wait() + if err != nil { + return nil, err + } + + applicationJob := make(map[string]*jobModels.JobSummary, len(radixRegistations)) + for _, rr := range radixRegistations { + job, _ := jobSummaries.Load(rr.GetName()) + if job != nil { + applicationJob[rr.GetName()] = job.(*jobModels.JobSummary) + } + } + + return applicationJob, nil +} diff --git a/api/applications/start_job_handler.go b/api/applications/start_job_handler.go new file mode 100644 index 00000000..634747db --- /dev/null +++ b/api/applications/start_job_handler.go @@ -0,0 +1,127 @@ +package applications + +import ( + "context" + "fmt" + + jobController "github.com/equinor/radix-api/api/jobs" + jobModels "github.com/equinor/radix-api/api/jobs/models" + "github.com/equinor/radix-api/api/middleware/auth" + "github.com/equinor/radix-operator/pkg/apis/defaults" + "github.com/equinor/radix-operator/pkg/apis/kube" + pipelineJob "github.com/equinor/radix-operator/pkg/apis/pipeline" + v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + "github.com/equinor/radix-operator/pkg/apis/radixvalidators" + k8sObjectUtils "github.com/equinor/radix-operator/pkg/apis/utils" + "github.com/equinor/radix-operator/pkg/client/clientset/versioned" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// HandleStartPipelineJob Handles the creation of a pipeline jobController for an application +func HandleStartPipelineJob(ctx context.Context, radixClient versioned.Interface, appName, pipelineImageTag, tektonImageTag string, pipeline *pipelineJob.Definition, jobParameters *jobModels.JobParameters) (*jobModels.JobSummary, error) { + radixRegistration, _ := radixClient.RadixV1().RadixRegistrations().Get(ctx, appName, metav1.GetOptions{}) + + radixConfigFullName, err := getRadixConfigFullName(radixRegistration) + if err != nil { + return nil, err + } + + job := buildPipelineJob(ctx, appName, radixRegistration.Spec.CloneURL, radixConfigFullName, pipelineImageTag, tektonImageTag, pipeline, jobParameters) + return createPipelineJob(ctx, radixClient, appName, job) +} + +func createPipelineJob(ctx context.Context, radixClient versioned.Interface, appName string, job *v1.RadixJob) (*jobModels.JobSummary, error) { + log.Ctx(ctx).Info().Msgf("Starting jobController: %s, %s", job.GetName(), jobController.WorkerImage) + appNamespace := k8sObjectUtils.GetAppNamespace(appName) + job, err := radixClient.RadixV1().RadixJobs(appNamespace).Create(ctx, job, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + + log.Ctx(ctx).Info().Msgf("Started jobController: %s, %s", job.GetName(), jobController.WorkerImage) + return jobModels.GetSummaryFromRadixJob(job), nil +} + +func getRadixConfigFullName(radixRegistration *v1.RadixRegistration) (string, error) { + if len(radixRegistration.Spec.RadixConfigFullName) == 0 { + return defaults.DefaultRadixConfigFileName, nil + } + if err := radixvalidators.ValidateRadixConfigFullName(radixRegistration.Spec.RadixConfigFullName); err != nil { + return "", err + } + return radixRegistration.Spec.RadixConfigFullName, nil +} + +func buildPipelineJob(ctx context.Context, appName, cloneURL, radixConfigFullName, pipelineImageTag, tektonImageTag string, pipeline *pipelineJob.Definition, jobSpec *jobModels.JobParameters) *v1.RadixJob { + jobName, imageTag := jobController.GetUniqueJobName() + if len(jobSpec.ImageTag) > 0 { + imageTag = jobSpec.ImageTag + } + + var buildSpec v1.RadixBuildSpec + var promoteSpec v1.RadixPromoteSpec + var deploySpec v1.RadixDeploySpec + + log.Ctx(ctx).Info().Msgf("Using %s pipeline image tag", pipelineImageTag) + log.Ctx(ctx).Info().Msgf("Using %s as tekton image tag", tektonImageTag) + + switch pipeline.Type { + case v1.BuildDeploy, v1.Build: + buildSpec = v1.RadixBuildSpec{ + ImageTag: imageTag, + Branch: jobSpec.Branch, + CommitID: jobSpec.CommitID, + PushImage: jobSpec.PushImage, + OverrideUseBuildCache: jobSpec.OverrideUseBuildCache, + } + case v1.Promote: + promoteSpec = v1.RadixPromoteSpec{ + DeploymentName: jobSpec.DeploymentName, + FromEnvironment: jobSpec.FromEnvironment, + ToEnvironment: jobSpec.ToEnvironment, + CommitID: jobSpec.CommitID, + } + case v1.Deploy: + deploySpec = v1.RadixDeploySpec{ + ToEnvironment: jobSpec.ToEnvironment, + ImageTagNames: jobSpec.ImageTagNames, + CommitID: jobSpec.CommitID, + ComponentsToDeploy: jobSpec.ComponentsToDeploy, + } + } + + job := v1.RadixJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName, + Labels: map[string]string{ + kube.RadixAppLabel: appName, + }, + Annotations: map[string]string{ + kube.RadixBranchAnnotation: jobSpec.Branch, + }, + }, + Spec: v1.RadixJobSpec{ + AppName: appName, + CloneURL: cloneURL, + PipeLineType: pipeline.Type, + PipelineImage: pipelineImageTag, + TektonImage: tektonImageTag, + Build: buildSpec, + Promote: promoteSpec, + Deploy: deploySpec, + TriggeredBy: getTriggeredBy(ctx, jobSpec.TriggeredBy), + RadixConfigFullName: fmt.Sprintf("/workspace/%s", radixConfigFullName), + }, + } + + return &job +} + +func getTriggeredBy(ctx context.Context, triggeredBy string) string { + if triggeredBy != "" && triggeredBy != "" { + return triggeredBy + } + + return auth.GetOriginator(ctx) +} diff --git a/api/buildsecrets/buildsecrets_test.go b/api/buildsecrets/buildsecrets_test.go index 3d5f2bba..c5069f45 100644 --- a/api/buildsecrets/buildsecrets_test.go +++ b/api/buildsecrets/buildsecrets_test.go @@ -6,6 +6,8 @@ import ( "testing" environmentModels "github.com/equinor/radix-api/api/secrets/models" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" + "github.com/golang/mock/gomock" kedafake "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/fake" "github.com/stretchr/testify/require" secretproviderfake "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/fake" @@ -41,7 +43,9 @@ func setupTest(t *testing.T) (*commontest.Utils, *controllertest.Utils, *kubefak err := commonTestUtils.CreateClusterPrerequisites(clusterName, egressIps, subscriptionId) require.NoError(t, err) // controllerTestUtils is used for issuing HTTP request and processing responses - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewBuildSecretsController()) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewBuildSecretsController()) return &commonTestUtils, &controllerTestUtils, kubeclient, radixclient, kedaClient } diff --git a/api/buildstatus/build_status_controller_test.go b/api/buildstatus/build_status_controller_test.go index e588fd28..62c30eb8 100644 --- a/api/buildstatus/build_status_controller_test.go +++ b/api/buildstatus/build_status_controller_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" kedafake "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/fake" "github.com/stretchr/testify/require" secretproviderfake "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/fake" @@ -105,8 +106,9 @@ func TestGetBuildStatus(t *testing.T) { Return(expected, nil). Times(1) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewBuildStatusController(fakeBuildStatus)) - + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewBuildStatusController(fakeBuildStatus)) responseChannel := controllerTestUtils.ExecuteUnAuthorizedRequest("GET", "/api/v1/applications/my-app/environments/test/buildstatus") response := <-responseChannel @@ -134,7 +136,9 @@ func TestGetBuildStatus(t *testing.T) { return nil, nil }) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewBuildStatusController(fakeBuildStatus)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewBuildStatusController(fakeBuildStatus)) responseChannel := controllerTestUtils.ExecuteUnAuthorizedRequest("GET", "/api/v1/applications/my-app/environments/test/buildstatus") response := <-responseChannel @@ -162,7 +166,9 @@ func TestGetBuildStatus(t *testing.T) { return nil, nil }) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewBuildStatusController(fakeBuildStatus)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewBuildStatusController(fakeBuildStatus)) responseChannel := controllerTestUtils.ExecuteUnAuthorizedRequest("GET", "/api/v1/applications/my-app/environments/test/buildstatus?pipeline=deploy") response := <-responseChannel @@ -190,7 +196,9 @@ func TestGetBuildStatus(t *testing.T) { return nil, nil }) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewBuildStatusController(fakeBuildStatus)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewBuildStatusController(fakeBuildStatus)) responseChannel := controllerTestUtils.ExecuteUnAuthorizedRequest("GET", "/api/v1/applications/my-app/environments/test/buildstatus?pipeline=promote") response := <-responseChannel @@ -212,7 +220,9 @@ func TestGetBuildStatus(t *testing.T) { Return(nil, errors.New("error")). Times(1) - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewBuildStatusController(fakeBuildStatus)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewBuildStatusController(fakeBuildStatus)) responseChannel := controllerTestUtils.ExecuteUnAuthorizedRequest("GET", "/api/v1/applications/my-app/environments/test/buildstatus") response := <-responseChannel diff --git a/api/deployments/deployment_controller_test.go b/api/deployments/deployment_controller_test.go index 2e8db687..b9204dc7 100644 --- a/api/deployments/deployment_controller_test.go +++ b/api/deployments/deployment_controller_test.go @@ -8,7 +8,9 @@ import ( "time" certfake "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/fake" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" "github.com/equinor/radix-operator/pkg/apis/kube" + "github.com/golang/mock/gomock" kedav2 "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned" "github.com/stretchr/testify/require" "k8s.io/client-go/kubernetes" @@ -40,7 +42,9 @@ func createGetLogEndpoint(appName, podName string) string { func setupTest(t *testing.T) (*commontest.Utils, *controllertest.Utils, kubernetes.Interface, radixclient.Interface, kedav2.Interface, prometheusclient.Interface, secretsstorevclient.Interface, *certfake.Clientset) { commonTestUtils, kubeclient, radixClient, kedaClient, prometheusClient, secretproviderclient, certClient := apiUtils.SetupTest(t) // controllerTestUtils is used for issuing HTTP request and processing responses - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixClient, kedaClient, secretproviderclient, certClient, NewDeploymentController()) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixClient, kedaClient, secretproviderclient, certClient, mockValidator, NewDeploymentController()) return commonTestUtils, &controllerTestUtils, kubeclient, radixClient, kedaClient, prometheusClient, secretproviderclient, certClient } func TestGetPodLog_no_radixconfig(t *testing.T) { diff --git a/api/environments/environment_controller_test.go b/api/environments/environment_controller_test.go index 4f520e45..87ff7c8a 100644 --- a/api/environments/environment_controller_test.go +++ b/api/environments/environment_controller_test.go @@ -20,8 +20,8 @@ import ( "github.com/equinor/radix-api/api/secrets/suffix" controllertest "github.com/equinor/radix-api/api/test" "github.com/equinor/radix-api/api/utils" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" "github.com/equinor/radix-api/models" - radixmodels "github.com/equinor/radix-common/models" radixhttp "github.com/equinor/radix-common/net/http" radixutils "github.com/equinor/radix-common/utils" "github.com/equinor/radix-common/utils/numbers" @@ -81,10 +81,12 @@ func setupTest(t *testing.T, envHandlerOpts []EnvironmentHandlerOptions) (*commo err := commonTestUtils.CreateClusterPrerequisites(clusterName, egressIps, subscriptionId) require.NoError(t, err) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) // secretControllerTestUtils is used for issuing HTTP request and processing responses - secretControllerTestUtils := controllertest.NewTestUtils(kubeclient, radixClient, kedaClient, secretproviderclient, certClient, secrets.NewSecretController(nil)) + secretControllerTestUtils := controllertest.NewTestUtils(kubeclient, radixClient, kedaClient, secretproviderclient, certClient, mockValidator, secrets.NewSecretController(nil)) // controllerTestUtils is used for issuing HTTP request and processing responses - environmentControllerTestUtils := controllertest.NewTestUtils(kubeclient, radixClient, kedaClient, secretproviderclient, certClient, NewEnvironmentController(NewEnvironmentHandlerFactory(envHandlerOpts...))) + environmentControllerTestUtils := controllertest.NewTestUtils(kubeclient, radixClient, kedaClient, secretproviderclient, certClient, mockValidator, NewEnvironmentController(NewEnvironmentHandlerFactory(envHandlerOpts...))) return &commonTestUtils, &environmentControllerTestUtils, &secretControllerTestUtils, kubeclient, radixClient, kedaClient, prometheusclient, secretproviderclient, certClient } @@ -2706,7 +2708,7 @@ func initHandler(client kubernetes.Interface, secretproviderclient secretsstorevclient.Interface, certClient certclient.Interface, handlerConfig ...EnvironmentHandlerOptions) EnvironmentHandler { - accounts := models.NewAccounts(client, radixclient, kedaClient, secretproviderclient, nil, certClient, client, radixclient, kedaClient, secretproviderclient, nil, certClient, "", radixmodels.Impersonation{}) + accounts := models.NewAccounts(client, radixclient, kedaClient, secretproviderclient, nil, certClient, client, radixclient, kedaClient, secretproviderclient, nil, certClient) options := []EnvironmentHandlerOptions{WithAccounts(accounts)} options = append(options, handlerConfig...) return Init(options...) diff --git a/api/environmentvariables/env_vars_controller_test.go b/api/environmentvariables/env_vars_controller_test.go index 428b2ce3..550fe632 100644 --- a/api/environmentvariables/env_vars_controller_test.go +++ b/api/environmentvariables/env_vars_controller_test.go @@ -9,6 +9,7 @@ import ( certclientfake "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/fake" envvarsmodels "github.com/equinor/radix-api/api/environmentvariables/models" controllertest "github.com/equinor/radix-api/api/test" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" "github.com/equinor/radix-operator/pkg/apis/config" "github.com/equinor/radix-operator/pkg/apis/deployment" "github.com/equinor/radix-operator/pkg/apis/kube" @@ -44,8 +45,10 @@ func setupTestWithMockHandler(t *testing.T, mockCtrl *gomock.Controller) (*commo handlerFactory := NewMockenvVarsHandlerFactory(mockCtrl) handlerFactory.EXPECT().createHandler(gomock.Any()).Return(handler) controller := (&envVarsController{}).withHandlerFactory(handlerFactory) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) // controllerTestUtils is used for issuing HTTP request and processing responses - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, controller) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, controller) return &commonTestUtils, &controllerTestUtils, kubeclient, radixclient, prometheusclient, certClient, handler } diff --git a/api/jobs/job_controller.go b/api/jobs/job_controller.go index 48c1cabd..2ceeebad 100644 --- a/api/jobs/job_controller.go +++ b/api/jobs/job_controller.go @@ -644,7 +644,7 @@ func (jc *jobController) GetTektonPipelineRunTaskStepLogs(accounts models.Accoun jc.ErrorResponse(w, r, err) return } - defer func() {_ = log.Close()}() + defer func() { _ = log.Close() }() if asFile { fileName := fmt.Sprintf("%s.log", time.Now().Format("20060102150405")) @@ -727,7 +727,7 @@ func (jc *jobController) GetPipelineJobStepLogs(accounts models.Accounts, w http jc.ErrorResponse(w, r, err) return } - defer func() {_ = log.Close()}() + defer func() { _ = log.Close() }() if asFile { fileName := fmt.Sprintf("%s.log", time.Now().Format("20060102150405")) diff --git a/api/jobs/job_controller_test.go b/api/jobs/job_controller_test.go index 401d2d7e..9f8afc1a 100644 --- a/api/jobs/job_controller_test.go +++ b/api/jobs/job_controller_test.go @@ -5,7 +5,10 @@ import ( "fmt" "testing" + "github.com/equinor/radix-api/api/applications" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" "github.com/equinor/radix-common/utils/pointers" + "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" "github.com/stretchr/testify/require" @@ -19,12 +22,9 @@ import ( "github.com/stretchr/testify/assert" certclientfake "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/fake" - "github.com/equinor/radix-api/api/deployments" . "github.com/equinor/radix-api/api/jobs" jobmodels "github.com/equinor/radix-api/api/jobs/models" controllertest "github.com/equinor/radix-api/api/test" - "github.com/equinor/radix-api/models" - radixmodels "github.com/equinor/radix-common/models" commontest "github.com/equinor/radix-operator/pkg/apis/test" builders "github.com/equinor/radix-operator/pkg/apis/utils" radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" @@ -44,7 +44,7 @@ const ( anyUser = "a_user@equinor.com" ) -func setupTest() (*commontest.Utils, *controllertest.Utils, kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretsstorevclient.Interface, *certclientfake.Clientset) { +func setupTest(t *testing.T) (*commontest.Utils, *controllertest.Utils, kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretsstorevclient.Interface, *certclientfake.Clientset) { // Setup kubeclient := kubefake.NewSimpleClientset() radixclient := fake.NewSimpleClientset() @@ -56,14 +56,16 @@ func setupTest() (*commontest.Utils, *controllertest.Utils, kubernetes.Interface commonTestUtils := commontest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient) // controllerTestUtils is used for issuing HTTP request and processing responses - controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewJobController()) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + controllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewJobController()) return &commonTestUtils, &controllerTestUtils, kubeclient, radixclient, kedaClient, secretproviderclient, certClient } func TestGetApplicationJob(t *testing.T) { // Setup - commonTestUtils, controllerTestUtils, client, radixclient, kedaClient, secretproviderclient, certClient := setupTest() + commonTestUtils, controllerTestUtils, client, radixclient, _, _, _ := setupTest(t) _, err := commonTestUtils.ApplyRegistration(builders.ARadixRegistration(). WithName(anyAppName). @@ -78,11 +80,10 @@ func TestGetApplicationJob(t *testing.T) { OverrideUseBuildCache: pointers.Ptr(true), } - accounts := models.NewAccounts(client, radixclient, kedaClient, secretproviderclient, nil, certClient, client, radixclient, kedaClient, secretproviderclient, nil, certClient, "", radixmodels.Impersonation{}) - handler := Init(accounts, deployments.Init(accounts)) - anyPipeline, _ := pipeline.GetPipelineFromName(anyPipelineName) - jobSummary, _ := handler.HandleStartPipelineJob(context.Background(), anyAppName, anyPipeline, jobParameters) + anyPipelineTagName := "latestPipelineImageTag" + anyTektonTagName := "latestTektonImageTag" + jobSummary, _ := applications.HandleStartPipelineJob(context.Background(), radixclient, anyAppName, anyPipelineTagName, anyTektonTagName, anyPipeline, jobParameters) _, err = createPipelinePod(client, builders.GetAppNamespace(anyAppName), jobSummary.Name) require.NoError(t, err) @@ -130,7 +131,7 @@ func TestGetApplicationJob_RadixJobSpecExists(t *testing.T) { anyJobName := "any-job" // Setup - commonTestUtils, controllerTestUtils, _, _, _, _, _ := setupTest() + commonTestUtils, controllerTestUtils, _, _, _, _, _ := setupTest(t) job, _ := commonTestUtils.ApplyJob(builders.AStartedBuildDeployJob().WithAppName(anyAppName).WithJobName(anyJobName)) // Test @@ -148,7 +149,7 @@ func TestGetApplicationJob_RadixJobSpecExists(t *testing.T) { } func TestGetPipelineJobLogsError(t *testing.T) { - commonTestUtils, controllerTestUtils, _, _, _, _, _ := setupTest() + commonTestUtils, controllerTestUtils, _, _, _, _, _ := setupTest(t) t.Run("job doesn't exist", func(t *testing.T) { aJobName := "aJobName" diff --git a/api/jobs/job_handler.go b/api/jobs/job_handler.go index d1d87614..f5ffdb20 100644 --- a/api/jobs/job_handler.go +++ b/api/jobs/job_handler.go @@ -8,15 +8,16 @@ import ( "github.com/equinor/radix-api/api/deployments" jobModels "github.com/equinor/radix-api/api/jobs/models" + "github.com/equinor/radix-api/api/kubequery" "github.com/equinor/radix-api/api/utils" "github.com/equinor/radix-api/api/utils/tekton" "github.com/equinor/radix-api/models" radixutils "github.com/equinor/radix-common/utils" "github.com/equinor/radix-common/utils/slice" "github.com/equinor/radix-operator/pkg/apis/kube" + v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" crdUtils "github.com/equinor/radix-operator/pkg/apis/utils" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - "golang.org/x/sync/errgroup" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/apis" @@ -24,7 +25,7 @@ import ( ) const ( - workerImage = "radix-pipeline" + WorkerImage = "radix-pipeline" tektonRealNameAnnotation = "radix.equinor.com/tekton-pipeline-name" ) @@ -46,28 +47,19 @@ func Init(accounts models.Accounts, deployHandler deployments.DeployHandler) Job } } -// GetLatestJobPerApplication Handler for GetApplicationJobs - NOTE: does not get latestJob.Environments -func (jh JobHandler) GetLatestJobPerApplication(ctx context.Context, forApplications map[string]bool) (map[string]*jobModels.JobSummary, error) { - return jh.getLatestJobPerApplication(ctx, forApplications) -} - // GetApplicationJobs Handler for GetApplicationJobs func (jh JobHandler) GetApplicationJobs(ctx context.Context, appName string) ([]*jobModels.JobSummary, error) { - return jh.getApplicationJobs(ctx, appName) -} - -// GetLatestApplicationJob Get last run application job -func (jh JobHandler) GetLatestApplicationJob(ctx context.Context, appName string) (*jobModels.JobSummary, error) { - jobs, err := jh.getApplicationJobs(ctx, appName) + jobs, err := jh.getJobs(ctx, appName) if err != nil { return nil, err } - if len(jobs) == 0 { - return nil, nil - } + // Sort jobs descending + sort.Slice(jobs, func(i, j int) bool { + return utils.IsBefore(jobs[j], jobs[i]) + }) - return jobs[0], nil + return jobs, nil } // GetApplicationJob Handler for GetApplicationJob @@ -288,115 +280,13 @@ func sortPipelineTasks(tasks []jobModels.PipelineRunTask) []jobModels.PipelineRu return tasks } -func (jh JobHandler) getApplicationJobs(ctx context.Context, appName string) ([]*jobModels.JobSummary, error) { - jobs, err := jh.getJobs(ctx, appName) - if err != nil { - return nil, err - } - - // Sort jobs descending - sort.Slice(jobs, func(i, j int) bool { - return utils.IsBefore(jobs[j], jobs[i]) - }) - - return jobs, nil -} - -func (jh JobHandler) getDefinedJobs(ctx context.Context, appNames []string) ([]*jobModels.JobSummary, error) { - var g errgroup.Group - g.SetLimit(25) - - jobsCh := make(chan []*jobModels.JobSummary, len(appNames)) - for _, appName := range appNames { - name := appName // locally scope appName to avoid race condition in go routines - g.Go(func() error { - jobs, err := jh.getJobs(ctx, name) - if err == nil { - jobsCh <- jobs - } - return err - }) - } - - err := g.Wait() - close(jobsCh) - if err != nil { - return nil, err - } - - var jobSummaries []*jobModels.JobSummary - for jobs := range jobsCh { - jobSummaries = append(jobSummaries, jobs...) - } - return jobSummaries, nil -} - func (jh JobHandler) getJobs(ctx context.Context, appName string) ([]*jobModels.JobSummary, error) { - return jh.getJobsInNamespace(ctx, crdUtils.GetAppNamespace(appName)) -} - -func (jh JobHandler) getJobsInNamespace(ctx context.Context, namespace string) ([]*jobModels.JobSummary, error) { - jobList, err := jh.userAccount.RadixClient.RadixV1().RadixJobs(namespace).List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } - - jobs := make([]*jobModels.JobSummary, len(jobList.Items)) - for i, job := range jobList.Items { - jobs[i] = jobModels.GetSummaryFromRadixJob(&job) - } - - return jobs, nil -} - -func (jh JobHandler) getLatestJobPerApplication(ctx context.Context, forApplications map[string]bool) (map[string]*jobModels.JobSummary, error) { - // Primarily use Radix Jobs - var apps []string - for name, shouldAdd := range forApplications { - if shouldAdd { - apps = append(apps, name) - } - } - - someJobs, err := jh.getDefinedJobs(ctx, apps) + jobs, err := kubequery.GetRadixJobs(ctx, jh.accounts.UserAccount.RadixClient, appName) if err != nil { return nil, err } - sort.Slice(someJobs, func(i, j int) bool { - switch strings.Compare(someJobs[i].AppName, someJobs[j].AppName) { - case -1: - return true - case 1: - return false - } - - return utils.IsBefore(someJobs[j], someJobs[i]) - }) - - applicationJob := make(map[string]*jobModels.JobSummary) - for _, job := range someJobs { - if applicationJob[job.AppName] != nil { - continue - } - if !forApplications[job.AppName] { - continue - } - - if job.Started == "" { - // Job may still be queued or waiting to be scheduled by the operator - continue - } - - applicationJob[job.AppName] = job - } - - forApplicationsWithNoRadixJob := make(map[string]bool) - for applicationName := range forApplications { - if applicationJob[applicationName] == nil { - forApplicationsWithNoRadixJob[applicationName] = true - } - } - - return applicationJob, nil + return slice.Map(jobs, func(j v1.RadixJob) *jobModels.JobSummary { + return jobModels.GetSummaryFromRadixJob(&j) + }), nil } diff --git a/api/jobs/job_handler_test.go b/api/jobs/job_handler_test.go index 25045e95..1acb13cd 100644 --- a/api/jobs/job_handler_test.go +++ b/api/jobs/job_handler_test.go @@ -10,7 +10,6 @@ import ( deploymentModels "github.com/equinor/radix-api/api/deployments/models" jobModels "github.com/equinor/radix-api/api/jobs/models" "github.com/equinor/radix-api/models" - radixmodels "github.com/equinor/radix-common/models" radixutils "github.com/equinor/radix-common/utils" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/utils" @@ -75,7 +74,7 @@ func (s *JobHandlerTestSuite) SetupTest() { func (s *JobHandlerTestSuite) setupTest() { s.kubeClient, s.radixClient, s.kedaClient, s.secretProviderClient, s.certClient = s.getUtils() - accounts := models.NewAccounts(s.kubeClient, s.radixClient, s.kedaClient, s.secretProviderClient, nil, s.certClient, s.kubeClient, s.radixClient, s.kedaClient, s.secretProviderClient, nil, s.certClient, "", radixmodels.Impersonation{}) + accounts := models.NewAccounts(s.kubeClient, s.radixClient, s.kedaClient, s.secretProviderClient, nil, s.certClient, s.kubeClient, s.radixClient, s.kedaClient, s.secretProviderClient, nil, s.certClient) s.accounts = accounts } diff --git a/api/jobs/manage_job_handler.go b/api/jobs/manage_job_handler.go index 3c0c6a9e..1586d059 100644 --- a/api/jobs/manage_job_handler.go +++ b/api/jobs/manage_job_handler.go @@ -3,11 +3,16 @@ package jobs import ( "context" "fmt" + "strings" + "time" jobModels "github.com/equinor/radix-api/api/jobs/models" "github.com/equinor/radix-api/api/kubequery" + "github.com/equinor/radix-api/api/middleware/auth" + radixutils "github.com/equinor/radix-common/utils" "github.com/equinor/radix-common/utils/slice" 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" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -60,9 +65,20 @@ func (jh JobHandler) RerunJob(ctx context.Context, appName, jobName string) erro return nil } +func (jh JobHandler) createPipelineJob(ctx context.Context, appName string, job *radixv1.RadixJob) (*jobModels.JobSummary, error) { + log.Ctx(ctx).Info().Msgf("Starting job: %s, %s", job.GetName(), WorkerImage) + appNamespace := k8sObjectUtils.GetAppNamespace(appName) + job, err := jh.userAccount.RadixClient.RadixV1().RadixJobs(appNamespace).Create(ctx, job, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + + log.Ctx(ctx).Info().Msgf("Started job: %s, %s", job.GetName(), WorkerImage) + return jobModels.GetSummaryFromRadixJob(job), nil +} func (jh JobHandler) buildPipelineJobToRerunFrom(ctx context.Context, radixJob *radixv1.RadixJob) *radixv1.RadixJob { - rerunJobName, imageTag := getUniqueJobName(workerImage) + rerunJobName, imageTag := GetUniqueJobName() rerunRadixJob := radixv1.RadixJob{ ObjectMeta: metav1.ObjectMeta{ Name: rerunJobName, @@ -79,11 +95,7 @@ func (jh JobHandler) buildPipelineJobToRerunFrom(ctx context.Context, radixJob * rerunRadixJob.Spec.Build.ImageTag = imageTag } rerunRadixJob.Spec.Stop = false - triggeredBy, err := jh.getTriggeredBy("") - if err != nil { - log.Ctx(ctx).Warn().Msgf("failed to get triggeredBy: %v", err) - } - rerunRadixJob.Spec.TriggeredBy = triggeredBy + rerunRadixJob.Spec.TriggeredBy = auth.GetOriginator(ctx) return &rerunRadixJob } @@ -97,3 +109,12 @@ func (jh JobHandler) getPipelineJobByName(ctx context.Context, appName string, j } return radixJob, nil } + +func GetUniqueJobName() (string, string) { + var jobName []string + randomStr := strings.ToLower(radixutils.RandString(5)) + timestamp := time.Now().Format("20060102150405") + jobName = append(jobName, WorkerImage, "-", timestamp, "-", randomStr) + + return strings.Join(jobName, ""), randomStr +} diff --git a/api/jobs/start_job_handler.go b/api/jobs/start_job_handler.go deleted file mode 100644 index a6ffa857..00000000 --- a/api/jobs/start_job_handler.go +++ /dev/null @@ -1,177 +0,0 @@ -package jobs - -import ( - "context" - "fmt" - "os" - "strings" - "time" - - "github.com/equinor/radix-operator/pkg/apis/defaults" - "github.com/equinor/radix-operator/pkg/apis/radixvalidators" - "github.com/rs/zerolog/log" - - jobModels "github.com/equinor/radix-api/api/jobs/models" - "github.com/equinor/radix-api/api/metrics" - radixutils "github.com/equinor/radix-common/utils" - "github.com/equinor/radix-operator/pkg/apis/kube" - pipelineJob "github.com/equinor/radix-operator/pkg/apis/pipeline" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" - k8sObjectUtils "github.com/equinor/radix-operator/pkg/apis/utils" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - pipelineTagEnvironmentVariable = "PIPELINE_IMG_TAG" - tektonTagEnvironmentVariable = "TEKTON_IMG_TAG" -) - -// HandleStartPipelineJob Handles the creation of a pipeline job for an application -func (jh JobHandler) HandleStartPipelineJob(ctx context.Context, appName string, pipeline *pipelineJob.Definition, jobParameters *jobModels.JobParameters) (*jobModels.JobSummary, error) { - radixRegistration, _ := jh.userAccount.RadixClient.RadixV1().RadixRegistrations().Get(ctx, appName, metav1.GetOptions{}) - - radixConfigFullName, err := getRadixConfigFullName(radixRegistration) - if err != nil { - return nil, err - } - - job := jh.buildPipelineJob(ctx, appName, radixRegistration.Spec.CloneURL, radixConfigFullName, pipeline, jobParameters) - return jh.createPipelineJob(ctx, appName, job) -} - -func (jh JobHandler) createPipelineJob(ctx context.Context, appName string, job *v1.RadixJob) (*jobModels.JobSummary, error) { - log.Ctx(ctx).Info().Msgf("Starting job: %s, %s", job.GetName(), workerImage) - appNamespace := k8sObjectUtils.GetAppNamespace(appName) - job, err := jh.userAccount.RadixClient.RadixV1().RadixJobs(appNamespace).Create(ctx, job, metav1.CreateOptions{}) - if err != nil { - return nil, err - } - - metrics.AddJobTriggered(appName, string(job.Spec.PipeLineType)) - - log.Ctx(ctx).Info().Msgf("Started job: %s, %s", job.GetName(), workerImage) - return jobModels.GetSummaryFromRadixJob(job), nil -} - -func getRadixConfigFullName(radixRegistration *v1.RadixRegistration) (string, error) { - if len(radixRegistration.Spec.RadixConfigFullName) == 0 { - return defaults.DefaultRadixConfigFileName, nil - } - if err := radixvalidators.ValidateRadixConfigFullName(radixRegistration.Spec.RadixConfigFullName); err != nil { - return "", err - } - return radixRegistration.Spec.RadixConfigFullName, nil -} - -func (jh JobHandler) buildPipelineJob(ctx context.Context, appName, cloneURL, radixConfigFullName string, pipeline *pipelineJob.Definition, jobSpec *jobModels.JobParameters) *v1.RadixJob { - jobName, imageTag := getUniqueJobName(workerImage) - if len(jobSpec.ImageTag) > 0 { - imageTag = jobSpec.ImageTag - } - - var buildSpec v1.RadixBuildSpec - var promoteSpec v1.RadixPromoteSpec - var deploySpec v1.RadixDeploySpec - - triggeredBy, err := jh.getTriggeredBy(jobSpec.TriggeredBy) - if err != nil { - log.Ctx(ctx).Warn().Msgf("failed to get triggeredBy: %v", err) - } - - switch pipeline.Type { - case v1.BuildDeploy, v1.Build: - buildSpec = v1.RadixBuildSpec{ - ImageTag: imageTag, - Branch: jobSpec.Branch, - CommitID: jobSpec.CommitID, - PushImage: jobSpec.PushImage, - OverrideUseBuildCache: jobSpec.OverrideUseBuildCache, - } - case v1.Promote: - promoteSpec = v1.RadixPromoteSpec{ - DeploymentName: jobSpec.DeploymentName, - FromEnvironment: jobSpec.FromEnvironment, - ToEnvironment: jobSpec.ToEnvironment, - CommitID: jobSpec.CommitID, - } - case v1.Deploy: - deploySpec = v1.RadixDeploySpec{ - ToEnvironment: jobSpec.ToEnvironment, - ImageTagNames: jobSpec.ImageTagNames, - CommitID: jobSpec.CommitID, - ComponentsToDeploy: jobSpec.ComponentsToDeploy, - } - } - - job := v1.RadixJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: jobName, - Labels: map[string]string{ - kube.RadixAppLabel: appName, - }, - Annotations: map[string]string{ - kube.RadixBranchAnnotation: jobSpec.Branch, - }, - }, - Spec: v1.RadixJobSpec{ - AppName: appName, - CloneURL: cloneURL, - PipeLineType: pipeline.Type, - PipelineImage: getPipelineTag(ctx), - TektonImage: getTektonTag(ctx), - Build: buildSpec, - Promote: promoteSpec, - Deploy: deploySpec, - TriggeredBy: triggeredBy, - RadixConfigFullName: fmt.Sprintf("/workspace/%s", radixConfigFullName), - }, - } - - return &job -} - -func (jh JobHandler) getTriggeredBy(triggeredBy string) (string, error) { - if triggeredBy != "" && triggeredBy != "" { - return triggeredBy, nil - } - triggeredBy, err := jh.accounts.GetOriginator() - if err != nil { - return "", fmt.Errorf("failed to get originator: %w", err) - } - return triggeredBy, nil -} - -func getPipelineTag(ctx context.Context) string { - pipelineTag := os.Getenv(pipelineTagEnvironmentVariable) - if pipelineTag == "" { - log.Ctx(ctx).Warn().Msg("No pipeline image tag defined. Using latest") - pipelineTag = "latest" - } else { - log.Ctx(ctx).Info().Msgf("Using %s pipeline image tag", pipelineTag) - } - return pipelineTag -} - -func getTektonTag(ctx context.Context) string { - tektonTag := os.Getenv(tektonTagEnvironmentVariable) - if tektonTag == "" { - log.Ctx(ctx).Warn().Msg("No tekton image tag defined. Using release-latest") - tektonTag = "release-latest" - } else { - log.Ctx(ctx).Info().Msgf("Using %s as tekton image tag", tektonTag) - } - return tektonTag -} - -func getUniqueJobName(image string) (string, string) { - var jobName []string - randomStr := strings.ToLower(radixutils.RandString(5)) - jobName = append(jobName, image, "-", getCurrentTimestamp(), "-", randomStr) - - return strings.Join(jobName, ""), randomStr -} - -func getCurrentTimestamp() string { - t := time.Now() - return t.Format("20060102150405") // YYYYMMDDHHMISS in Go -} diff --git a/api/middleware/auth/anon_principal.go b/api/middleware/auth/anon_principal.go new file mode 100644 index 00000000..f76e64ed --- /dev/null +++ b/api/middleware/auth/anon_principal.go @@ -0,0 +1,8 @@ +package auth + +type anonPrincipal struct{} + +func (p *anonPrincipal) Token() string { return "" } +func (p *anonPrincipal) Id() string { return "anonymous" } +func (p *anonPrincipal) Name() string { return "anonymous" } +func (p *anonPrincipal) IsAuthenticated() bool { return false } diff --git a/api/middleware/auth/authentication.go b/api/middleware/auth/authentication.go new file mode 100644 index 00000000..b722bc9b --- /dev/null +++ b/api/middleware/auth/authentication.go @@ -0,0 +1,126 @@ +package auth + +import ( + "context" + "net/http" + + "github.com/equinor/radix-api/api/utils/token" + "github.com/equinor/radix-common/models" + radixhttp "github.com/equinor/radix-common/net/http" + "github.com/rs/zerolog/log" + "github.com/urfave/negroni/v3" +) + +type ctxUserKey struct{} +type ctxImpersonationKey struct{} + +func NewAuthenticationMiddleware(validator token.ValidatorInterface) negroni.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + ctx := r.Context() + logger := log.Ctx(ctx) + if r.Header.Get("authorization") == "" { + next(w, r) + return + } + + token, err := radixhttp.GetBearerTokenFromHeader(r) + if err != nil { + logger.Warn().Err(err).Msg("authentication error") + if err = radixhttp.ErrorResponse(w, r, err); err != nil { + logger.Err(err).Msg("failed to write response") + } + return + } + principal, err := validator.ValidateToken(ctx, token) + if err != nil { + logger.Warn().Err(err).Msg("authentication error") + if err = radixhttp.ErrorResponse(w, r, err); err != nil { + logger.Err(err).Msg("failed to write response") + } + return + } + + impersonation, err := radixhttp.GetImpersonationFromHeader(r) + if err != nil { + logger.Warn().Err(err).Msg("authorization error") + if err = radixhttp.ErrorResponse(w, r, radixhttp.UnexpectedError("Problems impersonating", err)); err != nil { + logger.Err(err).Msg("failed to write response") + } + return + } + + ctx = context.WithValue(ctx, ctxUserKey{}, principal) + ctx = context.WithValue(ctx, ctxImpersonationKey{}, impersonation) + r = r.WithContext(ctx) + + next(w, r) + } +} + +func CtxTokenPrincipal(ctx context.Context) token.TokenPrincipal { + val, ok := ctx.Value(ctxUserKey{}).(token.TokenPrincipal) + + if !ok { + return &anonPrincipal{} + } + + return val +} + +func CtxImpersonation(ctx context.Context) models.Impersonation { + if val, ok := ctx.Value(ctxImpersonationKey{}).(models.Impersonation); ok { + return val + } + + return models.Impersonation{} +} + +func GetOriginator(ctx context.Context) string { + impersonation := CtxImpersonation(ctx) + principal := CtxTokenPrincipal(ctx) + + if impersonation.PerformImpersonation() { + return impersonation.User + } + + return principal.Name() +} + +func NewZerologAuthenticationDetailsMiddleware() negroni.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + ctx := r.Context() + user := CtxTokenPrincipal(ctx) + impersonation := CtxImpersonation(ctx) + + logContext := log.Ctx(ctx).With() + if user.IsAuthenticated() { + logContext = logContext.Str("user_id", user.Id()) + } else { + logContext = logContext.Bool("anonymous", true) + } + if impersonation.PerformImpersonation() { + logContext = logContext.Str("impersonate_user", impersonation.User).Strs("impersonate_groups", impersonation.Groups) + } + ctx = logContext.Logger().WithContext(ctx) + + r = r.WithContext(ctx) + next(w, r) + } +} + +func NewAuthorizeRequiredMiddleware() negroni.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + logger := log.Ctx(r.Context()) + user := CtxTokenPrincipal(r.Context()) + + if !user.IsAuthenticated() { + logger.Warn().Msg("authorization error") + if err := radixhttp.ErrorResponse(w, r, radixhttp.ForbiddenError("Authorization is required")); err != nil { + logger.Err(err).Msg("failed to write response") + } + return + } + + next(w, r) + } +} diff --git a/api/middleware/auth/authentication_test.go b/api/middleware/auth/authentication_test.go new file mode 100644 index 00000000..7aa39c67 --- /dev/null +++ b/api/middleware/auth/authentication_test.go @@ -0,0 +1,94 @@ +package auth_test + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/equinor/radix-api/api/middleware/auth" + controllertest "github.com/equinor/radix-api/api/test" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestAuthenticatedRequest(t *testing.T) { + validator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + testPrincipal := controllertest.NewTestPrincipal(true) + validator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).Times(1).Return(testPrincipal, nil) + + handler := auth.NewAuthenticationMiddleware(validator) + rw := httptest.NewRecorder() + + var reqCtx context.Context + nextFn := func(w http.ResponseWriter, r *http.Request) { + reqCtx = r.Context() + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("hello world")) + } + + req, _ := http.NewRequest("POST", "/api/anyendpoint", nil) + req.Header.Add("Authorization", "Bearer "+testPrincipal.Token()) + handler.ServeHTTP(rw, req, nextFn) + + reqPrincipal := auth.CtxTokenPrincipal(reqCtx) + assert.Same(t, testPrincipal, reqPrincipal) + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "hello world", rw.Body.String()) +} + +func TestAnonumousRequest(t *testing.T) { + validator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + testPrincipal := controllertest.NewTestPrincipal(true) + validator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).Times(0).Return(testPrincipal, nil) + + handler := auth.NewAuthenticationMiddleware(validator) + rw := httptest.NewRecorder() + + req, _ := http.NewRequest("POST", "/api/anyendpoint", nil) + handler.ServeHTTP(rw, req, newNullMiddleware()) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "hello world", rw.Body.String()) +} + +func TestInvalidHeader(t *testing.T) { + validator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + testPrincipal := controllertest.NewTestPrincipal(true) + validator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).Times(0).Return(testPrincipal, nil) + + handler := auth.NewAuthenticationMiddleware(validator) + rw := httptest.NewRecorder() + + req, _ := http.NewRequest("POST", "/api/anyendpoint", nil) + req.Header.Add("Authorization", "Bearerhello-world") + + handler.ServeHTTP(rw, req, newNullMiddleware()) + assert.Equal(t, http.StatusBadRequest, rw.Code) + assert.Contains(t, rw.Body.String(), "Authentication header is invalid") +} + +func TestInvalidToken(t *testing.T) { + validator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + testPrincipal := controllertest.NewTestPrincipal(true) + validator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).Times(1).Return(testPrincipal, errors.New("some error")) + + handler := auth.NewAuthenticationMiddleware(validator) + rw := httptest.NewRecorder() + + req, _ := http.NewRequest("POST", "/api/anyendpoint", nil) + req.Header.Add("Authorization", "Bearer hello-world") + + handler.ServeHTTP(rw, req, newNullMiddleware()) + assert.Equal(t, http.StatusBadRequest, rw.Code) + assert.Contains(t, rw.Body.String(), "some error") +} + +func newNullMiddleware() func(writer http.ResponseWriter, request *http.Request) { + return func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + _, _ = writer.Write([]byte("hello world")) + } +} diff --git a/api/middleware/cors/cors.go b/api/middleware/cors/cors.go new file mode 100644 index 00000000..215b7c52 --- /dev/null +++ b/api/middleware/cors/cors.go @@ -0,0 +1,56 @@ +package cors + +import ( + "fmt" + + "github.com/rs/cors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func NewMiddleware(clusterName, radixDNSZone string) *cors.Cors { + + corsOptions := cors.Options{ + AllowedOrigins: []string{ + "http://localhost:3000", + "http://localhost:3001", + "http://127.0.0.1:3000", + "http://localhost:8000", + "http://localhost:8086", // For swaggerui testing + // TODO: We should consider: + // 1. "https://*.radix.equinor.com" + // 2. Keep cors rules in ingresses + fmt.Sprintf("https://console.%s", radixDNSZone), + getHostName("web", "radix-web-console-qa", clusterName, radixDNSZone), + getHostName("web", "radix-web-console-prod", clusterName, radixDNSZone), + getHostName("web", "radix-web-console-dev", clusterName, radixDNSZone), + // Due to active-cluster + getActiveClusterHostName("web", "radix-web-console-qa", radixDNSZone), + getActiveClusterHostName("web", "radix-web-console-prod", radixDNSZone), + getActiveClusterHostName("web", "radix-web-console-dev", radixDNSZone), + }, + AllowCredentials: true, + MaxAge: 600, + AllowedHeaders: []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"}, + AllowedMethods: []string{"GET", "PUT", "POST", "OPTIONS", "DELETE", "PATCH"}, + } + + if zerolog.GlobalLevel() <= zerolog.TraceLevel { + // debugging mode + corsOptions.Debug = true + corsLogger := log.Logger.With().Str("pkg", "cors-middleware").Logger() + corsOptions.Logger = &corsLogger + } + + c := cors.New(corsOptions) + + return c +} + +func getActiveClusterHostName(componentName, namespace, radixDNSZone string) string { + return fmt.Sprintf("https://%s-%s.%s", componentName, namespace, radixDNSZone) +} + +func getHostName(componentName, namespace, clustername, radixDNSZone string) string { + return fmt.Sprintf("https://%s-%s.%s.%s", componentName, namespace, clustername, radixDNSZone) +} diff --git a/api/router/middleware.go b/api/middleware/logger/middleware.go similarity index 57% rename from api/router/middleware.go rename to api/middleware/logger/middleware.go index 71b510bc..c8be393b 100644 --- a/api/router/middleware.go +++ b/api/middleware/logger/middleware.go @@ -1,17 +1,17 @@ -package router +package logger import ( - "context" "net" "net/http" "github.com/felixge/httpsnoop" "github.com/rs/xid" "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/urfave/negroni/v3" ) -func zerologRequestLogger() negroni.HandlerFunc { +func NewZerologResponseLoggerMiddleware() negroni.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { m := httpsnoop.CaptureMetrics(next, w, r) @@ -27,36 +27,35 @@ func zerologRequestLogger() negroni.HandlerFunc { ev = logger.Info() //nolint:zerologlint // Msg for ev is called later } - remoteIp, _, _ := net.SplitHostPort(r.RemoteAddr) ev. - Str("remote_addr", remoteIp). - Str("referer", r.Referer()). - Str("method", r.Method). - Str("path", r.URL.Path). - Str("query", r.URL.RawQuery). Int("status", m.Code). Int64("body_size", m.Written). Int64("elapsed_ms", m.Duration.Milliseconds()). - Str("user_agent", r.UserAgent()). Msg(http.StatusText(m.Code)) } } -type setZerologLoggerFn func(context.Context) zerolog.Logger +func NewZerologRequestIdMiddleware() negroni.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + logger := log.Ctx(r.Context()).With().Str("request_id", xid.New().String()).Logger() + r = r.WithContext(logger.WithContext(r.Context())) -// zerologLoggerWithRequestId returns a zerolog logger with a request_id field with a new GUID -func zerologLoggerWithRequestId(ctx context.Context) zerolog.Logger { - return zerolog.Ctx(ctx).With().Str("request_id", xid.New().String()).Logger() + next(w, r) + } } - -// setZerologLogger attaches the zerolog logger returned from each loggerFns function to a shallow copy of the request context -// The logger can then be accessed in a controller method by calling zerolog.Ctx(ctx) -func setZerologLogger(loggerFns ...setZerologLoggerFn) negroni.HandlerFunc { +func NewZerologRequestDetailsMiddleware() negroni.HandlerFunc { return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - for _, loggerFn := range loggerFns { - logger := loggerFn(r.Context()) - r = r.WithContext(logger.WithContext(r.Context())) - } - next.ServeHTTP(w, r) + remoteIp, _, _ := net.SplitHostPort(r.RemoteAddr) + logger := log.Ctx(r.Context()).With(). + Str("method", r.Method). + Str("path", r.URL.Path). + Str("remote_addr", remoteIp). + Str("referer", r.Referer()). + Str("query", r.URL.RawQuery). + Str("user_agent", r.UserAgent()). + Logger() + r = r.WithContext(logger.WithContext(r.Context())) + + next(w, r) } } diff --git a/api/middleware/recovery/recovery.go b/api/middleware/recovery/recovery.go new file mode 100644 index 00000000..ba778a5d --- /dev/null +++ b/api/middleware/recovery/recovery.go @@ -0,0 +1,13 @@ +package recovery + +import ( + "github.com/rs/zerolog/log" + "github.com/urfave/negroni/v3" +) + +func NewMiddleware() *negroni.Recovery { + rec := negroni.NewRecovery() + rec.PrintStack = false + rec.Logger = &log.Logger + return rec +} diff --git a/api/router/api.go b/api/router/api.go index 4fa47268..495ef556 100644 --- a/api/router/api.go +++ b/api/router/api.go @@ -1,137 +1,78 @@ package router import ( - "fmt" "net/http" - "os" - "github.com/equinor/radix-api/api/defaults" + "github.com/equinor/radix-api/api/middleware/auth" + "github.com/equinor/radix-api/api/middleware/logger" + "github.com/equinor/radix-api/api/middleware/recovery" "github.com/equinor/radix-api/api/utils" + "github.com/equinor/radix-api/api/utils/token" "github.com/equinor/radix-api/models" "github.com/equinor/radix-api/swaggerui" "github.com/gorilla/mux" - "github.com/rs/cors" - "github.com/rs/zerolog/log" "github.com/urfave/negroni/v3" ) const ( - apiVersionRoute = "/api/v1" - admissionControllerRootPath = "/admissioncontrollers" - buildstatusControllerRootPath = "/buildstatus" - healthControllerPath = "/health/" - radixDNSZoneEnvironmentVariable = "RADIX_DNS_ZONE" - swaggerUIPath = "/swaggerui" + apiVersionRoute = "/api/v1" ) // NewAPIHandler Constructor function -func NewAPIHandler(clusterName string, kubeUtil utils.KubeUtil, controllers ...models.Controller) http.Handler { - router := mux.NewRouter().StrictSlash(true) - - initializeSwaggerUI(router) - initializeAPIServer(kubeUtil, router, controllers) - initializeHealthEndpoint(router) - +func NewAPIHandler(validator token.ValidatorInterface, kubeUtil utils.KubeUtil, controllers ...models.Controller) http.Handler { serveMux := http.NewServeMux() - serveMux.Handle(healthControllerPath, negroni.New( - negroni.Wrap(router), - )) - serveMux.Handle("/api/", negroni.New( - negroni.Wrap(router), - )) - - // TODO: We should maybe have oauth to stop any non-radix user from being able to see the API - serveMux.Handle("/swaggerui/", negroni.New( - negroni.Wrap(router), - )) - - rec := negroni.NewRecovery() - rec.PrintStack = false + serveMux.Handle("/health/", createHealthHandler()) + serveMux.Handle("/swaggerui/", createSwaggerHandler()) + serveMux.Handle("/api/", createApiRouter(kubeUtil, controllers)) n := negroni.New( - rec, - setZerologLogger(zerologLoggerWithRequestId), - zerologRequestLogger(), + recovery.NewMiddleware(), + logger.NewZerologRequestIdMiddleware(), + logger.NewZerologRequestDetailsMiddleware(), + auth.NewAuthenticationMiddleware(validator), + auth.NewZerologAuthenticationDetailsMiddleware(), + logger.NewZerologResponseLoggerMiddleware(), ) n.UseHandler(serveMux) - useOutClusterClient := kubeUtil.IsUseOutClusterClient() - return getCORSHandler(clusterName, n, useOutClusterClient) -} - -func getCORSHandler(clusterName string, handler http.Handler, useOutClusterClient bool) http.Handler { - radixDNSZone := os.Getenv(defaults.RadixDNSZoneEnvironmentVariable) - - corsOptions := cors.Options{ - AllowedOrigins: []string{ - "http://localhost:3000", - "http://localhost:3001", - "http://127.0.0.1:3000", - "http://localhost:8000", - "http://localhost:8086", // For swaggerui testing - // TODO: We should consider: - // 1. "https://*.radix.equinor.com" - // 2. Keep cors rules in ingresses - fmt.Sprintf("https://console.%s", radixDNSZone), - getHostName("web", "radix-web-console-qa", clusterName, radixDNSZone), - getHostName("web", "radix-web-console-prod", clusterName, radixDNSZone), - getHostName("web", "radix-web-console-dev", clusterName, radixDNSZone), - // Due to active-cluster - getActiveClusterHostName("web", "radix-web-console-qa", radixDNSZone), - getActiveClusterHostName("web", "radix-web-console-prod", radixDNSZone), - getActiveClusterHostName("web", "radix-web-console-dev", radixDNSZone), - }, - AllowCredentials: true, - MaxAge: 600, - AllowedHeaders: []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"}, - AllowedMethods: []string{"GET", "PUT", "POST", "OPTIONS", "DELETE", "PATCH"}, - } - - if !useOutClusterClient { - // debugging mode - corsOptions.Debug = true - corsLogger := log.Logger.With().Str("pkg", "cors-middleware").Logger() - corsOptions.Logger = &corsLogger - // necessary header to allow ajax requests directly from radix-web-console app in browser - corsOptions.AllowedHeaders = append(corsOptions.AllowedHeaders, "X-Requested-With") - } - - c := cors.New(corsOptions) - - return c.Handler(handler) -} - -func getActiveClusterHostName(componentName, namespace, radixDNSZone string) string { - return fmt.Sprintf("https://%s-%s.%s", componentName, namespace, radixDNSZone) -} - -func getHostName(componentName, namespace, clustername, radixDNSZone string) string { - return fmt.Sprintf("https://%s-%s.%s.%s", componentName, namespace, clustername, radixDNSZone) + return n } - -func initializeAPIServer(kubeUtil utils.KubeUtil, router *mux.Router, controllers []models.Controller) { +func createApiRouter(kubeUtil utils.KubeUtil, controllers []models.Controller) *mux.Router { + router := mux.NewRouter().StrictSlash(true) for _, controller := range controllers { for _, route := range controller.GetRoutes() { - addHandlerRoute(kubeUtil, router, route) + path := apiVersionRoute + route.Path + handler := utils.NewRadixMiddleware( + kubeUtil, + path, + route.Method, + route.AllowUnauthenticatedUsers, + route.KubeApiConfig.QPS, + route.KubeApiConfig.Burst, + route.HandlerFunc, + ) + + n := negroni.New() + if !route.AllowUnauthenticatedUsers { + n.Use(auth.NewAuthorizeRequiredMiddleware()) + } + n.UseHandler(handler) + router.Handle(path, n).Methods(route.Method) } } + return router } -func initializeSwaggerUI(router *mux.Router) { +func createSwaggerHandler() http.Handler { swaggerFsHandler := http.FileServer(http.FS(swaggerui.FS())) - swaggerui := http.StripPrefix(swaggerUIPath, swaggerFsHandler) - router.PathPrefix(swaggerUIPath).Handler(swaggerui) -} + swaggerui := http.StripPrefix("/swaggerui", swaggerFsHandler) -func initializeHealthEndpoint(router *mux.Router) { - router.HandleFunc(healthControllerPath, func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }).Methods("GET") + return swaggerui } -func addHandlerRoute(kubeUtil utils.KubeUtil, router *mux.Router, route models.Route) { - path := apiVersionRoute + route.Path - router.HandleFunc(path, - utils.NewRadixMiddleware(kubeUtil, path, route.Method, route.AllowUnauthenticatedUsers, route.KubeApiConfig.QPS, route.KubeApiConfig.Burst, route.HandlerFunc).Handle).Methods(route.Method) +func createHealthHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + }) } diff --git a/api/router/metrics.go b/api/router/metrics.go index 0e36a9dd..c303f2d7 100644 --- a/api/router/metrics.go +++ b/api/router/metrics.go @@ -3,6 +3,8 @@ package router import ( "net/http" + "github.com/equinor/radix-api/api/middleware/logger" + "github.com/equinor/radix-api/api/middleware/recovery" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/urfave/negroni/v3" ) @@ -12,12 +14,11 @@ func NewMetricsHandler() http.Handler { serveMux := http.NewServeMux() serveMux.Handle("GET /metrics", promhttp.Handler()) - rec := negroni.NewRecovery() - rec.PrintStack = false n := negroni.New( - rec, - setZerologLogger(zerologLoggerWithRequestId), - zerologRequestLogger(), + recovery.NewMiddleware(), + logger.NewZerologRequestIdMiddleware(), + logger.NewZerologRequestDetailsMiddleware(), + logger.NewZerologResponseLoggerMiddleware(), ) n.UseHandler(serveMux) diff --git a/api/secrets/secret_controller_test.go b/api/secrets/secret_controller_test.go index 022cd8b6..6215eca4 100644 --- a/api/secrets/secret_controller_test.go +++ b/api/secrets/secret_controller_test.go @@ -12,6 +12,7 @@ import ( controllertest "github.com/equinor/radix-api/api/test" "github.com/equinor/radix-api/api/utils/tlsvalidation" tlsvalidationmock "github.com/equinor/radix-api/api/utils/tlsvalidation/mock" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" radixhttp "github.com/equinor/radix-common/net/http" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" commontest "github.com/equinor/radix-operator/pkg/apis/test" @@ -58,7 +59,9 @@ func setupTest(t *testing.T, tlsValidator tlsvalidation.Validator) (*commontest. require.NoError(t, err) // secretControllerTestUtils is used for issuing HTTP request and processing responses - secretControllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, NewSecretController(tlsValidator)) + mockValidator := authnmock.NewMockValidatorInterface(gomock.NewController(t)) + mockValidator.EXPECT().ValidateToken(gomock.Any(), gomock.Any()).AnyTimes().Return(controllertest.NewTestPrincipal(true), nil) + secretControllerTestUtils := controllertest.NewTestUtils(kubeclient, radixclient, kedaClient, secretproviderclient, certClient, mockValidator, NewSecretController(tlsValidator)) return &commonTestUtils, &secretControllerTestUtils, kubeclient, radixclient, prometheusclient, secretproviderclient } diff --git a/api/test/test_principal.go b/api/test/test_principal.go new file mode 100644 index 00000000..f1e250c5 --- /dev/null +++ b/api/test/test_principal.go @@ -0,0 +1,17 @@ +package test + +type TestPrincipal struct{ authenticated bool } + +func (p *TestPrincipal) Token() string { + if p.authenticated { + return "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyIsImtpZCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyJ9.eyJhdWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MjQ1YTJlYzEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xMjM0NTY3OC03NTY1LTIzNDItMjM0Mi0xMjM0MDViNDU5YjAvIiwiaWF0IjoxNTc1MzU1NTA4LCJuYmYiOjE1NzUzNTU1MDgsImV4cCI6MTU3NTM1OTQwOCwiYWNyIjoiMSIsImFpbyI6IjQyYXNkYXMiLCJhbXIiOlsicHdkIl0sImFwcGlkIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDc5MDM5YTkwIiwiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJKb2huIiwiZ2l2ZW5fbmFtZSI6IkRvZSIsImhhc2dyb3VwcyI6InRydWUiLCJpcGFkZHIiOiIxMC4xMC4xMC4xMCIsIm5hbWUiOiJKb2huIERvZSIsIm9pZCI6IjEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzRmYzhmYTBlYSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0xMjM0NTY3ODktMTIzNDU2OTc4MC0xMjM0NTY3ODktMTIzNDU2NyIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6IjBoa2JpbEo3MTIzNHpSU3h6eHZiSW1hc2RmZ3N4amI2YXNkZmVOR2FzZGYiLCJ0aWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MDViNDU5YjAiLCJ1bmlxdWVfbmFtZSI6Im5vdC1leGlzdGluZy1yYWRpeC1lbWFpbEBlcXVpbm9yLmNvbSIsInVwbiI6Im5vdC1leGlzdGluZy10ZXN0LXJhZGl4LWVtYWlsQGVxdWlub3IuY29tIiwidXRpIjoiQlMxMmFzR2R1RXlyZUVjRGN2aDJBRyIsInZlciI6IjEuMCJ9.EB5z7Mk34NkFPCP8MqaNMo4UeWgNyO4-qEmzOVPxfoBqbgA16Ar4xeONXODwjZn9iD-CwJccusW6GP0xZ_PJHBFpfaJO_tLaP1k0KhT-eaANt112TvDBt0yjHtJg6He6CEDqagREIsH3w1mSm40zWLKGZeRLdnGxnQyKsTmNJ1rFRdY3AyoEgf6-pnJweUt0LaFMKmIJ2HornStm2hjUstBaji_5cSS946zqp4tgrc-RzzDuaQXzqlVL2J22SR2S_Oux_3yw88KmlhEFFP9axNcbjZrzW3L9XWnPT6UzVIaVRaNRSWfqDATg-jeHg4Gm1bp8w0aIqLdDxc9CfFMjuQ" + } + return "" +} +func (p *TestPrincipal) Id() string { return "test-id" } +func (p *TestPrincipal) Name() string { return "test-principal" } +func (p *TestPrincipal) IsAuthenticated() bool { return p.authenticated } + +func NewTestPrincipal(authenticated bool) *TestPrincipal { + return &TestPrincipal{authenticated} +} diff --git a/api/test/utils.go b/api/test/utils.go index beb80900..42c3714a 100644 --- a/api/test/utils.go +++ b/api/test/utils.go @@ -7,6 +7,8 @@ import ( "net/http" "net/http/httptest" + token "github.com/equinor/radix-api/api/utils/token" + authnmock "github.com/equinor/radix-api/api/utils/token/mock" kedav2 "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned" kedafake "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/fake" "github.com/rs/zerolog/log" @@ -35,10 +37,11 @@ type Utils struct { secretProviderClient *secretsstorevclientfake.Clientset certClient *certclientfake.Clientset controllers []models.Controller + validator token.ValidatorInterface } // NewTestUtils Constructor -func NewTestUtils(kubeClient *kubernetesfake.Clientset, radixClient *radixclientfake.Clientset, kedaClient *kedafake.Clientset, secretProviderClient *secretsstorevclientfake.Clientset, certClient *certclientfake.Clientset, controllers ...models.Controller) Utils { +func NewTestUtils(kubeClient *kubernetesfake.Clientset, radixClient *radixclientfake.Clientset, kedaClient *kedafake.Clientset, secretProviderClient *secretsstorevclientfake.Clientset, certClient *certclientfake.Clientset, validator *authnmock.MockValidatorInterface, controllers ...models.Controller) Utils { return Utils{ kubeClient: kubeClient, radixClient: radixClient, @@ -46,6 +49,7 @@ func NewTestUtils(kubeClient *kubernetesfake.Clientset, radixClient *radixclient secretProviderClient: secretProviderClient, certClient: certClient, controllers: controllers, + validator: validator, } } @@ -63,7 +67,7 @@ func (tu *Utils) ExecuteUnAuthorizedRequest(method, endpoint string) <-chan *htt go func() { rr := httptest.NewRecorder() defer close(response) - router.NewAPIHandler("anyClusterName", NewKubeUtilMock(tu.kubeClient, tu.radixClient, tu.kedaClient, tu.secretProviderClient, tu.certClient), tu.controllers...).ServeHTTP(rr, req) + router.NewAPIHandler(tu.validator, NewKubeUtilMock(tu.kubeClient, tu.radixClient, tu.kedaClient, tu.secretProviderClient, tu.certClient), tu.controllers...).ServeHTTP(rr, req) response <- rr }() @@ -87,7 +91,7 @@ func (tu *Utils) ExecuteRequestWithParameters(method, endpoint string, parameter go func() { rr := httptest.NewRecorder() defer close(response) - router.NewAPIHandler("anyClusterName", NewKubeUtilMock(tu.kubeClient, tu.radixClient, tu.kedaClient, tu.secretProviderClient, tu.certClient), tu.controllers...).ServeHTTP(rr, req) + router.NewAPIHandler(tu.validator, NewKubeUtilMock(tu.kubeClient, tu.radixClient, tu.kedaClient, tu.secretProviderClient, tu.certClient), tu.controllers...).ServeHTTP(rr, req) response <- rr }() @@ -97,7 +101,7 @@ func (tu *Utils) ExecuteRequestWithParameters(method, endpoint string, parameter // Generates fake token. Use it to reduce noise of security scanner fail alerts func getFakeToken() string { - return "bea" + "rer " + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyIsImtpZCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyJ9.eyJhdWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MjQ1YTJlYzEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xMjM0NTY3OC03NTY1LTIzNDItMjM0Mi0xMjM0MDViNDU5YjAvIiwiaWF0IjoxNTc1MzU1NTA4LCJuYmYiOjE1NzUzNTU1MDgsImV4cCI6MTU3NTM1OTQwOCwiYWNyIjoiMSIsImFpbyI6IjQyYXNkYXMiLCJhbXIiOlsicHdkIl0sImFwcGlkIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDc5MDM5YTkwIiwiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJKb2huIiwiZ2l2ZW5fbmFtZSI6IkRvZSIsImhhc2dyb3VwcyI6InRydWUiLCJpcGFkZHIiOiIxMC4xMC4xMC4xMCIsIm5hbWUiOiJKb2huIERvZSIsIm9pZCI6IjEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzRmYzhmYTBlYSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0xMjM0NTY3ODktMTIzNDU2OTc4MC0xMjM0NTY3ODktMTIzNDU2NyIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6IjBoa2JpbEo3MTIzNHpSU3h6eHZiSW1hc2RmZ3N4amI2YXNkZmVOR2FzZGYiLCJ0aWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MDViNDU5YjAiLCJ1bmlxdWVfbmFtZSI6Im5vdC1leGlzdGluZy1yYWRpeC1lbWFpbEBlcXVpbm9yLmNvbSIsInVwbiI6Im5vdC1leGlzdGluZy10ZXN0LXJhZGl4LWVtYWlsQGVxdWlub3IuY29tIiwidXRpIjoiQlMxMmFzR2R1RXlyZUVjRGN2aDJBRyIsInZlciI6IjEuMCJ9.EB5z7Mk34NkFPCP8MqaNMo4UeWgNyO4-qEmzOVPxfoBqbgA16Ar4xeONXODwjZn9iD-CwJccusW6GP0xZ_PJHBFpfaJO_tLaP1k0KhT-eaANt112TvDBt0yjHtJg6He6CEDqagREIsH3w1mSm40zWLKGZeRLdnGxnQyKsTmNJ1rFRdY3AyoEgf6-pnJweUt0LaFMKmIJ2HornStm2hjUstBaji_5cSS946zqp4tgrc-RzzDuaQXzqlVL2J22SR2S_Oux_3yw88KmlhEFFP9axNcbjZrzW3L9XWnPT6UzVIaVRaNRSWfqDATg-jeHg4Gm1bp8w0aIqLdDxc9CfFMjuQ" + return "bearer " + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyIsImtpZCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyJ9.eyJhdWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MjQ1YTJlYzEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xMjM0NTY3OC03NTY1LTIzNDItMjM0Mi0xMjM0MDViNDU5YjAvIiwiaWF0IjoxNTc1MzU1NTA4LCJuYmYiOjE1NzUzNTU1MDgsImV4cCI6MTU3NTM1OTQwOCwiYWNyIjoiMSIsImFpbyI6IjQyYXNkYXMiLCJhbXIiOlsicHdkIl0sImFwcGlkIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDc5MDM5YTkwIiwiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJKb2huIiwiZ2l2ZW5fbmFtZSI6IkRvZSIsImhhc2dyb3VwcyI6InRydWUiLCJpcGFkZHIiOiIxMC4xMC4xMC4xMCIsIm5hbWUiOiJKb2huIERvZSIsIm9pZCI6IjEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzRmYzhmYTBlYSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0xMjM0NTY3ODktMTIzNDU2OTc4MC0xMjM0NTY3ODktMTIzNDU2NyIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6IjBoa2JpbEo3MTIzNHpSU3h6eHZiSW1hc2RmZ3N4amI2YXNkZmVOR2FzZGYiLCJ0aWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MDViNDU5YjAiLCJ1bmlxdWVfbmFtZSI6Im5vdC1leGlzdGluZy1yYWRpeC1lbWFpbEBlcXVpbm9yLmNvbSIsInVwbiI6Im5vdC1leGlzdGluZy10ZXN0LXJhZGl4LWVtYWlsQGVxdWlub3IuY29tIiwidXRpIjoiQlMxMmFzR2R1RXlyZUVjRGN2aDJBRyIsInZlciI6IjEuMCJ9.EB5z7Mk34NkFPCP8MqaNMo4UeWgNyO4-qEmzOVPxfoBqbgA16Ar4xeONXODwjZn9iD-CwJccusW6GP0xZ_PJHBFpfaJO_tLaP1k0KhT-eaANt112TvDBt0yjHtJg6He6CEDqagREIsH3w1mSm40zWLKGZeRLdnGxnQyKsTmNJ1rFRdY3AyoEgf6-pnJweUt0LaFMKmIJ2HornStm2hjUstBaji_5cSS946zqp4tgrc-RzzDuaQXzqlVL2J22SR2S_Oux_3yw88KmlhEFFP9axNcbjZrzW3L9XWnPT6UzVIaVRaNRSWfqDATg-jeHg4Gm1bp8w0aIqLdDxc9CfFMjuQ" } // GetErrorResponse Gets error repsonse @@ -127,10 +131,6 @@ type kubeUtilMock struct { kedaClient *kedafake.Clientset } -func (ku *kubeUtilMock) IsUseOutClusterClient() bool { - return true -} - // NewKubeUtilMock Constructor func NewKubeUtilMock(kubeClient *kubernetesfake.Clientset, radixClient *radixclientfake.Clientset, kedaClient *kedafake.Clientset, secretProviderClient *secretsstorevclientfake.Clientset, certClient *certclientfake.Clientset) utils.KubeUtil { return &kubeUtilMock{ @@ -142,17 +142,12 @@ func NewKubeUtilMock(kubeClient *kubernetesfake.Clientset, radixClient *radixcli } } -// GetOutClusterKubernetesClient Gets a kubefake client using the bearer token from the radix api client -func (ku *kubeUtilMock) GetOutClusterKubernetesClient(_ string, _ ...utils.RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretsstorevclient.Interface, tektonclient.Interface, certclient.Interface) { - return ku.kubeClient, ku.radixClient, ku.kedaClient, ku.secretProviderClient, nil, ku.certClient -} - -// GetOutClusterKubernetesClientWithImpersonation Gets a kubefake client -func (ku *kubeUtilMock) GetOutClusterKubernetesClientWithImpersonation(_ string, impersonation radixmodels.Impersonation, _ ...utils.RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretsstorevclient.Interface, tektonclient.Interface, certclient.Interface) { +// GetUserKubernetesClient Gets a kubefake client +func (ku *kubeUtilMock) GetUserKubernetesClient(_ string, impersonation radixmodels.Impersonation, _ ...utils.RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretsstorevclient.Interface, tektonclient.Interface, certclient.Interface) { return ku.kubeClient, ku.radixClient, ku.kedaClient, ku.secretProviderClient, nil, ku.certClient } -// GetInClusterKubernetesClient Gets a kubefake client using the config of the running pod -func (ku *kubeUtilMock) GetInClusterKubernetesClient(_ ...utils.RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretsstorevclient.Interface, tektonclient.Interface, certclient.Interface) { +// GetServerKubernetesClient Gets a kubefake client using the config of the running pod +func (ku *kubeUtilMock) GetServerKubernetesClient(_ ...utils.RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretsstorevclient.Interface, tektonclient.Interface, certclient.Interface) { return ku.kubeClient, ku.radixClient, ku.kedaClient, ku.secretProviderClient, nil, ku.certClient } diff --git a/api/utils/kubernetes.go b/api/utils/kubernetes.go index c4083cae..5031ab82 100644 --- a/api/utils/kubernetes.go +++ b/api/utils/kubernetes.go @@ -38,19 +38,11 @@ func WithBurst(burst int) RestClientConfigOption { // KubeUtil Interface to be mocked in tests type KubeUtil interface { - GetOutClusterKubernetesClient(string, ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) - GetOutClusterKubernetesClientWithImpersonation(string, radixmodels.Impersonation, ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) - GetInClusterKubernetesClient(...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) - IsUseOutClusterClient() bool + GetUserKubernetesClient(string, radixmodels.Impersonation, ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) + GetServerKubernetesClient(...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) } -type kubeUtil struct { - useOutClusterClient bool -} - -func (ku *kubeUtil) IsUseOutClusterClient() bool { - return ku.useOutClusterClient -} +type kubeUtil struct{} var ( nrRequests = promauto.NewHistogramVec(prometheus.HistogramOpts{ @@ -61,41 +53,27 @@ var ( ) // NewKubeUtil Constructor -func NewKubeUtil(useOutClusterClient bool) KubeUtil { - return &kubeUtil{ - useOutClusterClient, - } -} - -// GetOutClusterKubernetesClient Gets a kubernetes client using the bearer token from the radix api client -func (ku *kubeUtil) GetOutClusterKubernetesClient(token string, options ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) { - return ku.GetOutClusterKubernetesClientWithImpersonation(token, radixmodels.Impersonation{}, options...) +func NewKubeUtil() KubeUtil { + return &kubeUtil{} } -// GetOutClusterKubernetesClientWithImpersonation Gets a kubernetes client using the bearer token from the radix api client -func (ku *kubeUtil) GetOutClusterKubernetesClientWithImpersonation(token string, impersonation radixmodels.Impersonation, options ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) { - if ku.useOutClusterClient { - config := getOutClusterClientConfig(token, impersonation, options) - return getKubernetesClientFromConfig(config) - } - - return ku.GetInClusterKubernetesClient(options...) +// GetUserKubernetesClient Gets a kubernetes client using the bearer token from the radix api client +func (ku *kubeUtil) GetUserKubernetesClient(token string, impersonation radixmodels.Impersonation, options ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) { + config := getUserClientConfig(token, impersonation, options) + return getKubernetesClientFromConfig(config) } -// GetInClusterKubernetesClient Gets a kubernetes client using the config of the running pod -func (ku *kubeUtil) GetInClusterKubernetesClient(options ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) { - config := getInClusterClientConfig(options) +// GetServerKubernetesClient Gets a kubernetes client using the config of host or pod +func (ku *kubeUtil) GetServerKubernetesClient(options ...RestClientConfigOption) (kubernetes.Interface, radixclient.Interface, kedav2.Interface, secretproviderclient.Interface, tektonclient.Interface, certclient.Interface) { + config := getServerClientConfig(options) return getKubernetesClientFromConfig(config) } -func getOutClusterClientConfig(token string, impersonation radixmodels.Impersonation, options []RestClientConfigOption) *restclient.Config { - host := os.Getenv("K8S_API_HOST") - if host == "" { - host = "https://kubernetes.default.svc" - } +func getUserClientConfig(token string, impersonation radixmodels.Impersonation, options []RestClientConfigOption) *restclient.Config { + cfg := getServerClientConfig(options) kubeConfig := &restclient.Config{ - Host: host, + Host: cfg.Host, BearerToken: token, TLSClientConfig: restclient.TLSClientConfig{ Insecure: true, @@ -111,23 +89,23 @@ func getOutClusterClientConfig(token string, impersonation radixmodels.Impersona kubeConfig.Impersonate = impersonationConfig } kubeConfig.Wrap(logs.Logger(func(e *zerolog.Event) { - e.Str("client", "out-cluster") + e.Str("client", "user") })) return addCommonConfigs(kubeConfig, options) } -func getInClusterClientConfig(options []RestClientConfigOption) *restclient.Config { +func getServerClientConfig(options []RestClientConfigOption) *restclient.Config { kubeConfigPath := os.Getenv("HOME") + "/.kube/config" config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) if err != nil { config, err = restclient.InClusterConfig() if err != nil { - log.Fatal().Err(err).Msg("getClusterConfig InClusterConfig") + log.Fatal().Err(err).Msg("failed to create in cluster config") } } config.Wrap(logs.Logger(func(e *zerolog.Event) { - e.Str("client", "in-cluster") + e.Str("client", "server") })) return addCommonConfigs(config, options) diff --git a/api/utils/radix_middleware.go b/api/utils/radix_middleware.go index b8796d8b..906be29d 100644 --- a/api/utils/radix_middleware.go +++ b/api/utils/radix_middleware.go @@ -5,6 +5,7 @@ import ( "time" "github.com/equinor/radix-api/api/metrics" + "github.com/equinor/radix-api/api/middleware/auth" "github.com/equinor/radix-api/models" radixhttp "github.com/equinor/radix-common/net/http" "github.com/gorilla/mux" @@ -39,7 +40,7 @@ func NewRadixMiddleware(kubeUtil KubeUtil, path, method string, allowUnauthentic } // Handle Wraps radix handler methods -func (handler *RadixMiddleware) Handle(w http.ResponseWriter, r *http.Request) { +func (handler *RadixMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() defer func() { @@ -57,45 +58,14 @@ func (handler *RadixMiddleware) Handle(w http.ResponseWriter, r *http.Request) { func (handler *RadixMiddleware) handleAuthorization(w http.ResponseWriter, r *http.Request) { logger := log.Ctx(r.Context()) - useOutClusterClient := handler.kubeUtil.IsUseOutClusterClient() - token, err := getBearerTokenFromHeader(r, useOutClusterClient) - - if err != nil { - logger.Warn().Err(err).Msg("authorization error") - if err = radixhttp.ErrorResponse(w, r, err); err != nil { - logger.Err(err).Msg("failed to write response") - } - return - } - - impersonation, err := radixhttp.GetImpersonationFromHeader(r) - if err != nil { - logger.Warn().Err(err).Msg("authorization error") - if err = radixhttp.ErrorResponse(w, r, radixhttp.UnexpectedError("Problems impersonating", err)); err != nil { - logger.Err(err).Msg("failed to write response") - } - return - } + token := auth.CtxTokenPrincipal(r.Context()).Token() + impersonation := auth.CtxImpersonation(r.Context()) restOptions := handler.getRestClientOptions() - inClusterClient, inClusterRadixClient, inClusterKedaClient, inClusterSecretProviderClient, inClusterTektonClient, inClusterCertManagerClient := handler.kubeUtil.GetInClusterKubernetesClient(restOptions...) - outClusterClient, outClusterRadixClient, outClusterKedaClient, outClusterSecretProviderClient, outClusterTektonClient, outClusterCertManagerClient := handler.kubeUtil.GetOutClusterKubernetesClientWithImpersonation(token, impersonation, restOptions...) - - accounts := models.NewAccounts( - inClusterClient, - inClusterRadixClient, - inClusterKedaClient, - inClusterSecretProviderClient, - inClusterTektonClient, - inClusterCertManagerClient, - outClusterClient, - outClusterRadixClient, - outClusterKedaClient, - outClusterSecretProviderClient, - outClusterTektonClient, - outClusterCertManagerClient, - token, - impersonation) + inClusterClient, inClusterRadixClient, inClusterKedaClient, inClusterSecretProviderClient, inClusterTektonClient, inClusterCertManagerClient := handler.kubeUtil.GetServerKubernetesClient(restOptions...) + outClusterClient, outClusterRadixClient, outClusterKedaClient, outClusterSecretProviderClient, outClusterTektonClient, outClusterCertManagerClient := handler.kubeUtil.GetUserKubernetesClient(token, impersonation, restOptions...) + + accounts := models.NewAccounts(inClusterClient, inClusterRadixClient, inClusterKedaClient, inClusterSecretProviderClient, inClusterTektonClient, inClusterCertManagerClient, outClusterClient, outClusterRadixClient, outClusterKedaClient, outClusterSecretProviderClient, outClusterTektonClient, outClusterCertManagerClient) // Check if registration of application exists for application-specific requests if appName, exists := mux.Vars(r)["appName"]; exists { @@ -111,6 +81,16 @@ func (handler *RadixMiddleware) handleAuthorization(w http.ResponseWriter, r *ht handler.next(accounts, w, r) } +func (handler *RadixMiddleware) handleAnonymous(w http.ResponseWriter, r *http.Request) { + restOptions := handler.getRestClientOptions() + inClusterClient, inClusterRadixClient, inClusterKedaClient, inClusterSecretProviderClient, inClusterTektonClient, inClusterCertManagerClient := handler.kubeUtil.GetServerKubernetesClient(restOptions...) + + sa := models.NewServiceAccount(inClusterClient, inClusterRadixClient, inClusterKedaClient, inClusterSecretProviderClient, inClusterTektonClient, inClusterCertManagerClient) + accounts := models.Accounts{ServiceAccount: sa} + + handler.next(accounts, w, r) +} + func (handler *RadixMiddleware) getRestClientOptions() []RestClientConfigOption { var options []RestClientConfigOption @@ -124,21 +104,3 @@ func (handler *RadixMiddleware) getRestClientOptions() []RestClientConfigOption return options } - -func (handler *RadixMiddleware) handleAnonymous(w http.ResponseWriter, r *http.Request) { - restOptions := handler.getRestClientOptions() - inClusterClient, inClusterRadixClient, inClusterKedaClient, inClusterSecretProviderClient, inClusterTektonClient, inClusterCertManagerClient := handler.kubeUtil.GetInClusterKubernetesClient(restOptions...) - - sa := models.NewServiceAccount(inClusterClient, inClusterRadixClient, inClusterKedaClient, inClusterSecretProviderClient, inClusterTektonClient, inClusterCertManagerClient) - accounts := models.Accounts{ServiceAccount: sa} - - handler.next(accounts, w, r) -} - -func getBearerTokenFromHeader(r *http.Request, useOutClusterClient bool) (string, error) { - if useOutClusterClient { - return radixhttp.GetBearerTokenFromHeader(r) - } - // if we're in debug mode, arbitrary bearer token is injected - return "some_arbitrary_token", nil -} diff --git a/api/utils/token/azure_principal.go b/api/utils/token/azure_principal.go new file mode 100644 index 00000000..2cce1d28 --- /dev/null +++ b/api/utils/token/azure_principal.go @@ -0,0 +1,48 @@ +package token + +import ( + "context" + + "github.com/auth0/go-jwt-middleware/v2/validator" +) + +type azureClaims struct { + ObjectId string `json:"oid,omitempty"` + Upn string `json:"upn,omitempty"` + AppDisplayName string `json:"app_displayname,omitempty"` + AppId string `json:"appid,omitempty"` +} + +func (c *azureClaims) Validate(_ context.Context) error { + return nil +} + +type azurePrincipal struct { + token string + claims *validator.ValidatedClaims + azureClaims *azureClaims +} + +func (p *azurePrincipal) Token() string { + return p.token +} +func (p *azurePrincipal) IsAuthenticated() bool { + return true +} +func (p *azurePrincipal) Id() string { return p.azureClaims.ObjectId } + +func (p *azurePrincipal) Name() string { + if p.azureClaims.Upn != "" { + return p.azureClaims.Upn + } + + if p.azureClaims.AppDisplayName != "" { + return p.azureClaims.AppDisplayName + } + + if p.azureClaims.AppId != "" { + return p.azureClaims.AppId + } + + return p.azureClaims.ObjectId +} diff --git a/api/utils/token/mock/validator_mock.go b/api/utils/token/mock/validator_mock.go new file mode 100644 index 00000000..029f8d4d --- /dev/null +++ b/api/utils/token/mock/validator_mock.go @@ -0,0 +1,130 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./api/utils/token/validator.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + token "github.com/equinor/radix-api/api/utils/token" + gomock "github.com/golang/mock/gomock" +) + +// MockTokenPrincipal is a mock of TokenPrincipal interface. +type MockTokenPrincipal struct { + ctrl *gomock.Controller + recorder *MockTokenPrincipalMockRecorder +} + +// MockTokenPrincipalMockRecorder is the mock recorder for MockTokenPrincipal. +type MockTokenPrincipalMockRecorder struct { + mock *MockTokenPrincipal +} + +// NewMockTokenPrincipal creates a new mock instance. +func NewMockTokenPrincipal(ctrl *gomock.Controller) *MockTokenPrincipal { + mock := &MockTokenPrincipal{ctrl: ctrl} + mock.recorder = &MockTokenPrincipalMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTokenPrincipal) EXPECT() *MockTokenPrincipalMockRecorder { + return m.recorder +} + +// Id mocks base method. +func (m *MockTokenPrincipal) Id() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Id") + ret0, _ := ret[0].(string) + return ret0 +} + +// Id indicates an expected call of Id. +func (mr *MockTokenPrincipalMockRecorder) Id() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Id", reflect.TypeOf((*MockTokenPrincipal)(nil).Id)) +} + +// IsAuthenticated mocks base method. +func (m *MockTokenPrincipal) IsAuthenticated() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsAuthenticated") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsAuthenticated indicates an expected call of IsAuthenticated. +func (mr *MockTokenPrincipalMockRecorder) IsAuthenticated() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAuthenticated", reflect.TypeOf((*MockTokenPrincipal)(nil).IsAuthenticated)) +} + +// Name mocks base method. +func (m *MockTokenPrincipal) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockTokenPrincipalMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockTokenPrincipal)(nil).Name)) +} + +// Token mocks base method. +func (m *MockTokenPrincipal) Token() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Token") + ret0, _ := ret[0].(string) + return ret0 +} + +// Token indicates an expected call of Token. +func (mr *MockTokenPrincipalMockRecorder) Token() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Token", reflect.TypeOf((*MockTokenPrincipal)(nil).Token)) +} + +// MockValidatorInterface is a mock of ValidatorInterface interface. +type MockValidatorInterface struct { + ctrl *gomock.Controller + recorder *MockValidatorInterfaceMockRecorder +} + +// MockValidatorInterfaceMockRecorder is the mock recorder for MockValidatorInterface. +type MockValidatorInterfaceMockRecorder struct { + mock *MockValidatorInterface +} + +// NewMockValidatorInterface creates a new mock instance. +func NewMockValidatorInterface(ctrl *gomock.Controller) *MockValidatorInterface { + mock := &MockValidatorInterface{ctrl: ctrl} + mock.recorder = &MockValidatorInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockValidatorInterface) EXPECT() *MockValidatorInterfaceMockRecorder { + return m.recorder +} + +// ValidateToken mocks base method. +func (m *MockValidatorInterface) ValidateToken(arg0 context.Context, arg1 string) (token.TokenPrincipal, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateToken", arg0, arg1) + ret0, _ := ret[0].(token.TokenPrincipal) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateToken indicates an expected call of ValidateToken. +func (mr *MockValidatorInterfaceMockRecorder) ValidateToken(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateToken", reflect.TypeOf((*MockValidatorInterface)(nil).ValidateToken), arg0, arg1) +} diff --git a/api/utils/token/unchecked_principal.go b/api/utils/token/unchecked_principal.go new file mode 100644 index 00000000..8073d857 --- /dev/null +++ b/api/utils/token/unchecked_principal.go @@ -0,0 +1,52 @@ +package token + +import ( + "context" + "fmt" + + "github.com/go-jose/go-jose/v4/jwt" +) + +func (c *unchechedClaimsPrincipal) Validate(_ context.Context) error { + return nil +} + +type unchechedClaimsPrincipal struct { + token string + claims *jwt.Claims + azureClaims *azureClaims +} + +func (p *unchechedClaimsPrincipal) Token() string { + return p.token +} +func (p *unchechedClaimsPrincipal) IsAuthenticated() bool { + return true +} +func (p *unchechedClaimsPrincipal) Id() string { + if p.azureClaims.ObjectId != "" { + return fmt.Sprintf("oid:%s", p.azureClaims.ObjectId) + } + + return fmt.Sprintf("sub:%s", p.claims.Subject) +} + +func (p *unchechedClaimsPrincipal) Name() string { + if p.azureClaims.Upn != "" { + return p.azureClaims.Upn + } + + if p.azureClaims.AppDisplayName != "" { + return p.azureClaims.AppDisplayName + } + + if p.azureClaims.AppId != "" { + return p.azureClaims.AppId + } + + if p.azureClaims.ObjectId != "" { + return p.azureClaims.ObjectId + } + + return p.claims.Subject +} diff --git a/api/utils/token/unchecked_validator.go b/api/utils/token/unchecked_validator.go new file mode 100644 index 00000000..4446be22 --- /dev/null +++ b/api/utils/token/unchecked_validator.go @@ -0,0 +1,36 @@ +package token + +import ( + "context" + "fmt" + "net/url" + + "github.com/equinor/radix-common/net/http" + "github.com/go-jose/go-jose/v4" + josejwt "github.com/go-jose/go-jose/v4/jwt" +) + +type UncheckedValidator struct{} + +var _ ValidatorInterface = &UncheckedValidator{} + +func NewUncheckedValidator(_ *url.URL, _ string) (*UncheckedValidator, error) { + return &UncheckedValidator{}, nil +} + +func (v *UncheckedValidator) ValidateToken(_ context.Context, token string) (TokenPrincipal, error) { + var registeredClaims josejwt.Claims + var azureClaims azureClaims + + jwt, err := josejwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.HS256, jose.RS256}) + if err != nil { + return nil, http.ForbiddenError("invalid token") + } + err = jwt.UnsafeClaimsWithoutVerification(®isteredClaims, &azureClaims) + if err != nil { + return nil, http.ForbiddenError(fmt.Sprintf("failed to extract JWT unsafeClaims: %s", err.Error())) + } + + principal := &unchechedClaimsPrincipal{token: token, claims: ®isteredClaims, azureClaims: &azureClaims} + return principal, nil +} diff --git a/api/utils/token/validator.go b/api/utils/token/validator.go new file mode 100644 index 00000000..f72dd461 --- /dev/null +++ b/api/utils/token/validator.go @@ -0,0 +1,69 @@ +package token + +import ( + "context" + "net/url" + "time" + + "github.com/auth0/go-jwt-middleware/v2/jwks" + "github.com/auth0/go-jwt-middleware/v2/validator" + "github.com/equinor/radix-common/net/http" +) + +type TokenPrincipal interface { + IsAuthenticated() bool + Token() string + Id() string + Name() string +} + +type ValidatorInterface interface { + ValidateToken(context.Context, string) (TokenPrincipal, error) +} + +type Validator struct { + validator *validator.Validator +} + +var _ ValidatorInterface = &Validator{} + +type KeyFunc func(context.Context) (interface{}, error) + +func NewValidator(issuerUrl *url.URL, audience string) (*Validator, error) { + provider := jwks.NewCachingProvider(issuerUrl, 5*time.Hour) + + validator, err := validator.New( + provider.KeyFunc, + validator.RS256, + issuerUrl.String(), + []string{audience}, + validator.WithCustomClaims(func() validator.CustomClaims { + return &azureClaims{} + }), + ) + if err != nil { + return nil, err + } + + return &Validator{validator: validator}, nil +} + +func (v *Validator) ValidateToken(ctx context.Context, token string) (TokenPrincipal, error) { + validateToken, err := v.validator.ValidateToken(ctx, token) + if err != nil { + return nil, err + } + + claims, ok := validateToken.(*validator.ValidatedClaims) + if !ok { + return nil, http.ForbiddenError("invalid token") + } + + azureClaims, ok := claims.CustomClaims.(*azureClaims) + if !ok { + return nil, http.ForbiddenError("invalid azure token") + } + + principal := &azurePrincipal{token: token, claims: claims, azureClaims: azureClaims} + return principal, nil +} diff --git a/api/utils/token/validator_test.go b/api/utils/token/validator_test.go new file mode 100644 index 00000000..8df2854f --- /dev/null +++ b/api/utils/token/validator_test.go @@ -0,0 +1,32 @@ +package token + +import ( + "context" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + test_oidc_issuer = "https://radix.equinor.com" + test_oidc_audience = "testaudience" + test_jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyIsImtpZCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyJ9.eyJhdWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MjQ1YTJlYzEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xMjM0NTY3OC03NTY1LTIzNDItMjM0Mi0xMjM0MDViNDU5YjAvIiwiaWF0IjoxNTc1MzU1NTA4LCJuYmYiOjE1NzUzNTU1MDgsImV4cCI6MTU3NTM1OTQwOCwiYWNyIjoiMSIsImFpbyI6IjQyYXNkYXMiLCJhbXIiOlsicHdkIl0sImFwcGlkIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDc5MDM5YTkwIiwiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJKb2huIiwiZ2l2ZW5fbmFtZSI6IkRvZSIsImhhc2dyb3VwcyI6InRydWUiLCJpcGFkZHIiOiIxMC4xMC4xMC4xMCIsIm5hbWUiOiJKb2huIERvZSIsIm9pZCI6IjEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzRmYzhmYTBlYSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0xMjM0NTY3ODktMTIzNDU2OTc4MC0xMjM0NTY3ODktMTIzNDU2NyIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6IjBoa2JpbEo3MTIzNHpSU3h6eHZiSW1hc2RmZ3N4amI2YXNkZmVOR2FzZGYiLCJ0aWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MDViNDU5YjAiLCJ1bmlxdWVfbmFtZSI6Im5vdC1leGlzdGluZy1yYWRpeC1lbWFpbEBlcXVpbm9yLmNvbSIsInVwbiI6Im5vdC1leGlzdGluZy10ZXN0LXJhZGl4LWVtYWlsQGVxdWlub3IuY29tIiwidXRpIjoiQlMxMmFzR2R1RXlyZUVjRGN2aDJBRyIsInZlciI6IjEuMCJ9.EB5z7Mk34NkFPCP8MqaNMo4UeWgNyO4-qEmzOVPxfoBqbgA16Ar4xeONXODwjZn9iD-CwJccusW6GP0xZ_PJHBFpfaJO_tLaP1k0KhT-eaANt112TvDBt0yjHtJg6He6CEDqagREIsH3w1mSm40zWLKGZeRLdnGxnQyKsTmNJ1rFRdY3AyoEgf6-pnJweUt0LaFMKmIJ2HornStm2hjUstBaji_5cSS946zqp4tgrc-RzzDuaQXzqlVL2J22SR2S_Oux_3yw88KmlhEFFP9axNcbjZrzW3L9XWnPT6UzVIaVRaNRSWfqDATg-jeHg4Gm1bp8w0aIqLdDxc9CfFMjuQ" +) + +func TestNewValidator(t *testing.T) { + issuer, _ := url.Parse(test_oidc_issuer) + v, err := NewValidator(issuer, test_oidc_audience) + assert.NoError(t, err) + assert.NotNil(t, v) +} + +func TestValidateToken(t *testing.T) { + issuer, _ := url.Parse(test_oidc_issuer) + v, err := NewValidator(issuer, test_oidc_audience) + require.NoError(t, err) + + _, err = v.ValidateToken(context.Background(), test_jwt) + assert.Error(t, err) +} diff --git a/go.mod b/go.mod index e3b25aa0..23fb4970 100644 --- a/go.mod +++ b/go.mod @@ -5,26 +5,26 @@ go 1.22.0 toolchain go1.22.5 require ( + github.com/auth0/go-jwt-middleware/v2 v2.2.2 github.com/cert-manager/cert-manager v1.15.0 github.com/equinor/radix-common v1.9.5 github.com/equinor/radix-job-scheduler v1.11.0 - github.com/equinor/radix-operator v1.61.0 + github.com/equinor/radix-operator v1.62.0 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 + github.com/go-jose/go-jose/v4 v4.0.2 github.com/golang/mock v1.6.0 + github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/kedacore/keda/v2 v2.15.1 + github.com/kelseyhightower/envconfig v1.4.0 github.com/marstr/guid v1.1.0 - github.com/mitchellh/mapstructure v1.5.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.76.0 github.com/prometheus/client_golang v1.20.3 github.com/prometheus/common v0.55.0 github.com/rs/cors v1.11.0 github.com/rs/xid v1.5.0 github.com/rs/zerolog v1.33.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/tektoncd/pipeline v0.55.0 github.com/urfave/negroni/v3 v3.1.0 @@ -58,6 +58,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // 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 @@ -65,16 +66,13 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-containerregistry v0.16.1 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -82,7 +80,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.76.0 // indirect @@ -90,13 +87,8 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -116,8 +108,8 @@ require ( google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // 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 diff --git a/go.sum b/go.sum index a026954f..d3fb7c3f 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8V github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 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/auth0/go-jwt-middleware/v2 v2.2.2 h1:vrvkFZf72r3Qbt45KLjBG3/6Xq2r3NTixWKu2e8de9I= +github.com/auth0/go-jwt-middleware/v2 v2.2.2/go.mod h1:4vwxpVtu/Kl4c4HskT+gFLjq0dra8F1joxzamrje6J0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -89,6 +91,8 @@ github.com/equinor/radix-job-scheduler v1.11.0 h1:8wCmXOVl/1cto8q2WJQEE06Cw68/Qm github.com/equinor/radix-job-scheduler v1.11.0/go.mod h1:yPXn3kDcMY0Z3kBkosjuefsdY1x2g0NlBeybMmHz5hc= github.com/equinor/radix-operator v1.61.0 h1:kHWHn5p9S+wKqOTSKtju2URW5FKgAFng1p6RLRnaTmE= github.com/equinor/radix-operator v1.61.0/go.mod h1:uRW9SgVZ94hkpq87npVv2YVviRuXNJ1zgCleya1uvr8= +github.com/equinor/radix-operator v1.62.0 h1:lurDVymrDhlyopd46KMV28eUltrVUPCk3bnBRFuyCsU= +github.com/equinor/radix-operator v1.62.0/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= @@ -97,8 +101,6 @@ github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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= @@ -107,6 +109,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -226,8 +230,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= -github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -264,8 +266,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI= @@ -277,8 +277,6 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -298,8 +296,6 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -356,30 +352,17 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 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/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -387,12 +370,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tektoncd/pipeline v0.55.0 h1:RUfqSC/J1dMrdfu1ThJreHojwGXcWc8P131el/c+c1c= github.com/tektoncd/pipeline v0.55.0/go.mod h1:fFbFAhyNwsPQpitrwhi+Wp0Xse2EkIE1LtGKC08rVqo= github.com/urfave/negroni/v3 v3.1.0 h1:lzmuxGSpnJCT/ujgIAjkU3+LW3NX8alCglO/L6KjIGQ= @@ -741,10 +721,10 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 00000000..7b233c65 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,37 @@ +package config + +import ( + "github.com/kelseyhightower/envconfig" + "github.com/rs/zerolog/log" +) + +type Config struct { + Port int `envconfig:"PORT" default:"3002" desc:"Port where API will be served"` + MetricsPort int `envconfig:"METRICS_PORT" default:"9090" desc:"Port where Metrics will be served"` + ProfilePort int `envconfig:"PROFILE_PORT" default:"7070" desc:"Port where Profiler will be served"` + UseProfiler bool `envconfig:"USE_PROFILER" default:"false" desc:"Enable Profiler"` + PipelineImageTag string `envconfig:"PIPELINE_IMG_TAG" default:"latest"` + TektonImageTag string `envconfig:"TEKTON_IMG_TAG" default:"release-latest"` + RequireAppConfigurationItem bool `envconfig:"REQUIRE_APP_CONFIGURATION_ITEM" default:"true"` + RequireAppADGroups bool `envconfig:"REQUIRE_APP_AD_GROUPS" default:"true"` + LogLevel string `envconfig:"LOG_LEVEL" default:"info"` + LogPrettyPrint bool `envconfig:"LOG_PRETTY" default:"false"` + ClusterName string `envconfig:"RADIX_CLUSTERNAME" required:"true"` + DNSZone string `envconfig:"RADIX_DNS_ZONE" required:"true"` + OidcIssuer string `envconfig:"OIDC_ISSUER" required:"true"` + OidcAudience string `envconfig:"OIDC_AUDIENCE" required:"true"` + AppName string `envconfig:"RADIX_APP" required:"true"` + EnvironmentName string `envconfig:"RADIX_ENVIRONMENT" required:"true"` + PrometheusUrl string `envconfig:"PROMETHEUS_URL" required:"true"` +} + +func MustParse() Config { + var s Config + err := envconfig.Process("", &s) + if err != nil { + _ = envconfig.Usage("", &s) + log.Fatal().Msg(err.Error()) + } + + return s +} diff --git a/internal/flags/register.go b/internal/flags/register.go deleted file mode 100644 index d0582c15..00000000 --- a/internal/flags/register.go +++ /dev/null @@ -1,71 +0,0 @@ -package flags - -import ( - "fmt" - "reflect" - "strings" - - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -func Register(v *viper.Viper, prefix string, flagSet *pflag.FlagSet, config interface{}) error { - val := reflect.ValueOf(config) - var typ reflect.Type - if val.Kind() == reflect.Ptr { - typ = val.Elem().Type() - } else { - typ = val.Type() - } - - for i := 0; i < typ.NumField(); i++ { - // pull out the struct tags: - // flag - the name of the command line flag - // cfg - the name of the config file option - field := typ.Field(i) - fieldV := reflect.Indirect(val).Field(i) - fieldName := strings.Join([]string{prefix, field.Name}, ".") - - cfgName := field.Tag.Get("cfg") - if cfgName == ",internal" { - // Public but internal types that should not be exposed to users, skip them - continue - } - - if field.Name == strings.ToLower(field.Name) { - // Unexported fields cannot be set by a user, so won't have tags or flags, skip them - continue - } - - if field.Type.Kind() == reflect.Struct { - if cfgName != ",squash" { - return fmt.Errorf("field %q does not have required cfg tag: `,squash`", fieldName) - } - err := Register(v, fieldName, flagSet, fieldV.Interface()) - if err != nil { - return err - } - continue - } - - flagName := field.Tag.Get("flag") - if flagName == "" || cfgName == "" { - return fmt.Errorf("field %q does not have required tags (cfg, flag)", fieldName) - } - - if flagSet == nil { - return fmt.Errorf("flagset cannot be nil") - } - - f := flagSet.Lookup(flagName) - if f == nil { - return fmt.Errorf("field %q does not have a registered flag", flagName) - } - err := v.BindPFlag(cfgName, f) - if err != nil { - return fmt.Errorf("error binding flag for field %q: %w", fieldName, err) - } - } - - return nil -} diff --git a/main.go b/main.go index e326148c..c6848f57 100644 --- a/main.go +++ b/main.go @@ -7,9 +7,9 @@ import ( "io" "net/http" _ "net/http/pprof" + "net/url" "os" "os/signal" - "strconv" "sync" "syscall" "time" @@ -17,7 +17,8 @@ import ( "github.com/equinor/radix-api/api/metrics" "github.com/equinor/radix-api/api/secrets" "github.com/equinor/radix-api/api/utils/tlsvalidation" - "github.com/equinor/radix-operator/pkg/apis/defaults" + token "github.com/equinor/radix-api/api/utils/token" + "github.com/equinor/radix-api/internal/config" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -25,8 +26,6 @@ import ( "github.com/equinor/radix-api/api/buildstatus" - "github.com/spf13/pflag" - // Controllers "github.com/equinor/radix-api/api/alerting" "github.com/equinor/radix-api/api/applications" @@ -42,65 +41,60 @@ import ( "github.com/equinor/radix-api/models" ) -const ( - logLevelEnvironmentVariable = "LOG_LEVEL" - logPrettyEnvironmentVariable = "LOG_PRETTY" - useProfilerEnvironmentVariable = "USE_PROFILER" - defaultPort = "3002" - defaultMetricsPort = "9090" - defaultProfilePort = "7070" -) - //go:generate swagger generate spec func main() { - setupLogger() - fs := initializeFlagSet() - - var ( - port = fs.StringP("port", "p", defaultPort, "Port where API will be served") - metricsPort = fs.String("metrics-port", defaultMetricsPort, "The metrics API server port") - useOutClusterClient = fs.Bool("useOutClusterClient", true, "In case of testing on local machine you may want to set this to false") - clusterName = os.Getenv(defaults.ClusternameEnvironmentVariable) - ) + c := config.MustParse() + setupLogger(c.LogLevel, c.LogPrettyPrint) - parseFlagsFromArgs(fs) - - var servers []*http.Server - - srv, err := initializeServer(*port, clusterName, *useOutClusterClient) - if err != nil { - log.Fatal().Err(err).Msg("Failed to initialize API server") + servers := []*http.Server{ + initializeServer(c), + initializeMetricsServer(c), } - servers = append(servers, srv, initializeMetricsServer(*metricsPort)) - - if useProfiler, _ := strconv.ParseBool(os.Getenv(useProfilerEnvironmentVariable)); useProfiler { - log.Info().Msgf("Initializing profile server on port %s", defaultProfilePort) - servers = append(servers, &http.Server{Addr: fmt.Sprintf("localhost:%s", defaultProfilePort)}) + if c.UseProfiler { + log.Info().Msgf("Initializing profile server on port %d", c.ProfilePort) + servers = append(servers, &http.Server{Addr: fmt.Sprintf("localhost:%d", c.ProfilePort)}) } startServers(servers...) shutdownServersGracefulOnSignal(servers...) } -func initializeServer(port, clusterName string, useOutClusterClient bool) (*http.Server, error) { - controllers, err := getControllers() +func initializeServer(c config.Config) *http.Server { + jwtValidator := initializeTokenValidator(c) + controllers, err := getControllers(c) if err != nil { - return nil, fmt.Errorf("failed to initialize controllers: %w", err) + log.Fatal().Err(err).Msgf("failed to initialize controllers: %v", err) } - handler := router.NewAPIHandler(clusterName, utils.NewKubeUtil(useOutClusterClient), controllers...) + + handler := router.NewAPIHandler(jwtValidator, utils.NewKubeUtil(), controllers...) srv := &http.Server{ - Addr: fmt.Sprintf(":%s", port), + Addr: fmt.Sprintf(":%d", c.Port), Handler: handler, } - return srv, nil + return srv } -func initializeMetricsServer(port string) *http.Server { - log.Info().Msgf("Initializing metrics server on port %s", port) +func initializeTokenValidator(c config.Config) token.ValidatorInterface { + issuerUrl, err := url.Parse(c.OidcIssuer) + if err != nil { + log.Fatal().Err(err).Msg("Error parsing issuer url") + } + + // Set up the validator. + // jwtValidator, err := token.NewValidator(issuerUrl, c.OidcAudience) + jwtValidator, err := token.NewUncheckedValidator(issuerUrl, c.OidcAudience) + if err != nil { + log.Fatal().Err(err).Msg("Error creating JWT validator") + } + return jwtValidator +} + +func initializeMetricsServer(c config.Config) *http.Server { + log.Info().Msgf("Initializing metrics server on port %d", c.MetricsPort) return &http.Server{ - Addr: fmt.Sprintf(":%s", port), + Addr: fmt.Sprintf(":%d", c.MetricsPort), Handler: router.NewMetricsHandler(), } } @@ -142,8 +136,7 @@ func shutdownServersGracefulOnSignal(servers ...*http.Server) { wg.Wait() } -func setupLogger() { - logLevelStr := os.Getenv(logLevelEnvironmentVariable) +func setupLogger(logLevelStr string, prettyPrint bool) { if len(logLevelStr) == 0 { logLevelStr = zerolog.LevelInfoValue } @@ -154,10 +147,8 @@ func setupLogger() { log.Warn().Msgf("Invalid log level '%s', fallback to '%s'", logLevelStr, logLevel.String()) } - logPretty, _ := strconv.ParseBool(os.Getenv(logPrettyEnvironmentVariable)) - var logWriter io.Writer = os.Stderr - if logPretty { + if prettyPrint { logWriter = &zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.TimeOnly} } @@ -167,21 +158,16 @@ func setupLogger() { zerolog.DefaultContextLogger = &logger } -func getControllers() ([]models.Controller, error) { +func getControllers(config config.Config) ([]models.Controller, error) { buildStatus := build_models.NewPipelineBadge() - cfg, err := applications.LoadApplicationHandlerConfig(os.Args[1:]) - if err != nil { - return nil, err - } - prometheusClient, err := metrics.NewPrometheusClient(cfg.PrometheusUrl) + applicatinoFactory := applications.NewApplicationHandlerFactory(config) + prometheusClient, err := metrics.NewPrometheusClient(config.PrometheusUrl) if err != nil { return nil, err } prometheusHandler := metrics.NewPrometheusHandler(prometheusClient) - applicationHandlerFactory := applications.NewApplicationHandlerFactory(cfg) - return []models.Controller{ - applications.NewApplicationController(nil, applicationHandlerFactory, prometheusHandler), + applications.NewApplicationController(nil, applicatinoFactory, prometheusHandler), deployments.NewDeploymentController(), jobs.NewJobController(), environments.NewEnvironmentController(environments.NewEnvironmentHandlerFactory()), @@ -193,28 +179,3 @@ func getControllers() ([]models.Controller, error) { secrets.NewSecretController(tlsvalidation.DefaultValidator()), }, nil } - -func initializeFlagSet() *pflag.FlagSet { - // Flag domain. - fs := pflag.NewFlagSet("default", pflag.ContinueOnError) - fs.Usage = func() { - fmt.Fprintf(os.Stderr, "DESCRIPTION\n") - fmt.Fprintf(os.Stderr, " radix api-server.\n") - fmt.Fprintf(os.Stderr, "\n") - fmt.Fprintf(os.Stderr, "FLAGS\n") - fs.PrintDefaults() - } - return fs -} - -func parseFlagsFromArgs(fs *pflag.FlagSet) { - err := fs.Parse(os.Args[1:]) - switch { - case err == pflag.ErrHelp: - os.Exit(0) - case err != nil: - fmt.Fprintf(os.Stderr, "Error: %s\n\n", err.Error()) - fs.Usage() - os.Exit(2) - } -} diff --git a/models/accounts.go b/models/accounts.go index 60ceee10..cb308041 100644 --- a/models/accounts.go +++ b/models/accounts.go @@ -1,24 +1,17 @@ package models import ( - "fmt" - kedav2 "github.com/kedacore/keda/v2/pkg/generated/clientset/versioned" tektonclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" certclient "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" - radixmodels "github.com/equinor/radix-common/models" radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" - "github.com/golang-jwt/jwt/v5" "k8s.io/client-go/kubernetes" secretProviderClient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" ) // NewAccounts creates a new Accounts struct -func NewAccounts( - inClusterClient kubernetes.Interface, inClusterRadixClient radixclient.Interface, inClusterKedaClient kedav2.Interface, inClusterSecretProviderClient secretProviderClient.Interface, inClusterTektonClient tektonclient.Interface, inClusterCertManagerClient certclient.Interface, - outClusterClient kubernetes.Interface, outClusterRadixClient radixclient.Interface, outClusterKedaClient kedav2.Interface, outClusterSecretProviderClient secretProviderClient.Interface, outClusterTektonClient tektonclient.Interface, outClusterCertManagerClient certclient.Interface, - token string, impersonation radixmodels.Impersonation) Accounts { +func NewAccounts(inClusterClient kubernetes.Interface, inClusterRadixClient radixclient.Interface, inClusterKedaClient kedav2.Interface, inClusterSecretProviderClient secretProviderClient.Interface, inClusterTektonClient tektonclient.Interface, inClusterCertManagerClient certclient.Interface, outClusterClient kubernetes.Interface, outClusterRadixClient radixclient.Interface, outClusterKedaClient kedav2.Interface, outClusterSecretProviderClient secretProviderClient.Interface, outClusterTektonClient tektonclient.Interface, outClusterCertManagerClient certclient.Interface) Accounts { return Accounts{ UserAccount: Account{ @@ -37,8 +30,6 @@ func NewAccounts( TektonClient: inClusterTektonClient, CertManagerClient: inClusterCertManagerClient, }, - token: token, - impersonation: impersonation, } } @@ -57,53 +48,4 @@ func NewServiceAccount(inClusterClient kubernetes.Interface, inClusterRadixClien type Accounts struct { UserAccount Account ServiceAccount Account - token string - impersonation radixmodels.Impersonation -} - -// GetOriginator get the request originator name or id -func (accounts Accounts) GetOriginator() (string, error) { - if accounts.impersonation.PerformImpersonation() { - return accounts.impersonation.User, nil - } - if originator, err, done := accounts.getOriginator("upn", ""); done { - return originator, err - } - if originator, err, done := accounts.getOriginator("app_displayname", ""); done { - return originator, err - } - if originator, err, done := accounts.getOriginator("appid", "%s (appid)"); done { - return originator, err - } - if originator, err, done := accounts.getOriginator("sub", "%s (sub)"); done { - return originator, err - } - return "", nil -} - -func (accounts Accounts) getOriginator(claim, format string) (string, error, bool) { - originator, err := getTokenClaim(accounts.token, claim) - if err != nil { - return "", err, true - } - if originator == "" { - return "", nil, false - } - if format != "" { - return fmt.Sprintf(format, originator), nil, true - } - return originator, nil, true -} - -func getTokenClaim(token string, claim string) (string, error) { - claims := jwt.MapClaims{} - parser := jwt.Parser{} - _, _, err := parser.ParseUnverified(token, claims) - if err != nil { - return "", fmt.Errorf("could not parse token (%v)", err) - } - if val, ok := claims[claim]; ok { - return fmt.Sprintf("%v", val), nil - } - return "", nil } diff --git a/radixconfig.yaml b/radixconfig.yaml index 91269a0a..bb333a1a 100644 --- a/radixconfig.yaml +++ b/radixconfig.yaml @@ -35,6 +35,8 @@ spec: LOG_LEVEL: info LOG_PRETTY: "false" PROMETHEUS_URL: http://prometheus-operator-prometheus.monitor.svc.cluster.local:9090 + OIDC_AUDIENCE: "6dae42f8-4368-4678-94ff-3960e28e3630" + OIDC_ISSUER: "https://sts.windows.net/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/" environmentConfig: - environment: qa horizontalScaling: