Skip to content

Commit 534d4ea

Browse files
Emyrkstirby
authored andcommitted
chore: external auth validate response "Forbidden" should return invalid, not an error (#13446)
* chore: add unit test to delete workspace from suspended user * chore: account for forbidden as well as unauthorized response codes (cherry picked from commit 27f2691)
1 parent 8ce8700 commit 534d4ea

File tree

4 files changed

+98
-9
lines changed

4 files changed

+98
-9
lines changed

coderd/coderdtest/oidctest/idp.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,9 @@ type ExternalAuthConfigOptions struct {
12551255
// ValidatePayload is the payload that is used when the user calls the
12561256
// equivalent of "userinfo" for oauth2. This is not standardized, so is
12571257
// different for each provider type.
1258-
ValidatePayload func(email string) interface{}
1258+
//
1259+
// The int,error payload can control the response if set.
1260+
ValidatePayload func(email string) (interface{}, int, error)
12591261

12601262
// routes is more advanced usage. This allows the caller to
12611263
// completely customize the response. It captures all routes under the /external-auth-validate/*
@@ -1292,7 +1294,20 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
12921294
case "/user", "/", "":
12931295
var payload interface{} = "OK"
12941296
if custom.ValidatePayload != nil {
1295-
payload = custom.ValidatePayload(email)
1297+
var err error
1298+
var code int
1299+
payload, code, err = custom.ValidatePayload(email)
1300+
if code == 0 && err == nil {
1301+
code = http.StatusOK
1302+
}
1303+
if code == 0 && err != nil {
1304+
code = http.StatusUnauthorized
1305+
}
1306+
if err != nil {
1307+
http.Error(rw, fmt.Sprintf("failed validation via custom method: %s", err.Error()), code)
1308+
return
1309+
}
1310+
rw.WriteHeader(code)
12961311
}
12971312
_ = json.NewEncoder(rw).Encode(payload)
12981313
default:

coderd/externalauth/externalauth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ func (c *Config) ValidateToken(ctx context.Context, link *oauth2.Token) (bool, *
202202
return false, nil, err
203203
}
204204
defer res.Body.Close()
205-
if res.StatusCode == http.StatusUnauthorized {
205+
if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden {
206206
// The token is no longer valid!
207207
return false, nil, nil
208208
}

coderd/externalauth_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ func TestExternalAuthByID(t *testing.T) {
7979
client := coderdtest.New(t, &coderdtest.Options{
8080
ExternalAuthConfigs: []*externalauth.Config{
8181
fake.ExternalAuthConfig(t, providerID, &oidctest.ExternalAuthConfigOptions{
82-
ValidatePayload: func(_ string) interface{} {
82+
ValidatePayload: func(_ string) (interface{}, int, error) {
8383
return github.User{
8484
Login: github.String("kyle"),
8585
AvatarURL: github.String("https://avatars.githubusercontent.com/u/12345678?v=4"),
86-
}
86+
}, 0, nil
8787
},
8888
}, func(cfg *externalauth.Config) {
8989
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
@@ -108,11 +108,11 @@ func TestExternalAuthByID(t *testing.T) {
108108

109109
// routes includes a route for /install that returns a list of installations
110110
routes := (&oidctest.ExternalAuthConfigOptions{
111-
ValidatePayload: func(_ string) interface{} {
111+
ValidatePayload: func(_ string) (interface{}, int, error) {
112112
return github.User{
113113
Login: github.String("kyle"),
114114
AvatarURL: github.String("https://avatars.githubusercontent.com/u/12345678?v=4"),
115-
}
115+
}, 0, nil
116116
},
117117
}).AddRoute("/installs", func(_ string, rw http.ResponseWriter, r *http.Request) {
118118
httpapi.Write(r.Context(), rw, http.StatusOK, struct {
@@ -556,7 +556,7 @@ func TestExternalAuthCallback(t *testing.T) {
556556
// If the validation URL gives a non-OK status code, this
557557
// should be treated as an internal server error.
558558
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
559-
w.WriteHeader(http.StatusForbidden)
559+
w.WriteHeader(http.StatusBadRequest)
560560
w.Write([]byte("Something went wrong!"))
561561
})
562562
_, err = agentClient.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
@@ -565,7 +565,7 @@ func TestExternalAuthCallback(t *testing.T) {
565565
var apiError *codersdk.Error
566566
require.ErrorAs(t, err, &apiError)
567567
require.Equal(t, http.StatusInternalServerError, apiError.StatusCode())
568-
require.Equal(t, "validate external auth token: status 403: body: Something went wrong!", apiError.Detail)
568+
require.Equal(t, "validate external auth token: status 400: body: Something went wrong!", apiError.Detail)
569569
})
570570

571571
t.Run("ExpiredNoRefresh", func(t *testing.T) {

coderd/workspacebuilds_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import (
2020
"cdr.dev/slog/sloggers/slogtest"
2121
"github.com/coder/coder/v2/coderd/audit"
2222
"github.com/coder/coder/v2/coderd/coderdtest"
23+
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
2324
"github.com/coder/coder/v2/coderd/database"
2425
"github.com/coder/coder/v2/coderd/database/dbauthz"
2526
"github.com/coder/coder/v2/coderd/database/dbtime"
27+
"github.com/coder/coder/v2/coderd/externalauth"
2628
"github.com/coder/coder/v2/coderd/rbac"
2729
"github.com/coder/coder/v2/codersdk"
2830
"github.com/coder/coder/v2/provisioner/echo"
@@ -711,6 +713,78 @@ func TestWorkspaceBuildStatus(t *testing.T) {
711713
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
712714
}
713715

716+
func TestWorkspaceDeleteSuspendedUser(t *testing.T) {
717+
t.Parallel()
718+
const providerID = "fake-github"
719+
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
720+
721+
validateCalls := 0
722+
userSuspended := false
723+
owner := coderdtest.New(t, &coderdtest.Options{
724+
IncludeProvisionerDaemon: true,
725+
ExternalAuthConfigs: []*externalauth.Config{
726+
fake.ExternalAuthConfig(t, providerID, &oidctest.ExternalAuthConfigOptions{
727+
ValidatePayload: func(email string) (interface{}, int, error) {
728+
validateCalls++
729+
if userSuspended {
730+
// Simulate the user being suspended from the IDP too.
731+
return "", http.StatusForbidden, fmt.Errorf("user is suspended")
732+
}
733+
return "OK", 0, nil
734+
},
735+
}),
736+
},
737+
})
738+
739+
first := coderdtest.CreateFirstUser(t, owner)
740+
741+
// New user that we will suspend when we try to delete the workspace.
742+
client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleTemplateAdmin())
743+
fake.ExternalLogin(t, client)
744+
745+
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, &echo.Responses{
746+
Parse: echo.ParseComplete,
747+
ProvisionApply: echo.ApplyComplete,
748+
ProvisionPlan: []*proto.Response{{
749+
Type: &proto.Response_Plan{
750+
Plan: &proto.PlanComplete{
751+
Error: "",
752+
Resources: nil,
753+
Parameters: nil,
754+
ExternalAuthProviders: []*proto.ExternalAuthProviderResource{
755+
{
756+
Id: providerID,
757+
Optional: false,
758+
},
759+
},
760+
},
761+
},
762+
}},
763+
})
764+
765+
validateCalls = 0 // Reset
766+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
767+
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
768+
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
769+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
770+
require.Equal(t, 1, validateCalls) // Ensure the external link is working
771+
772+
// Suspend the user
773+
ctx := testutil.Context(t, testutil.WaitLong)
774+
_, err := owner.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended)
775+
require.NoError(t, err, "suspend user")
776+
777+
// Now delete the workspace build
778+
userSuspended = true
779+
build, err := owner.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
780+
Transition: codersdk.WorkspaceTransitionDelete,
781+
})
782+
require.NoError(t, err)
783+
build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, build.ID)
784+
require.Equal(t, 2, validateCalls)
785+
require.Equal(t, codersdk.WorkspaceStatusDeleted, build.Status)
786+
}
787+
714788
func TestWorkspaceBuildDebugMode(t *testing.T) {
715789
t.Parallel()
716790

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