mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
Merge branch 'main' of github.com:/coder/coder into dk/prebuilds
This commit is contained in:
28
coderd/apidoc/docs.go
generated
28
coderd/apidoc/docs.go
generated
@ -6167,6 +6167,31 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/oauth2/github/device": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get Github device auth.",
|
||||
"operationId": "get-github-device-auth",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthDevice"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/oidc/callback": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -12504,6 +12529,9 @@ const docTemplate = `{
|
||||
"client_secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enterprise_base_url": {
|
||||
"type": "string"
|
||||
}
|
||||
|
24
coderd/apidoc/swagger.json
generated
24
coderd/apidoc/swagger.json
generated
@ -5449,6 +5449,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/oauth2/github/device": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Users"],
|
||||
"summary": "Get Github device auth.",
|
||||
"operationId": "get-github-device-auth",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthDevice"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/oidc/callback": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -11244,6 +11265,9 @@
|
||||
"client_secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enterprise_base_url": {
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -1109,6 +1109,7 @@ func New(options *Options) *API {
|
||||
r.Post("/validate-password", api.validateUserPassword)
|
||||
r.Post("/otp/change-password", api.postChangePasswordWithOneTimePasscode)
|
||||
r.Route("/oauth2", func(r chi.Router) {
|
||||
r.Get("/github/device", api.userOAuth2GithubDevice)
|
||||
r.Route("/github", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil),
|
||||
|
@ -30,8 +30,8 @@ func New() *Set {
|
||||
// These will be updated when coderd is initialized.
|
||||
entitlements: codersdk.Entitlements{
|
||||
Features: map[codersdk.FeatureName]codersdk.Feature{},
|
||||
Warnings: nil,
|
||||
Errors: nil,
|
||||
Warnings: []string{},
|
||||
Errors: []string{},
|
||||
HasLicense: false,
|
||||
Trial: false,
|
||||
RequireTelemetry: false,
|
||||
@ -39,13 +39,21 @@ func New() *Set {
|
||||
},
|
||||
right2Update: make(chan struct{}, 1),
|
||||
}
|
||||
// Ensure all features are present in the entitlements. Our frontend
|
||||
// expects this.
|
||||
for _, featureName := range codersdk.FeatureNames {
|
||||
s.entitlements.AddFeature(featureName, codersdk.Feature{
|
||||
Entitlement: codersdk.EntitlementNotEntitled,
|
||||
Enabled: false,
|
||||
})
|
||||
}
|
||||
s.right2Update <- struct{}{} // one token, serialized updates
|
||||
return s
|
||||
}
|
||||
|
||||
// ErrLicenseRequiresTelemetry is an error returned by a fetch passed to Update to indicate that the
|
||||
// fetched license cannot be used because it requires telemetry.
|
||||
var ErrLicenseRequiresTelemetry = xerrors.New("License requires telemetry but telemetry is disabled")
|
||||
var ErrLicenseRequiresTelemetry = xerrors.New(codersdk.LicenseTelemetryRequiredErrorText)
|
||||
|
||||
func (l *Set) Update(ctx context.Context, fetch func(context.Context) (codersdk.Entitlements, error)) error {
|
||||
select {
|
||||
|
@ -154,6 +154,7 @@ func ResourceNotFound(rw http.ResponseWriter) {
|
||||
func Forbidden(rw http.ResponseWriter) {
|
||||
Write(context.Background(), rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Forbidden.",
|
||||
Detail: "You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials.",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -167,9 +167,16 @@ func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOp
|
||||
|
||||
oauthToken, err := config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error exchanging Oauth code.",
|
||||
Detail: err.Error(),
|
||||
errorCode := http.StatusInternalServerError
|
||||
detail := err.Error()
|
||||
if detail == "authorization_pending" {
|
||||
// In the device flow, the token may not be immediately
|
||||
// available. This is expected, and the client will retry.
|
||||
errorCode = http.StatusBadRequest
|
||||
}
|
||||
httpapi.Write(ctx, rw, errorCode, codersdk.Response{
|
||||
Message: "Failed exchanging Oauth code.",
|
||||
Detail: detail,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ func (api *API) postTestNotification(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, nil)
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @Summary Get user notification preferences
|
||||
|
@ -283,10 +283,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
Permissions(map[string][]policy.Action{
|
||||
// Reduced permission set on dormant workspaces. No build, ssh, or exec
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop},
|
||||
|
||||
// Users cannot do create/update/delete on themselves, but they
|
||||
// can read their own details.
|
||||
ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
|
||||
// Can read their own organization member record
|
||||
ResourceOrganizationMember.Type: {policy.ActionRead},
|
||||
// Users can create provisioner daemons scoped to themselves.
|
||||
ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
||||
})...,
|
||||
@ -423,12 +424,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ResourceAssignOrgRole.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
User: []Permission{
|
||||
{
|
||||
ResourceType: ResourceOrganizationMember.Type,
|
||||
Action: policy.ActionRead,
|
||||
},
|
||||
},
|
||||
User: []Permission{},
|
||||
}
|
||||
},
|
||||
orgAuditor: func(organizationID uuid.UUID) Role {
|
||||
@ -439,6 +435,12 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
Org: map[string][]Permission{
|
||||
organizationID.String(): Permissions(map[string][]policy.Action{
|
||||
ResourceAuditLog.Type: {policy.ActionRead},
|
||||
// Allow auditors to see the resources that audit logs reflect.
|
||||
ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights},
|
||||
ResourceGroup.Type: {policy.ActionRead},
|
||||
ResourceGroupMember.Type: {policy.ActionRead},
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
ResourceOrganizationMember.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
User: []Permission{},
|
||||
@ -458,6 +460,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
organizationID.String(): Permissions(map[string][]policy.Action{
|
||||
// Assign, remove, and read roles in the organization.
|
||||
ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead},
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
ResourceGroup.Type: ResourceGroup.AvailableActions(),
|
||||
ResourceGroupMember.Type: ResourceGroupMember.AvailableActions(),
|
||||
@ -479,10 +482,15 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
|
||||
ResourceWorkspace.Type: {policy.ActionRead},
|
||||
// Assigning template perms requires this permission.
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
ResourceOrganizationMember.Type: {policy.ActionRead},
|
||||
ResourceGroup.Type: {policy.ActionRead},
|
||||
ResourceGroupMember.Type: {policy.ActionRead},
|
||||
ResourceProvisionerJobs.Type: {policy.ActionRead},
|
||||
// Since templates have to correlate with provisioners,
|
||||
// the ability to create templates and provisioners has
|
||||
// a lot of overlap.
|
||||
ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
ResourceProvisionerJobs.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
User: []Permission{},
|
||||
|
@ -217,20 +217,20 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "Templates",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights},
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, orgMemberMe, userAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, orgMemberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ReadTemplates",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionViewInsights},
|
||||
Resource: rbac.ResourceTemplate.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin, orgMemberMe},
|
||||
true: {owner, orgAuditor, orgAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, memberMe, userAdmin, orgMemberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -377,8 +377,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, orgMemberMe, templateAdmin, orgUserAdmin, orgTemplateAdmin},
|
||||
false: {memberMe, setOtherOrg, orgAuditor},
|
||||
true: {owner, orgAuditor, orgAdmin, userAdmin, orgMemberMe, templateAdmin, orgUserAdmin, orgTemplateAdmin},
|
||||
false: {memberMe, setOtherOrg},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -404,7 +404,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
}),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, templateAdmin, orgTemplateAdmin, orgAuditor, groupMemberMe},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, templateAdmin, orgTemplateAdmin, groupMemberMe, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -416,8 +416,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
}),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, groupMemberMe},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, orgAuditor},
|
||||
true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, groupMemberMe, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -425,8 +425,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceGroupMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgMemberMe, groupMemberMe},
|
||||
false: {setOtherOrg, memberMe, orgAuditor},
|
||||
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgMemberMe, groupMemberMe},
|
||||
false: {setOtherOrg, memberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -434,8 +434,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceGroupMember.WithID(adminID).InOrg(orgID).WithOwner(adminID.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, orgAuditor, orgMemberMe, groupMemberMe},
|
||||
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, groupMemberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -534,8 +534,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, templateAdmin, orgAdmin},
|
||||
false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, memberMe, orgMemberMe, userAdmin, orgAuditor},
|
||||
true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, orgMemberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -552,8 +552,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, templateAdmin, orgMemberMe, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, userAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
true: {owner, templateAdmin, orgTemplateAdmin, orgMemberMe, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, userAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -748,12 +748,32 @@ type GithubOAuth2Config struct {
|
||||
ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error)
|
||||
TeamMembership func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error)
|
||||
|
||||
DeviceFlowEnabled bool
|
||||
ExchangeDeviceCode func(ctx context.Context, deviceCode string) (*oauth2.Token, error)
|
||||
AuthorizeDevice func(ctx context.Context) (*codersdk.ExternalAuthDevice, error)
|
||||
|
||||
AllowSignups bool
|
||||
AllowEveryone bool
|
||||
AllowOrganizations []string
|
||||
AllowTeams []GithubOAuth2Team
|
||||
}
|
||||
|
||||
func (c *GithubOAuth2Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
if !c.DeviceFlowEnabled {
|
||||
return c.OAuth2Config.Exchange(ctx, code, opts...)
|
||||
}
|
||||
return c.ExchangeDeviceCode(ctx, code)
|
||||
}
|
||||
|
||||
func (c *GithubOAuth2Config) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
if !c.DeviceFlowEnabled {
|
||||
return c.OAuth2Config.AuthCodeURL(state, opts...)
|
||||
}
|
||||
// This is an absolute path in the Coder app. The device flow is orchestrated
|
||||
// by the Coder frontend, so we need to redirect the user to the device flow page.
|
||||
return "/login/device?state=" + state
|
||||
}
|
||||
|
||||
// @Summary Get authentication methods
|
||||
// @ID get-authentication-methods
|
||||
// @Security CoderSessionToken
|
||||
@ -786,6 +806,53 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get Github device auth.
|
||||
// @ID get-github-device-auth
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Users
|
||||
// @Success 200 {object} codersdk.ExternalAuthDevice
|
||||
// @Router /users/oauth2/github/device [get]
|
||||
func (api *API) userOAuth2GithubDevice(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.APIKey](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionLogin,
|
||||
})
|
||||
)
|
||||
aReq.Old = database.APIKey{}
|
||||
defer commitAudit()
|
||||
|
||||
if api.GithubOAuth2Config == nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Github OAuth2 is not enabled.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !api.GithubOAuth2Config.DeviceFlowEnabled {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Device flow is not enabled for Github OAuth2.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
deviceAuth, err := api.GithubOAuth2Config.AuthorizeDevice(ctx)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to authorize device.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, deviceAuth)
|
||||
}
|
||||
|
||||
// @Summary OAuth 2.0 GitHub Callback
|
||||
// @ID oauth-20-github-callback
|
||||
// @Security CoderSessionToken
|
||||
@ -1016,7 +1083,14 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
redirect = uriFromURL(redirect)
|
||||
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
||||
if api.GithubOAuth2Config.DeviceFlowEnabled {
|
||||
// In the device flow, the redirect is handled client-side.
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.OAuth2DeviceFlowCallbackResponse{
|
||||
RedirectURL: redirect,
|
||||
})
|
||||
} else {
|
||||
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@ -882,6 +883,92 @@ func TestUserOAuth2Github(t *testing.T) {
|
||||
require.Equal(t, user.ID, userID, "user_id is different, a new user was likely created")
|
||||
require.Equal(t, user.Email, newEmail)
|
||||
})
|
||||
t.Run("DeviceFlow", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
GithubOAuth2Config: &coderd.GithubOAuth2Config{
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
AllowOrganizations: []string{"coder"},
|
||||
AllowSignups: true,
|
||||
ListOrganizationMemberships: func(_ context.Context, _ *http.Client) ([]*github.Membership, error) {
|
||||
return []*github.Membership{{
|
||||
State: &stateActive,
|
||||
Organization: &github.Organization{
|
||||
Login: github.String("coder"),
|
||||
},
|
||||
}}, nil
|
||||
},
|
||||
AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) {
|
||||
return &github.User{
|
||||
ID: github.Int64(100),
|
||||
Login: github.String("testuser"),
|
||||
Name: github.String("The Right Honorable Sir Test McUser"),
|
||||
}, nil
|
||||
},
|
||||
ListEmails: func(_ context.Context, _ *http.Client) ([]*github.UserEmail, error) {
|
||||
return []*github.UserEmail{{
|
||||
Email: github.String("testuser@coder.com"),
|
||||
Verified: github.Bool(true),
|
||||
Primary: github.Bool(true),
|
||||
}}, nil
|
||||
},
|
||||
DeviceFlowEnabled: true,
|
||||
ExchangeDeviceCode: func(_ context.Context, _ string) (*oauth2.Token, error) {
|
||||
return &oauth2.Token{
|
||||
AccessToken: "access_token",
|
||||
RefreshToken: "refresh_token",
|
||||
Expiry: time.Now().Add(time.Hour),
|
||||
}, nil
|
||||
},
|
||||
AuthorizeDevice: func(_ context.Context) (*codersdk.ExternalAuthDevice, error) {
|
||||
return &codersdk.ExternalAuthDevice{
|
||||
DeviceCode: "device_code",
|
||||
UserCode: "user_code",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
})
|
||||
client.HTTPClient.CheckRedirect = func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
// Ensure that we redirect to the device login page when the user is not logged in.
|
||||
oauthURL, err := client.URL.Parse("/api/v2/users/oauth2/github/callback")
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
res, err := client.HTTPClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode)
|
||||
location, err := res.Location()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/login/device", location.Path)
|
||||
query := location.Query()
|
||||
require.NotEmpty(t, query.Get("state"))
|
||||
|
||||
// Ensure that we return a JSON response when the code is successfully exchanged.
|
||||
oauthURL, err = client.URL.Parse("/api/v2/users/oauth2/github/callback?code=hey&state=somestate")
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "oauth_state",
|
||||
Value: "somestate",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
res, err = client.HTTPClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
var resp codersdk.OAuth2DeviceFlowCallbackResponse
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&resp))
|
||||
require.Equal(t, "/", resp.RedirectURL)
|
||||
})
|
||||
}
|
||||
|
||||
// nolint:bodyclose
|
||||
|
Reference in New Issue
Block a user