Migrate the last batch of authenticatedUser endpoints to the new pattern (#4210)

This commit is contained in:
Martin Angers 2022-02-15 15:22:19 -05:00 committed by GitHub
parent 531ef1eddc
commit e29797deb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 657 additions and 761 deletions

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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",

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)

63
server/service/status.go Normal file
View file

@ -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)
}

193
server/service/targets.go Normal file
View file

@ -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
}

View file

@ -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()

View file

@ -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
}

View file

@ -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
}

View file

@ -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),
)
}

View file

@ -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
}

View file

@ -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),
)
}

View file

@ -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) {

View file

@ -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)
}