Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -938,10 +938,14 @@ func (h *Handler) bindDefaultEndpoints() {

// GET Machine ID bot by name
h.GET("/webapi/sites/:site/machine-id/bot/:name", h.WithClusterAuth(h.getBot))
// GET Machine ID bots
h.GET("/webapi/sites/:site/machine-id/bot", h.WithClusterAuth(h.listBots))
// Create Machine ID bots
h.POST("/webapi/sites/:site/machine-id/bot", h.WithClusterAuth(h.createBot))
// Create bot join tokens
h.POST("/webapi/sites/:site/machine-id/token", h.WithClusterAuth(h.createBotJoinToken))
// Delete Machine ID bot
h.DELETE("/webapi/sites/:site/machine-id/bot/:name", h.WithClusterAuth(h.deleteBot))
}

// GetProxyClient returns authenticated auth server client
Expand Down
54 changes: 54 additions & 0 deletions lib/web/machineid.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ const (
webUIFlowBotGitHubActionsSSH = "github-actions-ssh"
)

type ListBotsResponse struct {
// Items is a list of resources retrieved.
Items []*machineidv1.Bot `json:"items"`
}

type CreateBotRequest struct {
// BotName is the name of the bot
BotName string `json:"botName"`
Expand All @@ -48,6 +53,36 @@ type CreateBotRequest struct {
Traits []*machineidv1.Trait `json:"traits"`
}

// listBots returns a list of bots for a given cluster site. It does not leverage pagination from the UI. Due to the
// nature of the bot:user relationship, pagination is not yet supported. This endpoint will return all bots.
func (h *Handler) listBots(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}

var items []*machineidv1.Bot
for pageToken := ""; ; {
bots, err := clt.BotServiceClient().ListBots(r.Context(), &machineidv1.ListBotsRequest{
PageSize: int32(1000),
PageToken: pageToken,
})
// todo (michellescripts) consider returning partial results
if err != nil {
return nil, trace.Wrap(err, "error getting bots")
}
items = append(items, bots.Bots...)
pageToken = bots.NextPageToken
if pageToken == "" {
break
}
}

return ListBotsResponse{
Items: items,
}, nil
}

// createBot creates a bot
func (h *Handler) createBot(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
var req *CreateBotRequest
Expand Down Expand Up @@ -80,6 +115,25 @@ func (h *Handler) createBot(w http.ResponseWriter, r *http.Request, p httprouter
return OK(), nil
}

func (h *Handler) deleteBot(_ http.ResponseWriter, r *http.Request, params httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}

name := params.ByName("name")
if name == "" {
return nil, trace.BadParameter("missing bot name")
}

_, err = clt.BotServiceClient().DeleteBot(r.Context(), &machineidv1.DeleteBotRequest{BotName: name})
if err != nil {
return nil, trace.Wrap(err, "error deleting bot")
}

return OK(), nil
}

// CreateBotJoinTokenRequest represents a client request to
// create a bot join token
type CreateBotJoinTokenRequest struct {
Expand Down
123 changes: 123 additions & 0 deletions lib/web/machineid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"slices"
"strconv"
"testing"

"github.com/gravitational/trace"
Expand All @@ -34,6 +36,68 @@ import (
"github.com/gravitational/teleport/lib/web/ui"
)

func TestListBots(t *testing.T) {
ctx := context.Background()
env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "admin", []types.Role{services.NewPresetEditorRole()})
clusterName := env.server.ClusterName()
endpoint := pack.clt.Endpoint(
"webapi",
"sites",
clusterName,
"machine-id",
"bot",
)

created := 5
n := 0
for n < created {
n += 1
_, err := pack.clt.PostJSON(ctx, endpoint, CreateBotRequest{
BotName: "test-bot-" + strconv.Itoa(n),
Roles: []string{""},
})
require.NoError(t, err)
}

response, err := pack.clt.Get(ctx, endpoint, url.Values{
"page_token": []string{""}, // default to the start
"page_size": []string{"2"}, // is ignored
})
require.NoError(t, err)

var bots ListBotsResponse
require.NoError(t, json.Unmarshal(response.Bytes(), &bots), "invalid response received")
assert.Equal(t, http.StatusOK, response.Code(), "unexpected status code getting connectors")

assert.Len(t, bots.Items, created)
}

func TestListBots_UnauthenticatedError(t *testing.T) {
ctx := context.Background()
s := newWebSuite(t)
env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "admin", []types.Role{services.NewPresetEditorRole()})
clusterName := env.server.ClusterName()
endpoint := pack.clt.Endpoint(
"webapi",
"sites",
clusterName,
"machine-id",
"bot",
)

publicClt := s.client(t)
_, err := publicClt.Get(ctx, endpoint, url.Values{
"page_token": []string{""},
"page_size": []string{""},
})
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err))
}

func TestCreateBot(t *testing.T) {
s := newWebSuite(t)
env := newWebPack(t, 1)
Expand Down Expand Up @@ -139,6 +203,65 @@ func TestCreateBotJoinToken(t *testing.T) {
require.Error(t, err)
}

func TestDeleteBot_UnauthenticatedError(t *testing.T) {
ctx := context.Background()
s := newWebSuite(t)
env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "admin", []types.Role{services.NewPresetEditorRole()})
clusterName := env.server.ClusterName()
endpoint := pack.clt.Endpoint(
"webapi",
"sites",
clusterName,
"machine-id",
"bot",
"testname",
)

publicClt := s.client(t)
_, err := publicClt.Delete(ctx, endpoint)
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err))
}

func TestDeleteBot(t *testing.T) {
botName := "bot-bravo"

ctx := context.Background()
env := newWebPack(t, 1)
proxy := env.proxies[0]
pack := proxy.authPack(t, "admin", []types.Role{services.NewPresetEditorRole()})
clusterName := env.server.ClusterName()
endpoint := pack.clt.Endpoint(
"webapi",
"sites",
clusterName,
"machine-id",
"bot",
)

// create bot to delete
_, err := pack.clt.PostJSON(ctx, endpoint, CreateBotRequest{
BotName: botName,
Roles: []string{""},
})
require.NoError(t, err)

endpoint = pack.clt.Endpoint(
"webapi",
"sites",
clusterName,
"machine-id",
"bot",
botName,
)

resp, err := pack.clt.Delete(ctx, endpoint)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.Code(), "unexpected status code getting connectors")
}

func TestGetBotByName(t *testing.T) {
ctx := context.Background()
env := newWebPack(t, 1)
Expand Down
78 changes: 78 additions & 0 deletions web/packages/design/src/Icon/Icons/Bots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
Comment thread
michellescripts marked this conversation as resolved.
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/* MIT License

Copyright (c) 2020 Phosphor Icons

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

*/

import React from 'react';

import { Icon, IconProps } from '../Icon';

/*

THIS FILE IS GENERATED. DO NOT EDIT.

*/

export function Bots({ size = 24, color, ...otherProps }: IconProps) {
return (
<Icon size={size} color={color} className="icon icon-users" {...otherProps}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.625 18H15.375C16.8247 18 18 16.8247 18 15.375C18 13.9253 16.8247 12.75 15.375 12.75H8.625C7.17525 12.75 6 13.9253 6 15.375C6 16.8247 7.17525 18 8.625 18ZM8.625 14.25C8.00368 14.25 7.5 14.7537 7.5 15.375C7.5 15.9963 8.00368 16.5 8.625 16.5H9.75V14.25H8.625ZM14.25 16.5H15.375C15.9963 16.5 16.5 15.9963 16.5 15.375C16.5 14.7537 15.9963 14.25 15.375 14.25H14.25V16.5ZM11.25 14.25V16.5H12.75V14.25H11.25Z"
fill="black"
/>
<path
d="M7.875 11.0625C8.39277 11.0625 8.8125 10.6428 8.8125 10.125C8.8125 9.60723 8.39277 9.1875 7.875 9.1875C7.35723 9.1875 6.9375 9.60723 6.9375 10.125C6.9375 10.6428 7.35723 11.0625 7.875 11.0625Z"
fill="black"
/>
<path
d="M17.0625 10.125C17.0625 10.6428 16.6428 11.0625 16.125 11.0625C15.6072 11.0625 15.1875 10.6428 15.1875 10.125C15.1875 9.60723 15.6072 9.1875 16.125 9.1875C16.6428 9.1875 17.0625 9.60723 17.0625 10.125Z"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.75 1.5C12.75 1.08579 12.4142 0.75 12 0.75C11.5858 0.75 11.25 1.08579 11.25 1.5V4.5H5.25C3.59315 4.5 2.25 5.84315 2.25 7.5V18C2.25 19.6569 3.59315 21 5.25 21H18.75C20.4069 21 21.75 19.6569 21.75 18V7.5C21.75 5.84315 20.4069 4.5 18.75 4.5H12.75V1.5ZM5.25 6C4.42157 6 3.75 6.67157 3.75 7.5V18C3.75 18.8284 4.42157 19.5 5.25 19.5H18.75C19.5784 19.5 20.25 18.8284 20.25 18V7.5C20.25 6.67157 19.5784 6 18.75 6H5.25Z"
fill="black"
/>
</Icon>
);
}
1 change: 1 addition & 0 deletions web/packages/design/src/Icon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export { ListThin } from './Icons/ListThin';
export { ListView } from './Icons/ListView';
export { Lock } from './Icons/Lock';
export { Logout } from './Icons/Logout';
export { Bots } from './Icons/Bots';
export { Magnifier } from './Icons/Magnifier';
export { MagnifyingMinus } from './Icons/MagnifyingMinus';
export { MagnifyingPlus } from './Icons/MagnifyingPlus';
Expand Down
Loading