feat: add API key scopes and application_connect scope (#4067)

This commit is contained in:
Dean Sheather
2022-09-19 17:39:02 +00:00
committed by GitHub
parent adad347902
commit 29d804e692
42 changed files with 476 additions and 88 deletions
@@ -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';
+162
View File
@@ -0,0 +1,162 @@
package migrations
import (
"context"
"database/sql"
"embed"
"errors"
"os"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/iofs"
"golang.org/x/xerrors"
)
//go:embed *.sql
var migrations embed.FS
func setup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
ctx := context.Background()
sourceDriver, err := iofs.New(migrations, ".")
if err != nil {
return nil, nil, xerrors.Errorf("create iofs: %w", err)
}
// there is a postgres.WithInstance() method that takes the DB instance,
// but, when you close the resulting Migrate, it closes the DB, which
// we don't want. Instead, create just a connection that will get closed
// when migration is done.
conn, err := db.Conn(ctx)
if err != nil {
return nil, nil, xerrors.Errorf("postgres connection: %w", err)
}
dbDriver, err := postgres.WithConnection(ctx, conn, &postgres.Config{})
if err != nil {
return nil, nil, xerrors.Errorf("wrap postgres connection: %w", err)
}
m, err := migrate.NewWithInstance("", sourceDriver, "", dbDriver)
if err != nil {
return nil, nil, xerrors.Errorf("new migrate instance: %w", err)
}
return sourceDriver, m, nil
}
// 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)
}
defer func() {
srcErr, dbErr := m.Close()
if retErr != nil {
return
}
if dbErr != nil {
retErr = dbErr
return
}
retErr = srcErr
}()
err = m.Up()
if err != nil {
if errors.Is(err, migrate.ErrNoChange) {
// It's OK if no changes happened!
return nil
}
return xerrors.Errorf("up: %w", err)
}
return nil
}
// 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)
}
err = m.Down()
if err != nil {
if errors.Is(err, migrate.ErrNoChange) {
// It's OK if no changes happened!
return nil
}
return xerrors.Errorf("down: %w", err)
}
return nil
}
// EnsureClean checks whether all migrations for the current version have been
// applied, without making any changes to the database. If not, returns a
// non-nil error.
func EnsureClean(db *sql.DB) error {
sourceDriver, m, err := setup(db)
if err != nil {
return xerrors.Errorf("migrate setup: %w", err)
}
version, dirty, err := m.Version()
if err != nil {
return xerrors.Errorf("get migration version: %w", err)
}
if dirty {
return xerrors.Errorf("database has not been cleanly migrated")
}
// Verify that the database's migration version is "current" by checking
// that a migration with that version exists, but there is no next version.
err = CheckLatestVersion(sourceDriver, version)
if err != nil {
return xerrors.Errorf("database needs migration: %w", err)
}
return nil
}
// Returns nil if currentVersion corresponds to the latest available migration,
// otherwise an error explaining why not.
func CheckLatestVersion(sourceDriver source.Driver, currentVersion uint) error {
// This is ugly, but seems like the only way to do it with the public
// interfaces provided by golang-migrate.
// Check that there is no later version
nextVersion, err := sourceDriver.Next(currentVersion)
if err == nil {
return xerrors.Errorf("current version is %d, but later version %d exists", currentVersion, nextVersion)
}
if !errors.Is(err, os.ErrNotExist) {
return xerrors.Errorf("get next migration after %d: %w", currentVersion, err)
}
// Once we reach this point, we know that either currentVersion doesn't
// exist, or it has no successor (the return value from
// sourceDriver.Next() is the same in either case). So we need to check
// that either it's the first version, or it has a predecessor.
firstVersion, err := sourceDriver.First()
if err != nil {
// the total number of migrations should be non-zero, so this must be
// an actual error, not just a missing file
return xerrors.Errorf("get first migration: %w", err)
}
if firstVersion == currentVersion {
return nil
}
_, err = sourceDriver.Prev(currentVersion)
if err != nil {
return xerrors.Errorf("get previous migration: %w", err)
}
return nil
}
+131
View File
@@ -0,0 +1,131 @@
//go:build linux
package migrations_test
import (
"database/sql"
"fmt"
"testing"
"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/stub"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"github.com/coder/coder/coderd/database/migrations"
"github.com/coder/coder/coderd/database/postgres"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestMigrate(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip()
return
}
t.Run("Once", func(t *testing.T) {
t.Parallel()
db := testSQLDB(t)
err := migrations.Up(db)
require.NoError(t, err)
})
t.Run("Twice", func(t *testing.T) {
t.Parallel()
db := testSQLDB(t)
err := migrations.Up(db)
require.NoError(t, err)
err = migrations.Up(db)
require.NoError(t, err)
})
t.Run("UpDownUp", func(t *testing.T) {
t.Parallel()
db := testSQLDB(t)
err := migrations.Up(db)
require.NoError(t, err)
err = migrations.Down(db)
require.NoError(t, err)
err = migrations.Up(db)
require.NoError(t, err)
})
}
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
}
// paralleltest linter doesn't correctly handle table-driven tests (https://github.com/kunwardeep/paralleltest/issues/8)
// nolint:paralleltest
func TestCheckLatestVersion(t *testing.T) {
t.Parallel()
type test struct {
currentVersion uint
existingVersions []uint
expectedResult string
}
tests := []test{
// successful cases
{1, []uint{1}, ""},
{3, []uint{1, 2, 3}, ""},
{3, []uint{1, 3}, ""},
// failure cases
{1, []uint{1, 2}, "current version is 1, but later version 2 exists"},
{2, []uint{1, 2, 3}, "current version is 2, but later version 3 exists"},
{4, []uint{1, 2, 3}, "get previous migration: prev for version 4 : file does not exist"},
{4, []uint{1, 2, 3, 5}, "get previous migration: prev for version 4 : file does not exist"},
}
for i, tc := range tests {
i, tc := i, tc
t.Run(fmt.Sprintf("entry %d", i), func(t *testing.T) {
t.Parallel()
driver, _ := stub.WithInstance(nil, &stub.Config{})
stub, ok := driver.(*stub.Stub)
require.True(t, ok)
for _, version := range tc.existingVersions {
stub.Migrations.Append(&source.Migration{
Version: version,
Identifier: "",
Direction: source.Up,
Raw: "",
})
}
err := migrations.CheckLatestVersion(driver, tc.currentVersion)
var errMessage string
if err != nil {
errMessage = err.Error()
}
require.Equal(t, tc.expectedResult, errMessage)
})
}
}