crossplane

Форк
0
287 строк · 8.1 Кб
1
/*
2
Copyright 2023 The Crossplane Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package xpkg
18

19
import (
20
	"bufio"
21
	"bytes"
22
	"context"
23
	"crypto/tls"
24
	"encoding/json"
25
	"fmt"
26
	"io"
27
	"net/http"
28
	"os"
29
	"strings"
30
	"time"
31

32
	"github.com/alecthomas/kong"
33
	"github.com/golang-jwt/jwt/v5"
34
	"github.com/upbound/up-sdk-go/service/userinfo"
35
	"golang.org/x/term"
36

37
	"github.com/crossplane/crossplane-runtime/pkg/errors"
38

39
	"github.com/crossplane/crossplane/internal/xpkg/upbound"
40
	"github.com/crossplane/crossplane/internal/xpkg/upbound/config"
41
)
42

43
const (
44
	defaultTimeout     = 30 * time.Second
45
	loginPath          = "/v1/login"
46
	defaultProfileName = "default"
47
)
48

49
type loginCmd struct {
50
	// Flags. We're intentionally making an exception to the rule here and not
51
	// sorting these alphabetically.
52
	Username string `short:"u" env:"UP_USER" xor:"identifier" help:"Username used to authenticate."`
53
	Password string `short:"p" env:"UP_PASSWORD" help:"Password for specified username. '-' to read from stdin."`
54
	Token    string `short:"t" env:"UP_TOKEN" xor:"identifier" help:"Token used to authenticate. '-' to read from stdin."`
55

56
	// Common Upbound API configuration.
57
	upbound.Flags `embed:""`
58

59
	// Internal state. These aren't part of the user-exposed CLI structure.
60
	stdin  *os.File
61
	client *http.Client
62
}
63

64
// Help prints out the help for the login command.
65
func (c *loginCmd) Help() string {
66
	return `
67
This command logs in to the xpkg.upbound.io package registry. The Crossplane CLI
68
uses xpkg.upbound.io if you don't explicitly specify a different registry.
69

70
You can create an xpkg.upbound.io account at https://accounts.upbound.io.
71
`
72
}
73

74
// BeforeApply sets default values in login before assignment and validation.
75
func (c *loginCmd) BeforeApply() error {
76
	c.stdin = os.Stdin
77
	return nil
78
}
79

80
func (c *loginCmd) AfterApply(kongCtx *kong.Context) error {
81
	upCtx, err := upbound.NewFromFlags(c.Flags, upbound.AllowMissingProfile())
82
	if err != nil {
83
		return err
84
	}
85
	c.client = &http.Client{
86
		Transport: &http.Transport{
87
			TLSClientConfig: &tls.Config{
88
				InsecureSkipVerify: upCtx.InsecureSkipTLSVerify, //nolint:gosec // we need to support insecure connections if requested
89
			},
90
		},
91
	}
92
	kongCtx.Bind(upCtx)
93
	if c.Token != "" {
94
		return nil
95
	}
96
	if err := c.setupCredentials(); err != nil {
97
		return errors.Wrapf(err, "failed to get credentials")
98
	}
99
	return nil
100
}
101

102
// Run executes the login command.
103
func (c *loginCmd) Run(k *kong.Context, upCtx *upbound.Context) error { //nolint:gocyclo // TODO(phisco): refactor
104
	auth, profType, err := constructAuth(c.Username, c.Token, c.Password)
105
	if err != nil {
106
		return errors.Wrap(err, "failed to construct auth")
107
	}
108
	jsonStr, err := json.Marshal(auth)
109
	if err != nil {
110
		return errors.Wrap(err, "failed to marshal auth")
111
	}
112
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
113
	defer cancel()
114
	loginEndpoint := *upCtx.APIEndpoint
115
	loginEndpoint.Path = loginPath
116
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, loginEndpoint.String(), bytes.NewReader(jsonStr))
117
	if err != nil {
118
		return errors.Wrap(err, "failed to create request")
119
	}
120
	req.Header.Set("Content-Type", "application/json")
121
	res, err := c.client.Do(req)
122
	if err != nil {
123
		return errors.Wrap(err, "failed to send request")
124
	}
125
	defer res.Body.Close() //nolint:errcheck // we don't care about the error here
126
	session, err := extractSession(res, upbound.CookieName)
127
	if err != nil {
128
		return errors.Wrap(err, "failed to extract session")
129
	}
130

131
	// Set session early so that it can be used to fetch user info if necessary.
132
	upCtx.Profile.Session = session
133

134
	// If the default account is not set, the user's personal account is used.
135
	if upCtx.Account == "" {
136
		conf, err := upCtx.BuildSDKConfig()
137
		if err != nil {
138
			return errors.Wrap(err, "failed to build SDK config")
139
		}
140
		info, err := userinfo.NewClient(conf).Get(ctx)
141
		if err != nil {
142
			return errors.Wrap(err, "failed to get user info")
143
		}
144
		upCtx.Account = info.User.Username
145
	}
146

147
	// If profile name was not provided and no default exists, set name to 'default'.
148
	if upCtx.ProfileName == "" {
149
		upCtx.ProfileName = defaultProfileName
150
	}
151

152
	upCtx.Profile.ID = auth.ID
153
	upCtx.Profile.Type = profType
154
	upCtx.Profile.Account = upCtx.Account
155

156
	if err := upCtx.Cfg.AddOrUpdateUpboundProfile(upCtx.ProfileName, upCtx.Profile); err != nil {
157
		return errors.Wrap(err, "failed to add or update profile")
158
	}
159
	if err := upCtx.Cfg.SetDefaultUpboundProfile(upCtx.ProfileName); err != nil {
160
		return errors.Wrap(err, "failed to set default profile")
161
	}
162
	if err := upCtx.CfgSrc.UpdateConfig(upCtx.Cfg); err != nil {
163
		return errors.Wrap(err, "failed to update config")
164
	}
165
	fmt.Fprintln(k.Stdout, "Login successful.")
166
	return nil
167
}
168

169
func (c *loginCmd) setupCredentials() error {
170
	if c.Token == "-" {
171
		b, err := io.ReadAll(c.stdin)
172
		if err != nil {
173
			return err
174
		}
175
		c.Token = strings.TrimSpace(string(b))
176
	}
177
	if c.Password == "-" {
178
		b, err := io.ReadAll(c.stdin)
179
		if err != nil {
180
			return err
181
		}
182
		c.Password = strings.TrimSpace(string(b))
183
	}
184
	if c.Token == "" {
185
		if c.Username == "" {
186
			username, err := getUsername(c.stdin)
187
			if err != nil {
188
				return errors.Wrap(err, "failed to get username")
189
			}
190
			c.Username = username
191
		}
192
		if c.Password == "" {
193
			password, err := getPassword(c.stdin)
194
			if err != nil {
195
				return errors.Wrap(err, "failed to get password")
196
			}
197
			c.Password = password
198
		}
199
	}
200
	return nil
201
}
202

203
func getPassword(f *os.File) (string, error) {
204
	if !term.IsTerminal(int(f.Fd())) {
205
		return "", errors.New("not a terminal")
206
	}
207
	fmt.Fprintf(f, "Password: ")
208
	password, err := term.ReadPassword(int(f.Fd()))
209
	if err != nil {
210
		return "", err
211
	}
212
	// Print a new line because ReadPassword does not.
213
	_, _ = fmt.Fprintf(f, "\n")
214
	return string(password), nil
215

216
}
217
func getUsername(f *os.File) (string, error) {
218
	if !term.IsTerminal(int(f.Fd())) {
219
		return "", errors.New("not a terminal")
220
	}
221
	fmt.Fprintf(f, "Username: ")
222
	reader := bufio.NewReader(f)
223
	s, err := reader.ReadString('\n')
224
	if err != nil {
225
		return "", err
226
	}
227
	return strings.TrimSpace(s), nil
228
}
229

230
// auth is the request body sent to authenticate a user or token.
231
type auth struct {
232
	ID       string `json:"id"`
233
	Password string `json:"password"`
234
	Remember bool   `json:"remember"`
235
}
236

237
// constructAuth constructs the body of an Upbound Cloud authentication request
238
// given the provided credentials.
239
func constructAuth(username, token, password string) (*auth, config.ProfileType, error) {
240
	if username == "" && token == "" {
241
		return nil, "", errors.New("no user or token provided")
242
	}
243
	id, profType, err := parseID(username, token)
244
	if err != nil {
245
		return nil, "", err
246
	}
247
	if profType == config.TokenProfileType {
248
		password = token
249
	}
250
	return &auth{
251
		ID:       id,
252
		Password: password,
253
		Remember: true,
254
	}, profType, nil
255
}
256

257
// parseID gets a user ID by either parsing a token or returning the username.
258
func parseID(user, token string) (string, config.ProfileType, error) {
259
	if token != "" {
260
		p := jwt.Parser{}
261
		claims := &jwt.RegisteredClaims{}
262
		_, _, err := p.ParseUnverified(token, claims)
263
		if err != nil {
264
			return "", "", err
265
		}
266
		if claims.ID == "" {
267
			return "", "", errors.New("no id in token")
268
		}
269
		return claims.ID, config.TokenProfileType, nil
270
	}
271
	return user, config.UserProfileType, nil
272
}
273

274
// extractSession extracts the specified cookie from an HTTP response. The
275
// caller is responsible for closing the response body.
276
func extractSession(res *http.Response, cookieName string) (string, error) {
277
	for _, cook := range res.Cookies() {
278
		if cook.Name == cookieName {
279
			return cook.Value, nil
280
		}
281
	}
282
	b, err := io.ReadAll(res.Body)
283
	if err != nil {
284
		return "", errors.Wrap(err, "failed to read body")
285
	}
286
	return "", errors.Errorf("failed to read cookie format: %v", string(b))
287
}
288

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.