Skip to content

Commit 29d804e

Browse files
authored
feat: add API key scopes and application_connect scope (#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';

0 commit comments

Comments
 (0)