From 19c5e3545ba724cd9831c9b5e9d24e593178194c Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 13 Jun 2022 16:07:08 -0300 Subject: [PATCH] add a dedicated endpoint that redirects to fleet_desktop.transparency_url (#6204) As part of https://github.com/fleetdm/fleet/issues/5947, and in order to have a simplified workflow in Fleet Desktop, we defined https://github.com/fleetdm/fleet/issues/6200 to add a new endpoint that redirects to the transparency url as defined in the config (for premium users only) ``` ~/projects/fleet $ curl -v -s https://localhost:8080/api/latest/fleet/device/bf34ab98-23b0-48bc-8e82-8c0143cba11c/transparency * Connection state changed (MAX_CONCURRENT_STREAMS == 250)! < HTTP/2 307 < content-type: application/json; charset=utf-8 < location: https://fleetdm.com/transparency < content-length: 0 < date: Mon, 13 Jun 2022 18:09:29 GMT < * Connection #0 to host localhost left intact ``` --- changes/issue-5408-transparency-url | 1 - server/service/devices.go | 66 +++++++++++++++---- server/service/handler.go | 1 + server/service/integration_core_test.go | 18 ++--- server/service/integration_enterprise_test.go | 18 ++--- 5 files changed, 71 insertions(+), 33 deletions(-) diff --git a/changes/issue-5408-transparency-url b/changes/issue-5408-transparency-url index 9671d85e67..f3b9a448f5 100644 --- a/changes/issue-5408-transparency-url +++ b/changes/issue-5408-transparency-url @@ -1,4 +1,3 @@ - Add `fleet_desktop.transparency_url` to `app_config_json` - Set default `transparency_url="https://fleetdm.com/transparency` - Enable Fleet Premium licensees to set custom `transparency_url` via REST API and `fleetctl apply` -- Add `transparency_url` to `GET /device/{token}` endpoint response diff --git a/server/service/devices.go b/server/service/devices.go index a3c56f506e..9d5e3595e2 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -2,6 +2,7 @@ package service import ( "context" + "net/http" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" hostctx "github.com/fleetdm/fleet/v4/server/contexts/host" @@ -21,11 +22,10 @@ func (r *getDeviceHostRequest) deviceAuthToken() string { } type getDeviceHostResponse struct { - Host *HostDetailResponse `json:"host"` - OrgLogoURL string `json:"org_logo_url"` - TransparencyURL string `json:"transparency_url"` - Err error `json:"error,omitempty"` - License fleet.LicenseInfo `json:"license"` + Host *HostDetailResponse `json:"host"` + OrgLogoURL string `json:"org_logo_url"` + Err error `json:"error,omitempty"` + License fleet.LicenseInfo `json:"license"` } func (r getDeviceHostResponse) error() error { return r.Err } @@ -65,16 +65,10 @@ func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.S return getDeviceHostResponse{Err: err}, nil } - transparencyURL := fleet.DefaultTransparencyURL - if license.Tier == "premium" && ac.FleetDesktop.TransparencyURL != "" { - transparencyURL = ac.FleetDesktop.TransparencyURL - } - return getDeviceHostResponse{ - Host: resp, - OrgLogoURL: ac.OrgInfo.OrgLogoURL, - TransparencyURL: transparencyURL, - License: *license, + Host: resp, + OrgLogoURL: ac.OrgInfo.OrgLogoURL, + License: *license, }, nil } @@ -244,3 +238,47 @@ func (r deviceAPIFeaturesResponse) error() error { return r.Err } func deviceAPIFeaturesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { return deviceAPIFeaturesResponse{Features: fleet.DeviceAPIFeatures{}}, nil } + +//////////////////////////////////////////////////////////////////////////////// +// Transparency URL Redirect +//////////////////////////////////////////////////////////////////////////////// + +type transparencyURLRequest struct { + Token string `url:"token"` +} + +func (r *transparencyURLRequest) deviceAuthToken() string { + return r.Token +} + +type transparencyURLResponse struct { + RedirectURL string `json:"-"` // used to control the redirect, see hijackRender method + Err error `json:"error,omitempty"` +} + +func (r transparencyURLResponse) hijackRender(ctx context.Context, w http.ResponseWriter) { + w.Header().Set("Location", r.RedirectURL) + w.WriteHeader(http.StatusTemporaryRedirect) +} + +func (r transparencyURLResponse) error() error { return r.Err } + +func transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + config, err := svc.AppConfig(ctx) + if err != nil { + return transparencyURLResponse{Err: err}, nil + } + + license, err := svc.License(ctx) + if err != nil { + return transparencyURLResponse{Err: err}, nil + } + + transparencyURL := fleet.DefaultTransparencyURL + // Fleet Premium license is required for custom transparency url + if license.Tier == "premium" && config.FleetDesktop.TransparencyURL != "" { + transparencyURL = config.FleetDesktop.TransparencyURL + } + + return transparencyURLResponse{RedirectURL: transparencyURL}, nil +} diff --git a/server/service/handler.go b/server/service/handler.go index a9b700da01..b2e6908f6b 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -401,6 +401,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC de.GET("/api/_version_/fleet/device/{token}/macadmins", getDeviceMacadminsDataEndpoint, getDeviceMacadminsDataRequest{}) de.GET("/api/_version_/fleet/device/{token}/policies", listDevicePoliciesEndpoint, listDevicePoliciesRequest{}) de.GET("/api/_version_/fleet/device/{token}/api_features", deviceAPIFeaturesEndpoint, deviceAPIFeaturesRequest{}) + de.GET("/api/_version_/fleet/device/{token}/transparency", transparencyURL, transparencyURLRequest{}) // host-authenticated endpoints he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 7d5034a706..1339f5cf49 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -4766,12 +4766,12 @@ func (s *integrationTestSuite) TestDefaultTransparencyURL() { require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL) // confirm device endpoint returns initial default url - deviceResp := &getDeviceHostResponse{} - rawResp := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK) + deviceResp := &transparencyURLResponse{} + rawResp := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect) json.NewDecoder(rawResp.Body).Decode(deviceResp) rawResp.Body.Close() require.NoError(t, deviceResp.Err) - require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL) + require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location")) // empty string applies default url acResp = appConfigResponse{} @@ -4780,24 +4780,24 @@ func (s *integrationTestSuite) TestDefaultTransparencyURL() { require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL) // device endpoint returns default url - deviceResp = &getDeviceHostResponse{} - rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK) + deviceResp = &transparencyURLResponse{} + rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect) json.NewDecoder(rawResp.Body).Decode(deviceResp) rawResp.Body.Close() require.NoError(t, deviceResp.Err) - require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL) + require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location")) // modify transparency url with custom url fails acResp = appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: "customURL"}}, http.StatusUnprocessableEntity, &acResp) // device endpoint still returns default url - deviceResp = &getDeviceHostResponse{} - rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK) + deviceResp = &transparencyURLResponse{} + rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect) json.NewDecoder(rawResp.Body).Decode(deviceResp) rawResp.Body.Close() require.NoError(t, deviceResp.Err) - require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL) + require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location")) } func (s *integrationTestSuite) TestModifyUser() { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index df49db535f..6c3905bbd7 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -1136,12 +1136,12 @@ func (s *integrationEnterpriseTestSuite) TestCustomTransparencyURL() { require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL) // confirm device endpoint returns initial default url - deviceResp := &getDeviceHostResponse{} - rawResp := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK) + deviceResp := &transparencyURLResponse{} + rawResp := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect) json.NewDecoder(rawResp.Body).Decode(deviceResp) rawResp.Body.Close() require.NoError(t, deviceResp.Err) - require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL) + require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location")) // set custom url acResp = appConfigResponse{} @@ -1150,12 +1150,12 @@ func (s *integrationEnterpriseTestSuite) TestCustomTransparencyURL() { require.Equal(t, "customURL", acResp.FleetDesktop.TransparencyURL) // device endpoint returns custom url - deviceResp = &getDeviceHostResponse{} - rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK) + deviceResp = &transparencyURLResponse{} + rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect) json.NewDecoder(rawResp.Body).Decode(deviceResp) rawResp.Body.Close() require.NoError(t, deviceResp.Err) - require.Equal(t, "customURL", deviceResp.TransparencyURL) + require.Equal(t, "customURL", rawResp.Header.Get("Location")) // empty string applies default url acResp = appConfigResponse{} @@ -1164,10 +1164,10 @@ func (s *integrationEnterpriseTestSuite) TestCustomTransparencyURL() { require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL) // device endpoint returns default url - deviceResp = &getDeviceHostResponse{} - rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK) + deviceResp = &transparencyURLResponse{} + rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect) json.NewDecoder(rawResp.Body).Decode(deviceResp) rawResp.Body.Close() require.NoError(t, deviceResp.Err) - require.Equal(t, fleet.DefaultTransparencyURL, deviceResp.TransparencyURL) + require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location")) }