Port spacelift join method to new join service (#61652)

* Port `spacelift` join method to new join service

This ports the `spacelift` join method to the new join service. It
moves the core validation logic from `lib/spacelift` into `lib/join`,
and adds a small compatibility layer to allow it to be reused between
the new and legacy join services. Where possible, existing logic
remains untouched.

See also: [RFD 27e](https://github.com/gravitational/teleport.e/blob/master/rfd/0027e-auth-assigned-uuids.md)

* Remove duped error declaration

* Move errMockInvalidToken again
This commit is contained in:
Tim Buckley
2025-11-27 02:31:56 +00:00
committed by GitHub
parent d1a15885c6
commit a057b603d0
16 changed files with 348 additions and 188 deletions
+2 -2
View File
@@ -122,6 +122,7 @@ import (
"github.com/gravitational/teleport/lib/join/gcp"
"github.com/gravitational/teleport/lib/join/githubactions"
"github.com/gravitational/teleport/lib/join/gitlab"
"github.com/gravitational/teleport/lib/join/spacelift"
"github.com/gravitational/teleport/lib/join/terraformcloud"
"github.com/gravitational/teleport/lib/join/tpmjoin"
kubetoken "github.com/gravitational/teleport/lib/kube/token"
@@ -138,7 +139,6 @@ import (
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/spacelift"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/tlsca"
@@ -1276,7 +1276,7 @@ type Server struct {
// spaceliftIDTokenValidator allows ID tokens from Spacelift to be validated
// by the auth server. It can be overridden for the purpose of tests.
spaceliftIDTokenValidator spaceliftIDTokenValidator
spaceliftIDTokenValidator spacelift.Validator
// gitlabIDTokenValidator allows ID tokens from GitLab CI to be validated by
// the auth server. It can be overridden for the purpose of tests.
+3
View File
@@ -27,6 +27,7 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
@@ -78,6 +79,8 @@ import (
"github.com/gravitational/teleport/lib/utils/log/logtest"
)
var errMockInvalidToken = errors.New("invalid token")
func renewBotCerts(
ctx context.Context,
srv *authtest.TLSServer,
-4
View File
@@ -199,10 +199,6 @@ func (a *Server) SetK8sTokenReviewValidator(validator k8sTokenReviewValidator) {
a.k8sTokenReviewValidator = validator
}
func (a *Server) SetSpaceliftIDTokenValidator(validator spaceliftIDTokenValidator) {
a.spaceliftIDTokenValidator = validator
}
func (a *Server) SetCreateBoundKeypairValidator(validator boundkeypair.CreateBoundKeypairValidator) {
a.createBoundKeypairValidator = validator
}
+6 -4
View File
@@ -56,10 +56,10 @@ import (
"github.com/gravitational/teleport/lib/join/circleci"
"github.com/gravitational/teleport/lib/join/githubactions"
"github.com/gravitational/teleport/lib/join/gitlab"
"github.com/gravitational/teleport/lib/join/spacelift"
"github.com/gravitational/teleport/lib/join/terraformcloud"
"github.com/gravitational/teleport/lib/jwt"
kubetoken "github.com/gravitational/teleport/lib/kube/token"
"github.com/gravitational/teleport/lib/spacelift"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/tpm"
"github.com/gravitational/teleport/lib/utils"
@@ -361,9 +361,11 @@ func Register(ctx context.Context, params RegisterParams) (result *RegisterResul
}
}
case types.JoinMethodSpacelift:
params.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()
if err != nil {
return nil, trace.Wrap(err)
if params.IDToken == "" {
params.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()
if err != nil {
return nil, trace.Wrap(err)
}
}
case types.JoinMethodTerraformCloud:
if params.IDToken == "" {
+19 -78
View File
@@ -20,20 +20,23 @@ package auth
import (
"context"
"fmt"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/join/joinutils"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/spacelift"
"github.com/gravitational/teleport/lib/join/spacelift"
)
type spaceliftIDTokenValidator interface {
Validate(
ctx context.Context, domain string, token string,
) (*spacelift.IDTokenClaims, error)
// GetSpaceliftIDTokenValidator returns the server's currently configured
// Spacelift OIDC token validator.
func (a *Server) GetSpaceliftIDTokenValidator() spacelift.Validator {
return a.spaceliftIDTokenValidator
}
// SetSpaceliftIDTokenValidator sets the current Spacelift OIDC token validator,
// used in tests.
func (a *Server) SetSpaceliftIDTokenValidator(validator spacelift.Validator) {
a.spaceliftIDTokenValidator = validator
}
func (a *Server) checkSpaceliftJoinRequest(
@@ -41,75 +44,13 @@ func (a *Server) checkSpaceliftJoinRequest(
req *types.RegisterUsingTokenRequest,
pt types.ProvisionToken,
) (*spacelift.IDTokenClaims, error) {
if req.IDToken == "" {
return nil, trace.BadParameter("id_token not provided for spacelift join request")
}
token, ok := pt.(*types.ProvisionTokenV2)
if !ok {
return nil, trace.BadParameter("spacelift join method only supports ProvisionTokenV2, '%T' was provided", pt)
}
claims, err := spacelift.CheckIDToken(ctx, &spacelift.CheckIDTokenParams{
ProvisionToken: pt,
IDToken: []byte(req.IDToken),
Validator: a.spaceliftIDTokenValidator,
})
if modules.GetModules().BuildType() != modules.BuildEnterprise {
return nil, fmt.Errorf(
"spacelift joining: %w",
ErrRequiresEnterprise,
)
}
claims, err := a.spaceliftIDTokenValidator.Validate(
ctx, token.Spec.Spacelift.Hostname, req.IDToken,
)
if err != nil {
return nil, trace.Wrap(err)
}
a.logger.InfoContext(ctx, "Spacelift run trying to join cluster",
"claims", claims,
"token", pt.GetName(),
)
return claims, trace.Wrap(checkSpaceliftAllowRules(token, claims))
}
func checkSpaceliftAllowRules(token *types.ProvisionTokenV2, claims *spacelift.IDTokenClaims) error {
globCheck := func(want string, got string) (bool, error) {
if token.Spec.Spacelift.EnableGlobMatching {
return joinutils.GlobMatchAllowEmptyPattern(want, got)
}
if want == "" {
return true, nil
}
return want == got, nil
}
// If a single rule passes, accept the IDToken
for i, rule := range token.Spec.Spacelift.Allow {
// Please consider keeping these field validators in the same order they
// are defined within the ProvisionTokenSpecV2Spacelift proto spec.
spaceIDMatch, err := globCheck(rule.SpaceID, claims.SpaceID)
if err != nil {
return trace.Wrap(err, "evaluating rule (%d) space_id glob match", i)
}
if !spaceIDMatch {
continue
}
callerIDMatch, err := globCheck(rule.CallerID, claims.CallerID)
if err != nil {
return trace.Wrap(err, "evaluating rule (%d) caller_id glob match", i)
}
if !callerIDMatch {
continue
}
if rule.CallerType != "" && claims.CallerType != rule.CallerType {
continue
}
if rule.Scope != "" && claims.Scope != rule.Scope {
continue
}
// All provided rules met.
return nil
}
return trace.AccessDenied("id token claims did not match any allow rules")
// Attempt to return any claims along with the error, used to improve audit
// logging on failed join attempts.
return claims, trace.Wrap(err)
}
@@ -16,29 +16,28 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth_test
package join_test
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/authtest"
"github.com/gravitational/teleport/lib/auth/state"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/join/joinclient"
"github.com/gravitational/teleport/lib/join/spacelift"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/modules/modulestest"
"github.com/gravitational/teleport/lib/spacelift"
)
var errMockInvalidToken = errors.New("invalid token")
type mockSpaceliftTokenValidator struct {
tokens map[string]spacelift.IDTokenClaims
}
@@ -58,7 +57,7 @@ func (m *mockSpaceliftTokenValidator) Validate(
return &claims, nil
}
func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
func TestJoinSpacelift(t *testing.T) {
validIDToken := "test.fake.jwt"
idTokenValidator := &mockSpaceliftTokenValidator{
tokens: map[string]spacelift.IDTokenClaims{
@@ -73,18 +72,19 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
},
},
}
var withTokenValidator auth.ServerOption = func(server *auth.Server) error {
server.SetSpaceliftIDTokenValidator(idTokenValidator)
return nil
}
ctx := t.Context()
p, err := newTestPack(ctx, testPackOptions{
DataDir: t.TempDir(),
MutateAuth: withTokenValidator,
authServer, err := authtest.NewTestServer(authtest.ServerConfig{
Auth: authtest.AuthServerConfig{
Dir: t.TempDir(),
},
})
require.NoError(t, err)
auth := p.a
t.Cleanup(func() { assert.NoError(t, authServer.Shutdown(t.Context())) })
auth := authServer.Auth()
auth.SetSpaceliftIDTokenValidator(idTokenValidator)
// helper for creating RegisterUsingTokenRequest
sshPrivateKey, sshPublicKey, err := testauthority.New().GenerateKeyPair()
@@ -142,7 +142,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: require.NoError,
},
{
name: "success with glob",
name: "success-with-glob",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -162,7 +162,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: require.NoError,
},
{
name: "fail with glob",
name: "fail-with-glob",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -181,7 +181,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: allowRulesNotMatched,
},
{
name: "fail with disabled glob",
name: "fail-with-disabled-glob",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -201,7 +201,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: allowRulesNotMatched,
},
{
name: "missing enterprise",
name: "missing-enterprise",
setEnterprise: false,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -219,7 +219,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
},
},
{
name: "multiple allow rules",
name: "multiple-allow-rules",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -238,7 +238,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: require.NoError,
},
{
name: "incorrect space_id",
name: "incorrect-space_id",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -256,7 +256,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: allowRulesNotMatched,
},
{
name: "incorrect caller_id",
name: "incorrect-caller_id",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -274,7 +274,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: allowRulesNotMatched,
},
{
name: "incorrect caller_type",
name: "incorrect-caller_type",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -292,7 +292,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: allowRulesNotMatched,
},
{
name: "incorrect scope",
name: "incorrect-scope",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -310,7 +310,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
assertError: allowRulesNotMatched,
},
{
name: "invalid token",
name: "invalid-token",
setEnterprise: true,
tokenSpec: types.ProvisionTokenSpecV2{
JoinMethod: types.JoinMethodSpacelift,
@@ -324,7 +324,7 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
},
request: newRequest("some other token"),
assertError: func(t require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, errMockInvalidToken)
require.ErrorContains(t, err, "invalid token")
},
},
}
@@ -344,8 +344,42 @@ func TestAuth_RegisterUsingToken_Spacelift(t *testing.T) {
require.NoError(t, auth.CreateToken(ctx, token))
tt.request.Token = tt.name
_, err = auth.RegisterUsingToken(ctx, tt.request)
tt.assertError(t, err)
nopClient, err := authServer.NewClient(authtest.TestNop())
require.NoError(t, err)
t.Run("legacy", func(t *testing.T) {
_, err = auth.RegisterUsingToken(ctx, tt.request)
tt.assertError(t, err)
})
t.Run("legacy joinclient", func(t *testing.T) {
_, err := joinclient.LegacyJoin(t.Context(), joinclient.JoinParams{
Token: tt.request.Token,
JoinMethod: types.JoinMethodSpacelift,
ID: state.IdentityID{
Role: tt.request.Role,
NodeName: "testnode",
HostUUID: tt.request.HostID,
},
IDToken: tt.request.IDToken,
AuthClient: nopClient,
})
tt.assertError(t, err)
})
t.Run("new joinclient", func(t *testing.T) {
_, err := joinclient.Join(t.Context(), joinclient.JoinParams{
Token: tt.request.Token,
JoinMethod: types.JoinMethodSpacelift,
ID: state.IdentityID{
Role: types.RoleInstance, // RoleNode is not allowed
NodeName: "testnode",
},
IDToken: tt.request.IDToken,
AuthClient: nopClient,
})
tt.assertError(t, err)
})
})
}
}
+11
View File
@@ -42,6 +42,7 @@ import (
"github.com/gravitational/teleport/lib/join/gitlab"
"github.com/gravitational/teleport/lib/join/internal/messages"
"github.com/gravitational/teleport/lib/join/joinv1"
"github.com/gravitational/teleport/lib/join/spacelift"
"github.com/gravitational/teleport/lib/join/terraformcloud"
"github.com/gravitational/teleport/lib/utils/hostid"
)
@@ -213,6 +214,7 @@ func joinWithClient(ctx context.Context, params JoinParams, client *joinv1.Clien
types.JoinMethodGitLab,
types.JoinMethodIAM,
types.JoinMethodOracle,
types.JoinMethodSpacelift,
types.JoinMethodTPM,
types.JoinMethodTerraformCloud:
joinMethod := string(params.JoinMethod)
@@ -371,6 +373,15 @@ func joinWithMethod(
}
}
return oidcJoin(stream, joinParams, clientParams)
case types.JoinMethodSpacelift:
if joinParams.IDToken == "" {
joinParams.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()
if err != nil {
return nil, trace.Wrap(err)
}
}
return oidcJoin(stream, joinParams, clientParams)
case types.JoinMethodTPM:
return tpmJoin(ctx, stream, joinParams, clientParams)
+4
View File
@@ -58,6 +58,7 @@ import (
"github.com/gravitational/teleport/lib/join/joinutils"
"github.com/gravitational/teleport/lib/join/oraclejoin"
"github.com/gravitational/teleport/lib/join/provision"
"github.com/gravitational/teleport/lib/join/spacelift"
"github.com/gravitational/teleport/lib/join/terraformcloud"
"github.com/gravitational/teleport/lib/join/tpmjoin"
"github.com/gravitational/teleport/lib/scopes/joining"
@@ -97,6 +98,7 @@ type AuthService interface {
GetGHAIDTokenJWKSValidator() githubactions.GithubIDTokenJWKSValidator
GetGitlabIDTokenValidator() gitlab.Validator
GetTPMValidator() tpmjoin.TPMValidator
GetSpaceliftIDTokenValidator() spacelift.Validator
GetTerraformIDTokenValidator() terraformcloud.Validator
services.Presence
}
@@ -314,6 +316,8 @@ func (s *Server) handleJoinMethod(
return s.handleOIDCJoin(stream, authCtx, clientInit, token, s.validateGithubToken)
case types.JoinMethodGitLab:
return s.handleOIDCJoin(stream, authCtx, clientInit, token, s.validateGitlabToken)
case types.JoinMethodSpacelift:
return s.handleOIDCJoin(stream, authCtx, clientInit, token, s.validateSpaceliftToken)
case types.JoinMethodTPM:
return s.handleTPMJoin(stream, authCtx, clientInit, token)
case types.JoinMethodTerraformCloud:
+53
View File
@@ -0,0 +1,53 @@
/*
* Teleport
* Copyright (C) 2025 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/>.
*/
package join
import (
"context"
"github.com/gravitational/trace"
workloadidentityv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/lib/join/provision"
"github.com/gravitational/teleport/lib/join/spacelift"
)
func (a *Server) validateSpaceliftToken(
ctx context.Context,
pt provision.Token,
idToken []byte,
) (any, *workloadidentityv1.JoinAttrs, error) {
claims, err := spacelift.CheckIDToken(ctx, &spacelift.CheckIDTokenParams{
ProvisionToken: pt,
IDToken: idToken,
Validator: a.cfg.AuthService.GetSpaceliftIDTokenValidator(),
})
// If possible, attach claims and workload ID attrs regardless of the error
// return. If the token fails to validate, these claims will ensure audit
// events remain useful.
var workloadIDAttrs *workloadidentityv1.JoinAttrs
if claims != nil {
workloadIDAttrs = &workloadidentityv1.JoinAttrs{
Spacelift: claims.JoinAttrs(),
}
}
return claims, workloadIDAttrs, trace.Wrap(err)
}
+187
View File
@@ -0,0 +1,187 @@
/*
* 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/>.
*/
package spacelift
import (
"context"
"github.com/gravitational/trace"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/gravitational/teleport"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/join/joinutils"
"github.com/gravitational/teleport/lib/join/provision"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/services"
logutils "github.com/gravitational/teleport/lib/utils/log"
)
var log = logutils.NewPackageLogger(teleport.ComponentKey, "spacelift")
type Validator interface {
Validate(
ctx context.Context, domain string, token string,
) (*IDTokenClaims, error)
}
// IDTokenClaims
// See the following for the structure:
// https://docs.spacelift.io/integrations/cloud-providers/oidc/#standard-claims
type IDTokenClaims struct {
oidc.TokenClaims
// Sub provides some information about the Spacelift run that generated this
// token.
// space:<space_id>:(stack|module):<stack_id|module_id>:run_type:<run_type>:scope:<read|write>
Sub string `json:"sub"`
// SpaceID is the ID of the space in which the run that owns the token was
// executed.
SpaceID string `json:"spaceId"`
// CallerType is the type of the caller, ie. the entity that owns the run -
// either stack or module.
CallerType string `json:"callerType"`
// CallerID is the ID of the caller, ie. the stack or module that generated
// the run.
CallerID string `json:"callerId"`
// RunType is the type of the run.
// (PROPOSED, TRACKED, TASK, TESTING or DESTROY)
RunType string `json:"runType"`
// RunID is the ID of the run that owns the token.
RunID string `json:"runId"`
// Scope is the scope of the token - either read or write.
Scope string `json:"scope"`
}
func (c *IDTokenClaims) GetSubject() string {
return c.Sub
}
// JoinAttrs returns the protobuf representation of the attested identity.
// This is used for auditing and for evaluation of WorkloadIdentity rules and
// templating.
func (c *IDTokenClaims) JoinAttrs() *workloadidentityv1pb.JoinAttrsSpacelift {
return &workloadidentityv1pb.JoinAttrsSpacelift{
Sub: c.Sub,
SpaceId: c.SpaceID,
CallerType: c.CallerType,
CallerId: c.CallerID,
RunType: c.RunType,
RunId: c.RunID,
Scope: c.Scope,
}
}
// CheckIDTokenParams are parameters used to validate Spacelift OIDC tokens.
type CheckIDTokenParams struct {
ProvisionToken provision.Token
IDToken []byte
Validator Validator
}
func (p *CheckIDTokenParams) validate() error {
switch {
case p.ProvisionToken == nil:
return trace.BadParameter("ProvisionToken is required")
case len(p.IDToken) == 0:
return trace.BadParameter("IDToken is required")
case p.Validator == nil:
return trace.BadParameter("Validator is required")
}
return nil
}
// CheckIDToken validates a Spacelift OIDC token, verifying both the validity of
// the OIDC token itself, as well as ensuring claims match any configured allow
// rules in the provided provision token.
func CheckIDToken(
ctx context.Context,
params *CheckIDTokenParams,
) (*IDTokenClaims, error) {
if err := params.validate(); err != nil {
return nil, trace.Wrap(err)
}
token, ok := params.ProvisionToken.(*types.ProvisionTokenV2)
if !ok {
return nil, trace.BadParameter("spacelift join method only supports ProvisionTokenV2, '%T' was provided", params.ProvisionToken)
}
if modules.GetModules().BuildType() != modules.BuildEnterprise {
return nil, trace.Wrap(services.ErrRequiresEnterprise, "spacelift joining")
}
claims, err := params.Validator.Validate(
ctx, token.Spec.Spacelift.Hostname, string(params.IDToken),
)
if err != nil {
return nil, trace.Wrap(err)
}
log.InfoContext(ctx, "Spacelift run trying to join cluster",
"claims", claims,
"token", token.GetName(),
)
return claims, trace.Wrap(checkSpaceliftAllowRules(token, claims))
}
func checkSpaceliftAllowRules(token *types.ProvisionTokenV2, claims *IDTokenClaims) error {
globCheck := func(want string, got string) (bool, error) {
if token.Spec.Spacelift.EnableGlobMatching {
return joinutils.GlobMatchAllowEmptyPattern(want, got)
}
if want == "" {
return true, nil
}
return want == got, nil
}
// If a single rule passes, accept the IDToken
for i, rule := range token.Spec.Spacelift.Allow {
// Please consider keeping these field validators in the same order they
// are defined within the ProvisionTokenSpecV2Spacelift proto spec.
spaceIDMatch, err := globCheck(rule.SpaceID, claims.SpaceID)
if err != nil {
return trace.Wrap(err, "evaluating rule (%d) space_id glob match", i)
}
if !spaceIDMatch {
continue
}
callerIDMatch, err := globCheck(rule.CallerID, claims.CallerID)
if err != nil {
return trace.Wrap(err, "evaluating rule (%d) caller_id glob match", i)
}
if !callerIDMatch {
continue
}
if rule.CallerType != "" && claims.CallerType != rule.CallerType {
continue
}
if rule.Scope != "" && claims.Scope != rule.Scope {
continue
}
// All provided rules met.
return nil
}
return trace.AccessDenied("id token claims did not match any allow rules")
}
-72
View File
@@ -1,72 +0,0 @@
/*
* 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/>.
*/
package spacelift
import (
"github.com/zitadel/oidc/v3/pkg/oidc"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
)
// IDTokenClaims
// See the following for the structure:
// https://docs.spacelift.io/integrations/cloud-providers/oidc/#standard-claims
type IDTokenClaims struct {
oidc.TokenClaims
// Sub provides some information about the Spacelift run that generated this
// token.
// space:<space_id>:(stack|module):<stack_id|module_id>:run_type:<run_type>:scope:<read|write>
Sub string `json:"sub"`
// SpaceID is the ID of the space in which the run that owns the token was
// executed.
SpaceID string `json:"spaceId"`
// CallerType is the type of the caller, ie. the entity that owns the run -
// either stack or module.
CallerType string `json:"callerType"`
// CallerID is the ID of the caller, ie. the stack or module that generated
// the run.
CallerID string `json:"callerId"`
// RunType is the type of the run.
// (PROPOSED, TRACKED, TASK, TESTING or DESTROY)
RunType string `json:"runType"`
// RunID is the ID of the run that owns the token.
RunID string `json:"runId"`
// Scope is the scope of the token - either read or write.
Scope string `json:"scope"`
}
func (c *IDTokenClaims) GetSubject() string {
return c.Sub
}
// JoinAttrs returns the protobuf representation of the attested identity.
// This is used for auditing and for evaluation of WorkloadIdentity rules and
// templating.
func (c *IDTokenClaims) JoinAttrs() *workloadidentityv1pb.JoinAttrsSpacelift {
return &workloadidentityv1pb.JoinAttrsSpacelift{
Sub: c.Sub,
SpaceId: c.SpaceID,
CallerType: c.CallerType,
CallerId: c.CallerID,
RunType: c.RunType,
RunId: c.RunID,
Scope: c.Scope,
}
}
+1
View File
@@ -74,6 +74,7 @@ func Generate(ctx context.Context, joinMethod types.JoinMethod) (string, error)
types.JoinMethodAzure,
types.JoinMethodGCP,
types.JoinMethodTPM,
types.JoinMethodSpacelift,
types.JoinMethodTerraformCloud,
types.JoinMethodOracle,
types.JoinMethodEnv0: