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:
![image](https://user-images.githubusercontent.com/689271/182440692-97c75ae0-1e14-4f76-b9ff-d41060c73ed0.png)
This commit is contained in:
Marco André Dinis
2022-08-08 17:06:41 +00:00
committed by GitHub
parent 18c94db3a2
commit 117d7d2d40
8 changed files with 922 additions and 783 deletions
+4
View File
@@ -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.
+9
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -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.
+10
View File
@@ -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
View File
@@ -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)
+17
View File
@@ -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)
+8 -1
View File
@@ -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