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.
176 lines
5.0 KiB
Go
176 lines
5.0 KiB
Go
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/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.
|
|
hashed := sha256.Sum256([]byte(keytokens[1]))
|
|
assert.ElementsMatch(t, hashed, key.HashedSecret)
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|