mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add session token injection to provisioner (#7461)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package apikey
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tabbed/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
)
|
||||
|
||||
type CreateParams struct {
|
||||
UserID uuid.UUID
|
||||
LoginType database.LoginType
|
||||
DeploymentValues *codersdk.DeploymentValues
|
||||
|
||||
// Optional.
|
||||
ExpiresAt time.Time
|
||||
LifetimeSeconds int64
|
||||
Scope database.APIKeyScope
|
||||
TokenName string
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
// 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 = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
|
||||
} else {
|
||||
params.ExpiresAt = database.Now().Add(params.DeploymentValues.SessionDuration.Value())
|
||||
params.LifetimeSeconds = int64(params.DeploymentValues.SessionDuration.Value().Seconds())
|
||||
}
|
||||
}
|
||||
if params.LifetimeSeconds == 0 {
|
||||
params.LifetimeSeconds = int64(time.Until(params.ExpiresAt).Seconds())
|
||||
}
|
||||
|
||||
ip := net.ParseIP(params.RemoteAddr)
|
||||
if ip == nil {
|
||||
ip = net.IPv4(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
bitlen := len(ip) * 8
|
||||
|
||||
scope := database.APIKeyScopeAll
|
||||
if params.Scope != "" {
|
||||
scope = params.Scope
|
||||
}
|
||||
switch scope {
|
||||
case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect:
|
||||
default:
|
||||
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("invalid API key scope: %q", scope)
|
||||
}
|
||||
|
||||
token := fmt.Sprintf("%s-%s", keyID, keySecret)
|
||||
|
||||
return database.InsertAPIKeyParams{
|
||||
ID: keyID,
|
||||
UserID: params.UserID,
|
||||
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: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
HashedSecret: hashed[:],
|
||||
LoginType: params.LoginType,
|
||||
Scope: scope,
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package apikey_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/clibase"
|
||||
"github.com/coder/coder/coderd/apikey"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testcase struct {
|
||||
name string
|
||||
params apikey.CreateParams
|
||||
fail bool
|
||||
}
|
||||
|
||||
cases := []testcase{
|
||||
{
|
||||
name: "OK",
|
||||
params: apikey.CreateParams{
|
||||
UserID: uuid.New(),
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
DeploymentValues: &codersdk.DeploymentValues{},
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
LifetimeSeconds: int64(time.Hour.Seconds()),
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InvalidScope",
|
||||
params: apikey.CreateParams{
|
||||
UserID: uuid.New(),
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
DeploymentValues: &codersdk.DeploymentValues{},
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
LifetimeSeconds: int64(time.Hour.Seconds()),
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Scope: database.APIKeyScope("test"),
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "DeploymentSessionDuration",
|
||||
params: apikey.CreateParams{
|
||||
UserID: uuid.New(),
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
DeploymentValues: &codersdk.DeploymentValues{
|
||||
SessionDuration: clibase.Duration(time.Hour),
|
||||
},
|
||||
LifetimeSeconds: 0,
|
||||
ExpiresAt: time.Time{},
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultIP",
|
||||
params: apikey.CreateParams{
|
||||
UserID: uuid.New(),
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
DeploymentValues: &codersdk.DeploymentValues{},
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
LifetimeSeconds: int64(time.Hour.Seconds()),
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "",
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultScope",
|
||||
params: apikey.CreateParams{
|
||||
UserID: uuid.New(),
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
DeploymentValues: &codersdk.DeploymentValues{},
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
LifetimeSeconds: int64(time.Hour.Seconds()),
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Scope: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key, keystr, err := apikey.Generate(tc.params)
|
||||
if tc.fail {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, keystr)
|
||||
require.NotEmpty(t, key.ID)
|
||||
require.NotEmpty(t, key.HashedSecret)
|
||||
|
||||
// Assert the string secret is formatted correctly
|
||||
keytokens := strings.Split(keystr, "-")
|
||||
require.Len(t, keytokens, 2)
|
||||
require.Equal(t, key.ID, keytokens[0])
|
||||
|
||||
// Assert that the hashed secret is correct.
|
||||
hashed := sha256.Sum256([]byte(keytokens[1]))
|
||||
assert.ElementsMatch(t, hashed, key.HashedSecret[:])
|
||||
|
||||
assert.Equal(t, tc.params.UserID, key.UserID)
|
||||
assert.WithinDuration(t, database.Now(), key.CreatedAt, time.Second*5)
|
||||
assert.WithinDuration(t, database.Now(), key.UpdatedAt, time.Second*5)
|
||||
|
||||
if tc.params.LifetimeSeconds > 0 {
|
||||
assert.Equal(t, tc.params.LifetimeSeconds, key.LifetimeSeconds)
|
||||
} else if !tc.params.ExpiresAt.IsZero() {
|
||||
// Should not be a delta greater than 5 seconds.
|
||||
assert.InDelta(t, time.Until(tc.params.ExpiresAt).Seconds(), key.LifetimeSeconds, 5)
|
||||
} else {
|
||||
assert.Equal(t, int64(tc.params.DeploymentValues.SessionDuration.Value().Seconds()), key.LifetimeSeconds)
|
||||
}
|
||||
|
||||
if !tc.params.ExpiresAt.IsZero() {
|
||||
assert.Equal(t, tc.params.ExpiresAt.UTC(), key.ExpiresAt)
|
||||
} else if tc.params.LifetimeSeconds > 0 {
|
||||
assert.WithinDuration(t, database.Now().Add(time.Duration(tc.params.LifetimeSeconds)), key.ExpiresAt, time.Second*5)
|
||||
} else {
|
||||
assert.WithinDuration(t, database.Now().Add(tc.params.DeploymentValues.SessionDuration.Value()), key.ExpiresAt, time.Second*5)
|
||||
}
|
||||
|
||||
if tc.params.RemoteAddr != "" {
|
||||
assert.Equal(t, tc.params.RemoteAddr, key.IPAddress.IPNet.IP.String())
|
||||
} else {
|
||||
assert.Equal(t, "0.0.0.0", key.IPAddress.IPNet.IP.String())
|
||||
}
|
||||
|
||||
if tc.params.Scope != "" {
|
||||
assert.Equal(t, tc.params.Scope, key.Scope)
|
||||
} else {
|
||||
assert.Equal(t, database.APIKeyScopeAll, key.Scope)
|
||||
}
|
||||
|
||||
if tc.params.TokenName != "" {
|
||||
assert.Equal(t, tc.params.TokenName, key.TokenName)
|
||||
}
|
||||
if tc.params.LoginType != "" {
|
||||
assert.Equal(t, tc.params.LoginType, key.LoginType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user