diff --git a/lib/auth/auth.go b/lib/auth/auth.go
index 8c75b768c6e..d96705cae68 100644
--- a/lib/auth/auth.go
+++ b/lib/auth/auth.go
@@ -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.
diff --git a/lib/auth/bot_test.go b/lib/auth/bot_test.go
index a7f47491013..b773cc09989 100644
--- a/lib/auth/bot_test.go
+++ b/lib/auth/bot_test.go
@@ -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,
diff --git a/lib/auth/export_test.go b/lib/auth/export_test.go
index aef3a4a4231..e0d295d2fd7 100644
--- a/lib/auth/export_test.go
+++ b/lib/auth/export_test.go
@@ -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
}
diff --git a/lib/auth/join/join.go b/lib/auth/join/join.go
index 26c23542cba..4b3648bcb38 100644
--- a/lib/auth/join/join.go
+++ b/lib/auth/join/join.go
@@ -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 == "" {
diff --git a/lib/auth/join_spacelift.go b/lib/auth/join_spacelift.go
index 3a2360c6036..891808fd176 100644
--- a/lib/auth/join_spacelift.go
+++ b/lib/auth/join_spacelift.go
@@ -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)
}
diff --git a/lib/auth/join_spacelift_test.go b/lib/join/join_spacelift_test.go
similarity index 82%
rename from lib/auth/join_spacelift_test.go
rename to lib/join/join_spacelift_test.go
index 94a702e7544..16a721cf9b0 100644
--- a/lib/auth/join_spacelift_test.go
+++ b/lib/join/join_spacelift_test.go
@@ -16,29 +16,28 @@
* along with this program. If not, see .
*/
-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)
+ })
})
}
}
diff --git a/lib/join/joinclient/join.go b/lib/join/joinclient/join.go
index 5c7de72180f..6c1020bfa47 100644
--- a/lib/join/joinclient/join.go
+++ b/lib/join/joinclient/join.go
@@ -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)
diff --git a/lib/join/server.go b/lib/join/server.go
index e31306d1813..c2809d14be9 100644
--- a/lib/join/server.go
+++ b/lib/join/server.go
@@ -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:
diff --git a/lib/join/server_spacelift.go b/lib/join/server_spacelift.go
new file mode 100644
index 00000000000..628a195af14
--- /dev/null
+++ b/lib/join/server_spacelift.go
@@ -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 .
+ */
+
+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)
+}
diff --git a/lib/join/spacelift/spacelift.go b/lib/join/spacelift/spacelift.go
new file mode 100644
index 00000000000..5d22c54c778
--- /dev/null
+++ b/lib/join/spacelift/spacelift.go
@@ -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 .
+ */
+
+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::(stack|module)::run_type::scope:
+ 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")
+}
diff --git a/lib/spacelift/token_source.go b/lib/join/spacelift/token_source.go
similarity index 100%
rename from lib/spacelift/token_source.go
rename to lib/join/spacelift/token_source.go
diff --git a/lib/spacelift/token_source_test.go b/lib/join/spacelift/token_source_test.go
similarity index 100%
rename from lib/spacelift/token_source_test.go
rename to lib/join/spacelift/token_source_test.go
diff --git a/lib/spacelift/token_validator.go b/lib/join/spacelift/token_validator.go
similarity index 100%
rename from lib/spacelift/token_validator.go
rename to lib/join/spacelift/token_validator.go
diff --git a/lib/spacelift/token_validator_test.go b/lib/join/spacelift/token_validator_test.go
similarity index 100%
rename from lib/spacelift/token_validator_test.go
rename to lib/join/spacelift/token_validator_test.go
diff --git a/lib/spacelift/spacelift.go b/lib/spacelift/spacelift.go
deleted file mode 100644
index a0ca76304f6..00000000000
--- a/lib/spacelift/spacelift.go
+++ /dev/null
@@ -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 .
- */
-
-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::(stack|module)::run_type::scope:
- 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,
- }
-}
diff --git a/lib/utils/hostid/hostid.go b/lib/utils/hostid/hostid.go
index e3fe6c88c06..ee941038ab7 100644
--- a/lib/utils/hostid/hostid.go
+++ b/lib/utils/hostid/hostid.go
@@ -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: