2026-08-17 10:14:44 -07:00
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-08-21 20:09:48 -07:00
|
|
|
kmsvc "forgejo.riotpiao.com/rock/kmsvc-sdk"
|
2026-08-17 10:14:44 -07:00
|
|
|
"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
|
|
|
|
|
}
|