Skip to content

Commit 35d7f5a

Browse files
committed
chore: add OAuth2 device flow test scripts
Change-Id: Ic232851727e683ab3d8b7ce970c505588da2f827 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent c032537 commit 35d7f5a

Some content is hidden

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

47 files changed

+1368
-810
lines changed

.claude/scripts/format.sh

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,30 +101,36 @@ fi
101101
# Get the file extension to determine the appropriate formatter
102102
file_ext="${file_path##*.}"
103103

104+
# Helper function to run formatter and handle errors
105+
run_formatter() {
106+
local target="$1"
107+
local file_type="$2"
108+
109+
if ! make FILE="$file_path" "$target"; then
110+
echo "Error: Failed to format $file_type file: $file_path" >&2
111+
exit 2
112+
fi
113+
echo "✓ Formatted $file_type file: $file_path"
114+
}
104115
# Change to the project root directory (where the Makefile is located)
105116
cd "$(dirname "$0")/../.."
106117

107118
# Call the appropriate Makefile target based on file extension
108119
case "$file_ext" in
109120
go)
110-
make fmt/go FILE="$file_path"
111-
echo "✓ Formatted Go file: $file_path"
121+
run_formatter "fmt/go" "Go"
112122
;;
113123
js | jsx | ts | tsx)
114-
make fmt/ts FILE="$file_path"
115-
echo "✓ Formatted TypeScript/JavaScript file: $file_path"
124+
run_formatter "fmt/ts" "TypeScript/JavaScript"
116125
;;
117126
tf | tfvars)
118-
make fmt/terraform FILE="$file_path"
119-
echo "✓ Formatted Terraform file: $file_path"
127+
run_formatter "fmt/terraform" "Terraform"
120128
;;
121129
sh)
122-
make fmt/shfmt FILE="$file_path"
123-
echo "✓ Formatted shell script: $file_path"
130+
run_formatter "fmt/shfmt" "shell script"
124131
;;
125132
md)
126-
make fmt/markdown FILE="$file_path"
127-
echo "✓ Formatted Markdown file: $file_path"
133+
run_formatter "fmt/markdown" "Markdown"
128134
;;
129135
*)
130136
echo "No formatter available for file extension: $file_ext"

coderd/apidoc/docs.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/audit/diff.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Auditable interface {
2424
database.NotificationsSettings |
2525
database.OAuth2ProviderApp |
2626
database.OAuth2ProviderAppSecret |
27+
database.OAuth2ProviderDeviceCode |
2728
database.PrebuildsSettings |
2829
database.CustomRole |
2930
database.AuditableOrganizationMember |

coderd/audit/request.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ func ResourceTarget[T Auditable](tgt T) string {
117117
return typed.Name
118118
case database.OAuth2ProviderAppSecret:
119119
return typed.DisplaySecret
120+
case database.OAuth2ProviderDeviceCode:
121+
return typed.UserCode
120122
case database.CustomRole:
121123
return typed.Name
122124
case database.AuditableOrganizationMember:
@@ -179,6 +181,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
179181
return typed.ID
180182
case database.OAuth2ProviderAppSecret:
181183
return typed.ID
184+
case database.OAuth2ProviderDeviceCode:
185+
return typed.ID
182186
case database.CustomRole:
183187
return typed.ID
184188
case database.AuditableOrganizationMember:
@@ -232,6 +236,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
232236
return database.ResourceTypeOauth2ProviderApp
233237
case database.OAuth2ProviderAppSecret:
234238
return database.ResourceTypeOauth2ProviderAppSecret
239+
case database.OAuth2ProviderDeviceCode:
240+
return database.ResourceTypeOauth2ProviderDeviceCode
235241
case database.CustomRole:
236242
return database.ResourceTypeCustomRole
237243
case database.AuditableOrganizationMember:
@@ -288,6 +294,8 @@ func ResourceRequiresOrgID[T Auditable]() bool {
288294
return false
289295
case database.OAuth2ProviderAppSecret:
290296
return false
297+
case database.OAuth2ProviderDeviceCode:
298+
return false
291299
case database.CustomRole:
292300
return true
293301
case database.AuditableOrganizationMember:

coderd/coderd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ func New(options *Options) *API {
996996
r.Route("/device", func(r chi.Router) {
997997
r.Post("/", api.postOAuth2DeviceAuthorization()) // RFC 8628 compliant endpoint
998998
r.Route("/verify", func(r chi.Router) {
999-
r.Use(apiKeyMiddleware)
999+
r.Use(apiKeyMiddlewareRedirect)
10001000
r.Get("/", api.getOAuth2DeviceVerification())
10011001
r.Post("/", api.postOAuth2DeviceVerification())
10021002
})

coderd/database/check_constraint.go

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

coderd/database/dbauthz/dbauthz.go

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,35 @@ var (
417417
rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
418418
rbac.ResourceOauth2App.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
419419
rbac.ResourceOauth2AppSecret.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
420+
rbac.ResourceOauth2AppCodeToken.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
421+
}),
422+
Org: map[string][]rbac.Permission{},
423+
User: []rbac.Permission{},
424+
},
425+
}),
426+
Scope: rbac.ScopeAll,
427+
}.WithCachedASTValue()
428+
429+
subjectSystemOAuth2 = rbac.Subject{
430+
Type: rbac.SubjectTypeSystemRestricted,
431+
FriendlyName: "System OAuth2",
432+
ID: uuid.Nil.String(),
433+
Roles: rbac.Roles([]rbac.Role{
434+
{
435+
Identifier: rbac.RoleIdentifier{Name: "system-oauth2"},
436+
DisplayName: "System OAuth2",
437+
Site: rbac.Permissions(map[string][]policy.Action{
438+
// OAuth2 resources - full CRUD permissions
439+
rbac.ResourceOauth2App.Type: rbac.ResourceOauth2App.AvailableActions(),
440+
rbac.ResourceOauth2AppSecret.Type: rbac.ResourceOauth2AppSecret.AvailableActions(),
441+
rbac.ResourceOauth2AppCodeToken.Type: rbac.ResourceOauth2AppCodeToken.AvailableActions(),
442+
443+
// API key permissions needed for OAuth2 token revocation
444+
rbac.ResourceApiKey.Type: {policy.ActionRead, policy.ActionDelete},
445+
446+
// Minimal read permissions that might be needed for OAuth2 operations
447+
rbac.ResourceUser.Type: {policy.ActionRead},
448+
rbac.ResourceOrganization.Type: {policy.ActionRead},
420449
}),
421450
Org: map[string][]rbac.Permission{},
422451
User: []rbac.Permission{},
@@ -567,6 +596,12 @@ func AsSystemRestricted(ctx context.Context) context.Context {
567596
return As(ctx, subjectSystemRestricted)
568597
}
569598

599+
// AsSystemOAuth2 returns a context with an actor that has permissions
600+
// required for OAuth2 provider operations (token revocation, device codes, registration).
601+
func AsSystemOAuth2(ctx context.Context) context.Context {
602+
return As(ctx, subjectSystemOAuth2)
603+
}
604+
570605
// AsSystemReadProvisionerDaemons returns a context with an actor that has permissions
571606
// to read provisioner daemons.
572607
func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context {
@@ -1346,6 +1381,14 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
13461381
return q.db.CleanTailnetTunnels(ctx)
13471382
}
13481383

1384+
func (q *querier) ConsumeOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (database.OAuth2ProviderAppCode, error) {
1385+
return updateWithReturn(q.log, q.auth, q.db.GetOAuth2ProviderAppCodeByPrefix, q.db.ConsumeOAuth2ProviderAppCodeByPrefix)(ctx, secretPrefix)
1386+
}
1387+
1388+
func (q *querier) ConsumeOAuth2ProviderDeviceCodeByPrefix(ctx context.Context, deviceCodePrefix string) (database.OAuth2ProviderDeviceCode, error) {
1389+
return updateWithReturn(q.log, q.auth, q.db.GetOAuth2ProviderDeviceCodeByPrefix, q.db.ConsumeOAuth2ProviderDeviceCodeByPrefix)(ctx, deviceCodePrefix)
1390+
}
1391+
13491392
func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLogsParams) (int64, error) {
13501393
// Shortcut if the user is an owner. The SQL filter is noticeable,
13511394
// and this is an easy win for owners. Which is the common case.
@@ -1489,7 +1532,7 @@ func (q *querier) DeleteExpiredOAuth2ProviderDeviceCodes(ctx context.Context) er
14891532
func (q *querier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
14901533
return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, func(ctx context.Context, arg database.DeleteExternalAuthLinkParams) (database.ExternalAuthLink, error) {
14911534
//nolint:gosimple
1492-
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
1535+
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams(arg))
14931536
}, q.db.DeleteExternalAuthLink)(ctx, arg)
14941537
}
14951538

@@ -1568,27 +1611,30 @@ func (q *querier) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Contex
15681611
return q.db.DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx, arg)
15691612
}
15701613

1571-
func (q *querier) DeleteOldAuditLogConnectionEvents(ctx context.Context, threshold database.DeleteOldAuditLogConnectionEventsParams) error {
1572-
// `ResourceSystem` is deprecated, but it doesn't make sense to add
1573-
// `policy.ActionDelete` to `ResourceAuditLog`, since this is the one and
1574-
// only time we'll be deleting from the audit log.
1575-
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil {
1576-
return err
1577-
}
1578-
return q.db.DeleteOldAuditLogConnectionEvents(ctx, threshold)
1579-
}
1580-
15811614
func (q *querier) DeleteOAuth2ProviderDeviceCodeByID(ctx context.Context, id uuid.UUID) error {
15821615
// Fetch the device code first to check authorization
15831616
deviceCode, err := q.db.GetOAuth2ProviderDeviceCodeByID(ctx, id)
15841617
if err != nil {
1585-
return err
1618+
return xerrors.Errorf("get oauth2 provider device code: %w", err)
15861619
}
15871620
if err := q.authorizeContext(ctx, policy.ActionDelete, deviceCode); err != nil {
1588-
return err
1621+
return xerrors.Errorf("authorize oauth2 provider device code deletion: %w", err)
15891622
}
15901623

1591-
return q.db.DeleteOAuth2ProviderDeviceCodeByID(ctx, id)
1624+
if err := q.db.DeleteOAuth2ProviderDeviceCodeByID(ctx, id); err != nil {
1625+
return xerrors.Errorf("delete oauth2 provider device code: %w", err)
1626+
}
1627+
return nil
1628+
}
1629+
1630+
func (q *querier) DeleteOldAuditLogConnectionEvents(ctx context.Context, threshold database.DeleteOldAuditLogConnectionEventsParams) error {
1631+
// `ResourceSystem` is deprecated, but it doesn't make sense to add
1632+
// `policy.ActionDelete` to `ResourceAuditLog`, since this is the one and
1633+
// only time we'll be deleting from the audit log.
1634+
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil {
1635+
return err
1636+
}
1637+
return q.db.DeleteOldAuditLogConnectionEvents(ctx, threshold)
15921638
}
15931639

15941640
func (q *querier) DeleteOldNotificationMessages(ctx context.Context) error {
@@ -1620,7 +1666,7 @@ func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
16201666
}
16211667

16221668
func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error {
1623-
return deleteQ[database.OrganizationMember](q.log, q.auth, func(ctx context.Context, arg database.DeleteOrganizationMemberParams) (database.OrganizationMember, error) {
1669+
return deleteQ(q.log, q.auth, func(ctx context.Context, arg database.DeleteOrganizationMemberParams) (database.OrganizationMember, error) {
16241670
member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{
16251671
OrganizationID: arg.OrganizationID,
16261672
UserID: arg.UserID,
@@ -2213,7 +2259,7 @@ func (q *querier) GetLicenseByID(ctx context.Context, id int32) (database.Licens
22132259
}
22142260

22152261
func (q *querier) GetLicenses(ctx context.Context) ([]database.License, error) {
2216-
fetch := func(ctx context.Context, _ interface{}) ([]database.License, error) {
2262+
fetch := func(ctx context.Context, _ any) ([]database.License, error) {
22172263
return q.db.GetLicenses(ctx)
22182264
}
22192265
return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil)
@@ -2377,8 +2423,8 @@ func (q *querier) GetOAuth2ProviderDeviceCodeByUserCode(ctx context.Context, use
23772423
}
23782424

23792425
func (q *querier) GetOAuth2ProviderDeviceCodesByClientID(ctx context.Context, clientID uuid.UUID) ([]database.OAuth2ProviderDeviceCode, error) {
2380-
// This requires access to read the OAuth2 app
2381-
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil {
2426+
// This requires access to read OAuth2 app code tokens
2427+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppCodeToken); err != nil {
23822428
return []database.OAuth2ProviderDeviceCode{}, err
23832429
}
23842430
return q.db.GetOAuth2ProviderDeviceCodesByClientID(ctx, clientID)
@@ -2435,7 +2481,7 @@ func (q *querier) GetOrganizationResourceCountByID(ctx context.Context, organiza
24352481
}
24362482

24372483
func (q *querier) GetOrganizations(ctx context.Context, args database.GetOrganizationsParams) ([]database.Organization, error) {
2438-
fetch := func(ctx context.Context, _ interface{}) ([]database.Organization, error) {
2484+
fetch := func(ctx context.Context, _ any) ([]database.Organization, error) {
24392485
return q.db.GetOrganizations(ctx, args)
24402486
}
24412487
return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil)
@@ -2563,7 +2609,7 @@ func (q *querier) GetPreviousTemplateVersion(ctx context.Context, arg database.G
25632609
}
25642610

25652611
func (q *querier) GetProvisionerDaemons(ctx context.Context) ([]database.ProvisionerDaemon, error) {
2566-
fetch := func(ctx context.Context, _ interface{}) ([]database.ProvisionerDaemon, error) {
2612+
fetch := func(ctx context.Context, _ any) ([]database.ProvisionerDaemon, error) {
25672613
return q.db.GetProvisionerDaemons(ctx)
25682614
}
25692615
return fetchWithPostFilter(q.auth, policy.ActionRead, fetch)(ctx, nil)
@@ -3554,7 +3600,7 @@ func (q *querier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt
35543600
}
35553601

35563602
func (q *querier) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) {
3557-
return fetchWithPostFilter(q.auth, policy.ActionRead, func(ctx context.Context, _ interface{}) ([]database.WorkspaceProxy, error) {
3603+
return fetchWithPostFilter(q.auth, policy.ActionRead, func(ctx context.Context, _ any) ([]database.WorkspaceProxy, error) {
35583604
return q.db.GetWorkspaceProxies(ctx)
35593605
})(ctx, nil)
35603606
}
@@ -3848,8 +3894,8 @@ func (q *querier) InsertOAuth2ProviderAppToken(ctx context.Context, arg database
38483894
}
38493895

38503896
func (q *querier) InsertOAuth2ProviderDeviceCode(ctx context.Context, arg database.InsertOAuth2ProviderDeviceCodeParams) (database.OAuth2ProviderDeviceCode, error) {
3851-
// Creating device codes requires OAuth2 app access
3852-
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2App); err != nil {
3897+
// Creating device codes requires OAuth2 app code token creation access
3898+
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2AppCodeToken); err != nil {
38533899
return database.OAuth2ProviderDeviceCode{}, err
38543900
}
38553901
return q.db.InsertOAuth2ProviderDeviceCode(ctx, arg)
@@ -4156,10 +4202,11 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
41564202
return xerrors.Errorf("get workspace by id: %w", err)
41574203
}
41584204

4159-
var action policy.Action = policy.ActionWorkspaceStart
4160-
if arg.Transition == database.WorkspaceTransitionDelete {
4205+
action := policy.ActionWorkspaceStart
4206+
switch arg.Transition {
4207+
case database.WorkspaceTransitionDelete:
41614208
action = policy.ActionDelete
4162-
} else if arg.Transition == database.WorkspaceTransitionStop {
4209+
case database.WorkspaceTransitionStop:
41634210
action = policy.ActionWorkspaceStop
41644211
}
41654212

@@ -4536,13 +4583,10 @@ func (q *querier) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg dat
45364583
}
45374584

45384585
func (q *querier) UpdateOAuth2ProviderDeviceCodeAuthorization(ctx context.Context, arg database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams) (database.OAuth2ProviderDeviceCode, error) {
4539-
// Verify the user is authenticated for device code authorization
4540-
_, ok := ActorFromContext(ctx)
4541-
if !ok {
4542-
return database.OAuth2ProviderDeviceCode{}, ErrNoActor
4586+
fetch := func(ctx context.Context, arg database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams) (database.OAuth2ProviderDeviceCode, error) {
4587+
return q.db.GetOAuth2ProviderDeviceCodeByID(ctx, arg.ID)
45434588
}
4544-
4545-
return q.db.UpdateOAuth2ProviderDeviceCodeAuthorization(ctx, arg)
4589+
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateOAuth2ProviderDeviceCodeAuthorization)(ctx, arg)
45464590
}
45474591

45484592
func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) {

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