flex-auth/internal/callerauth/tokenreview.go
tegwick c473f1971d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 37s
feat(authz): bind decisions to exact actions
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02e47-6aac-7ee1-914d-0584c75d3c81
2026-08-23 13:18:26 +02:00

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
}