mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add API key scopes and application_connect scope (#4067)
This commit is contained in:
@@ -1588,6 +1588,7 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
LastUsed: arg.LastUsed,
|
||||
LoginType: arg.LoginType,
|
||||
Scope: arg.Scope,
|
||||
}
|
||||
q.apiKeys = append(q.apiKeys, key)
|
||||
return key, nil
|
||||
|
||||
@@ -4,12 +4,15 @@ package database_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/migrations"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
)
|
||||
|
||||
func TestNestedInTx(t *testing.T) {
|
||||
@@ -20,7 +23,7 @@ func TestNestedInTx(t *testing.T) {
|
||||
|
||||
uid := uuid.New()
|
||||
sqlDB := testSQLDB(t)
|
||||
err := database.MigrateUp(sqlDB)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err, "migrations")
|
||||
|
||||
db := database.New(sqlDB)
|
||||
@@ -48,3 +51,17 @@ func TestNestedInTx(t *testing.T) {
|
||||
require.NoError(t, err, "user exists")
|
||||
require.Equal(t, uid, user.ID, "user id expected")
|
||||
}
|
||||
|
||||
func testSQLDB(t testing.TB) *sql.DB {
|
||||
t.Helper()
|
||||
|
||||
connection, closeFn, err := postgres.Open()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(closeFn)
|
||||
|
||||
db, err := sql.Open("postgres", connection)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
Generated
+7
-1
@@ -1,5 +1,10 @@
|
||||
-- Code generated by 'make coderd/database/generate'. DO NOT EDIT.
|
||||
|
||||
CREATE TYPE api_key_scope AS ENUM (
|
||||
'all',
|
||||
'application_connect'
|
||||
);
|
||||
|
||||
CREATE TYPE audit_action AS ENUM (
|
||||
'create',
|
||||
'write',
|
||||
@@ -109,7 +114,8 @@ CREATE TABLE api_keys (
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
login_type login_type NOT NULL,
|
||||
lifetime_seconds bigint DEFAULT 86400 NOT NULL,
|
||||
ip_address inet DEFAULT '0.0.0.0'::inet NOT NULL
|
||||
ip_address inet DEFAULT '0.0.0.0'::inet NOT NULL,
|
||||
scope api_key_scope DEFAULT 'all'::public.api_key_scope NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE audit_logs (
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/migrations"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = database.MigrateUp(db)
|
||||
err = migrations.Up(db)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Avoid "upgrading" devurl keys to fully fledged API keys.
|
||||
DELETE FROM api_keys WHERE scope != 'all';
|
||||
|
||||
ALTER TABLE api_keys DROP COLUMN scope;
|
||||
|
||||
DROP TYPE api_key_scope;
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TYPE api_key_scope AS ENUM (
|
||||
'all',
|
||||
'application_connect'
|
||||
);
|
||||
|
||||
ALTER TABLE api_keys ADD COLUMN scope api_key_scope NOT NULL DEFAULT 'all';
|
||||
@@ -1,4 +1,4 @@
|
||||
package database
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
//go:embed *.sql
|
||||
var migrations embed.FS
|
||||
|
||||
func migrateSetup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
|
||||
func setup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
|
||||
ctx := context.Background()
|
||||
sourceDriver, err := iofs.New(migrations, "migrations")
|
||||
sourceDriver, err := iofs.New(migrations, ".")
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("create iofs: %w", err)
|
||||
}
|
||||
@@ -45,9 +45,9 @@ func migrateSetup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
|
||||
return sourceDriver, m, nil
|
||||
}
|
||||
|
||||
// MigrateUp runs SQL migrations to ensure the database schema is up-to-date.
|
||||
func MigrateUp(db *sql.DB) (retErr error) {
|
||||
_, m, err := migrateSetup(db)
|
||||
// Up runs SQL migrations to ensure the database schema is up-to-date.
|
||||
func Up(db *sql.DB) (retErr error) {
|
||||
_, m, err := setup(db)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("migrate setup: %w", err)
|
||||
}
|
||||
@@ -76,9 +76,9 @@ func MigrateUp(db *sql.DB) (retErr error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateDown runs all down SQL migrations.
|
||||
func MigrateDown(db *sql.DB) error {
|
||||
_, m, err := migrateSetup(db)
|
||||
// Down runs all down SQL migrations.
|
||||
func Down(db *sql.DB) error {
|
||||
_, m, err := setup(db)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("migrate setup: %w", err)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func MigrateDown(db *sql.DB) error {
|
||||
// applied, without making any changes to the database. If not, returns a
|
||||
// non-nil error.
|
||||
func EnsureClean(db *sql.DB) error {
|
||||
sourceDriver, m, err := migrateSetup(db)
|
||||
sourceDriver, m, err := setup(db)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("migrate setup: %w", err)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build linux
|
||||
|
||||
package database_test
|
||||
package migrations_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/migrations"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestMigrate(t *testing.T) {
|
||||
|
||||
db := testSQLDB(t)
|
||||
|
||||
err := database.MigrateUp(db)
|
||||
err := migrations.Up(db)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -42,10 +42,10 @@ func TestMigrate(t *testing.T) {
|
||||
|
||||
db := testSQLDB(t)
|
||||
|
||||
err := database.MigrateUp(db)
|
||||
err := migrations.Up(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = database.MigrateUp(db)
|
||||
err = migrations.Up(db)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -54,13 +54,13 @@ func TestMigrate(t *testing.T) {
|
||||
|
||||
db := testSQLDB(t)
|
||||
|
||||
err := database.MigrateUp(db)
|
||||
err := migrations.Up(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = database.MigrateDown(db)
|
||||
err = migrations.Down(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = database.MigrateUp(db)
|
||||
err = migrations.Up(db)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func TestCheckLatestVersion(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
err := database.CheckLatestVersion(driver, tc.currentVersion)
|
||||
err := migrations.CheckLatestVersion(driver, tc.currentVersion)
|
||||
var errMessage string
|
||||
if err != nil {
|
||||
errMessage = err.Error()
|
||||
@@ -4,6 +4,17 @@ import (
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
)
|
||||
|
||||
func (s APIKeyScope) ToRBAC() rbac.Scope {
|
||||
switch s {
|
||||
case APIKeyScopeAll:
|
||||
return rbac.ScopeAll
|
||||
case APIKeyScopeApplicationConnect:
|
||||
return rbac.ScopeApplicationConnect
|
||||
default:
|
||||
panic("developer error: unknown scope type " + string(s))
|
||||
}
|
||||
}
|
||||
|
||||
func (t Template) RBACObject() rbac.Object {
|
||||
return rbac.ResourceTemplate.InOrg(t.OrganizationID)
|
||||
}
|
||||
@@ -21,6 +32,10 @@ func (w Workspace) ExecutionRBAC() rbac.Object {
|
||||
return rbac.ResourceWorkspaceExecution.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (w Workspace) ApplicationConnectRBAC() rbac.Object {
|
||||
return rbac.ResourceWorkspaceApplicationConnect.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (m OrganizationMember) RBACObject() rbac.Object {
|
||||
return rbac.ResourceOrganizationMember.InOrg(m.OrganizationID)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,25 @@ import (
|
||||
"github.com/tabbed/pqtype"
|
||||
)
|
||||
|
||||
type APIKeyScope string
|
||||
|
||||
const (
|
||||
APIKeyScopeAll APIKeyScope = "all"
|
||||
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = APIKeyScope(s)
|
||||
case string:
|
||||
*e = APIKeyScope(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for APIKeyScope: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuditAction string
|
||||
|
||||
const (
|
||||
@@ -324,6 +343,7 @@ type APIKey struct {
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
Scope APIKeyScope `db:"scope" json:"scope"`
|
||||
}
|
||||
|
||||
type AgentStat struct {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/migrations"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
)
|
||||
|
||||
@@ -143,7 +143,7 @@ func Open() (string, func(), error) {
|
||||
return retryErr
|
||||
}
|
||||
|
||||
err = database.MigrateUp(db)
|
||||
err = migrations.Up(db)
|
||||
if err != nil {
|
||||
retryErr = xerrors.Errorf("migrate db: %w", err)
|
||||
// Only try to migrate once.
|
||||
|
||||
@@ -128,7 +128,7 @@ func (q *sqlQuerier) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
||||
|
||||
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
|
||||
SELECT
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope
|
||||
FROM
|
||||
api_keys
|
||||
WHERE
|
||||
@@ -151,12 +151,13 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address FROM api_keys WHERE last_used > $1
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE last_used > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
|
||||
@@ -179,6 +180,7 @@ func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,7 +207,8 @@ INSERT INTO
|
||||
expires_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
login_type
|
||||
login_type,
|
||||
scope
|
||||
)
|
||||
VALUES
|
||||
($1,
|
||||
@@ -214,7 +217,7 @@ VALUES
|
||||
WHEN 0 THEN 86400
|
||||
ELSE $2::bigint
|
||||
END
|
||||
, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address
|
||||
, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope
|
||||
`
|
||||
|
||||
type InsertAPIKeyParams struct {
|
||||
@@ -228,6 +231,7 @@ type InsertAPIKeyParams struct {
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
Scope APIKeyScope `db:"scope" json:"scope"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
|
||||
@@ -242,6 +246,7 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
arg.LoginType,
|
||||
arg.Scope,
|
||||
)
|
||||
var i APIKey
|
||||
err := row.Scan(
|
||||
@@ -255,6 +260,7 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ INSERT INTO
|
||||
expires_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
login_type
|
||||
login_type,
|
||||
scope
|
||||
)
|
||||
VALUES
|
||||
(@id,
|
||||
@@ -32,7 +33,7 @@ VALUES
|
||||
WHEN 0 THEN 86400
|
||||
ELSE @lifetime_seconds::bigint
|
||||
END
|
||||
, @hashed_secret, @ip_address, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type) RETURNING *;
|
||||
, @hashed_secret, @ip_address, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @scope) RETURNING *;
|
||||
|
||||
-- name: UpdateAPIKeyByID :exec
|
||||
UPDATE
|
||||
|
||||
@@ -18,6 +18,9 @@ packages:
|
||||
|
||||
rename:
|
||||
api_key: APIKey
|
||||
api_key_scope: APIKeyScope
|
||||
api_key_scope_all: APIKeyScopeAll
|
||||
api_key_scope_application_connect: APIKeyScopeApplicationConnect
|
||||
avatar_url: AvatarURL
|
||||
login_type_oidc: LoginTypeOIDC
|
||||
oauth_access_token: OAuthAccessToken
|
||||
|
||||
Reference in New Issue
Block a user