github.com/google/osv-scalibr@v0.4.1/veles/secrets/hcp/validator.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package hcp 16 17 import ( 18 "net/http" 19 "net/url" 20 "strings" 21 22 sv "github.com/google/osv-scalibr/veles/secrets/common/simplevalidate" 23 ) 24 25 // Defaults derived from public HCP API docs. 26 const ( 27 defaultTokenURL = "https://auth.idp.hashicorp.com/oauth2/token" 28 // Use the Cloud API host for identity introspection. 29 defaultAPIBase = "https://api.cloud.hashicorp.com" 30 callerIdentityEndpoint = "/iam/2019-12-10/caller-identity" 31 ) 32 33 type cc = ClientCredentials 34 35 // NewClientCredentialsValidator creates a new HCP client credential pair validator. 36 // It validates ClientCredentials by attempting a client_credentials OAuth2 37 // token exchange against the configured token endpoint. A 200 response indicates 38 // valid credentials; 400/401 indicates invalid; other responses are treated as 39 // validation failures. 40 func NewClientCredentialsValidator(opts ...ClientCredentialsOption) *sv.Validator[cc] { 41 v := &sv.Validator[cc]{ 42 Endpoint: defaultTokenURL, 43 HTTPMethod: http.MethodPost, 44 HTTPHeaders: func(_ cc) map[string]string { 45 return map[string]string{"Content-Type": "application/x-www-form-urlencoded"} 46 }, 47 Body: func(s cc) (string, error) { 48 form := url.Values{} 49 form.Set("grant_type", "client_credentials") 50 form.Set("client_id", s.ClientID) 51 form.Set("client_secret", s.ClientSecret) 52 return form.Encode(), nil 53 }, 54 ValidResponseCodes: []int{http.StatusOK}, 55 InvalidResponseCodes: []int{http.StatusBadRequest, http.StatusUnauthorized}, 56 } 57 for _, opt := range opts { 58 opt(v) 59 } 60 return v 61 } 62 63 // ClientCredentialsOption configures a ClientCredentialsValidator. 64 type ClientCredentialsOption func(*sv.Validator[cc]) 65 66 // WithTokenURL overrides the token endpoint URL. 67 func WithTokenURL(u string) ClientCredentialsOption { 68 return func(v *sv.Validator[cc]) { v.Endpoint = u } 69 } 70 71 type at = AccessToken 72 73 // NewAccessTokenValidator creates a new validator for HCP access tokens. 74 // It validates an AccessToken by sending a GET request to the HCP 75 // caller-identity endpoint with the token as a Bearer credential. 76 // 77 // Documentation: https://developer.hashicorp.com/hcp/api-docs/identity#IamService_GetCallerIdentity 78 // 79 // - 200: token is valid (identity returned) 80 // - 401: token is invalid 81 // - any other response: treated as ValidationFailed 82 func NewAccessTokenValidator(opts ...AccessTokenOption) *sv.Validator[at] { 83 v := &sv.Validator[at]{ 84 Endpoint: defaultAPIBase + callerIdentityEndpoint, 85 HTTPMethod: http.MethodGet, 86 HTTPHeaders: func(s at) map[string]string { 87 return map[string]string{"Authorization": "Bearer " + s.Token} 88 }, 89 ValidResponseCodes: []int{http.StatusOK}, 90 InvalidResponseCodes: []int{http.StatusUnauthorized}, 91 } 92 for _, opt := range opts { 93 opt(v) 94 } 95 return v 96 } 97 98 // AccessTokenOption configures an AccessTokenValidator. 99 type AccessTokenOption func(*sv.Validator[at]) 100 101 // WithAPIBase overrides the base API URL (default: https://api.cloud.hashicorp.com). 102 func WithAPIBase(base string) AccessTokenOption { 103 return func(v *sv.Validator[at]) { 104 v.Endpoint = strings.TrimRight(base, "/") + callerIdentityEndpoint 105 } 106 }