Skip to content

Commit aca2f6a

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

Some content is hidden

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

46 files changed

+1359
-803
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/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)