mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Add Suggested Labels to Provision Tokens (#15114)
For Teleport Discover, the user will be able to test connecting to a resource right after adding it. The flow should look like this: - User selects the resource type - in this case Server/Nodes - WebUI generates a new Token - WebUI shows the `sudo bash .../<token>/install.sh` to the user - When the user runs this command, and assuming everything works out, the WebUI should be able to receive the Server/Node that was generated from that specific Token. To achieve this, here's a more detailed flow, which this PR implements - User selects the resource type - in this case Server/Nodes - WebUI generates a new Token This generates a Provision Token which contains a RefResourceID WebUI receives back that ID - WebUI shows the `sudo bash .../<token>/install.sh` to the user This generates a script which includes setting the labels as part of the `teleport configure` command: `$ teleport configure ... --labels teleport.internal/resource-id=<refResourceID> ...` - User runs the provided command on the target host - WebUI should be able to query the Servers/Nodes which contain that specific `<resource-id>` and allow the user to connect to it. Demo: 
This commit is contained in:
@@ -429,6 +429,10 @@ const (
|
||||
|
||||
// BotGenerationLabel is a label used to record the certificate generation counter.
|
||||
BotGenerationLabel = "teleport.internal/bot-generation"
|
||||
|
||||
// InternalResourceIDLabel is a label used to store an ID to correlate between two resources
|
||||
// A pratical example of this is to create a correlation between a Node Provision Token and the Node that used that token to join the cluster
|
||||
InternalResourceIDLabel = "teleport.internal/resource-id"
|
||||
)
|
||||
|
||||
// RequestableResourceKinds lists all Teleport resource kinds users can request access to.
|
||||
|
||||
@@ -58,6 +58,10 @@ type ProvisionToken interface {
|
||||
GetJoinMethod() JoinMethod
|
||||
// GetBotName returns the BotName field which must be set for joining bots.
|
||||
GetBotName() string
|
||||
|
||||
// GetSuggestedLabels returns the set of labels that the resource should add when adding itself to the cluster
|
||||
GetSuggestedLabels() Labels
|
||||
|
||||
// V1 returns V1 version of the resource
|
||||
V1() *ProvisionTokenV1
|
||||
// String returns user friendly representation of the resource
|
||||
@@ -250,6 +254,11 @@ func (p *ProvisionTokenV2) SetMetadata(meta Metadata) {
|
||||
p.Metadata = meta
|
||||
}
|
||||
|
||||
// GetSuggestedLabels returns the labels the resource should set when using this token
|
||||
func (p *ProvisionTokenV2) GetSuggestedLabels() Labels {
|
||||
return p.Spec.SuggestedLabels
|
||||
}
|
||||
|
||||
// V1 returns V1 version of the resource
|
||||
func (p *ProvisionTokenV2) V1() *ProvisionTokenV1 {
|
||||
return &ProvisionTokenV1{
|
||||
|
||||
+827
-778
File diff suppressed because it is too large
Load Diff
@@ -828,6 +828,13 @@ message ProvisionTokenSpecV2 {
|
||||
[ (gogoproto.jsontag) = "join_method", (gogoproto.casttype) = "JoinMethod" ];
|
||||
// BotName is the name of the bot this token grants access to, if any
|
||||
string BotName = 5 [ (gogoproto.jsontag) = "bot_name,omitempty" ];
|
||||
// SuggestedLabels is a set of labels that resources should set when using this token to enroll
|
||||
// themselves in the cluster
|
||||
wrappers.LabelValues SuggestedLabels = 6 [
|
||||
(gogoproto.nullable) = false,
|
||||
(gogoproto.jsontag) = "suggested_labels,omitempty",
|
||||
(gogoproto.customtype) = "Labels"
|
||||
];
|
||||
}
|
||||
|
||||
// StaticTokensV2 implements the StaticTokens interface.
|
||||
|
||||
@@ -2171,6 +2171,16 @@ func TestTokenGeneration(t *testing.T) {
|
||||
err = json.Unmarshal(re.Bytes(), &responseToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, responseToken.SuggestedLabels)
|
||||
require.Condition(t, func() (success bool) {
|
||||
for _, uiLabel := range responseToken.SuggestedLabels {
|
||||
if uiLabel.Name == types.InternalResourceIDLabel && uiLabel.Value != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// generated token roles should match the requested ones
|
||||
generatedToken, err := proxy.auth.Auth().GetToken(context.Background(), responseToken.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
+40
-4
@@ -31,14 +31,17 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
apiutils "github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/lib/auth"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/httplib"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/web/scripts"
|
||||
"github.com/gravitational/teleport/lib/web/ui"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
@@ -52,6 +55,8 @@ type nodeJoinToken struct {
|
||||
Expiry time.Time `json:"expiry,omitempty"`
|
||||
// Method is the join method that the token supports
|
||||
Method types.JoinMethod `json:"method"`
|
||||
// SuggestedLabels contains the set of labels we expect the node to set when using this token
|
||||
SuggestedLabels []ui.Label `json:"suggestedLabels,omitempty"`
|
||||
}
|
||||
|
||||
// scriptSettings is used to hold values which are passed into the function that
|
||||
@@ -115,6 +120,20 @@ func (h *Handler) createTokenHandle(w http.ResponseWriter, r *http.Request, para
|
||||
expires = time.Now().UTC().Add(defaults.NodeJoinTokenTTL)
|
||||
}
|
||||
|
||||
// If using the automatic method to add a Node, the `install.sh` will add the token's suggested labels
|
||||
// as part of the initial Labels configuration for that Node
|
||||
// Script install-node.sh:
|
||||
// ...
|
||||
// $ teleport configure ... --labels <suggested_label=value>,<suggested_label=value> ...
|
||||
// ...
|
||||
//
|
||||
// We create an ID and return it as part of the Token, so the UI can use this ID to query the Node that joined using this token
|
||||
// WebUI can then query the resources by this id and answer the question:
|
||||
// - Which Node joined the cluster from this token Y?
|
||||
req.SuggestedLabels = types.Labels{
|
||||
types.InternalResourceIDLabel: apiutils.Strings{uuid.NewString()},
|
||||
}
|
||||
|
||||
provisionToken, err := types.NewProvisionTokenFromSpec(tokenName, expires, req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -125,10 +144,20 @@ func (h *Handler) createTokenHandle(w http.ResponseWriter, r *http.Request, para
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
suggestedLabels := make([]ui.Label, 0, len(req.SuggestedLabels))
|
||||
|
||||
for labelKey, labelValues := range req.SuggestedLabels {
|
||||
suggestedLabels = append(suggestedLabels, ui.Label{
|
||||
Name: labelKey,
|
||||
Value: strings.Join(labelValues, " "),
|
||||
})
|
||||
}
|
||||
|
||||
return &nodeJoinToken{
|
||||
ID: tokenName,
|
||||
Expiry: expires,
|
||||
Method: provisionToken.GetJoinMethod(),
|
||||
ID: tokenName,
|
||||
Expiry: expires,
|
||||
Method: provisionToken.GetJoinMethod(),
|
||||
SuggestedLabels: suggestedLabels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -247,7 +276,7 @@ func getJoinScript(ctx context.Context, settings scriptSettings, m nodeAPIGetter
|
||||
|
||||
// The provided token can be attacker controlled, so we must validate
|
||||
// it with the backend before using it to generate the script.
|
||||
_, err := m.GetToken(ctx, settings.token)
|
||||
token, err := m.GetToken(ctx, settings.token)
|
||||
if err != nil {
|
||||
return "", trace.BadParameter("invalid token")
|
||||
}
|
||||
@@ -278,6 +307,12 @@ func getJoinScript(ctx context.Context, settings scriptSettings, m nodeAPIGetter
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
labelsList := []string{}
|
||||
for labelKey, labelValues := range token.GetSuggestedLabels() {
|
||||
labels := strings.Join(labelValues, " ")
|
||||
labelsList = append(labelsList, fmt.Sprintf("%s=%s", labelKey, labels))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
// If app install mode is requested but parameters are blank for some reason,
|
||||
// we need to return an error.
|
||||
@@ -307,6 +342,7 @@ func getJoinScript(ctx context.Context, settings scriptSettings, m nodeAPIGetter
|
||||
"appName": settings.appName,
|
||||
"appURI": settings.appURI,
|
||||
"joinMethod": settings.joinMethod,
|
||||
"labels": strings.Join(labelsList, ","),
|
||||
})
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
|
||||
@@ -19,6 +19,7 @@ package web
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/fixtures"
|
||||
"github.com/gravitational/trace"
|
||||
@@ -286,6 +288,7 @@ func toHex(s string) string { return hex.EncodeToString([]byte(s)) }
|
||||
func TestGetNodeJoinScript(t *testing.T) {
|
||||
validToken := "f18da1c9f6630a51e8daf121e7451daa"
|
||||
validIAMToken := "valid-iam-token"
|
||||
internalResourceID := "967d38ff-7a61-4f42-bd2d-c61965b44db0"
|
||||
|
||||
m := &mockedNodeAPIGetter{
|
||||
mockGetProxyServers: func() ([]types.Server, error) {
|
||||
@@ -304,6 +307,11 @@ func TestGetNodeJoinScript(t *testing.T) {
|
||||
Metadata: types.Metadata{
|
||||
Name: token,
|
||||
},
|
||||
Spec: types.ProvisionTokenSpecV2{
|
||||
SuggestedLabels: types.Labels{
|
||||
types.InternalResourceIDLabel: utils.Strings{internalResourceID},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, trace.NotFound("token does not exist")
|
||||
@@ -362,6 +370,15 @@ func TestGetNodeJoinScript(t *testing.T) {
|
||||
require.Contains(t, script, "JOIN_METHOD='iam'")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "internal resourceid label",
|
||||
settings: scriptSettings{token: validToken},
|
||||
errAssert: require.NoError,
|
||||
extraAssertions: func(script string) {
|
||||
require.Contains(t, script, "--labels ")
|
||||
require.Contains(t, script, fmt.Sprintf("%s=%s", types.InternalResourceIDLabel, internalResourceID))
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
script, err := getJoinScript(context.Background(), test.settings, m)
|
||||
|
||||
@@ -41,7 +41,13 @@ TARGET_PORT='{{.port}}'
|
||||
JOIN_TOKEN='{{.token}}'
|
||||
JOIN_METHOD='{{.joinMethod}}'
|
||||
JOIN_METHOD_FLAG=""
|
||||
[ ! -z "$JOIN_METHOD" ] && JOIN_METHOD_FLAG="--join-method ${JOIN_METHOD}"
|
||||
[ -n "$JOIN_METHOD" ] && JOIN_METHOD_FLAG="--join-method ${JOIN_METHOD}"
|
||||
|
||||
# inject labels into the configuration
|
||||
LABELS='{{.labels}}'
|
||||
LABELS_FLAG=""
|
||||
[ -n "$LABELS" ] && LABELS_FLAG=(--labels "${LABELS}")
|
||||
|
||||
# When all stanza generators have been updated to use the new
|
||||
# `teleport <service> configure` commands CA_PIN_HASHES can be removed along
|
||||
# with the script passing it in in `join_tokens.go`.
|
||||
@@ -439,6 +445,7 @@ install_teleport_node_config() {
|
||||
${JOIN_METHOD_FLAG} \
|
||||
--ca-pin ${CA_PINS} \
|
||||
--auth-server ${TARGET_HOSTNAME}:${TARGET_PORT} \
|
||||
"${LABELS_FLAG[@]}" \
|
||||
--output ${TELEPORT_CONFIG_PATH}
|
||||
}
|
||||
# checks whether the given host is running MacOS
|
||||
|
||||
Reference in New Issue
Block a user