feat: add session token injection to provisioner (#7461)

This commit is contained in:
Jon Ayers
2023-05-17 23:29:22 -05:00
committed by GitHub
parent 00a2413c03
commit ec5ef51b49
13 changed files with 578 additions and 219 deletions
+110
View File
@@ -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
}
+164
View File
@@ -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)
}
})
}
}