mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
# Add API key allow_list for resource-scoped tokens This PR adds support for API key allow lists, enabling tokens to be scoped to specific resources. The implementation: 1. Adds a new `allow_list` field to the `CreateTokenRequest` struct, allowing clients to specify resource-specific scopes when creating API tokens 2. Implements `APIAllowListTarget` type to represent resource targets in the format `<type>:<id>` with support for wildcards 3. Adds validation and normalization logic for allow lists to handle wildcards and deduplication 4. Integrates with RBAC by creating an `APIKeyEffectiveScope` that merges API key scopes with allow list restrictions 5. Updates API documentation and TypeScript types to reflect the new functionality This feature enables creating tokens that are limited to specific resources (like workspaces or templates) by ID, making it possible to create more granular API tokens with limited access.
145 lines
3.9 KiB
Go
145 lines
3.9 KiB
Go
package apikey
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/sqlc-dev/pqtype"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
"github.com/coder/coder/v2/cryptorand"
|
|
)
|
|
|
|
type CreateParams struct {
|
|
UserID uuid.UUID
|
|
LoginType database.LoginType
|
|
// DefaultLifetime is configured in DeploymentValues.
|
|
// It is used if both ExpiresAt and LifetimeSeconds are not set.
|
|
DefaultLifetime time.Duration
|
|
|
|
// Optional.
|
|
ExpiresAt time.Time
|
|
LifetimeSeconds int64
|
|
|
|
// Scope is legacy single-scope input kept for backward compatibility.
|
|
//
|
|
// Deprecated: use Scopes instead.
|
|
Scope database.APIKeyScope
|
|
// Scopes is the full list of scopes to attach to the key.
|
|
Scopes database.APIKeyScopes
|
|
TokenName string
|
|
RemoteAddr string
|
|
// AllowList is an optional, normalized allow-list
|
|
// of resource type and uuid entries. If empty, defaults to wildcard.
|
|
AllowList database.AllowList
|
|
}
|
|
|
|
// Generate generates an API key, returning the key as a string as well as the
|
|
// database representation. It is the responsibility of the caller to insert it
|
|
// into the database.
|
|
func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error) {
|
|
keyID, keySecret, err := generateKey()
|
|
if err != nil {
|
|
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("generate API key: %w", err)
|
|
}
|
|
|
|
hashed := sha256.Sum256([]byte(keySecret))
|
|
|
|
// Default expires at to now+lifetime, or use the configured value if not
|
|
// set.
|
|
if params.ExpiresAt.IsZero() {
|
|
if params.LifetimeSeconds != 0 {
|
|
params.ExpiresAt = dbtime.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
|
|
} else {
|
|
params.ExpiresAt = dbtime.Now().Add(params.DefaultLifetime)
|
|
params.LifetimeSeconds = int64(params.DefaultLifetime.Seconds())
|
|
}
|
|
}
|
|
if params.LifetimeSeconds == 0 {
|
|
params.LifetimeSeconds = int64(time.Until(params.ExpiresAt).Seconds())
|
|
}
|
|
|
|
if len(params.AllowList) == 0 {
|
|
params.AllowList = database.AllowList{{Type: policy.WildcardSymbol, ID: policy.WildcardSymbol}}
|
|
}
|
|
|
|
ip := net.ParseIP(params.RemoteAddr)
|
|
if ip == nil {
|
|
ip = net.IPv4(0, 0, 0, 0)
|
|
}
|
|
|
|
bitlen := len(ip) * 8
|
|
|
|
var scopes database.APIKeyScopes
|
|
switch {
|
|
case len(params.Scopes) > 0:
|
|
scopes = params.Scopes
|
|
case params.Scope != "":
|
|
var scope database.APIKeyScope
|
|
switch params.Scope {
|
|
case "all":
|
|
scope = database.ApiKeyScopeCoderAll
|
|
case "application_connect":
|
|
scope = database.ApiKeyScopeCoderApplicationConnect
|
|
default:
|
|
scope = params.Scope
|
|
}
|
|
scopes = database.APIKeyScopes{scope}
|
|
default:
|
|
// Default to coder:all scope for backward compatibility.
|
|
scopes = database.APIKeyScopes{database.ApiKeyScopeCoderAll}
|
|
}
|
|
|
|
for _, s := range scopes {
|
|
if !s.Valid() {
|
|
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("invalid API key scope: %q", s)
|
|
}
|
|
}
|
|
|
|
token := fmt.Sprintf("%s-%s", keyID, keySecret)
|
|
|
|
return database.InsertAPIKeyParams{
|
|
ID: keyID,
|
|
UserID: params.UserID,
|
|
LastUsed: time.Time{},
|
|
LifetimeSeconds: params.LifetimeSeconds,
|
|
IPAddress: pqtype.Inet{
|
|
IPNet: net.IPNet{
|
|
IP: ip,
|
|
Mask: net.CIDRMask(bitlen, bitlen),
|
|
},
|
|
Valid: true,
|
|
},
|
|
// Make sure in UTC time for common time zone
|
|
ExpiresAt: params.ExpiresAt.UTC(),
|
|
CreatedAt: dbtime.Now(),
|
|
UpdatedAt: dbtime.Now(),
|
|
HashedSecret: hashed[:],
|
|
LoginType: params.LoginType,
|
|
Scopes: scopes,
|
|
AllowList: params.AllowList,
|
|
TokenName: params.TokenName,
|
|
}, token, nil
|
|
}
|
|
|
|
// generateKey a new ID and secret for an API key.
|
|
func generateKey() (id string, secret string, err error) {
|
|
// Length of an API Key ID.
|
|
id, err = cryptorand.String(10)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
// Length of an API Key secret.
|
|
secret, err = cryptorand.String(22)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return id, secret, nil
|
|
}
|