From e29797deb01f9ad5724ec03ecb8285fe86498162 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 15 Feb 2022 15:22:19 -0500 Subject: [PATCH] Migrate the last batch of `authenticatedUser` endpoints to the new pattern (#4210) --- server/service/endpoint_change_email.go | 30 --- server/service/endpoint_invites.go | 70 ------- server/service/endpoint_status.go | 34 --- server/service/endpoint_targets.go | 118 ----------- server/service/handler.go | 59 +----- server/service/handler_test.go | 12 -- server/service/integration_core_test.go | 191 ++++++++++++++--- server/service/invites.go | 182 +++++++++++++++++ server/service/service_invites.go | 99 --------- server/service/service_status.go | 33 --- server/service/service_targets.go | 82 -------- server/service/service_users.go | 13 -- server/service/status.go | 63 ++++++ server/service/targets.go | 193 ++++++++++++++++++ ...ervice_targets_test.go => targets_test.go} | 0 server/service/testing_client.go | 4 +- server/service/transport_change_email.go | 22 -- server/service/transport_invites.go | 30 --- server/service/transport_invites_test.go | 65 ------ server/service/transport_targets.go | 16 -- server/service/transport_targets_test.go | 46 ----- server/service/users.go | 37 ++++ server/service/validation_invites.go | 19 -- 23 files changed, 657 insertions(+), 761 deletions(-) delete mode 100644 server/service/endpoint_change_email.go delete mode 100644 server/service/endpoint_status.go delete mode 100644 server/service/endpoint_targets.go delete mode 100644 server/service/service_status.go delete mode 100644 server/service/service_targets.go create mode 100644 server/service/status.go create mode 100644 server/service/targets.go rename server/service/{service_targets_test.go => targets_test.go} (100%) delete mode 100644 server/service/transport_change_email.go delete mode 100644 server/service/transport_invites_test.go delete mode 100644 server/service/transport_targets.go delete mode 100644 server/service/transport_targets_test.go delete mode 100644 server/service/validation_invites.go diff --git a/server/service/endpoint_change_email.go b/server/service/endpoint_change_email.go deleted file mode 100644 index ceadd1503d..0000000000 --- a/server/service/endpoint_change_email.go +++ /dev/null @@ -1,30 +0,0 @@ -package service - -import ( - "context" - - "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/go-kit/kit/endpoint" -) - -type changeEmailRequest struct { - Token string -} - -type changeEmailResponse struct { - NewEmail string `json:"new_email"` - Err error `json:"error,omitempty"` -} - -func (r changeEmailResponse) error() error { return r.Err } - -func makeChangeEmailEndpoint(svc fleet.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(changeEmailRequest) - newEmailAddress, err := svc.ChangeUserEmail(ctx, req.Token) - if err != nil { - return changeEmailResponse{Err: err}, nil - } - return changeEmailResponse{NewEmail: newEmailAddress}, nil - } -} diff --git a/server/service/endpoint_invites.go b/server/service/endpoint_invites.go index a6158c81aa..92a5daae29 100644 --- a/server/service/endpoint_invites.go +++ b/server/service/endpoint_invites.go @@ -7,76 +7,6 @@ import ( "github.com/go-kit/kit/endpoint" ) -type createInviteRequest struct { - payload fleet.InvitePayload -} - -type createInviteResponse struct { - Invite *fleet.Invite `json:"invite,omitempty"` - Err error `json:"error,omitempty"` -} - -func (r createInviteResponse) error() error { return r.Err } - -func makeCreateInviteEndpoint(svc fleet.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(createInviteRequest) - invite, err := svc.InviteNewUser(ctx, req.payload) - if err != nil { - return createInviteResponse{Err: err}, nil - } - return createInviteResponse{invite, nil}, nil - } -} - -type listInvitesRequest struct { - ListOptions fleet.ListOptions -} - -type listInvitesResponse struct { - Invites []fleet.Invite `json:"invites"` - Err error `json:"error,omitempty"` -} - -func (r listInvitesResponse) error() error { return r.Err } - -func makeListInvitesEndpoint(svc fleet.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(listInvitesRequest) - invites, err := svc.ListInvites(ctx, req.ListOptions) - if err != nil { - return listInvitesResponse{Err: err}, nil - } - - resp := listInvitesResponse{Invites: []fleet.Invite{}} - for _, invite := range invites { - resp.Invites = append(resp.Invites, *invite) - } - return resp, nil - } -} - -type deleteInviteRequest struct { - ID uint -} - -type deleteInviteResponse struct { - Err error `json:"error,omitempty"` -} - -func (r deleteInviteResponse) error() error { return r.Err } - -func makeDeleteInviteEndpoint(svc fleet.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(deleteInviteRequest) - err := svc.DeleteInvite(ctx, req.ID) - if err != nil { - return deleteInviteResponse{Err: err}, nil - } - return deleteInviteResponse{}, nil - } -} - type verifyInviteRequest struct { Token string } diff --git a/server/service/endpoint_status.go b/server/service/endpoint_status.go deleted file mode 100644 index d94263de90..0000000000 --- a/server/service/endpoint_status.go +++ /dev/null @@ -1,34 +0,0 @@ -package service - -import ( - "context" - - "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/go-kit/kit/endpoint" -) - -type statusResponse struct { - Err error `json:"error,omitempty"` -} - -func (m statusResponse) error() error { return m.Err } - -func makeStatusLiveQueryEndpoint(svc fleet.Service) endpoint.Endpoint { - return func(ctx context.Context, req interface{}) (interface{}, error) { - var resp statusResponse - if err := svc.StatusLiveQuery(ctx); err != nil { - resp.Err = err - } - return resp, nil - } -} - -func makeStatusResultStoreEndpoint(svc fleet.Service) endpoint.Endpoint { - return func(ctx context.Context, req interface{}) (interface{}, error) { - var resp statusResponse - if err := svc.StatusResultStore(ctx); err != nil { - resp.Err = err - } - return resp, nil - } -} diff --git a/server/service/endpoint_targets.go b/server/service/endpoint_targets.go deleted file mode 100644 index a95d01a0b2..0000000000 --- a/server/service/endpoint_targets.go +++ /dev/null @@ -1,118 +0,0 @@ -package service - -import ( - "context" - "time" - - "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/go-kit/kit/endpoint" -) - -//////////////////////////////////////////////////////////////////////////////// -// Search Targets -//////////////////////////////////////////////////////////////////////////////// - -type searchTargetsRequest struct { - // MatchQuery is the query SQL - MatchQuery string `json:"query"` - // QueryID is the ID of a saved query to run (used to determine if this is a - // query that observers can run). - QueryID *uint `json:"query_id"` - Selected fleet.HostTargets `json:"selected"` -} - -type hostSearchResult struct { - HostResponse - DisplayText string `json:"display_text"` -} - -type labelSearchResult struct { - *fleet.Label - DisplayText string `json:"display_text"` - Count int `json:"count"` -} - -type teamSearchResult struct { - *fleet.Team - DisplayText string `json:"display_text"` - Count int `json:"count"` -} - -type targetsData struct { - Hosts []hostSearchResult `json:"hosts"` - Labels []labelSearchResult `json:"labels"` - Teams []teamSearchResult `json:"teams"` -} - -type searchTargetsResponse struct { - Targets *targetsData `json:"targets,omitempty"` - TargetsCount uint `json:"targets_count"` - TargetsOnline uint `json:"targets_online"` - TargetsOffline uint `json:"targets_offline"` - TargetsMissingInAction uint `json:"targets_missing_in_action"` - Err error `json:"error,omitempty"` -} - -func (r searchTargetsResponse) error() error { return r.Err } - -func makeSearchTargetsEndpoint(svc fleet.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(searchTargetsRequest) - - results, err := svc.SearchTargets(ctx, req.MatchQuery, req.QueryID, req.Selected) - if err != nil { - return searchTargetsResponse{Err: err}, nil - } - - targets := &targetsData{ - Hosts: []hostSearchResult{}, - Labels: []labelSearchResult{}, - Teams: []teamSearchResult{}, - } - - for _, host := range results.Hosts { - targets.Hosts = append(targets.Hosts, - hostSearchResult{ - HostResponse{ - Host: host, - Status: host.Status(time.Now()), - }, - host.Hostname, - }, - ) - } - - for _, label := range results.Labels { - targets.Labels = append(targets.Labels, - labelSearchResult{ - Label: label, - DisplayText: label.Name, - Count: label.HostCount, - }, - ) - } - - for _, team := range results.Teams { - targets.Teams = append(targets.Teams, - teamSearchResult{ - Team: team, - DisplayText: team.Name, - Count: team.HostCount, - }, - ) - } - - metrics, err := svc.CountHostsInTargets(ctx, req.QueryID, req.Selected) - if err != nil { - return searchTargetsResponse{Err: err}, nil - } - - return searchTargetsResponse{ - Targets: targets, - TargetsCount: metrics.TotalHosts, - TargetsOnline: metrics.OnlineHosts, - TargetsOffline: metrics.OfflineHosts, - TargetsMissingInAction: metrics.MissingInActionHosts, - }, nil - } -} diff --git a/server/service/handler.go b/server/service/handler.go index 2a5bdb328b..f0d7b3a55f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -30,9 +30,6 @@ type FleetEndpoints struct { ResetPassword endpoint.Endpoint CreateUserWithInvite endpoint.Endpoint PerformRequiredPasswordReset endpoint.Endpoint - CreateInvite endpoint.Endpoint - ListInvites endpoint.Endpoint - DeleteInvite endpoint.Endpoint VerifyInvite endpoint.Endpoint EnrollAgent endpoint.Endpoint GetClientConfig endpoint.Endpoint @@ -41,13 +38,9 @@ type FleetEndpoints struct { SubmitLogs endpoint.Endpoint CarveBegin endpoint.Endpoint CarveBlock endpoint.Endpoint - SearchTargets endpoint.Endpoint - ChangeEmail endpoint.Endpoint InitiateSSO endpoint.Endpoint CallbackSSO endpoint.Endpoint SSOSettings endpoint.Endpoint - StatusResultStore endpoint.Endpoint - StatusLiveQuery endpoint.Endpoint } // MakeFleetServerEndpoints creates the Fleet API endpoints. @@ -75,18 +68,6 @@ func MakeFleetServerEndpoints(svc fleet.Service, urlPrefix string, limitStore th // logged in user PerformRequiredPasswordReset: logged(canPerformPasswordReset(makePerformRequiredPasswordResetEndpoint(svc))), - // Standard user authentication routes - CreateInvite: authenticatedUser(svc, makeCreateInviteEndpoint(svc)), - ListInvites: authenticatedUser(svc, makeListInvitesEndpoint(svc)), - DeleteInvite: authenticatedUser(svc, makeDeleteInviteEndpoint(svc)), - - SearchTargets: authenticatedUser(svc, makeSearchTargetsEndpoint(svc)), - ChangeEmail: authenticatedUser(svc, makeChangeEmailEndpoint(svc)), - - // Authenticated status endpoints - StatusResultStore: authenticatedUser(svc, makeStatusResultStoreEndpoint(svc)), - StatusLiveQuery: authenticatedUser(svc, makeStatusLiveQueryEndpoint(svc)), - // Osquery endpoints EnrollAgent: logged(makeEnrollAgentEndpoint(svc)), // Authenticated osquery endpoints @@ -109,9 +90,6 @@ type fleetHandlers struct { ResetPassword http.Handler CreateUserWithInvite http.Handler PerformRequiredPasswordReset http.Handler - CreateInvite http.Handler - ListInvites http.Handler - DeleteInvite http.Handler VerifyInvite http.Handler EnrollAgent http.Handler GetClientConfig http.Handler @@ -120,13 +98,9 @@ type fleetHandlers struct { SubmitLogs http.Handler CarveBegin http.Handler CarveBlock http.Handler - SearchTargets http.Handler - ChangeEmail http.Handler InitiateSSO http.Handler CallbackSSO http.Handler SettingsSSO http.Handler - StatusResultStore http.Handler - StatusLiveQuery http.Handler } func makeKitHandlers(e FleetEndpoints, opts []kithttp.ServerOption) *fleetHandlers { @@ -141,9 +115,6 @@ func makeKitHandlers(e FleetEndpoints, opts []kithttp.ServerOption) *fleetHandle ResetPassword: newServer(e.ResetPassword, decodeResetPasswordRequest), CreateUserWithInvite: newServer(e.CreateUserWithInvite, decodeCreateUserRequest), PerformRequiredPasswordReset: newServer(e.PerformRequiredPasswordReset, decodePerformRequiredPasswordResetRequest), - CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), - ListInvites: newServer(e.ListInvites, decodeListInvitesRequest), - DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), VerifyInvite: newServer(e.VerifyInvite, decodeVerifyInviteRequest), EnrollAgent: newServer(e.EnrollAgent, decodeEnrollAgentRequest), GetClientConfig: newServer(e.GetClientConfig, decodeGetClientConfigRequest), @@ -152,13 +123,9 @@ func makeKitHandlers(e FleetEndpoints, opts []kithttp.ServerOption) *fleetHandle SubmitLogs: newServer(e.SubmitLogs, decodeSubmitLogsRequest), CarveBegin: newServer(e.CarveBegin, decodeCarveBeginRequest), CarveBlock: newServer(e.CarveBlock, decodeCarveBlockRequest), - SearchTargets: newServer(e.SearchTargets, decodeSearchTargetsRequest), - ChangeEmail: newServer(e.ChangeEmail, decodeChangeEmailRequest), InitiateSSO: newServer(e.InitiateSSO, decodeInitiateSSORequest), CallbackSSO: newServer(e.CallbackSSO, decodeCallbackSSORequest), SettingsSSO: newServer(e.SSOSettings, decodeNoParamsRequest), - StatusResultStore: newServer(e.StatusResultStore, decodeNoParamsRequest), - StatusLiveQuery: newServer(e.StatusLiveQuery, decodeNoParamsRequest), } } @@ -340,21 +307,8 @@ func attachFleetAPIRoutes(r *mux.Router, h *fleetHandlers) { r.Handle("/api/v1/fleet/sso", h.InitiateSSO).Methods("POST").Name("intiate_sso") r.Handle("/api/v1/fleet/sso", h.SettingsSSO).Methods("GET").Name("sso_config") r.Handle("/api/v1/fleet/sso/callback", h.CallbackSSO).Methods("POST").Name("callback_sso") - r.Handle("/api/v1/fleet/users", h.CreateUserWithInvite).Methods("POST").Name("create_user_with_invite") - - r.Handle("/api/v1/fleet/invites", h.CreateInvite).Methods("POST").Name("create_invite") - r.Handle("/api/v1/fleet/invites", h.ListInvites).Methods("GET").Name("list_invites") - r.Handle("/api/v1/fleet/invites/{id:[0-9]+}", h.DeleteInvite).Methods("DELETE").Name("delete_invite") r.Handle("/api/v1/fleet/invites/{token}", h.VerifyInvite).Methods("GET").Name("verify_invite") - - r.Handle("/api/v1/fleet/email/change/{token}", h.ChangeEmail).Methods("GET").Name("change_email") - - r.Handle("/api/v1/fleet/targets", h.SearchTargets).Methods("POST").Name("search_targets") - - r.Handle("/api/v1/fleet/status/result_store", h.StatusResultStore).Methods("GET").Name("status_result_store") - r.Handle("/api/v1/fleet/status/live_query", h.StatusLiveQuery).Methods("GET").Name("status_live_query") - r.Handle("/api/v1/osquery/enroll", h.EnrollAgent).Methods("POST").Name("enroll_agent") r.Handle("/api/v1/osquery/config", h.GetClientConfig).Methods("POST").Name("get_client_config") r.Handle("/api/v1/osquery/distributed/read", h.GetDistributedQueries).Methods("POST").Name("get_distributed_queries") @@ -409,6 +363,14 @@ func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kitht e.DELETE("/api/_version_/fleet/users/{id:[0-9]+}/sessions", deleteSessionsForUserEndpoint, deleteSessionsForUserRequest{}) e.POST("/api/_version_/fleet/change_password", changePasswordEndpoint, changePasswordRequest{}) + e.GET("/api/_version_/fleet/email/change/{token}", changeEmailEndpoint, changeEmailRequest{}) + e.POST("/api/_version_/fleet/targets", searchTargetsEndpoint, searchTargetsRequest{}) + + e.POST("/api/_version_/fleet/invites", createInviteEndpoint, createInviteRequest{}) + e.GET("/api/_version_/fleet/invites", listInvitesEndpoint, listInvitesRequest{}) + e.DELETE("/api/_version_/fleet/invites/{id:[0-9]+}", deleteInviteEndpoint, deleteInviteRequest{}) + e.PATCH("/api/_version_/fleet/invites/{id:[0-9]+}", updateInviteEndpoint, updateInviteRequest{}) + e.POST("/api/_version_/fleet/global/policies", globalPolicyEndpoint, globalPolicyRequest{}) e.GET("/api/_version_/fleet/global/policies", listGlobalPoliciesEndpoint, nil) e.GET("/api/_version_/fleet/global/policies/{policy_id}", getPolicyByIDEndpoint, getPolicyByIDRequest{}) @@ -480,8 +442,6 @@ func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kitht e.POST("/api/_version_/fleet/queries/run", createDistributedQueryCampaignEndpoint, createDistributedQueryCampaignRequest{}) e.POST("/api/_version_/fleet/queries/run_by_names", createDistributedQueryCampaignByNamesEndpoint, createDistributedQueryCampaignByNamesRequest{}) - e.PATCH("/api/_version_/fleet/invites/{id:[0-9]+}", updateInviteEndpoint, updateInviteRequest{}) - e.GET("/api/_version_/fleet/activities", listActivitiesEndpoint, listActivitiesRequest{}) e.GET("/api/_version_/fleet/global/schedule", getGlobalScheduleEndpoint, getGlobalScheduleRequest{}) @@ -495,6 +455,9 @@ func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kitht e.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/macadmins", getMacadminsDataEndpoint, getMacadminsDataRequest{}) e.GET("/api/_version_/fleet/macadmins", getAggregatedMacadminsDataEndpoint, getAggregatedMacadminsDataRequest{}) + + e.GET("/api/_version_/fleet/status/result_store", statusResultStoreEndpoint, nil) + e.GET("/api/_version_/fleet/status/live_query", statusLiveQueryEndpoint, nil) } // TODO: this duplicates the one in makeKitHandler diff --git a/server/service/handler_test.go b/server/service/handler_test.go index be01755fcd..3faed0c01f 100644 --- a/server/service/handler_test.go +++ b/server/service/handler_test.go @@ -54,18 +54,6 @@ func TestAPIRoutes(t *testing.T) { verb: "POST", uri: "/api/v1/fleet/reset_password", }, - { - verb: "GET", - uri: "/api/v1/fleet/invites", - }, - { - verb: "POST", - uri: "/api/v1/fleet/invites", - }, - { - verb: "DELETE", - uri: "/api/v1/fleet/invites/1", - }, { verb: "POST", uri: "/api/v1/osquery/enroll", diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index d11f4acca9..32063769b0 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -74,6 +74,13 @@ func (s *integrationTestSuite) TearDownTest() { } } + teams, err := s.ds.ListTeams(ctx, fleet.TeamFilter{User: &u}, fleet.ListOptions{}) + require.NoError(t, err) + for _, tm := range teams { + err := s.ds.DeleteTeam(ctx, tm.ID) + require.NoError(t, err) + } + globalPolicies, err := s.ds.ListGlobalPolicies(ctx) require.NoError(t, err) if len(globalPolicies) > 0 { @@ -871,39 +878,91 @@ func (s *integrationTestSuite) TestInvites() { }) require.NoError(t, err) - createInviteReq := createInviteRequest{ - payload: fleet.InvitePayload{ - Email: ptr.String("some email"), - Name: ptr.String("some name"), - Position: nil, - SSOEnabled: nil, - GlobalRole: null.StringFrom(fleet.RoleAdmin), - Teams: nil, - }, - } + // list invites, none yet + var listResp listInvitesResponse + s.DoJSON("GET", "/api/v1/fleet/invites", nil, http.StatusOK, &listResp) + require.Len(t, listResp.Invites, 0) + + // create valid invite + createInviteReq := createInviteRequest{InvitePayload: fleet.InvitePayload{ + Email: ptr.String("some email"), + Name: ptr.String("some name"), + GlobalRole: null.StringFrom(fleet.RoleAdmin), + }} createInviteResp := createInviteResponse{} - s.DoJSON("POST", "/api/v1/fleet/invites", createInviteReq.payload, http.StatusOK, &createInviteResp) + s.DoJSON("POST", "/api/v1/fleet/invites", createInviteReq, http.StatusOK, &createInviteResp) require.NotNil(t, createInviteResp.Invite) require.NotZero(t, createInviteResp.Invite.ID) + validInvite := *createInviteResp.Invite - updateInviteReq := updateInviteRequest{ - InvitePayload: fleet.InvitePayload{ - Teams: []fleet.UserTeam{ - { - Team: fleet.Team{ID: team.ID}, - Role: fleet.RoleObserver, - }, - }, - }, - } + // create invite without an email + createInviteReq = createInviteRequest{InvitePayload: fleet.InvitePayload{ + Email: nil, + Name: ptr.String("some other name"), + GlobalRole: null.StringFrom(fleet.RoleObserver), + }} + createInviteResp = createInviteResponse{} + s.DoJSON("POST", "/api/v1/fleet/invites", createInviteReq, http.StatusUnprocessableEntity, &createInviteResp) + + // create invite for an existing user + existingEmail := "admin1@example.com" + createInviteReq = createInviteRequest{InvitePayload: fleet.InvitePayload{ + Email: ptr.String(existingEmail), + Name: ptr.String("some other name"), + GlobalRole: null.StringFrom(fleet.RoleObserver), + }} + createInviteResp = createInviteResponse{} + s.DoJSON("POST", "/api/v1/fleet/invites", createInviteReq, http.StatusUnprocessableEntity, &createInviteResp) + + // create invite for an existing user with email ALL CAPS + createInviteReq = createInviteRequest{InvitePayload: fleet.InvitePayload{ + Email: ptr.String(strings.ToUpper(existingEmail)), + Name: ptr.String("some other name"), + GlobalRole: null.StringFrom(fleet.RoleObserver), + }} + createInviteResp = createInviteResponse{} + s.DoJSON("POST", "/api/v1/fleet/invites", createInviteReq, http.StatusUnprocessableEntity, &createInviteResp) + + // list invites, we have one now + listResp = listInvitesResponse{} + s.DoJSON("GET", "/api/v1/fleet/invites", nil, http.StatusOK, &listResp) + require.Len(t, listResp.Invites, 1) + require.Equal(t, validInvite.ID, listResp.Invites[0].ID) + + // list invites, next page is empty + listResp = listInvitesResponse{} + s.DoJSON("GET", "/api/v1/fleet/invites", nil, http.StatusOK, &listResp, "page", "1", "per_page", "2") + require.Len(t, listResp.Invites, 0) + + // update a non-existing invite + updateInviteReq := updateInviteRequest{InvitePayload: fleet.InvitePayload{ + Teams: []fleet.UserTeam{ + {Team: fleet.Team{ID: team.ID}, Role: fleet.RoleObserver}, + }}} updateInviteResp := updateInviteResponse{} - s.DoJSON("PATCH", fmt.Sprintf("/api/v1/fleet/invites/%d", createInviteResp.Invite.ID), updateInviteReq, http.StatusOK, &updateInviteResp) + s.DoJSON("PATCH", fmt.Sprintf("/api/v1/fleet/invites/%d", validInvite.ID+1), updateInviteReq, http.StatusNotFound, &updateInviteResp) - verify, err := s.ds.Invite(context.Background(), createInviteResp.Invite.ID) + // update the valid invite created earlier, make it an observer of a team + updateInviteResp = updateInviteResponse{} + s.DoJSON("PATCH", fmt.Sprintf("/api/v1/fleet/invites/%d", validInvite.ID), updateInviteReq, http.StatusOK, &updateInviteResp) + + verify, err := s.ds.Invite(context.Background(), validInvite.ID) require.NoError(t, err) require.Equal(t, "", verify.GlobalRole.String) require.Len(t, verify.Teams, 1) assert.Equal(t, team.ID, verify.Teams[0].ID) + + // delete an existing invite + var delResp deleteInviteResponse + s.DoJSON("DELETE", fmt.Sprintf("/api/v1/fleet/invites/%d", validInvite.ID), nil, http.StatusOK, &delResp) + + // list invites, is now empty + listResp = listInvitesResponse{} + s.DoJSON("GET", "/api/v1/fleet/invites", nil, http.StatusOK, &listResp) + require.Len(t, listResp.Invites, 0) + + // delete a now non-existing invite + s.DoJSON("DELETE", fmt.Sprintf("/api/v1/fleet/invites/%d", validInvite.ID), nil, http.StatusNotFound, &delResp) } func (s *integrationTestSuite) TestGetHostSummary() { @@ -2756,6 +2815,92 @@ func (s *integrationTestSuite) TestPaginateListSoftware() { assertResp(lsResp, nil, time.Time{}) } +func (s *integrationTestSuite) TestChangeUserEmail() { + t := s.T() + + // create a new test user + user := &fleet.User{ + Name: t.Name(), + Email: "testchangeemail@example.com", + GlobalRole: ptr.String(fleet.RoleObserver), + } + userRawPwd := "foobarbaz1234!" + err := user.SetPassword(userRawPwd, 10, 10) + require.Nil(t, err) + user, err = s.ds.NewUser(context.Background(), user) + require.Nil(t, err) + + // try to change email with an invalid token + var changeResp changeEmailResponse + s.DoJSON("GET", "/api/v1/fleet/email/change/invalidtoken", nil, http.StatusNotFound, &changeResp) + + // create a valid token for the test user + err = s.ds.PendingEmailChange(context.Background(), user.ID, "testchangeemail2@example.com", "validtoken") + require.Nil(t, err) + + // try to change email with a valid token, but request made from different user + changeResp = changeEmailResponse{} + s.DoJSON("GET", "/api/v1/fleet/email/change/validtoken", nil, http.StatusNotFound, &changeResp) + + // switch to the test user and make the change email request + s.token = s.getTestToken(user.Email, userRawPwd) + defer func() { s.token = s.getTestAdminToken() }() + + changeResp = changeEmailResponse{} + s.DoJSON("GET", "/api/v1/fleet/email/change/validtoken", nil, http.StatusOK, &changeResp) + require.Equal(t, "testchangeemail2@example.com", changeResp.NewEmail) + + // using the token consumes it, so making another request with the same token fails + changeResp = changeEmailResponse{} + s.DoJSON("GET", "/api/v1/fleet/email/change/validtoken", nil, http.StatusNotFound, &changeResp) +} + +func (s *integrationTestSuite) TestSearchTargets() { + t := s.T() + + hosts := s.createHosts(t) + + lblIDs, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"}) + require.NoError(t, err) + require.Len(t, lblIDs, 1) + + // no search criteria + var searchResp searchTargetsResponse + s.DoJSON("POST", "/api/v1/fleet/targets", searchTargetsRequest{}, http.StatusOK, &searchResp) + require.Equal(t, uint(0), searchResp.TargetsCount) + require.Len(t, searchResp.Targets.Hosts, len(hosts)) // the HostTargets.HostIDs are actually host IDs to *omit* from the search + require.Len(t, searchResp.Targets.Labels, 1) + require.Len(t, searchResp.Targets.Teams, 0) + + searchResp = searchTargetsResponse{} + s.DoJSON("POST", "/api/v1/fleet/targets", searchTargetsRequest{Selected: fleet.HostTargets{LabelIDs: lblIDs}}, http.StatusOK, &searchResp) + require.Equal(t, uint(0), searchResp.TargetsCount) + require.Len(t, searchResp.Targets.Hosts, len(hosts)) // no omitted host id + require.Len(t, searchResp.Targets.Labels, 0) // labels have been omitted + require.Len(t, searchResp.Targets.Teams, 0) + + searchResp = searchTargetsResponse{} + s.DoJSON("POST", "/api/v1/fleet/targets", searchTargetsRequest{Selected: fleet.HostTargets{HostIDs: []uint{hosts[1].ID}}}, http.StatusOK, &searchResp) + require.Equal(t, uint(1), searchResp.TargetsCount) + require.Len(t, searchResp.Targets.Hosts, len(hosts)-1) // one omitted host id + require.Len(t, searchResp.Targets.Labels, 1) // labels have not been omitted + require.Len(t, searchResp.Targets.Teams, 0) + + searchResp = searchTargetsResponse{} + s.DoJSON("POST", "/api/v1/fleet/targets", searchTargetsRequest{MatchQuery: "foo.local1"}, http.StatusOK, &searchResp) + require.Equal(t, uint(0), searchResp.TargetsCount) + require.Len(t, searchResp.Targets.Hosts, 1) + require.Len(t, searchResp.Targets.Labels, 1) + require.Len(t, searchResp.Targets.Teams, 0) + require.Contains(t, searchResp.Targets.Hosts[0].Hostname, "foo.local1") +} + +func (s *integrationTestSuite) TestStatus() { + var statusResp statusResponse + s.DoJSON("GET", "/api/v1/fleet/status/result_store", nil, http.StatusOK, &statusResp) + s.DoJSON("GET", "/api/v1/fleet/status/live_query", nil, http.StatusOK, &statusResp) +} + // creates a session and returns it, its key is to be passed as authorization header. func createSession(t *testing.T, uid uint, ds fleet.Datastore) *fleet.Session { key := make([]byte, 64) diff --git a/server/service/invites.go b/server/service/invites.go index b717696001..cce5dc5575 100644 --- a/server/service/invites.go +++ b/server/service/invites.go @@ -2,10 +2,162 @@ package service import ( "context" + "encoding/base64" + "errors" + "html/template" + "strings" + "github.com/fleetdm/fleet/v4/server" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mail" ) +//////////////////////////////////////////////////////////////////////////////// +// Create invite +//////////////////////////////////////////////////////////////////////////////// + +type createInviteRequest struct { + fleet.InvitePayload +} + +type createInviteResponse struct { + Invite *fleet.Invite `json:"invite,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r createInviteResponse) error() error { return r.Err } + +func createInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + req := request.(*createInviteRequest) + invite, err := svc.InviteNewUser(ctx, req.InvitePayload) + if err != nil { + return createInviteResponse{Err: err}, nil + } + return createInviteResponse{invite, nil}, nil +} + +func (svc *Service) InviteNewUser(ctx context.Context, payload fleet.InvitePayload) (*fleet.Invite, error) { + if err := svc.authz.Authorize(ctx, &fleet.Invite{}, fleet.ActionWrite); err != nil { + return nil, err + } + + if payload.Email == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("email", "missing required argument")) + } + *payload.Email = strings.ToLower(*payload.Email) + + // verify that the user with the given email does not already exist + _, err := svc.ds.UserByEmail(ctx, *payload.Email) + if err == nil { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("email", "a user with this account already exists")) + } + var nfe fleet.NotFoundError + if !errors.As(err, &nfe) { + return nil, err + } + + // find the user who created the invite + v, ok := viewer.FromContext(ctx) + if !ok { + return nil, errors.New("missing viewer context for create invite") + } + inviter := v.User + + random, err := server.GenerateRandomText(svc.config.App.TokenKeySize) + if err != nil { + return nil, err + } + token := base64.URLEncoding.EncodeToString([]byte(random)) + + invite := &fleet.Invite{ + Email: *payload.Email, + InvitedBy: inviter.ID, + Token: token, + GlobalRole: payload.GlobalRole, + Teams: payload.Teams, + } + if payload.Position != nil { + invite.Position = *payload.Position + } + if payload.Name != nil { + invite.Name = *payload.Name + } + if payload.SSOEnabled != nil { + invite.SSOEnabled = *payload.SSOEnabled + } + + invite, err = svc.ds.NewInvite(ctx, invite) + if err != nil { + return nil, err + } + + config, err := svc.AppConfig(ctx) + if err != nil { + return nil, err + } + + invitedBy := inviter.Name + if invitedBy == "" { + invitedBy = inviter.Email + } + inviteEmail := fleet.Email{ + Subject: "You are Invited to Fleet", + To: []string{invite.Email}, + Config: config, + Mailer: &mail.InviteMailer{ + Invite: invite, + BaseURL: template.URL(config.ServerSettings.ServerURL + svc.config.Server.URLPrefix), + AssetURL: getAssetURL(), + OrgName: config.OrgInfo.OrgName, + InvitedBy: invitedBy, + }, + } + + err = svc.mailService.SendEmail(inviteEmail) + if err != nil { + return nil, err + } + return invite, nil +} + +//////////////////////////////////////////////////////////////////////////////// +// List invites +//////////////////////////////////////////////////////////////////////////////// + +type listInvitesRequest struct { + ListOptions fleet.ListOptions `url:"list_options"` +} + +type listInvitesResponse struct { + Invites []fleet.Invite `json:"invites"` + Err error `json:"error,omitempty"` +} + +func (r listInvitesResponse) error() error { return r.Err } + +func listInvitesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + req := request.(*listInvitesRequest) + invites, err := svc.ListInvites(ctx, req.ListOptions) + if err != nil { + return listInvitesResponse{Err: err}, nil + } + + resp := listInvitesResponse{Invites: []fleet.Invite{}} + for _, invite := range invites { + resp.Invites = append(resp.Invites, *invite) + } + return resp, nil +} + +func (svc *Service) ListInvites(ctx context.Context, opt fleet.ListOptions) ([]*fleet.Invite, error) { + if err := svc.authz.Authorize(ctx, &fleet.Invite{}, fleet.ActionRead); err != nil { + return nil, err + } + return svc.ds.ListInvites(ctx, opt) +} + //////////////////////////////////////////////////////////////////////////////// // Update invite //////////////////////////////////////////////////////////////////////////////// @@ -63,3 +215,33 @@ func (svc *Service) UpdateInvite(ctx context.Context, id uint, payload fleet.Inv return svc.ds.UpdateInvite(ctx, id, invite) } + +//////////////////////////////////////////////////////////////////////////////// +// Delete invite +//////////////////////////////////////////////////////////////////////////////// + +type deleteInviteRequest struct { + ID uint `url:"id"` +} + +type deleteInviteResponse struct { + Err error `json:"error,omitempty"` +} + +func (r deleteInviteResponse) error() error { return r.Err } + +func deleteInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + req := request.(*deleteInviteRequest) + err := svc.DeleteInvite(ctx, req.ID) + if err != nil { + return deleteInviteResponse{Err: err}, nil + } + return deleteInviteResponse{}, nil +} + +func (svc *Service) DeleteInvite(ctx context.Context, id uint) error { + if err := svc.authz.Authorize(ctx, &fleet.Invite{}, fleet.ActionWrite); err != nil { + return err + } + return svc.ds.DeleteInvite(ctx, id) +} diff --git a/server/service/service_invites.go b/server/service/service_invites.go index c6c5ec8504..7007214c4e 100644 --- a/server/service/service_invites.go +++ b/server/service/service_invites.go @@ -2,104 +2,12 @@ package service import ( "context" - "encoding/base64" - "errors" - "html/template" - "github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server/contexts/logging" - "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/fleetdm/fleet/v4/server/mail" ) -func (svc Service) InviteNewUser(ctx context.Context, payload fleet.InvitePayload) (*fleet.Invite, error) { - if err := svc.authz.Authorize(ctx, &fleet.Invite{}, fleet.ActionWrite); err != nil { - return nil, err - } - - // verify that the user with the given email does not already exist - _, err := svc.ds.UserByEmail(ctx, *payload.Email) - if err == nil { - return nil, fleet.NewInvalidArgumentError("email", "a user with this account already exists") - } - var nfe fleet.NotFoundError - if !errors.As(err, &nfe) { - return nil, err - } - - // find the user who created the invite - v, ok := viewer.FromContext(ctx) - if !ok { - return nil, errors.New("missing viewer context for create invite") - } - inviter := v.User - - random, err := server.GenerateRandomText(svc.config.App.TokenKeySize) - if err != nil { - return nil, err - } - token := base64.URLEncoding.EncodeToString([]byte(random)) - - invite := &fleet.Invite{ - Email: *payload.Email, - InvitedBy: inviter.ID, - Token: token, - GlobalRole: payload.GlobalRole, - Teams: payload.Teams, - } - if payload.Position != nil { - invite.Position = *payload.Position - } - if payload.Name != nil { - invite.Name = *payload.Name - } - if payload.SSOEnabled != nil { - invite.SSOEnabled = *payload.SSOEnabled - } - - invite, err = svc.ds.NewInvite(ctx, invite) - if err != nil { - return nil, err - } - - config, err := svc.AppConfig(ctx) - if err != nil { - return nil, err - } - - invitedBy := inviter.Name - if invitedBy == "" { - invitedBy = inviter.Email - } - inviteEmail := fleet.Email{ - Subject: "You are Invited to Fleet", - To: []string{invite.Email}, - Config: config, - Mailer: &mail.InviteMailer{ - Invite: invite, - BaseURL: template.URL(config.ServerSettings.ServerURL + svc.config.Server.URLPrefix), - AssetURL: getAssetURL(), - OrgName: config.OrgInfo.OrgName, - InvitedBy: invitedBy, - }, - } - - err = svc.mailService.SendEmail(inviteEmail) - if err != nil { - return nil, err - } - return invite, nil -} - -func (svc *Service) ListInvites(ctx context.Context, opt fleet.ListOptions) ([]*fleet.Invite, error) { - if err := svc.authz.Authorize(ctx, &fleet.Invite{}, fleet.ActionRead); err != nil { - return nil, err - } - return svc.ds.ListInvites(ctx, opt) -} - func (svc *Service) VerifyInvite(ctx context.Context, token string) (*fleet.Invite, error) { // skipauth: There is no viewer context at this point. We rely on verifying // the invite for authNZ. @@ -124,10 +32,3 @@ func (svc *Service) VerifyInvite(ctx context.Context, token string) (*fleet.Invi return invite, nil } - -func (svc *Service) DeleteInvite(ctx context.Context, id uint) error { - if err := svc.authz.Authorize(ctx, &fleet.Invite{}, fleet.ActionWrite); err != nil { - return err - } - return svc.ds.DeleteInvite(ctx, id) -} diff --git a/server/service/service_status.go b/server/service/service_status.go deleted file mode 100644 index e3f269bbd0..0000000000 --- a/server/service/service_status.go +++ /dev/null @@ -1,33 +0,0 @@ -package service - -import ( - "context" - - "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" - "github.com/fleetdm/fleet/v4/server/fleet" -) - -func (svc *Service) StatusResultStore(ctx context.Context) error { - if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { - return err - } - - return svc.resultStore.HealthCheck() -} - -func (svc *Service) StatusLiveQuery(ctx context.Context) error { - if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { - return err - } - - cfg, err := svc.ds.AppConfig(ctx) - if err != nil { - return ctxerr.Wrap(ctx, err, "retrieve app config") - } - - if cfg.ServerSettings.LiveQueryDisabled { - return ctxerr.New(ctx, "disabled by administrator") - } - - return svc.StatusResultStore(ctx) -} diff --git a/server/service/service_targets.go b/server/service/service_targets.go deleted file mode 100644 index 20524434c3..0000000000 --- a/server/service/service_targets.go +++ /dev/null @@ -1,82 +0,0 @@ -package service - -import ( - "context" - - "github.com/fleetdm/fleet/v4/server/contexts/viewer" - "github.com/fleetdm/fleet/v4/server/fleet" -) - -func (svc Service) SearchTargets(ctx context.Context, matchQuery string, queryID *uint, targets fleet.HostTargets) (*fleet.TargetSearchResults, error) { - if err := svc.authz.Authorize(ctx, &fleet.Target{}, fleet.ActionRead); err != nil { - return nil, err - } - - vc, ok := viewer.FromContext(ctx) - if !ok { - return nil, fleet.ErrNoContext - } - - includeObserver := false - if queryID != nil { - query, err := svc.ds.Query(ctx, *queryID) - if err != nil { - return nil, err - } - includeObserver = query.ObserverCanRun - } - - filter := fleet.TeamFilter{User: vc.User, IncludeObserver: includeObserver} - - results := &fleet.TargetSearchResults{} - - hosts, err := svc.ds.SearchHosts(ctx, filter, matchQuery, targets.HostIDs...) - if err != nil { - return nil, err - } - - results.Hosts = append(results.Hosts, hosts...) - - labels, err := svc.ds.SearchLabels(ctx, filter, matchQuery, targets.LabelIDs...) - if err != nil { - return nil, err - } - results.Labels = labels - - teams, err := svc.ds.SearchTeams(ctx, filter, matchQuery, targets.TeamIDs...) - if err != nil { - return nil, err - } - results.Teams = teams - - return results, nil -} - -func (svc Service) CountHostsInTargets(ctx context.Context, queryID *uint, targets fleet.HostTargets) (*fleet.TargetMetrics, error) { - if err := svc.authz.Authorize(ctx, &fleet.Target{}, fleet.ActionRead); err != nil { - return nil, err - } - - vc, ok := viewer.FromContext(ctx) - if !ok { - return nil, fleet.ErrNoContext - } - - includeObserver := false - if queryID != nil { - query, err := svc.ds.Query(ctx, *queryID) - if err != nil { - return nil, err - } - includeObserver = query.ObserverCanRun - } - - filter := fleet.TeamFilter{User: vc.User, IncludeObserver: includeObserver} - - metrics, err := svc.ds.CountHostsInTargets(ctx, filter, targets, svc.clock.Now()) - if err != nil { - return nil, err - } - - return &metrics, nil -} diff --git a/server/service/service_users.go b/server/service/service_users.go index a0e147bb85..ddde508650 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -84,19 +84,6 @@ func (svc *Service) newUser(ctx context.Context, p fleet.UserPayload) (*fleet.Us return user, nil } -func (svc *Service) ChangeUserEmail(ctx context.Context, token string) (string, error) { - vc, ok := viewer.FromContext(ctx) - if !ok { - return "", fleet.ErrNoContext - } - - if err := svc.authz.Authorize(ctx, &fleet.User{ID: vc.UserID()}, fleet.ActionWrite); err != nil { - return "", err - } - - return svc.ds.ConfirmPendingEmailChange(ctx, vc.UserID(), token) -} - func (svc *Service) UserUnauthorized(ctx context.Context, id uint) (*fleet.User, error) { // Explicitly no authorization check. Should only be used by middleware. return svc.ds.UserByID(ctx, id) diff --git a/server/service/status.go b/server/service/status.go new file mode 100644 index 0000000000..adc26a1ccb --- /dev/null +++ b/server/service/status.go @@ -0,0 +1,63 @@ +package service + +import ( + "context" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" +) + +//////////////////////////////////////////////////////////////////////////////// +// Status Result Store +//////////////////////////////////////////////////////////////////////////////// + +type statusResponse struct { + Err error `json:"error,omitempty"` +} + +func (m statusResponse) error() error { return m.Err } + +func statusResultStoreEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (interface{}, error) { + var resp statusResponse + if err := svc.StatusResultStore(ctx); err != nil { + resp.Err = err + } + return resp, nil +} + +func (svc *Service) StatusResultStore(ctx context.Context) error { + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { + return err + } + + return svc.resultStore.HealthCheck() +} + +//////////////////////////////////////////////////////////////////////////////// +// Status Live Query +//////////////////////////////////////////////////////////////////////////////// + +func statusLiveQueryEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (interface{}, error) { + var resp statusResponse + if err := svc.StatusLiveQuery(ctx); err != nil { + resp.Err = err + } + return resp, nil +} + +func (svc *Service) StatusLiveQuery(ctx context.Context) error { + if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil { + return err + } + + cfg, err := svc.ds.AppConfig(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "retrieve app config") + } + + if cfg.ServerSettings.LiveQueryDisabled { + return ctxerr.New(ctx, "disabled by administrator") + } + + return svc.StatusResultStore(ctx) +} diff --git a/server/service/targets.go b/server/service/targets.go new file mode 100644 index 0000000000..f7a0cba1a5 --- /dev/null +++ b/server/service/targets.go @@ -0,0 +1,193 @@ +package service + +import ( + "context" + "time" + + "github.com/fleetdm/fleet/v4/server/contexts/viewer" + "github.com/fleetdm/fleet/v4/server/fleet" +) + +//////////////////////////////////////////////////////////////////////////////// +// Search Targets +//////////////////////////////////////////////////////////////////////////////// + +type searchTargetsRequest struct { + // MatchQuery is the query SQL + MatchQuery string `json:"query"` + // QueryID is the ID of a saved query to run (used to determine if this is a + // query that observers can run). + QueryID *uint `json:"query_id"` + // Selected is the list of IDs that are already selected on the caller side + // (e.g. the UI), so those are IDs that will be omitted from the returned + // payload. + Selected fleet.HostTargets `json:"selected"` +} + +type hostSearchResult struct { + HostResponse + DisplayText string `json:"display_text"` +} + +type labelSearchResult struct { + *fleet.Label + DisplayText string `json:"display_text"` + Count int `json:"count"` +} + +type teamSearchResult struct { + *fleet.Team + DisplayText string `json:"display_text"` + Count int `json:"count"` +} + +type targetsData struct { + Hosts []hostSearchResult `json:"hosts"` + Labels []labelSearchResult `json:"labels"` + Teams []teamSearchResult `json:"teams"` +} + +type searchTargetsResponse struct { + Targets *targetsData `json:"targets,omitempty"` + TargetsCount uint `json:"targets_count"` + TargetsOnline uint `json:"targets_online"` + TargetsOffline uint `json:"targets_offline"` + TargetsMissingInAction uint `json:"targets_missing_in_action"` + Err error `json:"error,omitempty"` +} + +func (r searchTargetsResponse) error() error { return r.Err } + +func searchTargetsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + req := request.(*searchTargetsRequest) + + results, err := svc.SearchTargets(ctx, req.MatchQuery, req.QueryID, req.Selected) + if err != nil { + return searchTargetsResponse{Err: err}, nil + } + + targets := &targetsData{ + Hosts: []hostSearchResult{}, + Labels: []labelSearchResult{}, + Teams: []teamSearchResult{}, + } + + for _, host := range results.Hosts { + targets.Hosts = append(targets.Hosts, + hostSearchResult{ + HostResponse{ + Host: host, + Status: host.Status(time.Now()), + }, + host.Hostname, + }, + ) + } + + for _, label := range results.Labels { + targets.Labels = append(targets.Labels, + labelSearchResult{ + Label: label, + DisplayText: label.Name, + Count: label.HostCount, + }, + ) + } + + for _, team := range results.Teams { + targets.Teams = append(targets.Teams, + teamSearchResult{ + Team: team, + DisplayText: team.Name, + Count: team.HostCount, + }, + ) + } + + metrics, err := svc.CountHostsInTargets(ctx, req.QueryID, req.Selected) + if err != nil { + return searchTargetsResponse{Err: err}, nil + } + + return searchTargetsResponse{ + Targets: targets, + TargetsCount: metrics.TotalHosts, + TargetsOnline: metrics.OnlineHosts, + TargetsOffline: metrics.OfflineHosts, + TargetsMissingInAction: metrics.MissingInActionHosts, + }, nil +} + +func (svc *Service) SearchTargets(ctx context.Context, matchQuery string, queryID *uint, targets fleet.HostTargets) (*fleet.TargetSearchResults, error) { + if err := svc.authz.Authorize(ctx, &fleet.Target{}, fleet.ActionRead); err != nil { + return nil, err + } + + vc, ok := viewer.FromContext(ctx) + if !ok { + return nil, fleet.ErrNoContext + } + + includeObserver := false + if queryID != nil { + query, err := svc.ds.Query(ctx, *queryID) + if err != nil { + return nil, err + } + includeObserver = query.ObserverCanRun + } + + filter := fleet.TeamFilter{User: vc.User, IncludeObserver: includeObserver} + + results := &fleet.TargetSearchResults{} + + hosts, err := svc.ds.SearchHosts(ctx, filter, matchQuery, targets.HostIDs...) + if err != nil { + return nil, err + } + + results.Hosts = append(results.Hosts, hosts...) + + labels, err := svc.ds.SearchLabels(ctx, filter, matchQuery, targets.LabelIDs...) + if err != nil { + return nil, err + } + results.Labels = labels + + teams, err := svc.ds.SearchTeams(ctx, filter, matchQuery, targets.TeamIDs...) + if err != nil { + return nil, err + } + results.Teams = teams + + return results, nil +} + +func (svc *Service) CountHostsInTargets(ctx context.Context, queryID *uint, targets fleet.HostTargets) (*fleet.TargetMetrics, error) { + if err := svc.authz.Authorize(ctx, &fleet.Target{}, fleet.ActionRead); err != nil { + return nil, err + } + + vc, ok := viewer.FromContext(ctx) + if !ok { + return nil, fleet.ErrNoContext + } + + includeObserver := false + if queryID != nil { + query, err := svc.ds.Query(ctx, *queryID) + if err != nil { + return nil, err + } + includeObserver = query.ObserverCanRun + } + + filter := fleet.TeamFilter{User: vc.User, IncludeObserver: includeObserver} + + metrics, err := svc.ds.CountHostsInTargets(ctx, filter, targets, svc.clock.Now()) + if err != nil { + return nil, err + } + + return &metrics, nil +} diff --git a/server/service/service_targets_test.go b/server/service/targets_test.go similarity index 100% rename from server/service/service_targets_test.go rename to server/service/targets_test.go diff --git a/server/service/testing_client.go b/server/service/testing_client.go index b01df706f9..6e44782ea5 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -11,6 +11,7 @@ import ( "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/pubsub" "github.com/fleetdm/fleet/v4/server/test" "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" @@ -43,7 +44,8 @@ type withServer struct { func (ts *withServer) SetupSuite(dbName string) { ts.withDS.SetupSuite(dbName) - users, server := RunServerForTestsWithDS(ts.s.T(), ts.ds) + rs := pubsub.NewInmemQueryResults() + users, server := RunServerForTestsWithDS(ts.s.T(), ts.ds, TestServerOpts{Rs: rs}) ts.server = server ts.users = users ts.token = ts.getTestAdminToken() diff --git a/server/service/transport_change_email.go b/server/service/transport_change_email.go deleted file mode 100644 index 3599a64ff1..0000000000 --- a/server/service/transport_change_email.go +++ /dev/null @@ -1,22 +0,0 @@ -package service - -import ( - "context" - "net/http" - - "github.com/gorilla/mux" -) - -func decodeChangeEmailRequest(ctx context.Context, r *http.Request) (interface{}, error) { - vars := mux.Vars(r) - token, ok := vars["token"] - if !ok { - return nil, errBadRoute - } - - response := changeEmailRequest{ - Token: token, - } - - return response, nil -} diff --git a/server/service/transport_invites.go b/server/service/transport_invites.go index 5b83708ef2..442ed20a3e 100644 --- a/server/service/transport_invites.go +++ b/server/service/transport_invites.go @@ -2,33 +2,11 @@ package service import ( "context" - "encoding/json" "net/http" - "strings" "github.com/gorilla/mux" ) -func decodeCreateInviteRequest(ctx context.Context, r *http.Request) (interface{}, error) { - var req createInviteRequest - if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil { - return nil, err - } - if req.payload.Email != nil { - *req.payload.Email = strings.ToLower(*req.payload.Email) - } - - return req, nil -} - -func decodeDeleteInviteRequest(ctx context.Context, r *http.Request) (interface{}, error) { - id, err := uintFromRequest(r, "id") - if err != nil { - return nil, err - } - return deleteInviteRequest{ID: uint(id)}, nil -} - func decodeVerifyInviteRequest(ctx context.Context, r *http.Request) (interface{}, error) { vars := mux.Vars(r) token, ok := vars["token"] @@ -37,11 +15,3 @@ func decodeVerifyInviteRequest(ctx context.Context, r *http.Request) (interface{ } return verifyInviteRequest{Token: token}, nil } - -func decodeListInvitesRequest(ctx context.Context, r *http.Request) (interface{}, error) { - opt, err := listOptionsFromRequest(r) - if err != nil { - return nil, err - } - return listInvitesRequest{ListOptions: opt}, nil -} diff --git a/server/service/transport_invites_test.go b/server/service/transport_invites_test.go deleted file mode 100644 index 6f45cedd30..0000000000 --- a/server/service/transport_invites_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package service - -import ( - "bytes" - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" -) - -func TestDecodeCreateInviteRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/fleet/invites", func(writer http.ResponseWriter, request *http.Request) { - _, err := decodeCreateInviteRequest(context.Background(), request) - assert.Nil(t, err) - }).Methods("POST") - - t.Run("lowercase email", func(t *testing.T) { - var body bytes.Buffer - body.Write([]byte(`{ - "name": "foo", - "email": "foo@fleet.co" - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("POST", "/api/v1/fleet/invites", &body), - ) - }) - - t.Run("uppercase email", func(t *testing.T) { - // email string should be lowerased after decode. - var body bytes.Buffer - body.Write([]byte(`{ - "name": "foo", - "email": "Foo@fleet.co" - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("POST", "/api/v1/fleet/invites", &body), - ) - }) - -} - -func TestDecodeVerifyInviteRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/fleet/invites/{token}", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeCreateInviteRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(verifyInviteRequest) - assert.Equal(t, "test_token", params.Token) - }).Methods("GET") - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("GET", "/api/v1/fleet/tokens/test_token", nil), - ) - -} diff --git a/server/service/transport_targets.go b/server/service/transport_targets.go deleted file mode 100644 index f0d4eeb436..0000000000 --- a/server/service/transport_targets.go +++ /dev/null @@ -1,16 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "net/http" -) - -func decodeSearchTargetsRequest(ctx context.Context, r *http.Request) (interface{}, error) { - var req searchTargetsRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, err - } - - return req, nil -} diff --git a/server/service/transport_targets_test.go b/server/service/transport_targets_test.go deleted file mode 100644 index 42b99a823e..0000000000 --- a/server/service/transport_targets_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package service - -import ( - "bytes" - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" -) - -func TestDecodeSearchTargetsRequest(t *testing.T) { - router := mux.NewRouter() - router.HandleFunc("/api/v1/fleet/targets", func(writer http.ResponseWriter, request *http.Request) { - r, err := decodeSearchTargetsRequest(context.Background(), request) - assert.Nil(t, err) - - params := r.(searchTargetsRequest) - assert.Equal(t, "bar", params.MatchQuery) - assert.Len(t, params.Selected.HostIDs, 3) - assert.Len(t, params.Selected.LabelIDs, 2) - }).Methods("POST") - var body bytes.Buffer - - body.Write([]byte(`{ - "query": "bar", - "selected": { - "hosts": [ - 1, - 2, - 3 - ], - "labels": [ - 1, - 2 - ] - } - }`)) - - router.ServeHTTP( - httptest.NewRecorder(), - httptest.NewRequest("POST", "/api/v1/fleet/targets", &body), - ) -} diff --git a/server/service/users.go b/server/service/users.go index 4907212560..148c5da1f6 100644 --- a/server/service/users.go +++ b/server/service/users.go @@ -486,6 +486,43 @@ func (svc *Service) DeleteSessionsForUser(ctx context.Context, id uint) error { return svc.ds.DestroyAllSessionsForUser(ctx, id) } +//////////////////////////////////////////////////////////////////////////////// +// Change user email +//////////////////////////////////////////////////////////////////////////////// + +type changeEmailRequest struct { + Token string `url:"token"` +} + +type changeEmailResponse struct { + NewEmail string `json:"new_email"` + Err error `json:"error,omitempty"` +} + +func (r changeEmailResponse) error() error { return r.Err } + +func changeEmailEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + req := request.(*changeEmailRequest) + newEmailAddress, err := svc.ChangeUserEmail(ctx, req.Token) + if err != nil { + return changeEmailResponse{Err: err}, nil + } + return changeEmailResponse{NewEmail: newEmailAddress}, nil +} + +func (svc *Service) ChangeUserEmail(ctx context.Context, token string) (string, error) { + vc, ok := viewer.FromContext(ctx) + if !ok { + return "", fleet.ErrNoContext + } + + if err := svc.authz.Authorize(ctx, &fleet.User{ID: vc.UserID()}, fleet.ActionWrite); err != nil { + return "", err + } + + return svc.ds.ConfirmPendingEmailChange(ctx, vc.UserID(), token) +} + func isAdminOfTheModifiedTeams(currentUser *fleet.User, originalUserTeams, newUserTeams []fleet.UserTeam) bool { // If the user is of the right global role, then they can modify the teams if currentUser.GlobalRole != nil && (*currentUser.GlobalRole == fleet.RoleAdmin || *currentUser.GlobalRole == fleet.RoleMaintainer) { diff --git a/server/service/validation_invites.go b/server/service/validation_invites.go deleted file mode 100644 index b1f5e1d040..0000000000 --- a/server/service/validation_invites.go +++ /dev/null @@ -1,19 +0,0 @@ -package service - -import ( - "context" - - "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" - "github.com/fleetdm/fleet/v4/server/fleet" -) - -func (mw validationMiddleware) InviteNewUser(ctx context.Context, payload fleet.InvitePayload) (*fleet.Invite, error) { - invalid := &fleet.InvalidArgumentError{} - if payload.Email == nil { - invalid.Append("email", "missing required argument") - } - if invalid.HasErrors() { - return nil, ctxerr.Wrap(ctx, invalid) - } - return mw.Service.InviteNewUser(ctx, payload) -}