Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02e47-6aac-7ee1-914d-0584c75d3c81
110 lines
3.2 KiB
Go
110 lines
3.2 KiB
Go
package callerauth
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type KubernetesTokenReviewer struct {
|
|
endpoint string
|
|
audience string
|
|
reviewerTokenFile string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewKubernetesTokenReviewer(endpoint, audience, reviewerTokenFile, caFile string) (*KubernetesTokenReviewer, error) {
|
|
ca, err := os.ReadFile(caFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read Kubernetes CA: %w", err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(ca) {
|
|
return nil, fmt.Errorf("Kubernetes CA file contains no certificates")
|
|
}
|
|
return &KubernetesTokenReviewer{
|
|
endpoint: strings.TrimRight(endpoint, "/") + "/apis/authentication.k8s.io/v1/tokenreviews",
|
|
audience: audience,
|
|
reviewerTokenFile: reviewerTokenFile,
|
|
client: &http.Client{
|
|
Timeout: 3 * time.Second,
|
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
RootCAs: pool,
|
|
}},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
type tokenReview struct {
|
|
APIVersion string `json:"apiVersion"`
|
|
Kind string `json:"kind"`
|
|
Spec tokenReviewSpec `json:"spec"`
|
|
Status tokenReviewStatus `json:"status,omitempty"`
|
|
}
|
|
|
|
type tokenReviewSpec struct {
|
|
Token string `json:"token"`
|
|
Audiences []string `json:"audiences"`
|
|
}
|
|
|
|
type tokenReviewStatus struct {
|
|
Authenticated bool `json:"authenticated"`
|
|
Audiences []string `json:"audiences"`
|
|
Error string `json:"error"`
|
|
User struct {
|
|
Username string `json:"username"`
|
|
} `json:"user"`
|
|
}
|
|
|
|
func (r *KubernetesTokenReviewer) Review(ctx context.Context, callerToken string) (Identity, error) {
|
|
reviewerToken, err := os.ReadFile(r.reviewerTokenFile)
|
|
if err != nil {
|
|
return Identity{}, fmt.Errorf("read reviewer credential: %w", err)
|
|
}
|
|
payload, err := json.Marshal(tokenReview{
|
|
APIVersion: "authentication.k8s.io/v1",
|
|
Kind: "TokenReview",
|
|
Spec: tokenReviewSpec{
|
|
Token: callerToken,
|
|
Audiences: []string{r.audience},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return Identity{}, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.endpoint, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return Identity{}, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(string(reviewerToken)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := r.client.Do(req)
|
|
if err != nil {
|
|
return Identity{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return Identity{}, fmt.Errorf("TokenReview returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
|
}
|
|
var review tokenReview
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&review); err != nil {
|
|
return Identity{}, fmt.Errorf("decode TokenReview: %w", err)
|
|
}
|
|
if review.Status.Error != "" {
|
|
return Identity{}, fmt.Errorf("%w: TokenReview: %s", ErrUnauthenticated, review.Status.Error)
|
|
}
|
|
if !review.Status.Authenticated {
|
|
return Identity{}, nil
|
|
}
|
|
return Identity{Username: review.Status.User.Username, Audiences: review.Status.Audiences}, nil
|
|
}
|