diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index 8938f0986f..acea21be6c 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -187,6 +187,12 @@ func Start(params *APIServerParams) { handlers.RegisterTokenAuthRoutes(handler, debugRouter, loggingRouter) + /********************************************************************** + * License ID auth routes + **********************************************************************/ + + handlers.RegisterLicenseIDAuthRoutes(r.PathPrefix("").Subrouter(), kotsStore, handler) + /********************************************************************** * Session auth routes **********************************************************************/ diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index d6b43a9b47..aa711b7b89 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -354,7 +354,13 @@ func RegisterUnauthenticatedRoutes(handler *Handler, kotsStore store.Store, debu // These handlers should be called by the application only. loggingRouter.Path("/license/v1/license").Methods("GET").HandlerFunc(handler.GetPlatformLicenseCompatibility) - loggingRouter.Path("/api/v1/app/custom-metrics").Methods("POST").HandlerFunc(handler.GetSendCustomApplicationMetricsHandler(kotsStore)) + loggingRouter.Path("/api/v1/app/custom-metrics").Methods("POST").HandlerFunc(handler.GetSendCustomAppMetricsHandler(kotsStore)) +} + +func RegisterLicenseIDAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOTSHandler) { + r.Use(LoggingMiddleware, RequireValidLicenseMiddleware(kotsStore)) + + r.Name("GetAppMetrics").Path("/api/v1/app/metrics").Methods("GET").HandlerFunc(handler.GetAppMetrics) } func StreamJSON(c *websocket.Conn, payload interface{}) { diff --git a/pkg/handlers/interface.go b/pkg/handlers/interface.go index 69b3539288..c6cb2a00db 100644 --- a/pkg/handlers/interface.go +++ b/pkg/handlers/interface.go @@ -162,4 +162,7 @@ type KOTSHandler interface { // Helm IsHelmManaged(w http.ResponseWriter, r *http.Request) GetAppValuesFile(w http.ResponseWriter, r *http.Request) + + // APIs available to applications (except legacy /license/v1/license) + GetAppMetrics(w http.ResponseWriter, r *http.Request) } diff --git a/pkg/handlers/custom_metrics.go b/pkg/handlers/metrics.go similarity index 72% rename from pkg/handlers/custom_metrics.go rename to pkg/handlers/metrics.go index 64ad490260..38de662f2b 100644 --- a/pkg/handlers/custom_metrics.go +++ b/pkg/handlers/metrics.go @@ -10,16 +10,26 @@ import ( "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/replicatedapp" + "github.com/replicatedhq/kots/pkg/reporting" + "github.com/replicatedhq/kots/pkg/session" "github.com/replicatedhq/kots/pkg/store" ) -type SendCustomApplicationMetricsRequest struct { - Data ApplicationMetricsData `json:"data"` +func (h *Handler) GetAppMetrics(w http.ResponseWriter, r *http.Request) { + app := session.ContextGetApp(r) + reportingInfo := reporting.GetReportingInfo(app.ID) + headers := reporting.GetReportingInfoHeaders(reportingInfo) + + JSON(w, http.StatusOK, headers) +} + +type SendCustomAppMetricsRequest struct { + Data CustomAppMetricsData `json:"data"` } -type ApplicationMetricsData map[string]interface{} +type CustomAppMetricsData map[string]interface{} -func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) http.HandlerFunc { +func (h *Handler) GetSendCustomAppMetricsHandler(kotsStore store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if kotsadm.IsAirgap() { w.WriteHeader(http.StatusForbidden) @@ -48,20 +58,20 @@ func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) return } - request := SendCustomApplicationMetricsRequest{} + request := SendCustomAppMetricsRequest{} if err := json.NewDecoder(r.Body).Decode(&request); err != nil { logger.Error(errors.Wrap(err, "decode request")) w.WriteHeader(http.StatusBadRequest) return } - if err := validateCustomMetricsData(request.Data); err != nil { + if err := validateCustomAppMetricsData(request.Data); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } - err = replicatedapp.SendApplicationMetricsData(license, app, request.Data) + err = replicatedapp.SendCustomAppMetricsData(license, app, request.Data) if err != nil { logger.Error(errors.Wrap(err, "set application data")) w.WriteHeader(http.StatusBadRequest) @@ -72,7 +82,7 @@ func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) } } -func validateCustomMetricsData(data ApplicationMetricsData) error { +func validateCustomAppMetricsData(data CustomAppMetricsData) error { if len(data) == 0 { return errors.New("no data provided") } diff --git a/pkg/handlers/custom_metrics_test.go b/pkg/handlers/metrics_test.go similarity index 84% rename from pkg/handlers/custom_metrics_test.go rename to pkg/handlers/metrics_test.go index d588738764..79ae39b3bb 100644 --- a/pkg/handlers/custom_metrics_test.go +++ b/pkg/handlers/metrics_test.go @@ -16,15 +16,15 @@ import ( "github.com/stretchr/testify/require" ) -func Test_validateCustomMetricsData(t *testing.T) { +func Test_validateCustomAppMetricsData(t *testing.T) { tests := []struct { name string - data ApplicationMetricsData + data CustomAppMetricsData wantErr bool }{ { name: "all values are valid", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": "val1", "key2": 6, "key3": 6.6, @@ -34,12 +34,12 @@ func Test_validateCustomMetricsData(t *testing.T) { }, { name: "no data", - data: ApplicationMetricsData{}, + data: CustomAppMetricsData{}, wantErr: true, }, { name: "array value", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": 10, "key2": []string{"val1", "val2"}, }, @@ -47,7 +47,7 @@ func Test_validateCustomMetricsData(t *testing.T) { }, { name: "map value", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": 10, "key2": map[string]string{"key1": "val1"}, }, @@ -55,7 +55,7 @@ func Test_validateCustomMetricsData(t *testing.T) { }, { name: "nil value", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": nil, "key2": 4, }, @@ -65,7 +65,7 @@ func Test_validateCustomMetricsData(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := validateCustomMetricsData(test.data) + err := validateCustomAppMetricsData(test.data) if test.wantErr { require.Error(t, err) } else { @@ -75,7 +75,7 @@ func Test_validateCustomMetricsData(t *testing.T) { } } -func Test_SendCustomApplicationMetrics(t *testing.T) { +func Test_SendCustomAppMetrics(t *testing.T) { req := require.New(t) customMetricsData := []byte(`{"data":{"key1_string":"val1","key2_int":5,"key3_float":1.5,"key4_numeric_string":"1.6"}}`) appID := "app-id-123" @@ -122,7 +122,7 @@ spec: // Validate - handler.GetSendCustomApplicationMetricsHandler(mockStore)(clientWriter, clientRequest) + handler.GetSendCustomAppMetricsHandler(mockStore)(clientWriter, clientRequest) req.Equal(http.StatusOK, clientWriter.Code) } diff --git a/pkg/handlers/middleware.go b/pkg/handlers/middleware.go index ea328f8a89..82ca484693 100644 --- a/pkg/handlers/middleware.go +++ b/pkg/handlers/middleware.go @@ -107,3 +107,21 @@ func RequireValidSessionQuietMiddleware(kotsStore store.Store) mux.MiddlewareFun }) } } + +func RequireValidLicenseMiddleware(kotsStore store.Store) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + license, app, err := requireValidLicense(kotsStore, w, r) + if err != nil { + if !kotsStore.IsNotFound(err) { + logger.Error(errors.Wrapf(err, "request %q", r.RequestURI)) + } + return + } + + r = session.ContextSetLicense(r, license) + r = session.ContextSetApp(r, app) + next.ServeHTTP(w, r) + }) + } +} diff --git a/pkg/handlers/mock/mock.go b/pkg/handlers/mock/mock.go index 7440665850..cf9fe09ede 100644 --- a/pkg/handlers/mock/mock.go +++ b/pkg/handlers/mock/mock.go @@ -598,6 +598,18 @@ func (mr *MockKOTSHandlerMockRecorder) GetAppIdentityServiceConfig(w, r interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppIdentityServiceConfig", reflect.TypeOf((*MockKOTSHandler)(nil).GetAppIdentityServiceConfig), w, r) } +// GetAppMetrics mocks base method. +func (m *MockKOTSHandler) GetAppMetrics(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GetAppMetrics", w, r) +} + +// GetAppMetrics indicates an expected call of GetAppMetrics. +func (mr *MockKOTSHandlerMockRecorder) GetAppMetrics(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppMetrics", reflect.TypeOf((*MockKOTSHandler)(nil).GetAppMetrics), w, r) +} + // GetAppRegistry mocks base method. func (m *MockKOTSHandler) GetAppRegistry(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() diff --git a/pkg/handlers/session.go b/pkg/handlers/session.go index 2179f996ed..819e41db1c 100644 --- a/pkg/handlers/session.go +++ b/pkg/handlers/session.go @@ -8,13 +8,16 @@ import ( "time" "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/handlers/types" "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/session" sessiontypes "github.com/replicatedhq/kots/pkg/session/types" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -155,3 +158,47 @@ func requireValidKOTSToken(w http.ResponseWriter, r *http.Request) error { return errors.New("invalid auth") } + +func requireValidLicense(kotsStore store.Store, w http.ResponseWriter, r *http.Request) (*kotsv1beta1.License, *apptypes.App, error) { + if r.Method == "OPTIONS" { + return nil, nil, nil + } + + licenseID := r.Header.Get("authorization") + if licenseID == "" { + err := errors.New("missing authorization header") + response := types.ErrorResponse{Error: util.StrPointer(err.Error())} + JSON(w, http.StatusUnauthorized, response) + return nil, nil, err + } + + apps, err := kotsStore.ListInstalledApps() + if err != nil { + return nil, nil, errors.Wrap(err, "get all apps") + } + + var license *kotsv1beta1.License + var app *apptypes.App + + for _, a := range apps { + l, err := kotsutil.LoadLicenseFromBytes([]byte(a.License)) + if err != nil { + return nil, nil, errors.Wrap(err, "load license") + } + + if l.Spec.LicenseID == licenseID { + license = l + app = a + break + } + } + + if license == nil { + err := errors.New("license ID is not valid") + response := types.ErrorResponse{Error: util.StrPointer(err.Error())} + JSON(w, http.StatusUnauthorized, response) + return nil, nil, err + } + + return license, app, nil +} diff --git a/pkg/policy/middleware.go b/pkg/policy/middleware.go index 452bf2925f..3e3504880d 100644 --- a/pkg/policy/middleware.go +++ b/pkg/policy/middleware.go @@ -75,21 +75,6 @@ func (m *Middleware) EnforceAccess(p *Policy, handler http.HandlerFunc) http.Han } } -func (m *Middleware) EnforceLicense(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - license := session.ContextGetLicense(r) - if license == nil { - err := errors.New("no valid license for request") - logger.Error(err) - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(err.Error())) - return - } - - handler(w, r) - } -} - // TODO: move everything below here to a shared package type ErrorResponse struct { diff --git a/pkg/replicatedapp/api.go b/pkg/replicatedapp/api.go index d4e14c59be..710e4de1d5 100644 --- a/pkg/replicatedapp/api.go +++ b/pkg/replicatedapp/api.go @@ -172,7 +172,7 @@ func getApplicationMetadataFromHost(host string, endpoint string, upstream *url. return respBody, nil } -func SendApplicationMetricsData(license *kotsv1beta1.License, app *apptypes.App, data map[string]interface{}) error { +func SendCustomAppMetricsData(license *kotsv1beta1.License, app *apptypes.App, data map[string]interface{}) error { url := fmt.Sprintf("%s/application/custom-metrics", license.Spec.Endpoint) payload := struct { diff --git a/pkg/reporting/util.go b/pkg/reporting/util.go index b7b22e90a9..e9da1ba89f 100644 --- a/pkg/reporting/util.go +++ b/pkg/reporting/util.go @@ -10,57 +10,69 @@ import ( ) func InjectReportingInfoHeaders(req *http.Request, reportingInfo *types.ReportingInfo) { + headers := GetReportingInfoHeaders(reportingInfo) + + for key, value := range headers { + req.Header.Set(key, value) + } +} + +func GetReportingInfoHeaders(reportingInfo *types.ReportingInfo) map[string]string { + headers := make(map[string]string) + if reportingInfo == nil { - return + return headers } - req.Header.Set("X-Replicated-K8sVersion", reportingInfo.K8sVersion) - req.Header.Set("X-Replicated-IsKurl", strconv.FormatBool(reportingInfo.IsKurl)) - req.Header.Set("X-Replicated-AppStatus", reportingInfo.AppStatus) - req.Header.Set("X-Replicated-ClusterID", reportingInfo.ClusterID) - req.Header.Set("X-Replicated-InstanceID", reportingInfo.InstanceID) - req.Header.Set("X-Replicated-ReplHelmInstalls", strconv.Itoa(reportingInfo.Downstream.ReplHelmInstalls)) - req.Header.Set("X-Replicated-NativeHelmInstalls", strconv.Itoa(reportingInfo.Downstream.NativeHelmInstalls)) + headers["X-Replicated-K8sVersion"] = reportingInfo.K8sVersion + headers["X-Replicated-IsKurl"] = strconv.FormatBool(reportingInfo.IsKurl) + headers["X-Replicated-AppStatus"] = reportingInfo.AppStatus + headers["X-Replicated-ClusterID"] = reportingInfo.ClusterID + headers["X-Replicated-InstanceID"] = reportingInfo.InstanceID + headers["X-Replicated-ReplHelmInstalls"] = strconv.Itoa(reportingInfo.Downstream.ReplHelmInstalls) + headers["X-Replicated-NativeHelmInstalls"] = strconv.Itoa(reportingInfo.Downstream.NativeHelmInstalls) if reportingInfo.Downstream.Cursor != "" { - req.Header.Set("X-Replicated-DownstreamChannelSequence", reportingInfo.Downstream.Cursor) + headers["X-Replicated-DownstreamChannelSequence"] = reportingInfo.Downstream.Cursor } if reportingInfo.Downstream.ChannelID != "" { - req.Header.Set("X-Replicated-DownstreamChannelID", reportingInfo.Downstream.ChannelID) + headers["X-Replicated-DownstreamChannelID"] = reportingInfo.Downstream.ChannelID } else if reportingInfo.Downstream.ChannelName != "" { - req.Header.Set("X-Replicated-DownstreamChannelName", reportingInfo.Downstream.ChannelName) + headers["X-Replicated-DownstreamChannelName"] = reportingInfo.Downstream.ChannelName } if reportingInfo.Downstream.Status != "" { - req.Header.Set("X-Replicated-InstallStatus", reportingInfo.Downstream.Status) + headers["X-Replicated-InstallStatus"] = reportingInfo.Downstream.Status } if reportingInfo.Downstream.PreflightState != "" { - req.Header.Set("X-Replicated-PreflightStatus", reportingInfo.Downstream.PreflightState) + headers["X-Replicated-PreflightStatus"] = reportingInfo.Downstream.PreflightState } if reportingInfo.Downstream.Sequence != nil { - req.Header.Set("X-Replicated-DownstreamSequence", strconv.FormatInt(*reportingInfo.Downstream.Sequence, 10)) + headers["X-Replicated-DownstreamSequence"] = strconv.FormatInt(*reportingInfo.Downstream.Sequence, 10) } if reportingInfo.Downstream.Source != "" { - req.Header.Set("X-Replicated-DownstreamSource", reportingInfo.Downstream.Source) + headers["X-Replicated-DownstreamSource"] = reportingInfo.Downstream.Source } - req.Header.Set("X-Replicated-SkipPreflights", strconv.FormatBool(reportingInfo.Downstream.SkipPreflights)) + headers["X-Replicated-SkipPreflights"] = strconv.FormatBool(reportingInfo.Downstream.SkipPreflights) if reportingInfo.KOTSInstallID != "" { - req.Header.Set("X-Replicated-KotsInstallID", reportingInfo.KOTSInstallID) + headers["X-Replicated-KotsInstallID"] = reportingInfo.KOTSInstallID } if reportingInfo.KURLInstallID != "" { - req.Header.Set("X-Replicated-KurlInstallID", reportingInfo.KURLInstallID) + headers["X-Replicated-KurlInstallID"] = reportingInfo.KURLInstallID } - req.Header.Set("X-Replicated-KurlNodeCountTotal", strconv.Itoa(reportingInfo.KurlNodeCountTotal)) - req.Header.Set("X-Replicated-KurlNodeCountReady", strconv.Itoa(reportingInfo.KurlNodeCountReady)) + headers["X-Replicated-KurlNodeCountTotal"] = strconv.Itoa(reportingInfo.KurlNodeCountTotal) + headers["X-Replicated-KurlNodeCountReady"] = strconv.Itoa(reportingInfo.KurlNodeCountReady) - req.Header.Set("X-Replicated-IsGitOpsEnabled", strconv.FormatBool(reportingInfo.IsGitOpsEnabled)) - req.Header.Set("X-Replicated-GitOpsProvider", reportingInfo.GitOpsProvider) + headers["X-Replicated-IsGitOpsEnabled"] = strconv.FormatBool(reportingInfo.IsGitOpsEnabled) + headers["X-Replicated-GitOpsProvider"] = reportingInfo.GitOpsProvider if reportingInfo.K8sDistribution != "" { - req.Header.Set("X-Replicated-K8sDistribution", reportingInfo.K8sDistribution) + headers["X-Replicated-K8sDistribution"] = reportingInfo.K8sDistribution } + + return headers } func canReport(endpoint string) bool {