From b1a6c7dcdced002cbb05fe25322a011c3597ef44 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Mon, 14 Jul 2025 16:15:57 +0530 Subject: [PATCH] [server][cli] Generate API Keys/Tokens from CLI --- server/internal/auth/api_key.go | 6 +- server/internal/auth/openid.go | 4 +- server/internal/auth/token.go | 4 +- server/internal/command/user/cmd.go | 2 + server/internal/command/user/keys/cmd.go | 90 ++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 server/internal/command/user/keys/cmd.go diff --git a/server/internal/auth/api_key.go b/server/internal/auth/api_key.go index 6a751217..bd8b7822 100644 --- a/server/internal/auth/api_key.go +++ b/server/internal/auth/api_key.go @@ -12,8 +12,10 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +var apiTokenEncoder = base64.StdEncoding.WithPadding(base64.NoPadding) + func ReadEncodedAPIKey(db db.Handler, encodedKey string) (Auth, error) { - if b, err := base64.StdEncoding.DecodeString(encodedKey); err != nil { + if b, err := apiTokenEncoder.DecodeString(encodedKey); err != nil { return nil, ErrCredentialsInvalid } else if len(b) < 16 { return nil, ErrCredentialsInvalid @@ -69,6 +71,6 @@ func GenerateEncodedAPIKey(db db.TxHandler, userID int32, validity time.Duration if id, key, err := GenerateAPIKey(db, userID, validity, description, scopes); err != nil { return "", err } else { - return base64.StdEncoding.EncodeToString(append(id[:], key...)), nil + return apiTokenEncoder.EncodeToString(append(id[:], key...)), nil } } diff --git a/server/internal/auth/openid.go b/server/internal/auth/openid.go index d0ac28ac..f846f775 100644 --- a/server/internal/auth/openid.go +++ b/server/internal/auth/openid.go @@ -56,7 +56,7 @@ VALUES (@token_id, @expires, @token_hash, @oidc_provider, @oidc_client_type, @oi q.Add("client_id", clientID) q.Add("response_type", "code") q.Add("scope", "openid email profile") - q.Add("state", tokenEncoder.EncodeToString(append(tokenID[:], token...))) + q.Add("state", TokenEncoder.EncodeToString(append(tokenID[:], token...))) q.Add("redirect_uri", redirectURI) q.Add("code_challenge", codeChallenge) q.Add("code_challenge_method", "S256") @@ -70,7 +70,7 @@ func OpenIDValidateAuthCode(d db.Handler, state, authCode, redirectURI string) ( var tokenID uuid.UUID var tokenHash []byte - if b, err := tokenEncoder.DecodeString(state); err != nil { + if b, err := TokenEncoder.DecodeString(state); err != nil { return OpenIDClientNone, ErrTokenInvalid } else if len(b) < 16 { return OpenIDClientNone, ErrTokenInvalid diff --git a/server/internal/auth/token.go b/server/internal/auth/token.go index 6dcd9ee8..14be7f5c 100644 --- a/server/internal/auth/token.go +++ b/server/internal/auth/token.go @@ -12,10 +12,10 @@ import ( "github.com/jackc/pgx/v5" ) -var tokenEncoder = base32.StdEncoding.WithPadding(base32.NoPadding) +var TokenEncoder = base32.StdEncoding.WithPadding(base32.NoPadding) func PerformTokenLogin(db db.TxHandler, encodedToken string) (Auth, string, error) { - if b, err := tokenEncoder.DecodeString(encodedToken); err != nil { + if b, err := TokenEncoder.DecodeString(encodedToken); err != nil { return nil, "", ErrCredentialsInvalid } else if len(b) < 16 { return nil, "", ErrCredentialsInvalid diff --git a/server/internal/command/user/cmd.go b/server/internal/command/user/cmd.go index a93a5196..51a6ceb0 100644 --- a/server/internal/command/user/cmd.go +++ b/server/internal/command/user/cmd.go @@ -2,6 +2,7 @@ package user import ( "codeberg.org/shroff/phylum/server/internal/command/user/bookmarks" + "codeberg.org/shroff/phylum/server/internal/command/user/keys" "github.com/spf13/cobra" ) @@ -12,6 +13,7 @@ func SetupCommand() *cobra.Command { } cmd.PersistentFlags().StringP("user-email", "u", "", "User") cmd.AddCommand([]*cobra.Command{ + keys.SetupCommand(), bookmarks.SetupCommand(), }...) diff --git a/server/internal/command/user/keys/cmd.go b/server/internal/command/user/keys/cmd.go new file mode 100644 index 00000000..12b7398c --- /dev/null +++ b/server/internal/command/user/keys/cmd.go @@ -0,0 +1,90 @@ +package keys + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + "time" + + "codeberg.org/shroff/phylum/server/internal/auth" + "codeberg.org/shroff/phylum/server/internal/command/common" + "codeberg.org/shroff/phylum/server/internal/db" + "github.com/spf13/cobra" +) + +func SetupCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "keys", + Short: "Manager API Keys", + } + + cmd.AddCommand( + setupGenerateCommand(), + ) + + return cmd +} + +func setupGenerateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "generate", + Short: "Generate New API Key/Token", + Run: func(cmd *cobra.Command, args []string) { + u := common.User(cmd) + validity, _ := cmd.Flags().GetDuration("validity") + description, _ := cmd.Flags().GetString("description") + if description == "" { + description = "Generated via CLI on " + time.Now().Format(time.DateTime) + } + var scopes []string + if len(scopes) == 0 { + reader := bufio.NewReader(os.Stdin) + name, _ := cmd.Flags().GetString("name") + if name == "" { + fmt.Print("Comma-separated scopes [*]: ") + if val, err := reader.ReadString('\n'); err != nil { + fmt.Println("failed to read scopes: " + err.Error()) + os.Exit(1) + } else if len(strings.TrimSpace(val)) == 0 { + scopes = []string{"*"} + } else { + scopes = strings.Split(val, ",") + for i, s := range scopes { + scopes[i] = strings.TrimSpace(s) + } + } + } + } + + if err := db.Get(context.Background()).RunInTx(func(db db.TxHandler) error { + if token, _ := cmd.Flags().GetBool("token"); token { + if token, err := auth.GenerateEncodedAPIKey(db, u.ID, validity, description, scopes); err != nil { + return err + } else { + fmt.Println(token) + return nil + } + } else { + if id, key, err := auth.GenerateAPIKey(db, u.ID, validity, description, scopes); err != nil { + return err + } else { + fmt.Println("Key ID:", id.String()) + fmt.Println(" Key:", auth.TokenEncoder.EncodeToString(key)) + return nil + } + } + }); err != nil { + fmt.Println("unable to generate API key: " + err.Error()) + os.Exit(1) + } + }, + } + + cmd.Flags().BoolP("token", "t", false, "Generate API token instead of ID:Key pair") + cmd.Flags().Duration("validity", time.Duration(0), "Validity of API Key (0 means never)") + cmd.Flags().String("description", "", "Validity of API Key (0 means never)") + cmd.Flags().StringSlice("scopes", []string{}, "Scopes") + return cmd +}