Skip to content

Commit 29d804e

Browse files
authored
feat: add API key scopes and application_connect scope (coder#4067)
1 parent adad347 commit 29d804e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+476
-88
lines changed

cli/resetpassword.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/coder/coder/cli/cliflag"
1111
"github.com/coder/coder/cli/cliui"
1212
"github.com/coder/coder/coderd/database"
13+
"github.com/coder/coder/coderd/database/migrations"
1314
"github.com/coder/coder/coderd/userpassword"
1415
)
1516

@@ -35,7 +36,7 @@ func resetPassword() *cobra.Command {
3536
return xerrors.Errorf("ping postgres: %w", err)
3637
}
3738

38-
err = database.EnsureClean(sqlDB)
39+
err = migrations.EnsureClean(sqlDB)
3940
if err != nil {
4041
return xerrors.Errorf("database needs migration: %w", err)
4142
}

cli/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import (
5353
"github.com/coder/coder/coderd/autobuild/executor"
5454
"github.com/coder/coder/coderd/database"
5555
"github.com/coder/coder/coderd/database/databasefake"
56+
"github.com/coder/coder/coderd/database/migrations"
5657
"github.com/coder/coder/coderd/devtunnel"
5758
"github.com/coder/coder/coderd/gitsshkey"
5859
"github.com/coder/coder/coderd/prometheusmetrics"
@@ -430,7 +431,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
430431
if err != nil {
431432
return xerrors.Errorf("ping postgres: %w", err)
432433
}
433-
err = database.MigrateUp(sqlDB)
434+
err = migrations.Up(sqlDB)
434435
if err != nil {
435436
return xerrors.Errorf("migrate up: %w", err)
436437
}

coderd/authorize.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import (
1111
)
1212

1313
func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action rbac.Action, objects []O) ([]O, error) {
14-
roles := httpmw.AuthorizationUserRoles(r)
15-
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles.ID.String(), roles.Roles, action, objects)
14+
roles := httpmw.UserAuthorization(r)
15+
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), action, objects)
1616
if err != nil {
1717
// Log the error as Filter should not be erroring.
1818
h.Logger.Error(r.Context(), "filter failed",
1919
slog.Error(err),
2020
slog.F("user_id", roles.ID),
2121
slog.F("username", roles.Username),
22+
slog.F("scope", roles.Scope),
2223
slog.F("route", r.URL.Path),
2324
slog.F("action", action),
2425
)
@@ -55,8 +56,8 @@ func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objec
5556
// return
5657
// }
5758
func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
58-
roles := httpmw.AuthorizationUserRoles(r)
59-
err := h.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object.RBACObject())
59+
roles := httpmw.UserAuthorization(r)
60+
err := h.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), action, object.RBACObject())
6061
if err != nil {
6162
// Log the errors for debugging
6263
internalError := new(rbac.UnauthorizedError)
@@ -70,6 +71,7 @@ func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object r
7071
slog.F("roles", roles.Roles),
7172
slog.F("user_id", roles.ID),
7273
slog.F("username", roles.Username),
74+
slog.F("scope", roles.Scope),
7375
slog.F("route", r.URL.Path),
7476
slog.F("action", action),
7577
slog.F("object", object),

coderd/coderdtest/authtest.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
163163
// Some quick reused objects
164164
workspaceRBACObj := rbac.ResourceWorkspace.InOrg(a.Organization.ID).WithOwner(a.Workspace.OwnerID.String())
165165
workspaceExecObj := rbac.ResourceWorkspaceExecution.InOrg(a.Organization.ID).WithOwner(a.Workspace.OwnerID.String())
166+
applicationConnectObj := rbac.ResourceWorkspaceApplicationConnect.InOrg(a.Organization.ID).WithOwner(a.Workspace.OwnerID.String())
167+
166168
// skipRoutes allows skipping routes from being checked.
167169
skipRoutes := map[string]string{
168170
"POST:/api/v2/users/logout": "Logging out deletes the API Key for other routes",
@@ -408,11 +410,11 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
408410

409411
assertAllHTTPMethods("/%40{user}/{workspace_and_agent}/apps/{workspaceapp}/*", RouteCheck{
410412
AssertAction: rbac.ActionCreate,
411-
AssertObject: workspaceExecObj,
413+
AssertObject: applicationConnectObj,
412414
})
413415
assertAllHTTPMethods("/@{user}/{workspace_and_agent}/apps/{workspaceapp}/*", RouteCheck{
414416
AssertAction: rbac.ActionCreate,
415-
AssertObject: workspaceExecObj,
417+
AssertObject: applicationConnectObj,
416418
})
417419

418420
return skipRoutes, assertRoute
@@ -518,6 +520,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
518520
type authCall struct {
519521
SubjectID string
520522
Roles []string
523+
Scope rbac.Scope
521524
Action rbac.Action
522525
Object rbac.Object
523526
}
@@ -527,21 +530,25 @@ type recordingAuthorizer struct {
527530
AlwaysReturn error
528531
}
529532

530-
func (r *recordingAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
533+
var _ rbac.Authorizer = (*recordingAuthorizer)(nil)
534+
535+
func (r *recordingAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, scope rbac.Scope, action rbac.Action, object rbac.Object) error {
531536
r.Called = &authCall{
532537
SubjectID: subjectID,
533538
Roles: roleNames,
539+
Scope: scope,
534540
Action: action,
535541
Object: object,
536542
}
537543
return r.AlwaysReturn
538544
}
539545

540-
func (r *recordingAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
546+
func (r *recordingAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, scope rbac.Scope, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
541547
return &fakePreparedAuthorizer{
542548
Original: r,
543549
SubjectID: subjectID,
544550
Roles: roles,
551+
Scope: scope,
545552
Action: action,
546553
}, nil
547554
}
@@ -554,9 +561,10 @@ type fakePreparedAuthorizer struct {
554561
Original *recordingAuthorizer
555562
SubjectID string
556563
Roles []string
564+
Scope rbac.Scope
557565
Action rbac.Action
558566
}
559567

560568
func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
561-
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Action, object)
569+
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Scope, f.Action, object)
562570
}

coderd/database/databasefake/databasefake.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,6 +1588,7 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
15881588
UpdatedAt: arg.UpdatedAt,
15891589
LastUsed: arg.LastUsed,
15901590
LoginType: arg.LoginType,
1591+
Scope: arg.Scope,
15911592
}
15921593
q.apiKeys = append(q.apiKeys, key)
15931594
return key, nil

coderd/database/db_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ package database_test
44

55
import (
66
"context"
7+
"database/sql"
78
"testing"
89

910
"github.com/google/uuid"
1011
"github.com/stretchr/testify/require"
1112

1213
"github.com/coder/coder/coderd/database"
14+
"github.com/coder/coder/coderd/database/migrations"
15+
"github.com/coder/coder/coderd/database/postgres"
1316
)
1417

1518
func TestNestedInTx(t *testing.T) {
@@ -20,7 +23,7 @@ func TestNestedInTx(t *testing.T) {
2023

2124
uid := uuid.New()
2225
sqlDB := testSQLDB(t)
23-
err := database.MigrateUp(sqlDB)
26+
err := migrations.Up(sqlDB)
2427
require.NoError(t, err, "migrations")
2528

2629
db := database.New(sqlDB)
@@ -48,3 +51,17 @@ func TestNestedInTx(t *testing.T) {
4851
require.NoError(t, err, "user exists")
4952
require.Equal(t, uid, user.ID, "user id expected")
5053
}
54+
55+
func testSQLDB(t testing.TB) *sql.DB {
56+
t.Helper()
57+
58+
connection, closeFn, err := postgres.Open()
59+
require.NoError(t, err)
60+
t.Cleanup(closeFn)
61+
62+
db, err := sql.Open("postgres", connection)
63+
require.NoError(t, err)
64+
t.Cleanup(func() { _ = db.Close() })
65+
66+
return db
67+
}

coderd/database/dump.sql

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/gen/dump/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"path/filepath"
1010
"runtime"
1111

12-
"github.com/coder/coder/coderd/database"
12+
"github.com/coder/coder/coderd/database/migrations"
1313
"github.com/coder/coder/coderd/database/postgres"
1414
)
1515

@@ -25,7 +25,7 @@ func main() {
2525
panic(err)
2626
}
2727

28-
err = database.MigrateUp(db)
28+
err = migrations.Up(db)
2929
if err != nil {
3030
panic(err)
3131
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Avoid "upgrading" devurl keys to fully fledged API keys.
2+
DELETE FROM api_keys WHERE scope != 'all';
3+
4+
ALTER TABLE api_keys DROP COLUMN scope;
5+
6+
DROP TYPE api_key_scope;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TYPE api_key_scope AS ENUM (
2+
'all',
3+
'application_connect'
4+
);
5+
6+
ALTER TABLE api_keys ADD COLUMN scope api_key_scope NOT NULL DEFAULT 'all';

coderd/database/migrate.go renamed to coderd/database/migrations/migrate.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package database
1+
package migrations
22

33
import (
44
"context"
@@ -14,12 +14,12 @@ import (
1414
"golang.org/x/xerrors"
1515
)
1616

17-
//go:embed migrations/*.sql
17+
//go:embed *.sql
1818
var migrations embed.FS
1919

20-
func migrateSetup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
20+
func setup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
2121
ctx := context.Background()
22-
sourceDriver, err := iofs.New(migrations, "migrations")
22+
sourceDriver, err := iofs.New(migrations, ".")
2323
if err != nil {
2424
return nil, nil, xerrors.Errorf("create iofs: %w", err)
2525
}
@@ -45,9 +45,9 @@ func migrateSetup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
4545
return sourceDriver, m, nil
4646
}
4747

48-
// MigrateUp runs SQL migrations to ensure the database schema is up-to-date.
49-
func MigrateUp(db *sql.DB) (retErr error) {
50-
_, m, err := migrateSetup(db)
48+
// Up runs SQL migrations to ensure the database schema is up-to-date.
49+
func Up(db *sql.DB) (retErr error) {
50+
_, m, err := setup(db)
5151
if err != nil {
5252
return xerrors.Errorf("migrate setup: %w", err)
5353
}
@@ -76,9 +76,9 @@ func MigrateUp(db *sql.DB) (retErr error) {
7676
return nil
7777
}
7878

79-
// MigrateDown runs all down SQL migrations.
80-
func MigrateDown(db *sql.DB) error {
81-
_, m, err := migrateSetup(db)
79+
// Down runs all down SQL migrations.
80+
func Down(db *sql.DB) error {
81+
_, m, err := setup(db)
8282
if err != nil {
8383
return xerrors.Errorf("migrate setup: %w", err)
8484
}
@@ -100,7 +100,7 @@ func MigrateDown(db *sql.DB) error {
100100
// applied, without making any changes to the database. If not, returns a
101101
// non-nil error.
102102
func EnsureClean(db *sql.DB) error {
103-
sourceDriver, m, err := migrateSetup(db)
103+
sourceDriver, m, err := setup(db)
104104
if err != nil {
105105
return xerrors.Errorf("migrate setup: %w", err)
106106
}

coderd/database/migrate_test.go renamed to coderd/database/migrations/migrate_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//go:build linux
22

3-
package database_test
3+
package migrations_test
44

55
import (
66
"database/sql"
@@ -12,7 +12,7 @@ import (
1212
"github.com/stretchr/testify/require"
1313
"go.uber.org/goleak"
1414

15-
"github.com/coder/coder/coderd/database"
15+
"github.com/coder/coder/coderd/database/migrations"
1616
"github.com/coder/coder/coderd/database/postgres"
1717
)
1818

@@ -33,7 +33,7 @@ func TestMigrate(t *testing.T) {
3333

3434
db := testSQLDB(t)
3535

36-
err := database.MigrateUp(db)
36+
err := migrations.Up(db)
3737
require.NoError(t, err)
3838
})
3939

@@ -42,10 +42,10 @@ func TestMigrate(t *testing.T) {
4242

4343
db := testSQLDB(t)
4444

45-
err := database.MigrateUp(db)
45+
err := migrations.Up(db)
4646
require.NoError(t, err)
4747

48-
err = database.MigrateUp(db)
48+
err = migrations.Up(db)
4949
require.NoError(t, err)
5050
})
5151

@@ -54,13 +54,13 @@ func TestMigrate(t *testing.T) {
5454

5555
db := testSQLDB(t)
5656

57-
err := database.MigrateUp(db)
57+
err := migrations.Up(db)
5858
require.NoError(t, err)
5959

60-
err = database.MigrateDown(db)
60+
err = migrations.Down(db)
6161
require.NoError(t, err)
6262

63-
err = database.MigrateUp(db)
63+
err = migrations.Up(db)
6464
require.NoError(t, err)
6565
})
6666
}
@@ -120,7 +120,7 @@ func TestCheckLatestVersion(t *testing.T) {
120120
})
121121
}
122122

123-
err := database.CheckLatestVersion(driver, tc.currentVersion)
123+
err := migrations.CheckLatestVersion(driver, tc.currentVersion)
124124
var errMessage string
125125
if err != nil {
126126
errMessage = err.Error()

coderd/database/modelmethods.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import (
44
"github.com/coder/coder/coderd/rbac"
55
)
66

7+
func (s APIKeyScope) ToRBAC() rbac.Scope {
8+
switch s {
9+
case APIKeyScopeAll:
10+
return rbac.ScopeAll
11+
case APIKeyScopeApplicationConnect:
12+
return rbac.ScopeApplicationConnect
13+
default:
14+
panic("developer error: unknown scope type " + string(s))
15+
}
16+
}
17+
718
func (t Template) RBACObject() rbac.Object {
819
return rbac.ResourceTemplate.InOrg(t.OrganizationID)
920
}
@@ -21,6 +32,10 @@ func (w Workspace) ExecutionRBAC() rbac.Object {
2132
return rbac.ResourceWorkspaceExecution.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
2233
}
2334

35+
func (w Workspace) ApplicationConnectRBAC() rbac.Object {
36+
return rbac.ResourceWorkspaceApplicationConnect.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
37+
}
38+
2439
func (m OrganizationMember) RBACObject() rbac.Object {
2540
return rbac.ResourceOrganizationMember.InOrg(m.OrganizationID)
2641
}

0 commit comments

Comments
 (0)