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

Commit2606fda

Browse files
authored
Merge branch 'main' into workspaceagent
2 parentsfa7489a +3f77814 commit2606fda

File tree

16 files changed

+448
-80
lines changed

16 files changed

+448
-80
lines changed

‎cli/login.go‎

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,39 @@ package cli
22

33
import (
44
"fmt"
5+
"io/ioutil"
56
"net/url"
7+
"os/exec"
68
"os/user"
9+
"runtime"
710
"strings"
811

912
"github.com/fatih/color"
1013
"github.com/go-playground/validator/v10"
1114
"github.com/manifoldco/promptui"
15+
"github.com/pkg/browser"
1216
"github.com/spf13/cobra"
1317
"golang.org/x/xerrors"
1418

1519
"github.com/coder/coder/coderd"
1620
"github.com/coder/coder/codersdk"
1721
)
1822

23+
const (
24+
goosWindows="windows"
25+
goosDarwin="darwin"
26+
)
27+
28+
funcinit() {
29+
// Hide output from the browser library,
30+
// otherwise we can get really verbose and non-actionable messages
31+
// when in SSH or another type of headless session
32+
// NOTE: This needs to be in `init` to prevent data races
33+
// (multiple threads trying to set the global browser.Std* variables)
34+
browser.Stderr=ioutil.Discard
35+
browser.Stdout=ioutil.Discard
36+
}
37+
1938
funclogin()*cobra.Command {
2039
return&cobra.Command{
2140
Use:"login <url>",
@@ -116,8 +135,10 @@ func login() *cobra.Command {
116135
iferr!=nil {
117136
returnxerrors.Errorf("login with password: %w",err)
118137
}
138+
139+
sessionToken:=resp.SessionToken
119140
config:=createConfig(cmd)
120-
err=config.Session().Write(resp.SessionToken)
141+
err=config.Session().Write(sessionToken)
121142
iferr!=nil {
122143
returnxerrors.Errorf("write session token: %w",err)
123144
}
@@ -130,7 +151,82 @@ func login() *cobra.Command {
130151
returnnil
131152
}
132153

154+
authURL:=*serverURL
155+
authURL.Path=serverURL.Path+"/cli-auth"
156+
iferr:=openURL(authURL.String());err!=nil {
157+
_,_=fmt.Fprintf(cmd.OutOrStdout(),"Open the following in your browser:\n\n\t%s\n\n",authURL.String())
158+
}else {
159+
_,_=fmt.Fprintf(cmd.OutOrStdout(),"Your browser has been opened to visit:\n\n\t%s\n\n",authURL.String())
160+
}
161+
162+
sessionToken,err:=prompt(cmd,&promptui.Prompt{
163+
Label:"Paste your token here:",
164+
Mask:'*',
165+
Validate:func(tokenstring)error {
166+
client.SessionToken=token
167+
_,err:=client.User(cmd.Context(),"me")
168+
iferr!=nil {
169+
returnxerrors.New("That's not a valid token!")
170+
}
171+
returnerr
172+
},
173+
})
174+
iferr!=nil {
175+
returnxerrors.Errorf("paste token prompt: %w",err)
176+
}
177+
178+
// Login to get user data - verify it is OK before persisting
179+
client.SessionToken=sessionToken
180+
resp,err:=client.User(cmd.Context(),"me")
181+
iferr!=nil {
182+
returnxerrors.Errorf("get user: %w",err)
183+
}
184+
185+
config:=createConfig(cmd)
186+
err=config.Session().Write(sessionToken)
187+
iferr!=nil {
188+
returnxerrors.Errorf("write session token: %w",err)
189+
}
190+
err=config.URL().Write(serverURL.String())
191+
iferr!=nil {
192+
returnxerrors.Errorf("write server url: %w",err)
193+
}
194+
195+
_,_=fmt.Fprintf(cmd.OutOrStdout(),"%s Welcome to Coder, %s! You're authenticated.\n",color.HiBlackString(">"),color.HiCyanString(resp.Username))
133196
returnnil
134197
},
135198
}
136199
}
200+
201+
// isWSL determines if coder-cli is running within Windows Subsystem for Linux
202+
funcisWSL() (bool,error) {
203+
ifruntime.GOOS==goosDarwin||runtime.GOOS==goosWindows {
204+
returnfalse,nil
205+
}
206+
data,err:=ioutil.ReadFile("/proc/version")
207+
iferr!=nil {
208+
returnfalse,xerrors.Errorf("read /proc/version: %w",err)
209+
}
210+
returnstrings.Contains(strings.ToLower(string(data)),"microsoft"),nil
211+
}
212+
213+
// openURL opens the provided URL via user's default browser
214+
funcopenURL(urlToOpenstring)error {
215+
varcmdstring
216+
varargs []string
217+
218+
wsl,err:=isWSL()
219+
iferr!=nil {
220+
returnxerrors.Errorf("test running Windows Subsystem for Linux: %w",err)
221+
}
222+
223+
ifwsl {
224+
cmd="cmd.exe"
225+
args= []string{"/c","start"}
226+
urlToOpen=strings.ReplaceAll(urlToOpen,"&","^&")
227+
args=append(args,urlToOpen)
228+
returnexec.Command(cmd,args...).Start()
229+
}
230+
231+
returnbrowser.OpenURL(urlToOpen)
232+
}

‎cli/login_test.go‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cli_test
22

33
import (
4+
"context"
45
"testing"
56

67
"github.com/stretchr/testify/require"
78

89
"github.com/coder/coder/cli/clitest"
10+
"github.com/coder/coder/coderd"
911
"github.com/coder/coder/coderd/coderdtest"
1012
"github.com/coder/coder/pty/ptytest"
1113
)
@@ -50,4 +52,60 @@ func TestLogin(t *testing.T) {
5052
}
5153
pty.ExpectMatch("Welcome to Coder")
5254
})
55+
56+
t.Run("ExistingUserValidTokenTTY",func(t*testing.T) {
57+
t.Parallel()
58+
client:=coderdtest.New(t)
59+
_,err:=client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
60+
Username:"test-user",
61+
Email:"test-user@coder.com",
62+
Organization:"acme-corp",
63+
Password:"password",
64+
})
65+
require.NoError(t,err)
66+
token,err:=client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
67+
Email:"test-user@coder.com",
68+
Password:"password",
69+
})
70+
require.NoError(t,err)
71+
72+
root,_:=clitest.New(t,"login",client.URL.String(),"--force-tty")
73+
pty:=ptytest.New(t)
74+
root.SetIn(pty.Input())
75+
root.SetOut(pty.Output())
76+
gofunc() {
77+
err:=root.Execute()
78+
require.NoError(t,err)
79+
}()
80+
81+
pty.ExpectMatch("Paste your token here:")
82+
pty.WriteLine(token.SessionToken)
83+
pty.ExpectMatch("Welcome to Coder")
84+
})
85+
86+
t.Run("ExistingUserInvalidTokenTTY",func(t*testing.T) {
87+
t.Parallel()
88+
client:=coderdtest.New(t)
89+
_,err:=client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
90+
Username:"test-user",
91+
Email:"test-user@coder.com",
92+
Organization:"acme-corp",
93+
Password:"password",
94+
})
95+
require.NoError(t,err)
96+
97+
root,_:=clitest.New(t,"login",client.URL.String(),"--force-tty")
98+
pty:=ptytest.New(t)
99+
root.SetIn(pty.Input())
100+
root.SetOut(pty.Output())
101+
gofunc() {
102+
err:=root.Execute()
103+
// An error is expected in this case, since the login wasn't successful:
104+
require.Error(t,err)
105+
}()
106+
107+
pty.ExpectMatch("Paste your token here:")
108+
pty.WriteLine("an-invalid-token")
109+
pty.ExpectMatch("That's not a valid token!")
110+
})
53111
}

‎coderd/coderd.go‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func New(options *Options) http.Handler {
3636
})
3737
r.Post("/login",api.postLogin)
3838
r.Post("/logout",api.postLogout)
39+
3940
// Used for setup.
4041
r.Get("/user",api.user)
4142
r.Post("/user",api.postUser)
@@ -44,10 +45,12 @@ func New(options *Options) http.Handler {
4445
httpmw.ExtractAPIKey(options.Database,nil),
4546
)
4647
r.Post("/",api.postUsers)
47-
r.Group(func(r chi.Router) {
48+
49+
r.Route("/{user}",func(r chi.Router) {
4850
r.Use(httpmw.ExtractUserParam(options.Database))
49-
r.Get("/{user}",api.userByName)
50-
r.Get("/{user}/organizations",api.organizationsByUser)
51+
r.Get("/",api.userByName)
52+
r.Get("/organizations",api.organizationsByUser)
53+
r.Post("/keys",api.postKeyForUser)
5154
})
5255
})
5356
r.Route("/projects",func(r chi.Router) {

‎coderd/users.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ type LoginWithPasswordResponse struct {
5555
SessionTokenstring`json:"session_token" validate:"required"`
5656
}
5757

58+
// GenerateAPIKeyResponse contains an API key for a user.
59+
typeGenerateAPIKeyResponsestruct {
60+
Keystring`json:"key"`
61+
}
62+
5863
// Returns whether the initial user has been created or not.
5964
func (api*api)user(rw http.ResponseWriter,r*http.Request) {
6065
userCount,err:=api.Database.GetUserCount(r.Context())
@@ -312,6 +317,50 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
312317
})
313318
}
314319

320+
// Creates a new session key, used for logging in via the CLI
321+
func (api*api)postKeyForUser(rw http.ResponseWriter,r*http.Request) {
322+
user:=httpmw.UserParam(r)
323+
apiKey:=httpmw.APIKey(r)
324+
325+
ifuser.ID!=apiKey.UserID {
326+
httpapi.Write(rw,http.StatusUnauthorized, httpapi.Response{
327+
Message:"Keys can only be generated for the authenticated user",
328+
})
329+
return
330+
}
331+
332+
keyID,keySecret,err:=generateAPIKeyIDSecret()
333+
iferr!=nil {
334+
httpapi.Write(rw,http.StatusInternalServerError, httpapi.Response{
335+
Message:fmt.Sprintf("generate api key parts: %s",err.Error()),
336+
})
337+
return
338+
}
339+
hashed:=sha256.Sum256([]byte(keySecret))
340+
341+
_,err=api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
342+
ID:keyID,
343+
UserID:apiKey.UserID,
344+
ExpiresAt:database.Now().AddDate(1,0,0),// Expire after 1 year (same as v1)
345+
CreatedAt:database.Now(),
346+
UpdatedAt:database.Now(),
347+
HashedSecret:hashed[:],
348+
LoginType:database.LoginTypeBuiltIn,
349+
})
350+
iferr!=nil {
351+
httpapi.Write(rw,http.StatusInternalServerError, httpapi.Response{
352+
Message:fmt.Sprintf("insert api key: %s",err.Error()),
353+
})
354+
return
355+
}
356+
357+
// This format is consumed by the APIKey middleware.
358+
generatedAPIKey:=fmt.Sprintf("%s-%s",keyID,keySecret)
359+
360+
render.Status(r,http.StatusCreated)
361+
render.JSON(rw,r,GenerateAPIKeyResponse{Key:generatedAPIKey})
362+
}
363+
315364
// Clear the user's session cookie
316365
func (*api)postLogout(rw http.ResponseWriter,r*http.Request) {
317366
// Get a blank token cookie

‎coderd/users_test.go‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,33 @@ func TestOrganizationsByUser(t *testing.T) {
119119
require.Len(t,orgs,1)
120120
}
121121

122+
funcTestPostKey(t*testing.T) {
123+
t.Parallel()
124+
t.Run("InvalidUser",func(t*testing.T) {
125+
t.Parallel()
126+
client:=coderdtest.New(t)
127+
_=coderdtest.CreateInitialUser(t,client)
128+
129+
// Clear session token
130+
client.SessionToken=""
131+
// ...and request an API key
132+
_,err:=client.CreateAPIKey(context.Background())
133+
varapiErr*codersdk.Error
134+
require.ErrorAs(t,err,&apiErr)
135+
require.Equal(t,http.StatusUnauthorized,apiErr.StatusCode())
136+
})
137+
138+
t.Run("Success",func(t*testing.T) {
139+
t.Parallel()
140+
client:=coderdtest.New(t)
141+
_=coderdtest.CreateInitialUser(t,client)
142+
apiKey,err:=client.CreateAPIKey(context.Background())
143+
require.NotNil(t,apiKey)
144+
require.GreaterOrEqual(t,len(apiKey.Key),2)
145+
require.NoError(t,err)
146+
})
147+
}
148+
122149
funcTestPostLogin(t*testing.T) {
123150
t.Parallel()
124151
t.Run("InvalidUser",func(t*testing.T) {

‎codersdk/users.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) (
5656
returnuser,json.NewDecoder(res.Body).Decode(&user)
5757
}
5858

59+
// CreateAPIKey calls the /api-key API
60+
func (c*Client)CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse,error) {
61+
res,err:=c.request(ctx,http.MethodPost,"/api/v2/users/me/keys",nil)
62+
iferr!=nil {
63+
returnnil,err
64+
}
65+
deferres.Body.Close()
66+
ifres.StatusCode>http.StatusCreated {
67+
returnnil,readBodyAsError(res)
68+
}
69+
apiKey:=&coderd.GenerateAPIKeyResponse{}
70+
returnapiKey,json.NewDecoder(res.Body).Decode(apiKey)
71+
}
72+
5973
// LoginWithPassword creates a session token authenticating with an email and password.
6074
// Call `SetSessionToken()` to apply the newly acquired token to the client.
6175
func (c*Client)LoginWithPassword(ctx context.Context,req coderd.LoginWithPasswordRequest) (coderd.LoginWithPasswordResponse,error) {

‎go.mod‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ require (
115115
github.com/pion/stunv0.3.5// indirect
116116
github.com/pion/turn/v2v2.0.6// indirect
117117
github.com/pion/udpv0.1.1// indirect
118+
github.com/pkg/browserv0.0.0-20210911075715-681adbf594b8// indirect
118119
github.com/pkg/errorsv0.9.1// indirect
119120
github.com/pmezard/go-difflibv1.0.0// indirect
120121
github.com/sirupsen/logrusv1.8.1// indirect

‎go.sum‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,7 @@ github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M
10621062
github.com/pion/webrtc/v3v3.1.23 h1:suyNiF9o2/6SBsyWA1UweraUWYkaHCNJdt/16b61I5w=
10631063
github.com/pion/webrtc/v3v3.1.23/go.mod h1:L5S/oAhL0Fzt/rnftVQRrP80/j5jygY7XRZzWwFx6P4=
10641064
github.com/pkg/browserv0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
1065+
github.com/pkg/browserv0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
10651066
github.com/pkg/browserv0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
10661067
github.com/pkg/diffv0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
10671068
github.com/pkg/errorsv0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

‎site/api.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,16 @@ export const logout = async (): Promise<void> => {
139139

140140
return
141141
}
142+
143+
exportconstgetApiKey=async():Promise<{key:string}>=>{
144+
constresponse=awaitfetch("/api/v2/users/me/keys",{
145+
method:"POST",
146+
})
147+
148+
if(!response.ok){
149+
constbody=awaitresponse.json()
150+
thrownewError(body.message)
151+
}
152+
153+
returnawaitresponse.json()
154+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp