Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit67d89bb

Browse files
authored
feat: implement sign up with GitHub for the first user (#16629)
Second PR to address#16230. Seethe issue for more context and discussion.It adds a "Continue with GitHub" button to the `/setup` page, so thedeployment's admin can sign up with it. It also removes the "Username"and "Full Name" fields to make signing up with email faster. In theemail flow, the username is now auto-generated based on the email, andfull name is left empty.<img width="1512" alt="Screenshot 2025-02-21 at 17 51 22"src="https://github.com/user-attachments/assets/e7c6986b-c05e-458b-bb01-c3aea3b74c0e"/>There's a separate, follow up issue to visually align the `/setup` pagewith the new design system:#16653
1 parentb419b36 commit67d89bb

File tree

7 files changed

+171
-59
lines changed

7 files changed

+171
-59
lines changed

‎coderd/userauth.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/coder/coder/v2/coderd/cryptokeys"
2828
"github.com/coder/coder/v2/coderd/idpsync"
2929
"github.com/coder/coder/v2/coderd/jwtutils"
30+
"github.com/coder/coder/v2/coderd/telemetry"
3031
"github.com/coder/coder/v2/coderd/util/ptr"
3132

3233
"github.com/coder/coder/v2/coderd/apikey"
@@ -1054,6 +1055,10 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
10541055
deferparams.CommitAuditLogs()
10551056
iferr!=nil {
10561057
ifhttpErr:=idpsync.IsHTTPError(err);httpErr!=nil {
1058+
// In the device flow, the error page is rendered client-side.
1059+
ifapi.GithubOAuth2Config.DeviceFlowEnabled&&httpErr.RenderStaticPage {
1060+
httpErr.RenderStaticPage=false
1061+
}
10571062
httpErr.Write(rw,r)
10581063
return
10591064
}
@@ -1634,7 +1639,17 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
16341639
isConvertLoginType=true
16351640
}
16361641

1637-
ifuser.ID==uuid.Nil&&!params.AllowSignups {
1642+
// nolint:gocritic // Getting user count is a system function.
1643+
userCount,err:=tx.GetUserCount(dbauthz.AsSystemRestricted(ctx))
1644+
iferr!=nil {
1645+
returnxerrors.Errorf("unable to fetch user count: %w",err)
1646+
}
1647+
1648+
// Allow the first user to sign up with OIDC, regardless of
1649+
// whether signups are enabled or not.
1650+
allowSignup:=userCount==0||params.AllowSignups
1651+
1652+
ifuser.ID==uuid.Nil&&!allowSignup {
16381653
signupsDisabledText:="Please contact your Coder administrator to request access."
16391654
ifapi.OIDCConfig!=nil&&api.OIDCConfig.SignupsDisabledText!="" {
16401655
signupsDisabledText=render.HTMLFromMarkdown(api.OIDCConfig.SignupsDisabledText)
@@ -1695,6 +1710,12 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
16951710
returnxerrors.Errorf("unable to fetch default organization: %w",err)
16961711
}
16971712

1713+
rbacRoles:= []string{}
1714+
// If this is the first user, add the owner role.
1715+
ifuserCount==0 {
1716+
rbacRoles=append(rbacRoles,rbac.RoleOwner().String())
1717+
}
1718+
16981719
//nolint:gocritic
16991720
user,err=api.CreateUser(dbauthz.AsSystemRestricted(ctx),tx,CreateUserRequest{
17001721
CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{
@@ -1709,10 +1730,20 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
17091730
},
17101731
LoginType:params.LoginType,
17111732
accountCreatorName:"oauth",
1733+
RBACRoles:rbacRoles,
17121734
})
17131735
iferr!=nil {
17141736
returnxerrors.Errorf("create user: %w",err)
17151737
}
1738+
1739+
ifuserCount==0 {
1740+
telemetryUser:=telemetry.ConvertUser(user)
1741+
// The email is not anonymized for the first user.
1742+
telemetryUser.Email=&user.Email
1743+
api.Telemetry.Report(&telemetry.Snapshot{
1744+
Users: []telemetry.User{telemetryUser},
1745+
})
1746+
}
17161747
}
17171748

17181749
// Activate dormant user on sign-in

‎coderd/userauth_test.go

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/prometheus/client_golang/prometheus"
2323
"github.com/stretchr/testify/assert"
2424
"github.com/stretchr/testify/require"
25+
"go.uber.org/atomic"
2526
"golang.org/x/oauth2"
2627
"golang.org/x/xerrors"
2728

@@ -254,37 +255,64 @@ func TestUserOAuth2Github(t *testing.T) {
254255
})
255256
t.Run("BlockSignups",func(t*testing.T) {
256257
t.Parallel()
258+
259+
db,ps:=dbtestutil.NewDB(t)
260+
261+
id:=atomic.NewInt64(100)
262+
login:=atomic.NewString("testuser")
263+
email:=atomic.NewString("testuser@coder.com")
264+
257265
client:=coderdtest.New(t,&coderdtest.Options{
266+
Database:db,
267+
Pubsub:ps,
258268
GithubOAuth2Config:&coderd.GithubOAuth2Config{
259269
OAuth2Config:&testutil.OAuth2Config{},
260270
AllowOrganizations: []string{"coder"},
261-
ListOrganizationMemberships:func(ctx context.Context,client*http.Client) ([]*github.Membership,error) {
271+
ListOrganizationMemberships:func(_ context.Context,_*http.Client) ([]*github.Membership,error) {
262272
return []*github.Membership{{
263273
State:&stateActive,
264274
Organization:&github.Organization{
265275
Login:github.String("coder"),
266276
},
267277
}},nil
268278
},
269-
AuthenticatedUser:func(ctx context.Context,client*http.Client) (*github.User,error) {
279+
AuthenticatedUser:func(_ context.Context,_*http.Client) (*github.User,error) {
280+
id:=id.Load()
281+
login:=login.Load()
270282
return&github.User{
271-
ID:github.Int64(100),
272-
Login:github.String("testuser"),
283+
ID:&id,
284+
Login:&login,
273285
Name:github.String("The Right Honorable Sir Test McUser"),
274286
},nil
275287
},
276-
ListEmails:func(ctx context.Context,client*http.Client) ([]*github.UserEmail,error) {
288+
ListEmails:func(_ context.Context,_*http.Client) ([]*github.UserEmail,error) {
289+
email:=email.Load()
277290
return []*github.UserEmail{{
278-
Email:github.String("testuser@coder.com"),
291+
Email:&email,
279292
Verified:github.Bool(true),
280293
Primary:github.Bool(true),
281294
}},nil
282295
},
283296
},
284297
})
285298

299+
// The first user in a deployment with signups disabled will be allowed to sign up,
300+
// but all the other users will not.
286301
resp:=oauth2Callback(t,client)
302+
require.Equal(t,http.StatusTemporaryRedirect,resp.StatusCode)
303+
304+
ctx:=testutil.Context(t,testutil.WaitLong)
305+
306+
// nolint:gocritic // Unit test
307+
count,err:=db.GetUserCount(dbauthz.AsSystemRestricted(ctx))
308+
require.NoError(t,err)
309+
require.Equal(t,int64(1),count)
310+
311+
id.Store(101)
312+
email.Store("someotheruser@coder.com")
313+
login.Store("someotheruser")
287314

315+
resp=oauth2Callback(t,client)
288316
require.Equal(t,http.StatusForbidden,resp.StatusCode)
289317
})
290318
t.Run("MultiLoginNotAllowed",func(t*testing.T) {
@@ -988,6 +1016,7 @@ func TestUserOIDC(t *testing.T) {
9881016
IgnoreEmailVerifiedbool
9891017
IgnoreUserInfobool
9901018
UseAccessTokenbool
1019+
PrecreateFirstUserbool
9911020
}{
9921021
{
9931022
Name:"NoSub",
@@ -1150,7 +1179,17 @@ func TestUserOIDC(t *testing.T) {
11501179
"email_verified":true,
11511180
"sub":uuid.NewString(),
11521181
},
1153-
StatusCode:http.StatusForbidden,
1182+
StatusCode:http.StatusForbidden,
1183+
PrecreateFirstUser:true,
1184+
},
1185+
{
1186+
Name:"FirstSignup",
1187+
IDTokenClaims: jwt.MapClaims{
1188+
"email":"kyle@kwc.io",
1189+
"email_verified":true,
1190+
"sub":uuid.NewString(),
1191+
},
1192+
StatusCode:http.StatusOK,
11541193
},
11551194
{
11561195
Name:"UsernameFromEmail",
@@ -1443,15 +1482,22 @@ func TestUserOIDC(t *testing.T) {
14431482
})
14441483
numLogs:=len(auditor.AuditLogs())
14451484

1485+
ctx:=testutil.Context(t,testutil.WaitShort)
1486+
iftc.PrecreateFirstUser {
1487+
owner.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
1488+
Email:"precreated@coder.com",
1489+
Username:"precreated",
1490+
Password:"SomeSecurePassword!",
1491+
})
1492+
}
1493+
14461494
client,resp:=fake.AttemptLogin(t,owner,tc.IDTokenClaims)
14471495
numLogs++// add an audit log for login
14481496
require.Equal(t,tc.StatusCode,resp.StatusCode)
14491497
iftc.AssertResponse!=nil {
14501498
tc.AssertResponse(t,resp)
14511499
}
14521500

1453-
ctx:=testutil.Context(t,testutil.WaitShort)
1454-
14551501
iftc.AssertUser!=nil {
14561502
user,err:=client.User(ctx,"me")
14571503
require.NoError(t,err)

‎coderd/users.go

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ func (api *API) firstUser(rw http.ResponseWriter, r *http.Request) {
118118
// @Success 201 {object} codersdk.CreateFirstUserResponse
119119
// @Router /users/first [post]
120120
func (api*API)postFirstUser(rw http.ResponseWriter,r*http.Request) {
121+
// The first user can also be created via oidc, so if making changes to the flow,
122+
// ensure that the oidc flow is also updated.
121123
ctx:=r.Context()
122124
varcreateUser codersdk.CreateFirstUserRequest
123125
if!httpapi.Read(ctx,rw,r,&createUser) {
@@ -198,6 +200,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
198200
OrganizationIDs: []uuid.UUID{defaultOrg.ID},
199201
},
200202
LoginType:database.LoginTypePassword,
203+
RBACRoles: []string{rbac.RoleOwner().String()},
201204
accountCreatorName:"coder",
202205
})
203206
iferr!=nil {
@@ -225,23 +228,6 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
225228
Users: []telemetry.User{telemetryUser},
226229
})
227230

228-
// TODO: @emyrk this currently happens outside the database tx used to create
229-
// the user. Maybe I add this ability to grant roles in the createUser api
230-
//and add some rbac bypass when calling api functions this way??
231-
// Add the admin role to this first user.
232-
//nolint:gocritic // needed to create first user
233-
_,err=api.Database.UpdateUserRoles(dbauthz.AsSystemRestricted(ctx), database.UpdateUserRolesParams{
234-
GrantedRoles: []string{rbac.RoleOwner().String()},
235-
ID:user.ID,
236-
})
237-
iferr!=nil {
238-
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
239-
Message:"Internal error updating user's roles.",
240-
Detail:err.Error(),
241-
})
242-
return
243-
}
244-
245231
httpapi.Write(ctx,rw,http.StatusCreated, codersdk.CreateFirstUserResponse{
246232
UserID:user.ID,
247233
OrganizationID:defaultOrg.ID,
@@ -1351,6 +1337,7 @@ type CreateUserRequest struct {
13511337
LoginType database.LoginType
13521338
SkipNotificationsbool
13531339
accountCreatorNamestring
1340+
RBACRoles []string
13541341
}
13551342

13561343
func (api*API)CreateUser(ctx context.Context,store database.Store,reqCreateUserRequest) (database.User,error) {
@@ -1360,6 +1347,13 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
13601347
return database.User{},xerrors.Errorf("invalid username %q: %w",req.Username,usernameValid)
13611348
}
13621349

1350+
// If the caller didn't specify rbac roles, default to
1351+
// a member of the site.
1352+
rbacRoles:= []string{}
1353+
ifreq.RBACRoles!=nil {
1354+
rbacRoles=req.RBACRoles
1355+
}
1356+
13631357
varuser database.User
13641358
err:=store.InTx(func(tx database.Store)error {
13651359
orgRoles:=make([]string,0)
@@ -1376,10 +1370,9 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
13761370
CreatedAt:dbtime.Now(),
13771371
UpdatedAt:dbtime.Now(),
13781372
HashedPassword: []byte{},
1379-
// All new users are defaulted to members of the site.
1380-
RBACRoles: []string{},
1381-
LoginType:req.LoginType,
1382-
Status:status,
1373+
RBACRoles:rbacRoles,
1374+
LoginType:req.LoginType,
1375+
Status:status,
13831376
}
13841377
// If a user signs up with OAuth, they can have no password!
13851378
ifreq.Password!="" {
@@ -1437,6 +1430,10 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
14371430
}
14381431

14391432
for_,u:=rangeuserAdmins {
1433+
ifu.ID==user.ID {
1434+
// If the new user is an admin, don't notify them about themselves.
1435+
continue
1436+
}
14401437
if_,err:=api.NotificationsEnqueuer.EnqueueWithData(
14411438
// nolint:gocritic // Need notifier actor to enqueue notifications
14421439
dbauthz.AsNotifier(ctx),

‎site/e2e/setup/addUsersAndLicense.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ test("setup deployment", async ({ page }) => {
1616
}
1717

1818
// Setup first user
19-
awaitpage.getByLabel(Language.usernameLabel).fill(users.admin.username);
2019
awaitpage.getByLabel(Language.emailLabel).fill(users.admin.email);
2120
awaitpage.getByLabel(Language.passwordLabel).fill(users.admin.password);
2221
awaitpage.getByTestId("create").click();

‎site/src/pages/SetupPage/SetupPage.test.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,15 @@ import { SetupPage } from "./SetupPage";
1313
import{LanguageasPageViewLanguage}from"./SetupPageView";
1414

1515
constfillForm=async({
16-
username="someuser",
1716
email="someone@coder.com",
1817
password="password",
1918
}:{
2019
username?:string;
2120
email?:string;
2221
password?:string;
2322
}={})=>{
24-
constusernameField=screen.getByLabelText(PageViewLanguage.usernameLabel);
2523
constemailField=screen.getByLabelText(PageViewLanguage.emailLabel);
2624
constpasswordField=screen.getByLabelText(PageViewLanguage.passwordLabel);
27-
awaituserEvent.type(usernameField,username);
2825
awaituserEvent.type(emailField,email);
2926
awaituserEvent.type(passwordField,password);
3027
constsubmitButton=screen.getByRole("button",{

‎site/src/pages/SetupPage/SetupPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import{buildInfo}from"api/queries/buildInfo";
2-
import{createFirstUser}from"api/queries/users";
2+
import{authMethods,createFirstUser}from"api/queries/users";
33
import{Loader}from"components/Loader/Loader";
44
import{useAuthContext}from"contexts/auth/AuthProvider";
55
import{useEmbeddedMetadata}from"hooks/useEmbeddedMetadata";
@@ -19,6 +19,7 @@ export const SetupPage: FC = () => {
1919
isSignedIn,
2020
isSigningIn,
2121
}=useAuthContext();
22+
constauthMethodsQuery=useQuery(authMethods());
2223
constcreateFirstUserMutation=useMutation(createFirstUser());
2324
constsetupIsComplete=!isConfiguringTheFirstUser;
2425
const{ metadata}=useEmbeddedMetadata();
@@ -34,7 +35,7 @@ export const SetupPage: FC = () => {
3435
});
3536
},[buildInfoQuery.data]);
3637

37-
if(isLoading){
38+
if(isLoading||authMethodsQuery.isLoading){
3839
return<Loaderfullscreen/>;
3940
}
4041

@@ -54,6 +55,7 @@ export const SetupPage: FC = () => {
5455
<title>{pageTitle("Set up your account")}</title>
5556
</Helmet>
5657
<SetupPageView
58+
authMethods={authMethodsQuery.data}
5759
isLoading={isSigningIn||createFirstUserMutation.isLoading}
5860
error={createFirstUserMutation.error}
5961
onSubmit={async(firstUser)=>{

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp