Files
kmsvc-cli/internal/cli/client.go
T

101 lines
3.2 KiB
Go

package cli
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
kmsvc "forgejo.riotpiao.homelab.com/homelab/kmsvc-sdk"
"google.golang.org/grpc/credentials"
)
// defaultTokenURL is the Authentik OAuth2 token endpoint used with
// --client-id/--client-secret when --token-url/KMSVC_TOKEN_URL is unset.
const defaultTokenURL = "https://authentik.riotpiao.homelab.com/application/o/token/"
// buildClient constructs a *kmsvc.Client from resolved global flags.
//
// Defaults to TLS: the SDK itself defaults to plaintext (appropriate for
// cluster-internal callers), but kmsvc-cli's own default --server
// (kmsvc.riotpiao.homelab.com:443, see README) is reached through an
// Ingress-terminated HTTPS/gRPC-passthrough endpoint, so a real external
// invocation needs a TLS handshake, not plaintext. --insecure opts back into
// plaintext for cluster-internal/dev targets.
func buildClient(ctx context.Context, flags *globalFlags) (*kmsvc.Client, error) {
if flags.server == "" {
return nil, fmt.Errorf("server address required (--server or KMSVC_SERVER)")
}
token := flags.token
if token == "" && flags.clientID != "" && flags.clientSecret != "" {
tokenURL := flags.tokenURL
if tokenURL == "" {
tokenURL = defaultTokenURL
}
fetched, err := fetchClientCredentialsToken(ctx, tokenURL, flags.clientID, flags.clientSecret)
if err != nil {
return nil, fmt.Errorf("fetch token via client_credentials: %w", err)
}
token = fetched
}
var opts []kmsvc.Option
if token != "" {
opts = append(opts, kmsvc.WithTokenSource(kmsvc.StaticToken(token)))
}
if !flags.insecure {
opts = append(opts, kmsvc.WithTransportCredentials(credentials.NewTLS(nil)))
}
client, err := kmsvc.New(ctx, flags.server, opts...)
if err != nil {
return nil, fmt.Errorf("connect to %s: %w", flags.server, err)
}
return client, nil
}
// fetchClientCredentialsToken performs an OAuth2 client_credentials grant
// against tokenURL, used when --token/KMSVC_TOKEN is unset but
// --client-id/--client-secret (KMSVC_CLIENT_ID/KMSVC_CLIENT_SECRET) are
// configured, so callers don't need a separate curl step to mint a token.
func fetchClientCredentialsToken(ctx context.Context, tokenURL, clientID, clientSecret string) (string, error) {
if tokenURL == "" {
return "", fmt.Errorf("token URL required (--token-url or KMSVC_TOKEN_URL)")
}
form := url.Values{
"grant_type": {"client_credentials"},
"client_id": {clientID},
"client_secret": {clientSecret},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token endpoint returned %s", resp.Status)
}
var body struct {
AccessToken string `json:"access_token"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return "", fmt.Errorf("decode token response: %w", err)
}
if body.AccessToken == "" {
return "", fmt.Errorf("token response missing access_token")
}
return body.AccessToken, nil
}