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 }