GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

providers.NewCLIProfileCredentialsProviderBuilder   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
package providers
2
3
import (
4
	"encoding/json"
5
	"errors"
6
	"fmt"
7
	"io/ioutil"
8
	"os"
9
	"path"
10
	"strconv"
11
	"strings"
12
	"sync"
13
	"time"
14
15
	"github.com/aliyun/credentials-go/credentials/internal/utils"
16
)
17
18
type CLIProfileCredentialsProvider struct {
19
	profileFile   string
20
	profileName   string
21
	innerProvider CredentialsProvider
22
	// 文件锁,用于并发安全
23
	fileMutex sync.RWMutex
24
}
25
26
type CLIProfileCredentialsProviderBuilder struct {
27
	provider *CLIProfileCredentialsProvider
28
}
29
30
func (b *CLIProfileCredentialsProviderBuilder) WithProfileFile(profileFile string) *CLIProfileCredentialsProviderBuilder {
31
	b.provider.profileFile = profileFile
32
	return b
33
}
34
35
func (b *CLIProfileCredentialsProviderBuilder) WithProfileName(profileName string) *CLIProfileCredentialsProviderBuilder {
36
	b.provider.profileName = profileName
37
	return b
38
}
39
40
func (b *CLIProfileCredentialsProviderBuilder) Build() (provider *CLIProfileCredentialsProvider, err error) {
41
	// 优先级:
42
	// 1. 使用显示指定的 profileFile
43
	// 2. 使用环境变量(ALIBABA_CLOUD_CONFIG_FILE)指定的 profileFile
44
	// 3. 兜底使用 path.Join(homeDir, ".aliyun/config") 作为 profileFile
45
	if b.provider.profileFile == "" {
46
		b.provider.profileFile = os.Getenv("ALIBABA_CLOUD_CONFIG_FILE")
47
	}
48
	// 优先级:
49
	// 1. 使用显示指定的 profileName
50
	// 2. 使用环境变量(ALIBABA_CLOUD_PROFILE)制定的 profileName
51
	// 3. 使用 CLI 配置中的当前 profileName
52
	if b.provider.profileName == "" {
53
		b.provider.profileName = os.Getenv("ALIBABA_CLOUD_PROFILE")
54
	}
55
56
	if strings.ToLower(os.Getenv("ALIBABA_CLOUD_CLI_PROFILE_DISABLED")) == "true" {
57
		err = errors.New("the CLI profile is disabled")
58
		return
59
	}
60
61
	provider = b.provider
62
	return
63
}
64
65
func NewCLIProfileCredentialsProviderBuilder() *CLIProfileCredentialsProviderBuilder {
66
	return &CLIProfileCredentialsProviderBuilder{
67
		provider: &CLIProfileCredentialsProvider{},
68
	}
69
}
70
71
type profile struct {
72
	Name                   string `json:"name"`
73
	Mode                   string `json:"mode"`
74
	AccessKeyID            string `json:"access_key_id"`
75
	AccessKeySecret        string `json:"access_key_secret"`
76
	SecurityToken          string `json:"sts_token"`
77
	RegionID               string `json:"region_id"`
78
	RoleArn                string `json:"ram_role_arn"`
79
	RoleSessionName        string `json:"ram_session_name"`
80
	DurationSeconds        int    `json:"expired_seconds"`
81
	StsRegion              string `json:"sts_region"`
82
	EnableVpc              bool   `json:"enable_vpc"`
83
	SourceProfile          string `json:"source_profile"`
84
	RoleName               string `json:"ram_role_name"`
85
	OIDCTokenFile          string `json:"oidc_token_file"`
86
	OIDCProviderARN        string `json:"oidc_provider_arn"`
87
	Policy                 string `json:"policy"`
88
	ExternalId             string `json:"external_id"`
89
	SignInUrl              string `json:"cloud_sso_sign_in_url"`
90
	AccountId              string `json:"cloud_sso_account_id"`
91
	AccessConfig           string `json:"cloud_sso_access_config"`
92
	AccessToken            string `json:"access_token"`
93
	AccessTokenExpire      int64  `json:"cloud_sso_access_token_expire"`
94
	OauthSiteType          string `json:"oauth_site_type"`
95
	OauthRefreshToken      string `json:"oauth_refresh_token"`
96
	OauthAccessToken       string `json:"oauth_access_token"`
97
	OauthAccessTokenExpire int64  `json:"oauth_access_token_expire"`
98
	StsExpire              int64  `json:"sts_expiration"`
99
	ProcessCommand         string `json:"process_command"`
100
}
101
102
type configuration struct {
103
	Current  string     `json:"current"`
104
	Profiles []*profile `json:"profiles"`
105
}
106
107
func newConfigurationFromPath(cfgPath string) (conf *configuration, err error) {
108
	bytes, err := ioutil.ReadFile(cfgPath)
109
	if err != nil {
110
		err = fmt.Errorf("reading aliyun cli config from '%s' failed %v", cfgPath, err)
111
		return
112
	}
113
114
	conf = &configuration{}
115
116
	err = json.Unmarshal(bytes, conf)
117
	if err != nil {
118
		err = fmt.Errorf("unmarshal aliyun cli config from '%s' failed: %s", cfgPath, string(bytes))
119
		return
120
	}
121
122
	if conf.Profiles == nil || len(conf.Profiles) == 0 {
123
		err = fmt.Errorf("no any configured profiles in '%s'", cfgPath)
124
		return
125
	}
126
127
	return
128
}
129
130
func (conf *configuration) getProfile(name string) (profile *profile, err error) {
131
	for _, p := range conf.Profiles {
132
		if p.Name == name {
133
			profile = p
134
			return
135
		}
136
	}
137
138
	err = fmt.Errorf("unable to get profile with '%s'", name)
139
	return
140
}
141
142
var oauthBaseUrlMap = map[string]string{
143
	"CN":   "https://oauth.aliyun.com",
144
	"INTL": "https://oauth.alibabacloud.com",
145
}
146
147
var oauthClientMap = map[string]string{
148
	"CN":   "4038181954557748008",
149
	"INTL": "4103531455503354461",
150
}
151
152
func (provider *CLIProfileCredentialsProvider) getCredentialsProvider(conf *configuration, profileName string) (credentialsProvider CredentialsProvider, err error) {
153
	p, err := conf.getProfile(profileName)
154
	if err != nil {
155
		return
156
	}
157
158
	switch p.Mode {
159
	case "AK":
160
		credentialsProvider, err = NewStaticAKCredentialsProviderBuilder().
161
			WithAccessKeyId(p.AccessKeyID).
162
			WithAccessKeySecret(p.AccessKeySecret).
163
			Build()
164
	case "StsToken":
165
		credentialsProvider, err = NewStaticSTSCredentialsProviderBuilder().
166
			WithAccessKeyId(p.AccessKeyID).
167
			WithAccessKeySecret(p.AccessKeySecret).
168
			WithSecurityToken(p.SecurityToken).
169
			Build()
170
	case "RamRoleArn":
171
		previousProvider, err1 := NewStaticAKCredentialsProviderBuilder().
172
			WithAccessKeyId(p.AccessKeyID).
173
			WithAccessKeySecret(p.AccessKeySecret).
174
			Build()
175
		if err1 != nil {
176
			return nil, err1
177
		}
178
179
		credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
180
			WithCredentialsProvider(previousProvider).
181
			WithRoleArn(p.RoleArn).
182
			WithRoleSessionName(p.RoleSessionName).
183
			WithDurationSeconds(p.DurationSeconds).
184
			WithStsRegionId(p.StsRegion).
185
			WithEnableVpc(p.EnableVpc).
186
			WithPolicy(p.Policy).
187
			WithExternalId(p.ExternalId).
188
			Build()
189
	case "EcsRamRole":
190
		credentialsProvider, err = NewECSRAMRoleCredentialsProviderBuilder().WithRoleName(p.RoleName).Build()
191
	case "OIDC":
192
		credentialsProvider, err = NewOIDCCredentialsProviderBuilder().
193
			WithOIDCTokenFilePath(p.OIDCTokenFile).
194
			WithOIDCProviderARN(p.OIDCProviderARN).
195
			WithRoleArn(p.RoleArn).
196
			WithStsRegionId(p.StsRegion).
197
			WithEnableVpc(p.EnableVpc).
198
			WithDurationSeconds(p.DurationSeconds).
199
			WithRoleSessionName(p.RoleSessionName).
200
			WithPolicy(p.Policy).
201
			Build()
202
	case "ChainableRamRoleArn":
203
		previousProvider, err1 := provider.getCredentialsProvider(conf, p.SourceProfile)
204
		if err1 != nil {
205
			err = fmt.Errorf("get source profile failed: %s", err1.Error())
206
			return
207
		}
208
		credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
209
			WithCredentialsProvider(previousProvider).
210
			WithRoleArn(p.RoleArn).
211
			WithRoleSessionName(p.RoleSessionName).
212
			WithDurationSeconds(p.DurationSeconds).
213
			WithStsRegionId(p.StsRegion).
214
			WithEnableVpc(p.EnableVpc).
215
			WithPolicy(p.Policy).
216
			WithExternalId(p.ExternalId).
217
			Build()
218
	case "CloudSSO":
219
		credentialsProvider, err = NewCloudSSOCredentialsProviderBuilder().
220
			WithSignInUrl(p.SignInUrl).
221
			WithAccountId(p.AccountId).
222
			WithAccessConfig(p.AccessConfig).
223
			WithAccessToken(p.AccessToken).
224
			WithAccessTokenExpire(p.AccessTokenExpire).
225
			Build()
226
	case "OAuth":
227
		siteType := strings.ToUpper(p.OauthSiteType)
228
		signInUrl := oauthBaseUrlMap[siteType]
229
		if signInUrl == "" {
230
			err = fmt.Errorf("invalid site type, support CN or INTL")
231
			return
232
		}
233
		clientId := oauthClientMap[siteType]
234
235
		credentialsProvider, err = NewOAuthCredentialsProviderBuilder().
236
			WithSignInUrl(signInUrl).
237
			WithClientId(clientId).
238
			WithRefreshToken(p.OauthRefreshToken).
239
			WithAccessToken(p.OauthAccessToken).
240
			WithAccessTokenExpire(p.OauthAccessTokenExpire).
241
			WithTokenUpdateCallback(provider.getOAuthTokenUpdateCallback()).
242
			Build()
243
	case "External":
244
		credentialsProvider, err = NewExternalCredentialsProviderBuilder().
245
			WithProcessCommand(p.ProcessCommand).
246
			WithCredentialUpdateCallback(provider.getExternalCredentialUpdateCallback()).
247
			Build()
248
	default:
249
		err = fmt.Errorf("unsupported profile mode '%s'", p.Mode)
250
	}
251
252
	return
253
}
254
255
// 默认设置为 GetHomePath,测试时便于 mock
256
var getHomePath = utils.GetHomePath
257
258
func (provider *CLIProfileCredentialsProvider) GetCredentials() (cc *Credentials, err error) {
259
	if provider.innerProvider == nil {
260
		cfgPath := provider.profileFile
261
		if cfgPath == "" {
262
			homeDir := getHomePath()
263
			if homeDir == "" {
264
				err = fmt.Errorf("cannot found home dir")
265
				return
266
			}
267
268
			cfgPath = path.Join(homeDir, ".aliyun/config.json")
269
			provider.profileFile = cfgPath
270
		}
271
272
		conf, err1 := newConfigurationFromPath(cfgPath)
273
		if err1 != nil {
274
			err = err1
275
			return
276
		}
277
278
		if provider.profileName == "" {
279
			provider.profileName = conf.Current
280
		}
281
282
		provider.innerProvider, err = provider.getCredentialsProvider(conf, provider.profileName)
283
		if err != nil {
284
			return
285
		}
286
	}
287
288
	innerCC, err := provider.innerProvider.GetCredentials()
289
	if err != nil {
290
		return
291
	}
292
293
	providerName := innerCC.ProviderName
294
	if providerName == "" {
295
		providerName = provider.innerProvider.GetProviderName()
296
	}
297
298
	cc = &Credentials{
299
		AccessKeyId:     innerCC.AccessKeyId,
300
		AccessKeySecret: innerCC.AccessKeySecret,
301
		SecurityToken:   innerCC.SecurityToken,
302
		ProviderName:    fmt.Sprintf("%s/%s", provider.GetProviderName(), providerName),
303
	}
304
305
	return
306
}
307
308
func (provider *CLIProfileCredentialsProvider) GetProviderName() string {
309
	return "cli_profile"
310
}
311
312
// updateOAuthTokens 更新OAuth令牌并写回配置文件
313
func (provider *CLIProfileCredentialsProvider) updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
314
	provider.fileMutex.Lock()
315
	defer provider.fileMutex.Unlock()
316
317
	cfgPath := provider.profileFile
318
	conf, err := newConfigurationFromPath(cfgPath)
319
	if err != nil {
320
		return fmt.Errorf("failed to read config file: %v", err)
321
	}
322
323
	profileName := provider.profileName
324
	profile, err := conf.getProfile(profileName)
325
	if err != nil {
326
		return fmt.Errorf("failed to get profile %s: %v", profileName, err)
327
	}
328
329
	// update
330
	profile.OauthRefreshToken = refreshToken
331
	profile.OauthAccessToken = accessToken
332
	profile.OauthAccessTokenExpire = accessTokenExpire
333
	profile.AccessKeyID = accessKey
334
	profile.AccessKeySecret = secret
335
	profile.SecurityToken = securityToken
336
	profile.StsExpire = stsExpire
337
338
	// write back with file lock
339
	return provider.writeConfigurationToFileWithLock(cfgPath, conf)
340
}
341
342
// writeConfigurationToFile 将配置写入文件,使用原子写入确保数据完整性
343
func (provider *CLIProfileCredentialsProvider) writeConfigurationToFile(cfgPath string, conf *configuration) error {
344
	// 获取原文件权限(如果存在)
345
	fileMode := os.FileMode(0644)
346
	if stat, err := os.Stat(cfgPath); err == nil {
347
		fileMode = stat.Mode()
348
	}
349
350
	// 创建唯一临时文件
351
	tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)
352
353
	// 写入临时文件
354
	err := provider.writeConfigFile(tempFile, fileMode, conf)
355
	if err != nil {
356
		return fmt.Errorf("failed to write temp file: %v", err)
357
	}
358
359
	// 原子性重命名,确保文件完整性
360
	err = os.Rename(tempFile, cfgPath)
361
	if err != nil {
362
		// 清理临时文件
363
		os.Remove(tempFile)
364
		return fmt.Errorf("failed to rename temp file: %v", err)
365
	}
366
367
	return nil
368
}
369
370
// writeConfigFile 写入配置文件
371
func (provider *CLIProfileCredentialsProvider) writeConfigFile(filename string, fileMode os.FileMode, conf *configuration) error {
372
	f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, fileMode)
373
	if err != nil {
374
		return fmt.Errorf("failed to create config file: %w", err)
375
	}
376
377
	defer func() {
378
		closeErr := f.Close()
379
		if err == nil && closeErr != nil {
380
			err = fmt.Errorf("failed to close config file: %w", closeErr)
381
		}
382
	}()
383
384
	encoder := json.NewEncoder(f)
385
	encoder.SetIndent("", "    ")
386
387
	if err = encoder.Encode(conf); err != nil {
388
		return fmt.Errorf("failed to serialize config: %w", err)
389
	}
390
391
	return nil
392
}
393
394
// writeConfigurationToFileWithLock 使用操作系统级别的文件锁写入配置文件
395
func (provider *CLIProfileCredentialsProvider) writeConfigurationToFileWithLock(cfgPath string, conf *configuration) error {
396
	// 获取原文件权限(如果存在)
397
	fileMode := os.FileMode(0644)
398
	if stat, err := os.Stat(cfgPath); err == nil {
399
		fileMode = stat.Mode()
400
	}
401
402
	// 打开文件用于锁定
403
	file, err := os.OpenFile(cfgPath, os.O_RDWR|os.O_CREATE, fileMode)
404
	if err != nil {
405
		return fmt.Errorf("failed to open config file: %v", err)
406
	}
407
408
	// 获取独占锁(阻塞其他进程)
409
	err = lockFile(int(file.Fd()))
410
	if err != nil {
411
		file.Close()
412
		return fmt.Errorf("failed to acquire file lock: %v", err)
413
	}
414
415
	// 创建唯一临时文件
416
	tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)
417
	err = provider.writeConfigFile(tempFile, fileMode, conf)
418
	if err != nil {
419
		unlockFile(int(file.Fd()))
420
		file.Close()
421
		return fmt.Errorf("failed to write temp file: %v", err)
422
	}
423
424
	// 关闭并解锁原文件,以便在Windows上可以重命名
425
	unlockFile(int(file.Fd()))
426
	file.Close()
427
428
	// 原子性重命名
429
	err = os.Rename(tempFile, cfgPath)
430
	if err != nil {
431
		os.Remove(tempFile)
432
		return fmt.Errorf("failed to rename temp file: %v", err)
433
	}
434
435
	return nil
436
}
437
438
// getOAuthTokenUpdateCallback 获取OAuth令牌更新回调函数
439
func (provider *CLIProfileCredentialsProvider) getOAuthTokenUpdateCallback() OAuthTokenUpdateCallback {
440
	return func(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
441
		return provider.updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken, accessTokenExpire, stsExpire)
442
	}
443
}
444
445
// getExternalCredentialUpdateCallback 获取External凭证更新回调函数
446
func (provider *CLIProfileCredentialsProvider) getExternalCredentialUpdateCallback() ExternalCredentialUpdateCallback {
447
	return func(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
448
		return provider.updateExternalCredentials(accessKeyId, accessKeySecret, securityToken, expiration)
449
	}
450
}
451
452
// updateExternalCredentials 更新External凭证并写回配置文件
453
func (provider *CLIProfileCredentialsProvider) updateExternalCredentials(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
454
	provider.fileMutex.Lock()
455
	defer provider.fileMutex.Unlock()
456
457
	cfgPath := provider.profileFile
458
	conf, err := newConfigurationFromPath(cfgPath)
459
	if err != nil {
460
		return fmt.Errorf("failed to read config file: %v", err)
461
	}
462
463
	profileName := provider.profileName
464
	profile, err := conf.getProfile(profileName)
465
	if err != nil {
466
		return fmt.Errorf("failed to get profile %s: %v", profileName, err)
467
	}
468
469
	// update
470
	profile.AccessKeyID = accessKeyId
471
	profile.AccessKeySecret = accessKeySecret
472
	profile.SecurityToken = securityToken
473
	if expiration > 0 {
474
		profile.StsExpire = expiration
475
	}
476
477
	// write back with file lock
478
	return provider.writeConfigurationToFileWithLock(cfgPath, conf)
479
}
480