Skip to content

Commit

Permalink
add support switch user profile
Browse files Browse the repository at this point in the history
  • Loading branch information
bearmini committed Jul 10, 2023
1 parent 4bda67b commit 4f0c4a1
Show file tree
Hide file tree
Showing 13 changed files with 519 additions and 138 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ The `soracom` command:
2. use `brew install bash-completion` instead of using Xcode version of bash-completion and then add the following to either your `.bash_profile` or `.profile`:

```
if [ -f $(brew --prefix)/etc/bash_completion ]; then
. $(brew --prefix)/etc/bash_completion
if [ -f "$(brew --prefix)/etc/bash_completion" ]; then
. "$(brew --prefix)/etc/bash_completion"
fi
```
otherwise you might be getting the error like the following:
Expand Down Expand Up @@ -118,12 +118,15 @@ Please select which authentication method to use.
1. Input AuthKeyId and AuthKey * Recommended *
2. Input Operator credentials (Operator Email and Password)
3. Input SAM credentials (OperatorId, User name and Password)
4. Switch user
select (1-3) >
select (1-4) >
```

Please select 1 if AuthKey (authentication key) has been issued to SAM user or root account.
(For details on how to issue an authentication key to SAM users, please see [Using SORACOM Access Management to Manage Operation Access](https://dev.soracom.io/en/start/sam/).
(For details on how to issue an authentication key to SAM users, please see [Users & Roles | SORACOM Developers](https://developers.soracom.io/en/docs/security/users-and-roles/).

If you select 4. Switch user, you can specify the Operator ID and SAM user name of the switch destination user. Please create a profile for the switch source user before configuring a switch user profile. If you specify a switch user profile, soracom-cli will automatically authenticate with the switch source profile and then switch to the SAM user before making API calls.

Thereafter, when executing the soracom command, an API call is made using the authentication information entered here.

Expand Down
15 changes: 11 additions & 4 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ soracom コマンドは以下のような特徴を備えています。
2. `brew install bash-completion` でインストールした bash-completion を使う(Xcode に付属の bash-completion では動作しない場合があります。)
そしてこの場合、`.bash_profile` または `.profile` ファイルに以下を追加します:
```
if [ -f $(brew --prefix)/etc/bash_completion ]; then
. $(brew --prefix)/etc/bash_completion
if [ -f "$(brew --prefix)/etc/bash_completion" ]; then
. "$(brew --prefix)/etc/bash_completion"
fi
```

Expand Down Expand Up @@ -119,12 +119,19 @@ soracom configure
1. AuthKeyId と AuthKey を入力する(推奨)
2. オペレーターのメールアドレスとパスワードを入力する
3. SAM ユーザーの認証情報を入力する(オペレーターID、ユーザー名、パスワード)
4. スイッチユーザー
選択してください (1-3) >
選択してください (1-4) >
```

SAM ユーザーもしくはルートアカウントに対し、AuthKey(認証キー)を発行している場合は 1 を選択してください。
(SAM ユーザーに対し認証キーを発行する方法については [SORACOM Access Managementを使用して操作権限を管理する](https://dev.soracom.io/jp/start/sam/) を参照してください)
SAM ユーザーに対し認証キーを発行する方法についてはソラコムユーザーサイトの [認証キーを生成する](https://users.soracom.io/ja-jp/docs/sam/create-sam-user/#%e8%aa%8d%e8%a8%bc%e3%82%ad%e3%83%bc%e3%82%92%e7%94%9f%e6%88%90%e3%81%99%e3%82%8b) を参照してください

4 のスイッチユーザーを選択すると、スイッチ元のユーザーのプロファイルとスイッチ先のユーザーの Operator ID および SAM ユーザー名を指定することができます。
実行前にあらかじめスイッチ元のユーザーのプロファイルを作成しておいてください。
いったんスイッチユーザーのプロファイルを作成すると、各種サブコマンドを実行する際に soracom-cli が自動的にスイッチ元のプロファイルで認証し、スイッチ先の SAM ユーザーへスイッチしてから API の呼び出しを行うようになります。

スイッチユーザーの詳細については、[スイッチユーザー | ドキュメント | ソラコムユーザーサイト - SORACOM Users](https://users.soracom.io/ja-jp/docs/switch-user/) を参照してください。

以後、soracom コマンド実行時は、ここで入力した認証情報を使って API 呼び出しが行われます。

Expand Down
12 changes: 10 additions & 2 deletions generators/assets/cli/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,16 @@ cli:
prompt: "\n\nPlease select which coverage type to use.\n\n1. Global\n2. Japan\n\n"
select: "select (1-2) > "
auth:
prompt: "\n\nPlease select which authentication method to use.\n\n1. Input AuthKeyId and AuthKey * Recommended * \n2. Input Operator credentials (Operator Email and Password)\n3. Input SAM credentials (OperatorId, User name and Password)\n\n"
select: "select (1-3) > "
prompt: |+
Please select which authentication method to use.
1. AuthKeyId and AuthKey * Recommended *
2. Operator credentials (Operator Email and Password)
3. SAM user credentials (OperatorId, User name and Password)
4. Switch user
select: "select (1-4) > "
overwrite: "\nProfile %s already exists. Overwrite it? (Y/n) "
get:
summary: Show specified profile configurations.
Expand Down
12 changes: 10 additions & 2 deletions generators/assets/cli/ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,16 @@ cli:
prompt: "\n\nカバレッジタイプを選択してください。\n\n1. Global\n2. Japan\n\n"
select: "選択してください (1-2) > "
auth:
prompt: "\n\n認証方法を選択してください。\n\n1. AuthKeyId と AuthKey を入力する(推奨)\n2. オペレーターのメールアドレスとパスワードを入力する\n3. SAM ユーザーの認証情報を入力する(オペレーターID、ユーザー名、パスワード)\n\n"
select: "選択してください (1-3) > "
prompt: |+
認証方法を選択してください。
1. AuthKeyId と AuthKey を入力する(推奨)
2. オペレーターのメールアドレスとパスワードを入力する
3. SAM ユーザーの認証情報を入力する(オペレーターID、ユーザー名、パスワード)
4. スイッチユーザー
select: "選択してください (1-4) > "
overwrite: "\nプロファイル %s はすでに存在しています。上書きしますか? (Y/n) "
get:
summary: プロファイル情報を表示します。
Expand Down
113 changes: 113 additions & 0 deletions generators/cmd/predefined/apiclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,119 @@ func (ac *apiClient) callAPI(params *apiParams) (string, error) {
return resBody, nil
}

func (ac *apiClient) authenticateWithProfile(profile *profile) (*authResult, error) {
if profile.SourceProfile != nil && *profile.SourceProfile != "" {
sourceProfile, err := loadProfile(*profile.SourceProfile)
if err != nil {
lib.PrintfStderr("unable to load the specified source profile: %s\n", *profile.SourceProfile)
return nil, err
}
return ac.authenticateWithSwitchUser(profile, sourceProfile)
}

var areq *authRequest
if providedAuthKeyID != "" && providedAuthKey != "" {
areq = &authRequest{
AuthKeyID: &providedAuthKeyID,
AuthKey: &providedAuthKey,
}
} else if providedProfileCommand != "" {
profile, err := getProfileFromExternalCommand(providedProfileCommand)
if err != nil {
lib.PrintfStderr("unable to get credentials from an external command.\n")
return nil, err
}
areq = authRequestFromProfile(profile)
} else {
areq = authRequestFromProfile(profile)

if profile.ProfileCommand != nil && *profile.ProfileCommand != "" {
p, err := getProfileFromExternalCommand(*profile.ProfileCommand)
if err != nil {
lib.PrintfStderr("unable to get credentials from an external command.\n")
return nil, err
}
areq = authRequestFromProfile(p)
}
}

params := &apiParams{
method: "POST",
path: "/auth",
query: map[string][]string{},
contentType: "application/json",
body: toJSON(areq),
noVersionCheck: true,
}

res, err := ac.callAPI(params)
if err != nil {
return nil, err
}

dec := json.NewDecoder(bytes.NewBufferString(res))
var ares authResult
err = dec.Decode(&ares)
if err != nil {
return nil, err
}

return &ares, nil
}

func (ac *apiClient) authenticateWithSwitchUser(profile, sourceProfile *profile) (*authResult, error) {
if sourceProfile.SourceProfile != nil {
return nil, errors.New("source profile should not have source profile (nested switch user is not allowed)")
}

if profile.OperatorID == nil || profile.Username == nil {
return nil, errors.New("both operatorId and username are required when authenticating using switch-user")
}

sourceAuthRes, err := ac.authenticateWithProfile(sourceProfile)
if err != nil {
return nil, err
}

switchUserReq := &switchUserRequest{
OperatorID: *profile.OperatorID,
UserName: *profile.Username,
TokenTimeoutSeconds: getProvidedTokenTimeoutSeconds(profile),
}

params := &apiParams{
method: "POST",
path: "/auth/switch_user",
contentType: "application/json",
body: toJSON(switchUserReq),
noVersionCheck: true,
}

// temporarily using the source profile's api key and token
ac.APIKey = sourceAuthRes.APIKey
ac.Token = sourceAuthRes.Token
ac.OperatorID = sourceAuthRes.OperatorID

res, err := ac.callAPI(params)
if err != nil {
return nil, err
}

// erase source profile's api key and token to prevent accidents
ac.APIKey = ""
ac.Token = ""
ac.OperatorID = ""

dec := json.NewDecoder(bytes.NewBufferString(res))
var ares authResult
err = dec.Decode(&ares)
if err != nil {
return nil, err
}

return &ares, nil
}

// arr1 = "[1,2]"
// arr2 = "[3]"
// returns "[1,2,3]"
Expand Down
84 changes: 38 additions & 46 deletions generators/cmd/predefined/auth_helper.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"bytes"
"encoding/json"
"strings"

Expand All @@ -10,6 +9,11 @@ import (
"github.com/spf13/cobra"
)

const (
minTokenTimeoutSeconds = 180
maxTokenTimeoutSeconds = 3600
)

type authRequest struct {
Email *string `json:"email,omitempty"`
Password *string `json:"password,omitempty"`
Expand All @@ -36,6 +40,12 @@ type authResult struct {
OperatorID string `json:"operatorId"`
}

type switchUserRequest struct {
OperatorID string `json:"operatorId"`
UserName string `json:"userName"`
TokenTimeoutSeconds *int `json:"tokenTimeoutSeconds,omitempty"`
}

func authHelper(ac *apiClient, cmd *cobra.Command, args []string) error {
apiKey, apiToken, operatorID, credentialsProvided := getProvidedCredentials()
if credentialsProvided {
Expand All @@ -45,62 +55,22 @@ func authHelper(ac *apiClient, cmd *cobra.Command, args []string) error {
return nil
}

var areq *authRequest
if providedAuthKeyID != "" && providedAuthKey != "" {
areq = &authRequest{
AuthKeyID: &providedAuthKeyID,
AuthKey: &providedAuthKey,
}
} else if providedProfileCommand != "" {
profile, err := getProfileFromExternalCommand(providedProfileCommand)
if err != nil {
lib.PrintfStderr("unable to get credentials from an external command.\n")
return err
}
areq = authRequestFromProfile(profile)
} else {
profile, err := getProfile()
if err != nil {
lib.PrintfStderr("unable to load the profile.\n")
lib.PrintfStderr("run `soracom configure` first.\n")
return err
}
areq = authRequestFromProfile(profile)

if profile.ProfileCommand != nil && *profile.ProfileCommand != "" {
p, err := getProfileFromExternalCommand(*profile.ProfileCommand)
if err != nil {
lib.PrintfStderr("unable to get credentials from an external command.\n")
return err
}
areq = authRequestFromProfile(p)
}
}

params := &apiParams{
method: "POST",
path: "/auth",
query: map[string][]string{},
contentType: "application/json",
body: toJSON(areq),
noVersionCheck: true,
}

res, err := ac.callAPI(params)
profile, err := getProfile()
if err != nil {
lib.PrintfStderr("unable to load the profile.\n")
lib.PrintfStderr("run `soracom configure` first.\n")
return err
}

dec := json.NewDecoder(bytes.NewBufferString(res))
var ares authResult
err = dec.Decode(&ares)
ares, err := ac.authenticateWithProfile(profile)
if err != nil {
return err
}

ac.APIKey = ares.APIKey
ac.Token = ares.Token
ac.OperatorID = ares.OperatorID

return nil
}

Expand All @@ -109,6 +79,28 @@ func getProvidedCredentials() (string, string, string, bool) {
return providedAPIKey, providedAPIToken, operatorID, (providedAPIKey != "" && providedAPIToken != "")
}

func getProvidedTokenTimeoutSeconds(profile *profile) *int {
// TODO: support providing tokenTimeoutSeconds from command line option
//if providedTokenTimeoutSeconds != 0 {
//return providedTokenTimeoutSeconds
//}
if profile.TokenTimeoutSeconds != nil && isValidTokenTimeoutSeconds(*profile.TokenTimeoutSeconds) {
return profile.TokenTimeoutSeconds
}
return nil
}

func isValidTokenTimeoutSeconds(tokenTimeoutSeconds int) bool {
if tokenTimeoutSeconds < minTokenTimeoutSeconds {
return false
}
if tokenTimeoutSeconds > maxTokenTimeoutSeconds {
return false
}

return true
}

type jwtPayload struct {
Operator jwtPayloadOperator `json:"operator"`
}
Expand Down
Loading

0 comments on commit 4f0c4a1

Please sign in to comment.