mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Issue 1359 fleetctl team transfer (#1413)
* wip * Add delete user command and translator * Add host transfer command * Add changes file * Undo bad refactor * Fix copypaste error * Implement with interfaces instead of assertions * Ad documentation and simplify implementation further * Update docs/1-Using-Fleet/3-REST-API.md Co-authored-by: Zach Wasserman <zach@fleetdm.com> Co-authored-by: Zach Wasserman <zach@fleetdm.com>
This commit is contained in:
parent
567f43d4a3
commit
484c6153e3
1
changes/issue-1359-fleetctl-host-transfer
Normal file
1
changes/issue-1359-fleetctl-host-transfer
Normal file
@ -0,0 +1 @@
|
||||
* Add host transfer capabilities to fleetctl. Fixes issue 1359.
|
1
changes/issue-1360-fleetctl-user-delete
Normal file
1
changes/issue-1360-fleetctl-user-delete
Normal file
@ -0,0 +1 @@
|
||||
* Add user delete capabilities to fleetctl. Fixes issue 1360
|
@ -60,6 +60,7 @@ func createApp(reader io.Reader, writer io.Writer, exitErrHandler cli.ExitErrHan
|
||||
debugCommand(),
|
||||
previewCommand(),
|
||||
eefleetctl.UpdatesCommand(),
|
||||
hostsCommand(),
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
82
cmd/fleetctl/hosts.go
Normal file
82
cmd/fleetctl/hosts.go
Normal file
@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
hostsFlagName = "hosts"
|
||||
labelFlagName = "label"
|
||||
statusFlagName = "status"
|
||||
searchQueryFlagName = "search_query"
|
||||
)
|
||||
|
||||
func hostsCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "hosts",
|
||||
Usage: "Manage Fleet hosts",
|
||||
Subcommands: []*cli.Command{
|
||||
transferCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func transferCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "transfer",
|
||||
Usage: "Transfer one or more hosts to a team",
|
||||
UsageText: `This command will gather the set of hosts specified and transfer them to the team.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: teamFlagName,
|
||||
Usage: "Team name hosts will be transferred to",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: hostsFlagName,
|
||||
Usage: "Comma separated hostnames to transfer",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: labelFlagName,
|
||||
Usage: "Label name to transfer",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: statusFlagName,
|
||||
Usage: "Status to use when filtering hosts",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: searchQueryFlagName,
|
||||
Usage: "A search query that returns matching hostnames to be transferred",
|
||||
},
|
||||
configFlag(),
|
||||
contextFlag(),
|
||||
yamlFlag(),
|
||||
debugFlag(),
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
client, err := clientFromCLI(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
team := c.String(teamFlagName)
|
||||
hosts := c.StringSlice(hostsFlagName)
|
||||
label := c.String(labelFlagName)
|
||||
status := c.String(statusFlagName)
|
||||
searchQuery := c.String(searchQueryFlagName)
|
||||
|
||||
if hosts != nil {
|
||||
if label != "" || searchQuery != "" || status != "" {
|
||||
return errors.New("--hosts cannot be used along side any other flag")
|
||||
}
|
||||
} else {
|
||||
if label == "" && searchQuery == "" && status == "" {
|
||||
return errors.New("You need to define either --hosts, or one or more of --label, --status, --search_query")
|
||||
}
|
||||
}
|
||||
|
||||
return client.TransferHosts(hosts, label, status, searchQuery, team)
|
||||
},
|
||||
}
|
||||
}
|
153
cmd/fleetctl/hosts_test.go
Normal file
153
cmd/fleetctl/hosts_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHostTransferFlagChecks(t *testing.T) {
|
||||
server, _ := runServerWithMockedDS(t)
|
||||
defer server.Close()
|
||||
|
||||
runAppCheckErr(t,
|
||||
[]string{"hosts", "transfer", "--team", "team1", "--hosts", "host1", "--label", "AAA"},
|
||||
"--hosts cannot be used along side any other flag",
|
||||
)
|
||||
runAppCheckErr(t,
|
||||
[]string{"hosts", "transfer", "--team", "team1"},
|
||||
"You need to define either --hosts, or one or more of --label, --status, --search_query",
|
||||
)
|
||||
}
|
||||
|
||||
func TestHostsTransferByHosts(t *testing.T) {
|
||||
server, ds := runServerWithMockedDS(t)
|
||||
defer server.Close()
|
||||
|
||||
ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
return &fleet.Host{ID: 42}, nil
|
||||
}
|
||||
|
||||
ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
|
||||
require.Equal(t, "team1", name)
|
||||
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
|
||||
require.NotNil(t, teamID)
|
||||
require.Equal(t, uint(99), *teamID)
|
||||
require.Equal(t, []uint{42}, hostIDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--hosts", "host1"}))
|
||||
}
|
||||
|
||||
func TestHostsTransferByLabel(t *testing.T) {
|
||||
server, ds := runServerWithMockedDS(t)
|
||||
defer server.Close()
|
||||
|
||||
ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
return &fleet.Host{ID: 42}, nil
|
||||
}
|
||||
|
||||
ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
|
||||
require.Equal(t, "team1", name)
|
||||
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.LabelIDsByNameFunc = func(labels []string) ([]uint, error) {
|
||||
require.Equal(t, []string{"label1"}, labels)
|
||||
return []uint{uint(11)}, nil
|
||||
}
|
||||
|
||||
ds.ListHostsInLabelFunc = func(filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||
require.Equal(t, fleet.HostStatus(""), opt.StatusFilter)
|
||||
require.Equal(t, uint(11), lid)
|
||||
return []*fleet.Host{{ID: 32}, {ID: 12}}, nil
|
||||
}
|
||||
|
||||
ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
|
||||
require.NotNil(t, teamID)
|
||||
require.Equal(t, uint(99), *teamID)
|
||||
require.Equal(t, []uint{32, 12}, hostIDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
assert.Equal(t, "", runAppForTest(t, []string{"hosts", "transfer", "--team", "team1", "--label", "label1"}))
|
||||
}
|
||||
|
||||
func TestHostsTransferByStatus(t *testing.T) {
|
||||
server, ds := runServerWithMockedDS(t)
|
||||
defer server.Close()
|
||||
|
||||
ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
return &fleet.Host{ID: 42}, nil
|
||||
}
|
||||
|
||||
ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
|
||||
require.Equal(t, "team1", name)
|
||||
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.LabelIDsByNameFunc = func(labels []string) ([]uint, error) {
|
||||
require.Equal(t, []string{"label1"}, labels)
|
||||
return []uint{uint(11)}, nil
|
||||
}
|
||||
|
||||
ds.ListHostsFunc = func(filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||
require.Equal(t, fleet.StatusOnline, opt.StatusFilter)
|
||||
return []*fleet.Host{{ID: 32}, {ID: 12}}, nil
|
||||
}
|
||||
|
||||
ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
|
||||
require.NotNil(t, teamID)
|
||||
require.Equal(t, uint(99), *teamID)
|
||||
require.Equal(t, []uint{32, 12}, hostIDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
assert.Equal(t, "", runAppForTest(t,
|
||||
[]string{"hosts", "transfer", "--team", "team1", "--status", "online"}))
|
||||
}
|
||||
|
||||
func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
|
||||
server, ds := runServerWithMockedDS(t)
|
||||
defer server.Close()
|
||||
|
||||
ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) {
|
||||
require.Equal(t, "host1", identifier)
|
||||
return &fleet.Host{ID: 42}, nil
|
||||
}
|
||||
|
||||
ds.TeamByNameFunc = func(name string) (*fleet.Team, error) {
|
||||
require.Equal(t, "team1", name)
|
||||
return &fleet.Team{ID: 99, Name: "team1"}, nil
|
||||
}
|
||||
|
||||
ds.LabelIDsByNameFunc = func(labels []string) ([]uint, error) {
|
||||
require.Equal(t, []string{"label1"}, labels)
|
||||
return []uint{uint(11)}, nil
|
||||
}
|
||||
|
||||
ds.ListHostsFunc = func(filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||
require.Equal(t, fleet.StatusOnline, opt.StatusFilter)
|
||||
require.Equal(t, "somequery", opt.MatchQuery)
|
||||
return []*fleet.Host{{ID: 32}, {ID: 12}}, nil
|
||||
}
|
||||
|
||||
ds.AddHostsToTeamFunc = func(teamID *uint, hostIDs []uint) error {
|
||||
require.NotNil(t, teamID)
|
||||
require.Equal(t, uint(99), *teamID)
|
||||
require.Equal(t, []uint{32, 12}, hostIDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
assert.Equal(t, "", runAppForTest(t,
|
||||
[]string{"hosts", "transfer", "--team", "team1", "--status", "online", "--search_query", "somequery"}))
|
||||
}
|
@ -50,6 +50,19 @@ func runServerWithMockedDS(t *testing.T, opts ...service.TestServerOpts) (*httpt
|
||||
}
|
||||
|
||||
func runAppForTest(t *testing.T, args []string) string {
|
||||
w, exitErr, err := runAppNoChecks(args)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, exitErr)
|
||||
return w.String()
|
||||
}
|
||||
|
||||
func runAppCheckErr(t *testing.T, args []string, errorMsg string) string {
|
||||
w, _, err := runAppNoChecks(args)
|
||||
require.Equal(t, errorMsg, err.Error())
|
||||
return w.String()
|
||||
}
|
||||
|
||||
func runAppNoChecks(args []string) (*bytes.Buffer, error, error) {
|
||||
w := new(bytes.Buffer)
|
||||
r, _, _ := os.Pipe()
|
||||
var exitErr error
|
||||
@ -57,7 +70,5 @@ func runAppForTest(t *testing.T, args []string) string {
|
||||
exitErr = err
|
||||
})
|
||||
err := app.Run(append([]string{""}, args...))
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, exitErr)
|
||||
return w.String()
|
||||
return w, exitErr, err
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ func userCommand() *cli.Command {
|
||||
Usage: "Manage Fleet users",
|
||||
Subcommands: []*cli.Command{
|
||||
createUserCommand(),
|
||||
deleteUserCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -171,3 +172,31 @@ func createUserCommand() *cli.Command {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func deleteUserCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete a user",
|
||||
UsageText: `This command will delete a user specified by their email in Fleet.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: emailFlagName,
|
||||
Usage: "Email for user (required)",
|
||||
Required: true,
|
||||
},
|
||||
configFlag(),
|
||||
contextFlag(),
|
||||
yamlFlag(),
|
||||
debugFlag(),
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
client, err := clientFromCLI(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
email := c.String(emailFlagName)
|
||||
return client.DeleteUser(email)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
31
cmd/fleetctl/users_test.go
Normal file
31
cmd/fleetctl/users_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserDelete(t *testing.T) {
|
||||
server, ds := runServerWithMockedDS(t)
|
||||
defer server.Close()
|
||||
|
||||
ds.UserByEmailFunc = func(email string) (*fleet.User, error) {
|
||||
return &fleet.User{
|
||||
ID: 42,
|
||||
Name: "test1",
|
||||
Email: "user1@test.com",
|
||||
}, nil
|
||||
}
|
||||
|
||||
deletedUser := uint(0)
|
||||
|
||||
ds.DeleteUserFunc = func(id uint) error {
|
||||
deletedUser = id
|
||||
return nil
|
||||
}
|
||||
|
||||
assert.Equal(t, "", runAppForTest(t, []string{"user", "delete", "--email", "user1@test.com"}))
|
||||
assert.Equal(t, uint(42), deletedUser)
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
- [Fleet configuration](#fleet-configuration)
|
||||
- [File carving](#file-carving)
|
||||
- [Teams](#teams)
|
||||
- [Translator](#translator)
|
||||
|
||||
## Overview
|
||||
|
||||
@ -5273,3 +5274,91 @@ _Available in Fleet Basic_
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Translator
|
||||
|
||||
### Translate IDs
|
||||
|
||||
|
||||
`POST /api/v1/fleet/translate`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| --------------- | ------- | ----- | ---------------------------------------- |
|
||||
| list | array | body | **Required** list of items to translate. |
|
||||
|
||||
#### Example
|
||||
|
||||
`POST /api/v1/fleet/translate`
|
||||
|
||||
##### Request body
|
||||
|
||||
```
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"type": "user",
|
||||
"payload": {
|
||||
"identifier": "some@email.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"payload": {
|
||||
"identifier": "labelA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "team",
|
||||
"payload": {
|
||||
"identifier": "team1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "host",
|
||||
"payload": {
|
||||
"identifier": "host-ABC"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"type": "user",
|
||||
"payload": {
|
||||
"identifier": "some@email.com",
|
||||
"id": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"payload": {
|
||||
"identifier": "labelA",
|
||||
"id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "team",
|
||||
"payload": {
|
||||
"identifier": "team1",
|
||||
"id": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "host",
|
||||
"payload": {
|
||||
"identifier": "host-ABC",
|
||||
"id": 45
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
@ -213,6 +213,8 @@ const (
|
||||
ErrNoRoleNeeded = 1
|
||||
// ErrNoOneAdminNeeded is the error number when all admins are about to be removed
|
||||
ErrNoOneAdminNeeded = 2
|
||||
//ErrNoUnknownTranslate is returned when an item type in the translate payload is unknown
|
||||
ErrNoUnknownTranslate = 3
|
||||
)
|
||||
|
||||
// NewError returns a fleet error with the code and message specified
|
||||
|
@ -21,4 +21,5 @@ type Service interface {
|
||||
ActivitiesService
|
||||
UserRolesService
|
||||
GlobalScheduleService
|
||||
TranslatorService
|
||||
}
|
||||
|
26
server/fleet/translator.go
Normal file
26
server/fleet/translator.go
Normal file
@ -0,0 +1,26 @@
|
||||
package fleet
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const (
|
||||
TranslatorTypeUserEmail = "user"
|
||||
TranslatorTypeLabel = "label"
|
||||
TranslatorTypeTeam = "team"
|
||||
TranslatorTypeHost = "host"
|
||||
)
|
||||
|
||||
type TranslatePayload struct {
|
||||
Type string `json:"type"`
|
||||
Payload StringIdentifierToIDPayload `json:"payload"`
|
||||
}
|
||||
|
||||
type StringIdentifierToIDPayload struct {
|
||||
Identifier string `json:"identifier"`
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
|
||||
type TranslatorService interface {
|
||||
Translate(ctx context.Context, payloads []TranslatePayload) ([]TranslatePayload, error)
|
||||
}
|
@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -97,3 +99,90 @@ func (c *Client) DeleteHost(id uint) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) translateTransferHostsToIDs(hosts []string, label string, team string) ([]uint, uint, uint, error) {
|
||||
verb, path := "POST", "/api/v1/fleet/translate"
|
||||
var responseBody translatorResponse
|
||||
|
||||
var translatePayloads []fleet.TranslatePayload
|
||||
for _, host := range hosts {
|
||||
translatedPayload, err := encodeTranslatedPayload(fleet.TranslatorTypeHost, host)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
translatePayloads = append(translatePayloads, translatedPayload)
|
||||
}
|
||||
|
||||
if label != "" {
|
||||
translatedPayload, err := encodeTranslatedPayload(fleet.TranslatorTypeLabel, label)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
translatePayloads = append(translatePayloads, translatedPayload)
|
||||
}
|
||||
|
||||
translatedPayload, err := encodeTranslatedPayload(fleet.TranslatorTypeTeam, team)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
translatePayloads = append(translatePayloads, translatedPayload)
|
||||
|
||||
params := translatorRequest{List: translatePayloads}
|
||||
|
||||
err = c.authenticatedRequest(¶ms, verb, path, &responseBody)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
var hostIDs []uint
|
||||
var labelID uint
|
||||
var teamID uint
|
||||
|
||||
for _, payload := range responseBody.List {
|
||||
switch payload.Type {
|
||||
case fleet.TranslatorTypeLabel:
|
||||
labelID = payload.Payload.ID
|
||||
case fleet.TranslatorTypeTeam:
|
||||
teamID = payload.Payload.ID
|
||||
case fleet.TranslatorTypeHost:
|
||||
hostIDs = append(hostIDs, payload.Payload.ID)
|
||||
}
|
||||
}
|
||||
return hostIDs, labelID, teamID, nil
|
||||
}
|
||||
|
||||
func encodeTranslatedPayload(translatorType string, identifier string) (fleet.TranslatePayload, error) {
|
||||
translatedPayload := fleet.TranslatePayload{
|
||||
Type: translatorType,
|
||||
Payload: fleet.StringIdentifierToIDPayload{Identifier: identifier},
|
||||
}
|
||||
return translatedPayload, nil
|
||||
}
|
||||
|
||||
func (c *Client) TransferHosts(hosts []string, label string, status, searchQuery string, team string) error {
|
||||
hostIDs, labelID, teamID, err := c.translateTransferHostsToIDs(hosts, label, team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(hosts) != 0 {
|
||||
verb, path := "POST", "/api/v1/fleet/hosts/transfer"
|
||||
var responseBody addHostsToTeamResponse
|
||||
params := addHostsToTeamRequest{TeamID: ptr.Uint(teamID), HostIDs: hostIDs}
|
||||
return c.authenticatedRequest(params, verb, path, &responseBody)
|
||||
}
|
||||
|
||||
var labelIDPtr *uint
|
||||
if label != "" {
|
||||
labelIDPtr = &labelID
|
||||
}
|
||||
|
||||
verb, path := "POST", "/api/v1/fleet/hosts/transfer/filter"
|
||||
var responseBody addHostsToTeamByFilterResponse
|
||||
params := addHostsToTeamByFilterRequest{TeamID: ptr.Uint(teamID), Filters: struct {
|
||||
MatchQuery string `json:"query"`
|
||||
Status fleet.HostStatus `json:"status"`
|
||||
LabelID *uint `json:"label_id"`
|
||||
}{MatchQuery: searchQuery, Status: fleet.HostStatus(status), LabelID: labelIDPtr}}
|
||||
return c.authenticatedRequest(params, verb, path, &responseBody)
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CreateUser creates a new user, skipping the invitation process.
|
||||
@ -31,3 +34,36 @@ func (c *Client) ApplyUsersRoleSecretSpec(spec *fleet.UsersRoleSpec) error {
|
||||
var responseBody applyUserRoleSpecsResponse
|
||||
return c.authenticatedRequest(req, verb, path, &responseBody)
|
||||
}
|
||||
|
||||
func (c *Client) userIdFromEmail(email string) (uint, error) {
|
||||
verb, path := "POST", "/api/v1/fleet/translate"
|
||||
var responseBody translatorResponse
|
||||
|
||||
params := translatorRequest{List: []fleet.TranslatePayload{
|
||||
{
|
||||
Type: fleet.TranslatorTypeUserEmail,
|
||||
Payload: fleet.StringIdentifierToIDPayload{Identifier: email},
|
||||
},
|
||||
}}
|
||||
|
||||
err := c.authenticatedRequest(¶ms, verb, path, &responseBody)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(responseBody.List) != 1 {
|
||||
return 0, errors.New("Expected 1 item translated, got none")
|
||||
}
|
||||
return responseBody.List[0].Payload.ID, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes the user specified by the email
|
||||
func (c *Client) DeleteUser(email string) error {
|
||||
userID, err := c.userIdFromEmail(email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verb, path := "DELETE", fmt.Sprintf("/api/v1/fleet/users/%d", userID)
|
||||
var responseBody deleteUserResponse
|
||||
return c.authenticatedRequest(nil, verb, path, &responseBody)
|
||||
}
|
||||
|
@ -668,6 +668,7 @@ func attachFleetAPIRoutes(r *mux.Router, h *fleetHandlers) {
|
||||
|
||||
func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kithttp.ServerOption) {
|
||||
handle("POST", "/api/v1/fleet/users/roles/spec", makeApplyUserRoleSpecsEndpoint(svc, opts), "apply_user_roles_spec", r)
|
||||
handle("POST", "/api/v1/fleet/translate", makeTranslatorEndpoint(svc, opts), "translator", r)
|
||||
handle("POST", "/api/v1/fleet/spec/teams", makeApplyTeamSpecsEndpoint(svc, opts), "apply_team_specs", r)
|
||||
}
|
||||
|
||||
|
@ -415,3 +415,25 @@ func TestTeamSpecs(t *testing.T) {
|
||||
require.Len(t, team.Secrets, 1)
|
||||
assert.Equal(t, "ABC", team.Secrets[0].Secret)
|
||||
}
|
||||
|
||||
func TestTranslator(t *testing.T) {
|
||||
ds := mysql.CreateMySQLDS(t)
|
||||
defer ds.Close()
|
||||
|
||||
users, server := RunServerForTestsWithDS(t, ds)
|
||||
token := getTestAdminToken(t, server)
|
||||
|
||||
payload := translatorResponse{}
|
||||
params := translatorRequest{List: []fleet.TranslatePayload{
|
||||
{
|
||||
Type: fleet.TranslatorTypeUserEmail,
|
||||
Payload: fleet.StringIdentifierToIDPayload{Identifier: "admin1@example.com"},
|
||||
},
|
||||
}}
|
||||
doJSONReq(t, ¶ms, "POST", server, "/api/v1/fleet/translate", token, http.StatusOK, &payload)
|
||||
|
||||
require.Nil(t, payload.Err)
|
||||
assert.Len(t, payload.List, 1)
|
||||
|
||||
assert.Equal(t, users[payload.List[0].Payload.Identifier].ID, payload.List[0].Payload.ID)
|
||||
}
|
||||
|
125
server/service/translator.go
Normal file
125
server/service/translator.go
Normal file
@ -0,0 +1,125 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
)
|
||||
|
||||
type translatorRequest struct {
|
||||
List []fleet.TranslatePayload `json:"list"`
|
||||
}
|
||||
|
||||
type translatorResponse struct {
|
||||
List []fleet.TranslatePayload `json:"list"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r translatorResponse) error() error { return r.Err }
|
||||
|
||||
func makeTranslatorEndpoint(svc fleet.Service, opts []kithttp.ServerOption) http.Handler {
|
||||
return newServer(
|
||||
makeAuthenticatedServiceEndpoint(svc, translatorEndpoint),
|
||||
makeDecoderForType(translatorRequest{}),
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
func translatorEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
||||
req := request.(*translatorRequest)
|
||||
resp, err := svc.Translate(ctx, req.List)
|
||||
if err != nil {
|
||||
return translatorResponse{Err: err}, nil
|
||||
}
|
||||
return translatorResponse{List: resp}, nil
|
||||
}
|
||||
|
||||
type translateFunc func(ds fleet.Datastore, identifier string) (uint, error)
|
||||
|
||||
func translateEmailToUserID(ds fleet.Datastore, identifier string) (uint, error) {
|
||||
user, err := ds.UserByEmail(identifier)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
func translateLabelToID(ds fleet.Datastore, identifier string) (uint, error) {
|
||||
labelIDs, err := ds.LabelIDsByName([]string{identifier})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return labelIDs[0], nil
|
||||
}
|
||||
|
||||
func translateTeamToID(ds fleet.Datastore, identifier string) (uint, error) {
|
||||
team, err := ds.TeamByName(identifier)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return team.ID, nil
|
||||
}
|
||||
|
||||
func translateHostToID(ds fleet.Datastore, identifier string) (uint, error) {
|
||||
host, err := ds.HostByIdentifier(identifier)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return host.ID, nil
|
||||
}
|
||||
|
||||
func (svc Service) Translate(ctx context.Context, payloads []fleet.TranslatePayload) ([]fleet.TranslatePayload, error) {
|
||||
var finalPayload []fleet.TranslatePayload
|
||||
|
||||
for _, payload := range payloads {
|
||||
var translateFunc translateFunc
|
||||
|
||||
switch payload.Type {
|
||||
case fleet.TranslatorTypeUserEmail:
|
||||
if err := svc.authz.Authorize(ctx, &fleet.User{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translateFunc = translateEmailToUserID
|
||||
case fleet.TranslatorTypeLabel:
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Label{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translateFunc = translateLabelToID
|
||||
case fleet.TranslatorTypeTeam:
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translateFunc = translateTeamToID
|
||||
case fleet.TranslatorTypeHost:
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translateFunc = translateHostToID
|
||||
default:
|
||||
return nil, fleet.NewErrorf(fleet.ErrNoUnknownTranslate, "Type %s is unknown.", payload.Type)
|
||||
}
|
||||
|
||||
id, err := translateFunc(svc.ds, payload.Payload.Identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload.Payload.ID = id
|
||||
finalPayload = append(finalPayload, fleet.TranslatePayload{
|
||||
Type: payload.Type,
|
||||
Payload: payload.Payload,
|
||||
})
|
||||
}
|
||||
|
||||
return finalPayload, nil
|
||||
}
|
||||
|
||||
func (mw loggingMiddleware) Translate(ctx context.Context, payloads []fleet.TranslatePayload) ([]fleet.TranslatePayload, error) {
|
||||
var err error
|
||||
defer func(begin time.Time) {
|
||||
_ = mw.loggerDebug(err).Log("method", "Translate", "err", err, "took", time.Since(begin))
|
||||
}(time.Now())
|
||||
return mw.Service.Translate(ctx, payloads)
|
||||
}
|
Loading…
Reference in New Issue
Block a user