1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6 "connectrpc.com/connect"
7 "github.com/spf13/cobra"
8
9 "bnema.dev/ermite-api/gen/ermite"
10)
11
12// jsonOIDCRotation is the stable JSON shape of `oidc rotate-key --json`.
13type jsonOIDCRotation struct {
14 Issuer string `json:"issuer"`
15 Kid string `json:"kid"`
16 RetiredKid *string `json:"retired_kid"`
17 RetiredUntil *string `json:"retired_until"`
18}
19
20func newOIDCCmd(env *cliEnv) *cobra.Command {
21 oidc := &cobra.Command{
22 Use: "oidc",
23 Short: "Manage the OIDC provider of CI jobs",
24 Long: "The server issues OIDC ID tokens to the CI jobs of trusted runs that have `permissions: id-token:\n" +
25 "write`, as GitHub does: a relying party (Vault, a cloud) that trusts the issuer\n" +
26 "<public URL>/-/oidc exchanges them for credentials. It signs them with its own key, sealed with\n" +
27 "ERMITE_SECRETS_KEY: a server without that key issues none. Needs a server with the ci.oidc\n" +
28 "feature (see `ermite version`).",
29 }
30 oidc.AddCommand(newOIDCRotateKeyCmd(env))
31 return oidc
32}
33
34func newOIDCRotateKeyCmd(env *cliEnv) *cobra.Command {
35 cmd := &cobra.Command{
36 Use: "rotate-key",
37 Short: "Replace the key that signs the ID tokens",
38 Long: "Generate a new signing key for the ID tokens. The new key signs at once; the previous one stays\n" +
39 "in the JWKS (<issuer>/.well-known/jwks) for 24 hours, so that the tokens it signed verify until they\n" +
40 "expire. Needs owner rights: the admin token, or a token with every scope, no repository list\n" +
41 "and no branch prefixes.\n\n" +
42 "JSON fields (--json): issuer, kid, retired_kid and retired_until (RFC 3339 UTC; both null when\n" +
43 "there was no previous key).",
44 Args: cobra.NoArgs,
45 }
46 asJSON := addJSONFlag(cmd)
47 cmd.RunE = func(cmd *cobra.Command, _ []string) error {
48 c, err := env.client(cmd)
49 if err != nil {
50 return err
51 }
52 res, err := c.Server().RotateOIDCKey(env.ctx(cmd), connect.NewRequest(&ermite.RotateOIDCKeyRequest{}))
53 if err != nil {
54 return c.Describe(err)
55 }
56 r := res.Msg
57 out := cmd.OutOrStdout()
58 if *asJSON {
59 j := jsonOIDCRotation{Issuer: r.GetIssuer(), Kid: r.GetKid(), RetiredUntil: jsonTime(r.GetRetiredUntil())}
60 if k := r.GetRetiredKid(); k != "" {
61 j.RetiredKid = &k
62 }
63 return writeJSON(out, j)
64 }
65 pairs := [][2]string{{"issuer", r.GetIssuer()}, {"kid", r.GetKid()}}
66 if r.GetRetiredKid() != "" {
67 pairs = append(pairs, [2]string{"retired kid", r.GetRetiredKid()}, [2]string{"published until", shortTime(r.GetRetiredUntil(), "")})
68 }
69 return fields(out, pairs)
70 }
71 return cmd
72}