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
6 changes: 6 additions & 0 deletions api/types/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ type ProvisionToken interface {
GetRoles() SystemRoles
// SetRoles sets teleport roles
SetRoles(SystemRoles)
// SetLabels sets the tokens labels

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment doesn't add value, consider skipping.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even when not adding value, it seems we always add a godoc comment to any public field.

SetLabels(map[string]string)
// GetAllowRules returns the list of allow rules
GetAllowRules() []*TokenRule
// SetAllowRules sets the allow rules
Expand Down Expand Up @@ -351,6 +353,10 @@ func (p *ProvisionTokenV2) SetRoles(r SystemRoles) {
p.Spec.Roles = r
}

func (p *ProvisionTokenV2) SetLabels(l map[string]string) {
p.Metadata.Labels = l
}

// GetAllowRules returns the list of allow rules
func (p *ProvisionTokenV2) GetAllowRules() []*TokenRule {
return p.Spec.Allow
Expand Down
9 changes: 9 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ const (
// assistantLimiterCapacity is the total capacity of the token bucket for the assistant rate limiter.
// The bucket starts full, prefilled for a week.
assistantLimiterCapacity = assistantTokensPerHour * 24 * 7
// webUIFlowLabelKey is a label that may be added to resources
// created via the web UI, indicating which flow the resource was created on.
// This label is used for enhancing UX in the web app, by showing icons related,
// to the workflow it was added, or providing unique features to those resources.
// Example values:
// - github-actions-ssh: indicates that the resource was added via the Bot GitHub Actions SSH flow
webUIFlowLabelKey = "teleport.internal/ui-flow"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither the comment, nor the name actually explains what is the general purpose of this label, and thus, I'm confused. What does "web UI flow" mean in this context? So far, there's just one value, could you give a speculative example of what other values may be? What decision will we make using this label's value?

@mcbattirola mcbattirola Jan 31, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial purpose of this is showing icons in the Bot listing pages based on the workflow used to create the bot, and providing a special editing page to those bots.

I've updated the comment daff149:

// webUIFlowLabelKey is a label that may be added to resources
// created via the web UI, indicating which flow the resource was created on.
// This label is used for enhancing UX in the web app, by showing icons related,
// to the workflow it was added, or providing unique features to those resources.
// Example values:
// - github-actions-ssh: indicates that the resource was added via the Bot GitHub Actions SSH flow

Please let me know if we should add more context.

)

// healthCheckAppServerFunc defines a function used to perform a health check
Expand Down Expand Up @@ -933,6 +940,8 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET("/webapi/sites/:site/machine-id/bot/:name", h.WithClusterAuth(h.getBot))
// 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))
}

// GetProxyClient returns authenticated auth server client
Expand Down
67 changes: 67 additions & 0 deletions lib/web/machineid.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,24 @@ package web

import (
"net/http"
"time"

"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"

headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/reversetunnelclient"
)

const (
// webUIFlowBotGitHubActionsSSH is the value of the webUIFlowLabelKey
// added to a resource created via the Bot GitHub Actions web UI flow.
webUIFlowBotGitHubActionsSSH = "github-actions-ssh"
)

type CreateBotRequest struct {
// BotName is the name of the bot
BotName string `json:"botName"`
Expand Down Expand Up @@ -55,6 +63,9 @@ func (h *Handler) createBot(w http.ResponseWriter, r *http.Request, p httprouter
Bot: &machineidv1.Bot{
Metadata: &headerv1.Metadata{
Name: req.BotName,
Labels: map[string]string{
webUIFlowLabelKey: webUIFlowBotGitHubActionsSSH,
},
},
Spec: &machineidv1.BotSpec{
Roles: req.Roles,
Expand All @@ -69,6 +80,62 @@ func (h *Handler) createBot(w http.ResponseWriter, r *http.Request, p httprouter
return OK(), nil
}

// CreateBotJoinTokenRequest represents a client request to
// create a bot join token
type CreateBotJoinTokenRequest struct {
// IntegrationName is the name attributed to the bot integration, which
// is used to name the resources created during the UI flow.
IntegrationName string `json:"integrationName"`
// JoinMethod is the joining method required in order to use this token.
JoinMethod types.JoinMethod `json:"joinMethod"`
// GitHub allows the configuration of options specific to the "github" join method.
GitHub *types.ProvisionTokenSpecV2GitHub `json:"gitHub"`
// WebFlowLabel is the value of the label attributed to bots created via the web UI
WebFlowLabel string `json:"webFlowLabel"`
}

// createBotJoinToken creates a bot join token
func (h *Handler) createBotJoinToken(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
var req *CreateBotJoinTokenRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}

if err := types.ValidateJoinMethod(req.JoinMethod); err != nil {
return nil, trace.Wrap(err)
}

clt, err := sctx.GetUserClient(r.Context(), site)
if err != nil {
return nil, trace.Wrap(err)
}

spec := types.ProvisionTokenSpecV2{
Roles: []types.SystemRole{types.RoleBot},
JoinMethod: req.JoinMethod,
GitHub: req.GitHub,
BotName: req.IntegrationName,
}
provisionToken, err := types.NewProvisionTokenFromSpec(req.IntegrationName, time.Time{}, spec)
if err != nil {
return nil, trace.Wrap(err)
}
provisionToken.SetLabels(map[string]string{
webUIFlowLabelKey: req.WebFlowLabel,
})

err = clt.CreateToken(r.Context(), provisionToken)
if err != nil {
return nil, trace.Wrap(err, "error creating join token")
}

return &nodeJoinToken{
ID: provisionToken.GetName(),
Expiry: provisionToken.Expiry(),
Method: provisionToken.GetJoinMethod(),
}, nil
}

// getBot retrieves a bot by name
func (h *Handler) getBot(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
botName := p.ByName("name")
Expand Down
52 changes: 52 additions & 0 deletions lib/web/machineid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,58 @@ func TestCreateBot(t *testing.T) {
require.True(t, trace.IsAccessDenied(err))
}

func TestCreateBotJoinToken(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",
"token",
)

// add github join token
integrationName := "my-app-deploy"
validReq := CreateBotJoinTokenRequest{
IntegrationName: integrationName,
JoinMethod: types.JoinMethodGitHub,
GitHub: &types.ProvisionTokenSpecV2GitHub{
Allow: []*types.ProvisionTokenSpecV2GitHub_Rule{
{
Repository: "gravitational/teleport",
Actor: "actor",
},
},
},
WebFlowLabel: webUIFlowBotGitHubActionsSSH,
}
resp, err := pack.clt.PostJSON(ctx, endpoint, validReq)
require.NoError(t, err)

var result nodeJoinToken
json.Unmarshal(resp.Bytes(), &result)
require.Equal(t, integrationName, result.ID)
require.Equal(t, types.JoinMethod("github"), result.Method)

// invalid join method
invalidJoinMethodReq := validReq
// use a different integration name so it doesn't error for being duplicated
invalidJoinMethodReq.IntegrationName = "invalid-join-method-test"
invalidJoinMethodReq.JoinMethod = "invalid-join-method"
_, err = pack.clt.PostJSON(ctx, endpoint, invalidJoinMethodReq)
require.Error(t, err)

// no integration name
invalidIntegrationNameReq := validReq
invalidIntegrationNameReq.IntegrationName = ""
_, err = pack.clt.PostJSON(ctx, endpoint, invalidIntegrationNameReq)
require.Error(t, err)
}

func TestGetBotByName(t *testing.T) {
ctx := context.Background()
env := newWebPack(t, 1)
Expand Down