mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
fix(ssh-key): title keys by their comment and recognize private keys
ssh-key create - required --title although the key carries a comment, and a private key failed with "supply exactly one valid SSH public key". The title now defaults to the key comment, then the computer's name, the file argument is optional (stdin when piped), and a private key points to its .pub file. Adding a registered key names the key it duplicates. ssh-key list shows ID, TITLE, FINGERPRINT and ADDED with a header, view shows the key instead of its list row, delete asks with the key's title and fingerprint, and mutations print ✓ lines. create gains the gh alias add and -t for --title, key IDs complete, and every command has examples. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
5 files changed
+269
-62
No files matched your search
@@ -557,10 +557,6 @@ const (
|
|||||||
secret delete
|
secret delete
|
||||||
secret list
|
secret list
|
||||||
secret set
|
secret set
|
||||||
ssh-key create
|
|
||||||
ssh-key delete
|
|
||||||
ssh-key list
|
|
||||||
ssh-key view
|
|
||||||
status
|
status
|
||||||
time create
|
time create
|
||||||
time delete
|
time delete
|
||||||
@@ -603,7 +599,6 @@ const (
|
|||||||
run artifact
|
run artifact
|
||||||
search
|
search
|
||||||
secret
|
secret
|
||||||
ssh-key
|
|
||||||
time
|
time
|
||||||
variable
|
variable
|
||||||
wiki
|
wiki
|
||||||
|
|||||||
+177
-42
@@ -4,9 +4,11 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -18,55 +20,196 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (a *application) sshKeyCommand() *cobra.Command {
|
func (a *application) sshKeyCommand() *cobra.Command {
|
||||||
group := &cobra.Command{Use: "ssh-key", Short: "Manage account SSH public keys"}
|
group := &cobra.Command{Use: "ssh-key", Short: "Manage SSH keys", Long: "Manage the SSH public keys of your account, which authenticate Git operations over SSH."}
|
||||||
|
group.AddCommand(a.sshKeyListCommand(), a.sshKeyViewCommand(), a.sshKeyCreateCommand(), a.sshKeyDeleteCommand())
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
// sshKeyLabel names a key in messages: `SSH key "laptop" (SHA256:…)`.
|
||||||
|
func sshKeyLabel(key services.SSHKey) string {
|
||||||
|
return fmt.Sprintf("SSH key %q (%s)", key.Title, key.Fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeSSHKeys completes the key IDs of the account with their titles.
|
||||||
|
func (a *application) completeSSHKeys() cobra.CompletionFunc {
|
||||||
|
return a.apiCompletion(false, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
|
||||||
|
keys, err := (services.SSHKeys{Client: t.client}).List(ctx, completionLimit, 1)
|
||||||
|
return describe(keys.Items, func(key services.SSHKey) (string, string) {
|
||||||
|
return strconv.FormatInt(key.ID, 10), key.Title + " (" + key.Fingerprint + ")"
|
||||||
|
}), err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) sshKeyListCommand() *cobra.Command {
|
||||||
var limit, page int
|
var limit, page int
|
||||||
list := &cobra.Command{Use: "list", Short: "List account SSH keys", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
cmd := &cobra.Command{
|
||||||
|
Use: "list", Short: "List SSH keys", Args: argsExact(0),
|
||||||
|
Example: `$ tea ssh-key list
|
||||||
|
$ tea ssh-key list --json title,fingerprint`,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
target, err := a.resolve(cmd.Context(), false)
|
target, err := a.resolve(cmd.Context(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
items, err := (services.SSHKeys{Client: target.client}).List(cmd.Context(), limit, page)
|
keys, err := (services.SSHKeys{Client: target.client}).List(cmd.Context(), limit, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeList(a, cmd, items, sshKeyRow)
|
account := target.login()
|
||||||
}}
|
return writeTable(a, cmd, keys.Items, listSpec[services.SSHKey]{
|
||||||
pageFlags(list, &limit, &page)
|
Columns: []terminal.Column{{Header: "ID"}, {Header: "Title", Flex: true}, {Header: "Fingerprint"}, {Header: "Added"}},
|
||||||
group.AddCommand(list)
|
Row: func(key services.SSHKey) []terminal.Cell {
|
||||||
|
return []terminal.Cell{{Text: strconv.FormatInt(key.ID, 10), Link: webURL(target, "user", "settings", "keys")}, {Text: key.Title, Bold: true}, {Text: key.Fingerprint, Muted: true}, {Time: terminal.ParseTime(key.CreatedAt)}}
|
||||||
|
},
|
||||||
|
Context: "SSH keys of " + account,
|
||||||
|
Empty: "no SSH keys for " + account + "; add one with: tea ssh-key add ~/.ssh/id_ed25519.pub",
|
||||||
|
}, servicePage(keys, page))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pageFlags(cmd, &limit, &page)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) sshKeyViewCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "view <id>", Short: "View an SSH key", Args: argsExact(1), ValidArgsFunction: a.completeSSHKeys(),
|
||||||
|
Example: `$ tea ssh-key view 2
|
||||||
|
# print the public key
|
||||||
|
$ tea ssh-key view 2 --jq .key`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
id, err := number(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target, err := a.resolve(cmd.Context(), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key, err := (services.SSHKeys{Client: target.client}).View(cmd.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d := a.display()
|
||||||
|
return writeView(a, cmd, key, func(key services.SSHKey) terminal.View {
|
||||||
|
added := timeField("Added", key.CreatedAt, d)
|
||||||
|
meta := ""
|
||||||
|
if added.Value != "" {
|
||||||
|
meta = "added " + added.Value
|
||||||
|
}
|
||||||
|
added.PipedOnly = true
|
||||||
|
algorithm, _, _ := strings.Cut(key.Key, " ")
|
||||||
|
return terminal.View{
|
||||||
|
Title: key.Title, ID: fmt.Sprintf("(ID %d)", key.ID), Meta: meta,
|
||||||
|
Fields: []terminal.Field{{Label: "Fingerprint", Value: key.Fingerprint}, {Label: "Type", Value: algorithm}, added, {Label: "Key", Value: key.Key}},
|
||||||
|
Bodyless: true, Kind: "SSH key", URL: webURL(target, "user", "settings", "keys"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// publicKey reads one SSH public key from data, which came from source, and returns it with its
|
||||||
|
// comment. It recognizes a private key and points to its public half.
|
||||||
|
func publicKey(data []byte, source string) (ssh.PublicKey, string, error) {
|
||||||
|
if bytes.Contains(data, []byte("PRIVATE KEY-----")) {
|
||||||
|
hint := "pipe the public key (.pub) instead"
|
||||||
|
if source != "-" {
|
||||||
|
hint = "add its public key instead: tea ssh-key add " + source + ".pub"
|
||||||
|
}
|
||||||
|
return nil, "", clierr.Wrap(clierr.Usage, fmt.Errorf("%s holds a private key; %s", sshKeySource(source), hint))
|
||||||
|
}
|
||||||
|
key, comment, _, rest, err := ssh.ParseAuthorizedKey(data)
|
||||||
|
if err != nil || len(bytes.TrimSpace(rest)) != 0 {
|
||||||
|
return nil, "", clierr.Wrap(clierr.Usage, fmt.Errorf("%s must hold exactly one valid SSH public key", sshKeySource(source)))
|
||||||
|
}
|
||||||
|
return key, comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshKeySource(source string) string {
|
||||||
|
if source == "-" {
|
||||||
|
return "stdin"
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) sshKeyCreateCommand() *cobra.Command {
|
||||||
var title string
|
var title string
|
||||||
create := &cobra.Command{Use: "create <public-key-file>", Short: "Register an SSH public key; - reads stdin", Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
cmd := &cobra.Command{
|
||||||
data, err := readFileOrStdin(a.deps.Streams.In, args[0])
|
Use: "create [<file>]", Aliases: []string{"add"}, Short: "Add an SSH public key from a file or stdin", Args: usageArgs(cobra.MaximumNArgs(1)),
|
||||||
|
Long: `Add an SSH public key to your account. Without a file, or with -, the key is read from stdin.
|
||||||
|
|
||||||
|
The title defaults to the key's comment, such as alice@laptop, or else to this computer's name.`,
|
||||||
|
Example: `$ tea ssh-key add ~/.ssh/id_ed25519.pub
|
||||||
|
$ tea ssh-key add ~/.ssh/id_ed25519.pub --title "Work laptop"
|
||||||
|
$ cat key.pub | tea ssh-key add`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
source := "-"
|
||||||
|
if len(args) > 0 {
|
||||||
|
source = args[0]
|
||||||
|
}
|
||||||
|
if source == "-" && a.deps.Streams.InTTY {
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("supply a public key file, such as ~/.ssh/id_ed25519.pub, or pipe a key to stdin"))
|
||||||
|
}
|
||||||
|
data, err := readFileOrStdin(a.deps.Streams.In, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read public key: %w", err)
|
return fmt.Errorf("read public key: %w", err)
|
||||||
}
|
}
|
||||||
_, _, _, rest, err := ssh.ParseAuthorizedKey(data)
|
key, comment, err := publicKey(data, source)
|
||||||
if err != nil || len(strings.TrimSpace(string(rest))) != 0 {
|
if err != nil {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("supply exactly one valid SSH public key"))
|
return err
|
||||||
}
|
}
|
||||||
|
if !cmd.Flags().Changed("title") {
|
||||||
|
title = comment
|
||||||
if title == "" {
|
if title == "" {
|
||||||
if args[0] == "-" {
|
title, _ = os.Hostname()
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--title is required when reading a key from stdin"))
|
|
||||||
}
|
}
|
||||||
base := filepath.Base(args[0])
|
|
||||||
title = strings.TrimSuffix(base, filepath.Ext(base))
|
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(title) == "" {
|
if strings.TrimSpace(title) == "" {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("key title cannot be empty"))
|
return clierr.Wrap(clierr.Usage, errors.New("key title cannot be empty; pass --title"))
|
||||||
}
|
}
|
||||||
target, err := a.resolve(cmd.Context(), false)
|
target, err := a.resolve(cmd.Context(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
item, err := (services.SSHKeys{Client: target.client}).Create(cmd.Context(), title, strings.TrimSpace(string(data)))
|
service := services.SSHKeys{Client: target.client}
|
||||||
|
created, err := service.Create(cmd.Context(), title, strings.TrimSpace(string(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return a.registeredKey(cmd.Context(), service, key, services.ClassifyRequest(err))
|
||||||
|
}
|
||||||
|
a.done(cmd, "Added %s", sshKeyLabel(created))
|
||||||
|
return a.jsonResult(cmd, created)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&title, "title", "t", "", "Key title (defaults to the key comment, then the computer's name)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// registeredKey names the key of the account that already holds key when Gitea refuses to add it.
|
||||||
|
func (a *application) registeredKey(ctx context.Context, service services.SSHKeys, key ssh.PublicKey, err error) error {
|
||||||
|
conflict, ok := errors.AsType[*services.ConflictError](err)
|
||||||
|
if !ok || conflict.Reason != services.ReasonKeyInUse {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeItem(a, cmd, item, sshKeyRow)
|
keys, listErr := service.List(ctx, maxLimit, 1)
|
||||||
}}
|
if listErr != nil {
|
||||||
create.Flags().StringVar(&title, "title", "", "Key title (defaults to filename)")
|
return conflict
|
||||||
group.AddCommand(create)
|
}
|
||||||
for _, verb := range []string{"view", "delete"} {
|
fingerprint := ssh.FingerprintSHA256(key)
|
||||||
command := &cobra.Command{Use: verb + " <id>", Short: titleCase(verb) + " an SSH key", Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
for _, registered := range keys.Items {
|
||||||
|
if registered.Fingerprint == fingerprint {
|
||||||
|
conflict.Hint = fmt.Sprintf("it is your SSH key %q (ID %d)", registered.Title, registered.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) sshKeyDeleteCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "delete <id>", Short: "Delete an SSH key", Args: argsExact(1), ValidArgsFunction: a.completeSSHKeys(),
|
||||||
|
Example: `$ tea ssh-key delete 2
|
||||||
|
$ tea ssh-key delete 2 --yes`,
|
||||||
|
Annotations: map[string]string{"tea.text": "SSH key deletion does not return a resource", "tea.confirm": "true"},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
id, err := number(args[0])
|
id, err := number(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -76,26 +219,18 @@ func (a *application) sshKeyCommand() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
service := services.SSHKeys{Client: target.client}
|
service := services.SSHKeys{Client: target.client}
|
||||||
if verb == "delete" {
|
key, err := service.View(cmd.Context(), id)
|
||||||
if err := a.confirm(cmd, "Delete SSH key "+args[0]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return service.Delete(cmd.Context(), id)
|
|
||||||
}
|
|
||||||
item, err := service.View(cmd.Context(), id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeItem(a, cmd, item, sshKeyRow)
|
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Delete " + sshKeyLabel(key) + "?", NonTTY: "delete " + sshKeyLabel(key)}); err != nil {
|
||||||
}}
|
return err
|
||||||
if verb == "delete" {
|
|
||||||
command.Annotations = textOutput("SSH key deletion does not return a resource")
|
|
||||||
}
|
}
|
||||||
group.AddCommand(command)
|
if err := service.Delete(cmd.Context(), id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.done(cmd, "Deleted %s", sshKeyLabel(key))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return group
|
|
||||||
}
|
|
||||||
|
|
||||||
func sshKeyRow(key services.SSHKey) []terminal.Cell {
|
|
||||||
return terminal.Row(strconv.FormatInt(key.ID, 10), key.Title, key.Fingerprint)
|
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,95 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSSHKeyValidationPrecedesTargetResolution(t *testing.T) {
|
func TestSSHKeyValidationPrecedesTargetResolution(t *testing.T) {
|
||||||
for _, content := range []string{"", "not a public key", "-----BEGIN OPENSSH PRIVATE KEY-----"} {
|
for _, tc := range []struct{ content, want string }{
|
||||||
t.Run(content, func(t *testing.T) {
|
{"", "must hold exactly one valid SSH public key"},
|
||||||
path := filepath.Join(t.TempDir(), "invalid.pub")
|
{"not a public key", "must hold exactly one valid SSH public key"},
|
||||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
{"-----BEGIN OPENSSH PRIVATE KEY-----\nAAAA\n-----END OPENSSH PRIVATE KEY-----\n", "holds a private key; add its public key instead: tea ssh-key add "},
|
||||||
requireUsageBeforeRequests(t, "exactly one valid SSH public key", "ssh-key", "create", path)
|
} {
|
||||||
|
t.Run(tc.content, func(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "id_ed25519")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(tc.content), 0o600))
|
||||||
|
requireUsageBeforeRequests(t, tc.want, "ssh-key", "create", path)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testPublicKey returns a new public key in authorized_keys format with comment, and its fingerprint.
|
||||||
|
func testPublicKey(t *testing.T, comment string) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
public, _, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
key, err := ssh.NewPublicKey(public)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))) + " " + comment, ssh.FingerprintSHA256(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHKeyAddTitlesKeysByTheirComment(t *testing.T) {
|
||||||
|
key, fingerprint := testPublicKey(t, "alice@laptop")
|
||||||
|
var titles []string
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodPost:
|
||||||
|
var body map[string]any
|
||||||
|
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
|
||||||
|
titles = append(titles, body["title"].(string))
|
||||||
|
if len(titles) > 1 {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
_, _ = w.Write([]byte(`{"message":"Key content has been used as non-deploy key"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = fmt.Fprintf(w, `{"id":3,"title":%q,"fingerprint":%q}`, body["title"], fingerprint)
|
||||||
|
case http.MethodGet:
|
||||||
|
_, _ = fmt.Fprintf(w, `[{"id":3,"title":"alice@laptop","fingerprint":%q}]`, fingerprint)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, _, diagnostics := serverDeps(t, server.URL)
|
||||||
|
deps.Streams.In = bytes.NewBufferString(key + "\n")
|
||||||
|
deps.Streams.OutTTY = true
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"ssh-key", "add"}))
|
||||||
|
require.Equal(t, []string{"alice@laptop"}, titles)
|
||||||
|
require.Equal(t, fmt.Sprintf("✓ Added SSH key \"alice@laptop\" (%s)\n", fingerprint), withoutANSI(diagnostics.String()))
|
||||||
|
|
||||||
|
deps.Streams.In = bytes.NewBufferString(key)
|
||||||
|
err := Execute(context.Background(), deps, []string{"ssh-key", "create", "-", "--title", "again"})
|
||||||
|
require.EqualError(t, err, `this SSH key is already registered; it is your SSH key "alice@laptop" (ID 3)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHKeyDeleteNamesTheKey(t *testing.T) {
|
||||||
|
var deleted bool
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
deleted = true
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"id":2,"title":"uxkey","fingerprint":"SHA256:aR5L"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, _, _ := serverDeps(t, server.URL)
|
||||||
|
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
|
||||||
|
prompt := &scriptedPrompter{}
|
||||||
|
deps.Prompter = prompt
|
||||||
|
require.Error(t, Execute(context.Background(), deps, []string{"ssh-key", "delete", "2"}))
|
||||||
|
require.Equal(t, []string{`Delete SSH key "uxkey" (SHA256:aR5L)?`}, prompt.confirmations)
|
||||||
|
require.False(t, deleted)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
gitea "gitea.dev/sdk"
|
gitea "gitea.dev/sdk"
|
||||||
"gitea.dev/tea/internal/transport"
|
"gitea.dev/tea/internal/transport"
|
||||||
@@ -27,15 +28,15 @@ func sshKeyFrom(key *gitea.PublicKey) SSHKey {
|
|||||||
// SSHKeys manages public keys for the authenticated account.
|
// SSHKeys manages public keys for the authenticated account.
|
||||||
type SSHKeys struct{ Client *transport.Client }
|
type SSHKeys struct{ Client *transport.Client }
|
||||||
|
|
||||||
// List returns a non-nil page of public keys.
|
// List returns a page of public keys.
|
||||||
func (s SSHKeys) List(ctx context.Context, limit, page int) ([]SSHKey, error) {
|
func (s SSHKeys) List(ctx context.Context, limit, page int) (Page[SSHKey], error) {
|
||||||
keys, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.PublicKey, *gitea.Response, error) {
|
keys, err := listPage(limit, page, func(limit, page int) ([]*gitea.PublicKey, http.Header, error) {
|
||||||
|
keys, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.PublicKey, *gitea.Response, error) {
|
||||||
return c.Users.ListMyPublicKeys(ctx, gitea.ListPublicKeysOptions{ListOptions: listOptions(limit, page)})
|
return c.Users.ListMyPublicKeys(ctx, gitea.ListPublicKeysOptions{ListOptions: listOptions(limit, page)})
|
||||||
})
|
})
|
||||||
if err != nil {
|
return keys, headerOf(resp), err
|
||||||
return nil, err
|
})
|
||||||
}
|
return convertPage(keys, sshKeyFrom), err
|
||||||
return convertAll(keys, sshKeyFrom), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// View retrieves a public key by ID.
|
// View retrieves a public key by ID.
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ func TestSSHKeyListPaginatesAndMapsKeys(t *testing.T) {
|
|||||||
service := SSHKeys{Client: client}
|
service := SSHKeys{Client: client}
|
||||||
keys, err := service.List(context.Background(), 20, 1)
|
keys, err := service.List(context.Background(), 20, 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []SSHKey{{ID: 3, Title: "laptop", Key: "ssh-ed25519 AAAA", Fingerprint: "SHA256:abc", CreatedAt: "2026-10-01T12:00:00Z", ReadOnly: true}}, keys)
|
require.Equal(t, []SSHKey{{ID: 3, Title: "laptop", Key: "ssh-ed25519 AAAA", Fingerprint: "SHA256:abc", CreatedAt: "2026-10-01T12:00:00Z", ReadOnly: true}}, keys.Items)
|
||||||
keys, err = service.List(context.Background(), 20, 2)
|
keys, err = service.List(context.Background(), 20, 2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, keys)
|
require.NotNil(t, keys.Items)
|
||||||
require.Empty(t, keys)
|
require.Empty(t, keys.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSSHKeyViewCreateAndDelete(t *testing.T) {
|
func TestSSHKeyViewCreateAndDelete(t *testing.T) {
|
||||||
|
|||||||
Reference in new issue
Block a user