coderd/apikey/apikey_test.go
Rama main · réplica de lectura del repositorio original.
package apikey_test
import (
"slices"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
)
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,
DefaultLifetime: time.Duration(0),
ExpiresAt: time.Now().Add(time.Hour),
LifetimeSeconds: int64(time.Hour.Seconds()),
TokenName: "hello",
RemoteAddr: "1.2.3.4",
Scope: database.ApiKeyScopeCoderApplicationConnect,
},
},
{
name: "InvalidScope",
params: apikey.CreateParams{
UserID: uuid.New(),
LoginType: database.LoginTypeOIDC,
DefaultLifetime: time.Duration(0),
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,
DefaultLifetime: time.Hour,
LifetimeSeconds: 0,
ExpiresAt: time.Time{},
TokenName: "hello",
RemoteAddr: "1.2.3.4",
Scope: database.ApiKeyScopeCoderApplicationConnect,
},
},
{
name: "LifetimeSeconds",
params: apikey.CreateParams{
UserID: uuid.New(),
LoginType: database.LoginTypeOIDC,
DefaultLifetime: time.Duration(0),
LifetimeSeconds: int64(time.Hour.Seconds()),
ExpiresAt: time.Time{},
TokenName: "hello",
RemoteAddr: "1.2.3.4",
Scope: database.ApiKeyScopeCoderApplicationConnect,
},
},
{
name: "DefaultIP",
params: apikey.CreateParams{
UserID: uuid.New(),
LoginType: database.LoginTypeOIDC,
DefaultLifetime: time.Duration(0),
ExpiresAt: time.Now().Add(time.Hour),
LifetimeSeconds: int64(time.Hour.Seconds()),
TokenName: "hello",
RemoteAddr: "",
Scope: database.ApiKeyScopeCoderApplicationConnect,
},
},
{
name: "DefaultScope",
params: apikey.CreateParams{
UserID: uuid.New(),
LoginType: database.LoginTypeOIDC,
DefaultLifetime: time.Duration(0),
ExpiresAt: time.Now().Add(time.Hour),
LifetimeSeconds: int64(time.Hour.Seconds()),
TokenName: "hello",
RemoteAddr: "1.2.3.4",
Scope: "",
},
},
}
for _, tc := range cases {
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.
equal := apikey.ValidateHash(key.HashedSecret, keytokens[1])
require.True(t, equal, "valid secret")
assert.Equal(t, tc.params.UserID, key.UserID)
assert.WithinDuration(t, dbtime.Now(), key.CreatedAt, time.Second*5)
assert.WithinDuration(t, dbtime.Now(), key.UpdatedAt, time.Second*5)
switch {
case tc.params.LifetimeSeconds > 0:
assert.Equal(t, tc.params.LifetimeSeconds, key.LifetimeSeconds)
case !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)
default:
assert.Equal(t, int64(tc.params.DefaultLifetime.Seconds()), key.LifetimeSeconds)
}
switch {
case !tc.params.ExpiresAt.IsZero():
assert.Equal(t, tc.params.ExpiresAt.UTC(), key.ExpiresAt)
case tc.params.LifetimeSeconds > 0:
assert.WithinDuration(t, dbtime.Now().Add(time.Duration(tc.params.LifetimeSeconds)*time.Second), key.ExpiresAt, time.Second*5)
default:
assert.WithinDuration(t, dbtime.Now().Add(tc.params.DefaultLifetime), 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.True(t, key.Scopes.Has(tc.params.Scope))
} else {
assert.True(t, key.Scopes.Has(database.ApiKeyScopeCoderAll))
}
if tc.params.TokenName != "" {
assert.Equal(t, tc.params.TokenName, key.TokenName)
}
if tc.params.LoginType != "" {
assert.Equal(t, tc.params.LoginType, key.LoginType)
}
})
}
}
func TestGenerateCanonicalizesScopeAliases(t *testing.T) {
t.Parallel()
cases := []struct {
name string
params apikey.CreateParams
want database.APIKeyScopes
fail bool
}{
{
name: "SingularAlias",
params: apikey.CreateParams{Scope: "all"},
want: database.APIKeyScopes{database.ApiKeyScopeCoderAll},
},
{
name: "PluralAlias",
params: apikey.CreateParams{Scopes: database.APIKeyScopes{"application_connect"}},
want: database.APIKeyScopes{database.ApiKeyScopeCoderApplicationConnect},
},
{
name: "PluralAliasAndCanonical",
params: apikey.CreateParams{
Scopes: database.APIKeyScopes{"all", database.ApiKeyScopeCoderAll},
},
want: database.APIKeyScopes{database.ApiKeyScopeCoderAll},
},
{
name: "PluralMixed",
params: apikey.CreateParams{
Scopes: database.APIKeyScopes{"all", database.ApiKeyScopeWorkspaceRead},
},
want: database.APIKeyScopes{database.ApiKeyScopeCoderAll, database.ApiKeyScopeWorkspaceRead},
},
{
name: "PluralInvalid",
params: apikey.CreateParams{Scopes: database.APIKeyScopes{"not_a_real_scope"}},
fail: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
params := tc.params
params.UserID = uuid.New()
params.LoginType = database.LoginTypePassword
params.DefaultLifetime = time.Hour
requested := slices.Clone(params.Scopes)
key, _, err := apikey.Generate(params)
if tc.fail {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.want, key.Scopes)
// Generate must not canonicalize through the caller's slice.
require.Equal(t, requested, params.Scopes)
})
}
}
// TestInvalid just ensures the false case is asserted by some tests.
// Otherwise, a function that just `returns true` might pass all tests incorrectly.
func TestInvalid(t *testing.T) {
t.Parallel()
require.Falsef(t, apikey.ValidateHash([]byte{}, "secret"), "empty hash")
secret, hash, err := apikey.GenerateSecret(10)
require.NoError(t, err)
require.Falsef(t, apikey.ValidateHash(hash, secret+"_"), "different secret")
require.Falsef(t, apikey.ValidateHash(hash[:len(hash)-1], secret), "different hash length")
}