mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
# Canonicalize API Key Scopes This PR introduces canonical API key scopes with a `coder:` namespace prefix to avoid collisions with low-level resource:action names. It: 1. Renames special API key scopes in the database: - `all` → `coder:all` - `application_connect` → `coder:application_connect` 2. Adds support for a new `scopes` field in the API key creation request, allowing multiple scopes to be specified while maintaining backward compatibility with the singular `scope` field. 3. Updates the API documentation to reflect these changes, including the new endpoint for listing public API key scopes. 4. Ensures backward compatibility by mapping between legacy and canonical scope names in relevant code paths.
185 lines
6.3 KiB
Go
185 lines
6.3 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// APIKey: do not ever return the HashedSecret
|
|
type APIKey struct {
|
|
ID string `json:"id" validate:"required"`
|
|
UserID uuid.UUID `json:"user_id" validate:"required" format:"uuid"`
|
|
LastUsed time.Time `json:"last_used" validate:"required" format:"date-time"`
|
|
ExpiresAt time.Time `json:"expires_at" validate:"required" format:"date-time"`
|
|
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
|
|
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
|
|
LoginType LoginType `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
|
|
Scope APIKeyScope `json:"scope" enums:"all,application_connect"` // Deprecated: use Scopes instead.
|
|
Scopes []APIKeyScope `json:"scopes"`
|
|
TokenName string `json:"token_name" validate:"required"`
|
|
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
|
}
|
|
|
|
// LoginType is the type of login used to create the API key.
|
|
type LoginType string
|
|
|
|
const (
|
|
LoginTypeUnknown LoginType = ""
|
|
LoginTypePassword LoginType = "password"
|
|
LoginTypeGithub LoginType = "github"
|
|
LoginTypeOIDC LoginType = "oidc"
|
|
LoginTypeToken LoginType = "token"
|
|
// LoginTypeNone is used if no login method is available for this user.
|
|
// If this is set, the user has no method of logging in.
|
|
// API keys can still be created by an owner and used by the user.
|
|
// These keys would use the `LoginTypeToken` type.
|
|
LoginTypeNone LoginType = "none"
|
|
)
|
|
|
|
type APIKeyScope string
|
|
|
|
type CreateTokenRequest struct {
|
|
Lifetime time.Duration `json:"lifetime"`
|
|
Scope APIKeyScope `json:"scope,omitempty"` // Deprecated: use Scopes instead.
|
|
Scopes []APIKeyScope `json:"scopes,omitempty"`
|
|
TokenName string `json:"token_name"`
|
|
}
|
|
|
|
// GenerateAPIKeyResponse contains an API key for a user.
|
|
type GenerateAPIKeyResponse struct {
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
// CreateToken generates an API key for the user ID provided with
|
|
// custom expiration. These tokens can be used for long-lived access,
|
|
// like for use with CI.
|
|
func (c *Client) CreateToken(ctx context.Context, userID string, req CreateTokenRequest) (GenerateAPIKeyResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), req)
|
|
if err != nil {
|
|
return GenerateAPIKeyResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode > http.StatusCreated {
|
|
return GenerateAPIKeyResponse{}, ReadBodyAsError(res)
|
|
}
|
|
|
|
var apiKey GenerateAPIKeyResponse
|
|
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
|
}
|
|
|
|
// CreateAPIKey generates an API key for the user ID provided.
|
|
// CreateToken should be used over CreateAPIKey. CreateToken allows better
|
|
// tracking of the token's usage and allows for custom expiration.
|
|
// Only use CreateAPIKey if you want to emulate the session created for
|
|
// a browser like login.
|
|
func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil)
|
|
if err != nil {
|
|
return GenerateAPIKeyResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode > http.StatusCreated {
|
|
return GenerateAPIKeyResponse{}, ReadBodyAsError(res)
|
|
}
|
|
|
|
var apiKey GenerateAPIKeyResponse
|
|
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
|
}
|
|
|
|
type TokensFilter struct {
|
|
IncludeAll bool `json:"include_all"`
|
|
}
|
|
|
|
type APIKeyWithOwner struct {
|
|
APIKey
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
type TokenConfig struct {
|
|
MaxTokenLifetime time.Duration `json:"max_token_lifetime"`
|
|
}
|
|
|
|
// asRequestOption returns a function that can be used in (*Client).Request.
|
|
// It modifies the request query parameters.
|
|
func (f TokensFilter) asRequestOption() RequestOption {
|
|
return func(r *http.Request) {
|
|
q := r.URL.Query()
|
|
q.Set("include_all", fmt.Sprintf("%t", f.IncludeAll))
|
|
r.URL.RawQuery = q.Encode()
|
|
}
|
|
}
|
|
|
|
// Tokens list machine API keys.
|
|
func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) ([]APIKeyWithOwner, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil, filter.asRequestOption())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode > http.StatusOK {
|
|
return nil, ReadBodyAsError(res)
|
|
}
|
|
apiKey := []APIKeyWithOwner{}
|
|
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
|
}
|
|
|
|
// APIKeyByID returns the api key by id.
|
|
func (c *Client) APIKeyByID(ctx context.Context, userID string, id string) (*APIKey, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode > http.StatusCreated {
|
|
return nil, ReadBodyAsError(res)
|
|
}
|
|
apiKey := &APIKey{}
|
|
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
|
}
|
|
|
|
// APIKeyByName returns the api key by name.
|
|
func (c *Client) APIKeyByName(ctx context.Context, userID string, name string) (*APIKey, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens/%s", userID, name), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode > http.StatusCreated {
|
|
return nil, ReadBodyAsError(res)
|
|
}
|
|
apiKey := &APIKey{}
|
|
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
|
}
|
|
|
|
// DeleteAPIKey deletes API key by id.
|
|
func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) error {
|
|
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode > http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetTokenConfig returns deployment options related to token management
|
|
func (c *Client) GetTokenConfig(ctx context.Context, userID string) (TokenConfig, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens/tokenconfig", userID), nil)
|
|
if err != nil {
|
|
return TokenConfig{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode > http.StatusOK {
|
|
return TokenConfig{}, ReadBodyAsError(res)
|
|
}
|
|
tokenConfig := TokenConfig{}
|
|
return tokenConfig, json.NewDecoder(res.Body).Decode(&tokenConfig)
|
|
}
|