github.com/google/osv-scalibr@v0.4.1/veles/secrets/grokxaiapikey/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  // Copyright 2025 Google LLC
    16  //
    17  // Licensed under the Apache License, Version 2.0 (the "License");
    18  // you may not use this file except in compliance with the License.
    19  // You may obtain a copy of the License at
    20  //
    21  //      http://www.apache.org/licenses/LICENSE-2.0
    22  //
    23  // Unless required by applicable law or agreed to in writing,
    24  // distributed under the License is distributed on an "AS IS" BASIS,
    25  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    26  // See the License for the specific language governing permissions and
    27  // limitations under the License.
    28  
    29  package grokxaiapikey
    30  
    31  import (
    32  	"encoding/json"
    33  	"fmt"
    34  	"io"
    35  	"net/http"
    36  	"time"
    37  
    38  	"github.com/google/osv-scalibr/veles"
    39  	"github.com/google/osv-scalibr/veles/secrets/common/simplevalidate"
    40  )
    41  
    42  const (
    43  	// Used to validate standard API keys directly by checking blocked/disabled flags.
    44  	apiEndpoint = "https://api.x.ai/v1/api-key"
    45  	// Uses a dummy teamId since every management API request requires {teamId}.
    46  	// This forces predictable error patterns that allow indirect validation of management keys.
    47  	managementEndpoint = "https://management-api.x.ai/auth/teams/ffffffff-ffff-ffff-ffff-ffffffffffff/api-keys"
    48  	// validationTimeout is timeout for API validation requests.
    49  	validationTimeout = 10 * time.Second
    50  )
    51  
    52  //
    53  // --- Grok XAI API Key Validator ---
    54  //
    55  
    56  // apiKeyResponse represents the JSON returned by the x.ai API key endpoint.
    57  type apiKeyResponse struct {
    58  	APIKeyBlocked  bool `json:"api_key_blocked"`
    59  	APIKeyDisabled bool `json:"api_key_disabled"`
    60  }
    61  
    62  // apiKeyStatusFromBody verifies responses from /v1/api-key.
    63  // If either api_key_blocked or api_key_disabled is true, the key is invalid.
    64  func apiKeyStatusFromBody(body io.Reader) (veles.ValidationStatus, error) {
    65  	bodyBytes, err := io.ReadAll(body)
    66  	if err != nil {
    67  		return veles.ValidationFailed, fmt.Errorf("failed to read response body: %w", err)
    68  	}
    69  	var resp apiKeyResponse
    70  	if err := json.Unmarshal(bodyBytes, &resp); err != nil {
    71  		return veles.ValidationFailed, fmt.Errorf("failed to parse JSON response: %w", err)
    72  	}
    73  
    74  	if resp.APIKeyBlocked || resp.APIKeyDisabled {
    75  		return veles.ValidationInvalid, nil
    76  	}
    77  	return veles.ValidationValid, nil
    78  }
    79  
    80  // NewAPIValidator creates a new Validator for GrokXAIAPIKey.
    81  func NewAPIValidator() *simplevalidate.Validator[GrokXAIAPIKey] {
    82  	return &simplevalidate.Validator[GrokXAIAPIKey]{
    83  		Endpoint:   apiEndpoint,
    84  		HTTPMethod: http.MethodGet,
    85  		HTTPHeaders: func(k GrokXAIAPIKey) map[string]string {
    86  			return map[string]string{
    87  				"Authorization": "Bearer " + k.Key,
    88  			}
    89  		},
    90  		StatusFromResponseBody: apiKeyStatusFromBody,
    91  		HTTPC: &http.Client{
    92  			Timeout: validationTimeout,
    93  		},
    94  	}
    95  }
    96  
    97  //
    98  // --- Grok XAI Management Key Validator ---
    99  //
   100  
   101  // managementErrorResponse represents the JSON returned when a management key is checked.
   102  type managementErrorResponse struct {
   103  	Code    int    `json:"code"`
   104  	Message string `json:"message"`
   105  }
   106  
   107  func managementKeyStatusFromBody(body io.Reader) (veles.ValidationStatus, error) {
   108  	bodyBytes, err := io.ReadAll(body)
   109  	if err != nil {
   110  		return veles.ValidationFailed, fmt.Errorf("failed to read response body: %w", err)
   111  	}
   112  
   113  	var resp managementErrorResponse
   114  	if err := json.Unmarshal(bodyBytes, &resp); err != nil {
   115  		return veles.ValidationFailed, fmt.Errorf("unable to parse response from %q: %w", managementEndpoint, err)
   116  	}
   117  	if resp.Code == 7 {
   118  		// Team mismatch error → means the key itself is valid.
   119  		// Every management API call requires a {teamId}, but we don't know the real one.
   120  		// By using a fake teamId, a valid key passes authentication but fails authorization,
   121  		// producing code 7 ("team mismatch"). This reliably distinguishes valid keys from invalid ones.
   122  		return veles.ValidationValid, nil
   123  	}
   124  	// Other 403 codes → the key was authenticated but failed authorization for reasons other than team mismatch.
   125  	// This indicates the key is not valid for use.
   126  	return veles.ValidationInvalid, nil
   127  }
   128  
   129  // NewManagementAPIValidator creates a new Validator for GrokXAIManagementKey.
   130  func NewManagementAPIValidator() *simplevalidate.Validator[GrokXAIManagementKey] {
   131  	return &simplevalidate.Validator[GrokXAIManagementKey]{
   132  		Endpoint:   managementEndpoint,
   133  		HTTPMethod: http.MethodGet,
   134  		HTTPHeaders: func(k GrokXAIManagementKey) map[string]string {
   135  			return map[string]string{
   136  				"Authorization": "Bearer " + k.Key,
   137  			}
   138  		},
   139  		ValidResponseCodes:     []int{http.StatusOK},
   140  		InvalidResponseCodes:   []int{http.StatusUnauthorized},
   141  		StatusFromResponseBody: managementKeyStatusFromBody,
   142  		HTTPC: &http.Client{
   143  			Timeout: validationTimeout,
   144  		},
   145  	}
   146  }