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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy