@@ -20,130 +20,36 @@ package auth
2020
2121import (
2222 "context"
23- "slices"
24- "strings"
2523
2624 "github.com/gravitational/trace"
2725
2826 "github.com/gravitational/teleport/api/types"
29- "github.com/gravitational/teleport/lib/gcp"
27+ "github.com/gravitational/teleport/lib/join/ gcp"
3028)
3129
32- type gcpIDTokenValidator interface {
33- Validate (ctx context.Context , token string ) (* gcp.IDTokenClaims , error )
30+ // GetGCPIDTokenValidator returns the server's configured GCP ID token
31+ // validator.
32+ func (a * Server ) GetGCPIDTokenValidator () gcp.Validator {
33+ return a .gcpIDTokenValidator
34+ }
35+
36+ // SetGCPIDTokenValidator sets a new GCP ID token validator, used in tests.
37+ func (a * Server ) SetGCPIDTokenValidator (validator gcp.Validator ) {
38+ a .gcpIDTokenValidator = validator
3439}
3540
3641func (a * Server ) checkGCPJoinRequest (
3742 ctx context.Context ,
3843 req * types.RegisterUsingTokenRequest ,
3944 pt types.ProvisionToken ,
4045) (* gcp.IDTokenClaims , error ) {
41- if req .IDToken == "" {
42- return nil , trace .BadParameter ("IDToken not provided for GCP join request" )
43- }
44- token , ok := pt .(* types.ProvisionTokenV2 )
45- if ! ok {
46- return nil , trace .BadParameter ("gcp join method only supports ProvisionTokenV2, '%T' was provided" , pt )
47- }
48-
49- claims , err := a .gcpIDTokenValidator .Validate (ctx , req .IDToken )
50- if err != nil {
51- a .logger .WarnContext (ctx , "Unable to validate GCP IDToken" ,
52- "error" , err ,
53- "claims" , claims ,
54- "token" , pt .GetName (),
55- )
56- return nil , trace .Wrap (err )
57- }
58-
59- a .logger .InfoContext (ctx , "GCP VM trying to join cluster" ,
60- "claims" , claims ,
61- "token" , pt .GetName (),
62- )
63-
64- if err := checkGCPAllowRules (token , claims ); err != nil {
65- return nil , trace .Wrap (err )
66- }
67-
68- return claims , nil
69- }
70-
71- func checkGCPAllowRules (token * types.ProvisionTokenV2 , claims * gcp.IDTokenClaims ) error {
72- compute := claims .Google .ComputeEngine
73- // unmatchedLocation is true if the location restriction is set and the "google.compute_engine.zone"
74- // claim is not present in the IDToken. This happens when the joining node is not a GCE VM.
75- unmatchedLocation := false
76- // If a single rule passes, accept the IDToken.
77- for _ , rule := range token .Spec .GCP .Allow {
78- if ! slices .Contains (rule .ProjectIDs , compute .ProjectID ) {
79- continue
80- }
81-
82- if len (rule .ServiceAccounts ) > 0 && ! slices .Contains (rule .ServiceAccounts , claims .Email ) {
83- continue
84- }
85-
86- if len (rule .Locations ) > 0 && ! slices .ContainsFunc (rule .Locations , func (location string ) bool {
87- return isGCPZoneInLocation (location , compute .Zone )
88- }) {
89- unmatchedLocation = true
90- continue
91- }
92-
93- // All provided rules met.
94- return nil
95- }
96-
97- // If the location restriction is set and the "google.compute_engine.zone" claim is not present in the IDToken,
98- // return a more specific error message.
99- if unmatchedLocation && compute .Zone == "" {
100- return trace .CompareFailed ("id token %q claim is empty and didn't match the %q. " +
101- "Services running outside of GCE VM instances are incompatible with %q restriction." , "google.compute_engine.zone" , "locations" , "location" )
102- }
103- return trace .AccessDenied ("id token claims did not match any allow rules" )
104- }
105-
106- type gcpLocation struct {
107- globalLocation string
108- region string
109- zone string
110- }
111-
112- func parseGCPLocation (location string ) (* gcpLocation , error ) {
113- parts := strings .Split (location , "-" )
114- if len (parts ) < 2 || len (parts ) > 3 {
115- return nil , trace .BadParameter ("location %q is not a valid GCP region or zone" , location )
116- }
117- globalLocation , region := parts [0 ], parts [1 ]
118- var zone string
119- if len (parts ) == 3 {
120- zone = parts [2 ]
121- }
122- return & gcpLocation {
123- globalLocation : globalLocation ,
124- region : region ,
125- zone : zone ,
126- }, nil
127- }
128-
129- // isGCPZoneInLocation checks if a zone belongs to a location, which can be
130- // either a zone or region.
131- func isGCPZoneInLocation (rawLocation , rawZone string ) bool {
132- location , err := parseGCPLocation (rawLocation )
133- if err != nil {
134- return false
135- }
136- zone , err := parseGCPLocation (rawZone )
137- if err != nil {
138- return false
139- }
140- // Make sure zone is, in fact, a zone.
141- if zone .zone == "" {
142- return false
143- }
144-
145- if location .globalLocation != zone .globalLocation || location .region != zone .region {
146- return false
147- }
148- return location .zone == "" || location .zone == zone .zone
46+ claims , err := gcp .CheckIDToken (ctx , & gcp.CheckIDTokenParams {
47+ ProvisionToken : pt ,
48+ IDToken : []byte (req .IDToken ),
49+ Validator : a .gcpIDTokenValidator ,
50+ })
51+
52+ // Where possible, try to return any extracted claims along with the error
53+ // to improve audit logs for failed join attempts.
54+ return claims , trace .Wrap (err )
14955}
0 commit comments