mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
4664318be2
The client/tailscale is a stable-ish API we try not to break. Revert the Client.CreateKey method as it was and add a new CreateKeyWithExpiry method to do the new thing. And document the expiry field and enforce that the time.Duration can't be between in range greater than 0 and less than a second. Updates #7143 Updates #8124 (reverts it, effectively) Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
167 lines
4.6 KiB
Go
167 lines
4.6 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package tailscale
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// Key represents a Tailscale API or auth key.
|
|
type Key struct {
|
|
ID string `json:"id"`
|
|
Created time.Time `json:"created"`
|
|
Expires time.Time `json:"expires"`
|
|
Capabilities KeyCapabilities `json:"capabilities"`
|
|
}
|
|
|
|
// KeyCapabilities are the capabilities of a Key.
|
|
type KeyCapabilities struct {
|
|
Devices KeyDeviceCapabilities `json:"devices,omitempty"`
|
|
}
|
|
|
|
// KeyDeviceCapabilities are the device-related capabilities of a Key.
|
|
type KeyDeviceCapabilities struct {
|
|
Create KeyDeviceCreateCapabilities `json:"create"`
|
|
}
|
|
|
|
// KeyDeviceCreateCapabilities are the device creation capabilities of a Key.
|
|
type KeyDeviceCreateCapabilities struct {
|
|
Reusable bool `json:"reusable"`
|
|
Ephemeral bool `json:"ephemeral"`
|
|
Preauthorized bool `json:"preauthorized"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
}
|
|
|
|
// Keys returns the list of keys for the current user.
|
|
func (c *Client) Keys(ctx context.Context) ([]string, error) {
|
|
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
|
|
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b, resp, err := c.sendRequest(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, handleErrorResponse(b, resp)
|
|
}
|
|
|
|
var keys struct {
|
|
Keys []*Key `json:"keys"`
|
|
}
|
|
if err := json.Unmarshal(b, &keys); err != nil {
|
|
return nil, err
|
|
}
|
|
ret := make([]string, 0, len(keys.Keys))
|
|
for _, k := range keys.Keys {
|
|
ret = append(ret, k.ID)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// CreateKey creates a new key for the current user. Currently, only auth keys
|
|
// can be created. It returns the secret key itself, which cannot be retrieved again
|
|
// later, and the key metadata.
|
|
//
|
|
// To create a key with a specific expiry, use CreateKeyWithExpiry.
|
|
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (keySecret string, keyMeta *Key, _ error) {
|
|
return c.CreateKeyWithExpiry(ctx, caps, 0)
|
|
}
|
|
|
|
// CreateKeyWithExpiry is like CreateKey, but allows specifying a expiration time.
|
|
//
|
|
// The time is truncated to a whole number of seconds. If zero, that means no expiration.
|
|
func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities, expiry time.Duration) (keySecret string, keyMeta *Key, _ error) {
|
|
|
|
// convert expirySeconds to an int64 (seconds)
|
|
expirySeconds := int64(expiry.Seconds())
|
|
if expirySeconds < 0 {
|
|
return "", nil, fmt.Errorf("expiry must be positive")
|
|
}
|
|
if expirySeconds == 0 && expiry != 0 {
|
|
return "", nil, fmt.Errorf("non-zero expiry must be at least one second")
|
|
}
|
|
|
|
keyRequest := struct {
|
|
Capabilities KeyCapabilities `json:"capabilities"`
|
|
ExpirySeconds int64 `json:"expirySeconds,omitempty"`
|
|
}{caps, int64(expirySeconds)}
|
|
bs, err := json.Marshal(keyRequest)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(bs))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
b, resp, err := c.sendRequest(req)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", nil, handleErrorResponse(b, resp)
|
|
}
|
|
|
|
var key struct {
|
|
Key
|
|
Secret string `json:"key"`
|
|
}
|
|
if err := json.Unmarshal(b, &key); err != nil {
|
|
return "", nil, err
|
|
}
|
|
return key.Secret, &key.Key, nil
|
|
}
|
|
|
|
// Key returns the metadata for the given key ID. Currently, capabilities are
|
|
// only returned for auth keys, API keys only return general metadata.
|
|
func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
|
|
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
|
|
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b, resp, err := c.sendRequest(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, handleErrorResponse(b, resp)
|
|
}
|
|
|
|
var key Key
|
|
if err := json.Unmarshal(b, &key); err != nil {
|
|
return nil, err
|
|
}
|
|
return &key, nil
|
|
}
|
|
|
|
// DeleteKey deletes the key with the given ID.
|
|
func (c *Client) DeleteKey(ctx context.Context, id string) error {
|
|
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
|
|
req, err := http.NewRequestWithContext(ctx, "DELETE", path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b, resp, err := c.sendRequest(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return handleErrorResponse(b, resp)
|
|
}
|
|
return nil
|
|
}
|