-
Notifications
You must be signed in to change notification settings - Fork 74
Add named login profiles and directory-scoped tenant selection #372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ package cmd | |
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/Infisical/infisical-merge/packages/api" | ||
| "github.com/Infisical/infisical-merge/packages/config" | ||
|
|
@@ -57,64 +58,81 @@ var initCmd = &cobra.Command{ | |
| } | ||
| httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken) | ||
|
|
||
| selectedOrgID, selectedSubOrgName, err := pickOrganization(httpClient, "Which Infisical organization would you like to select a project from?", userCreds.UserCredentials.Email) | ||
| if err != nil { | ||
| util.HandleError(err, "Unable to select organization") | ||
| // The profile already carries an organization (and --org can retarget it | ||
| // for this command), so don't ask again. Only fall back to the picker | ||
| // when the profile has no organization recorded, which happens for | ||
| // sessions migrated from a CLI that predates profiles. | ||
| // | ||
| // OrganizationID is the organization the session acts in. For a session | ||
| // scoped to a sub-organization that is the sub-organization, which is | ||
| // also where its projects are filed, not the root from the token. | ||
| selectedOrgID := userCreds.OrganizationID | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same issue here, the selectedOrgID wouldn't be the suborgID
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same fix. |
||
| var selectedSubOrgName *string | ||
| if parent, own := util.SplitOrgDisplayName(userCreds.OrganizationName); parent != "" { | ||
| selectedSubOrgName = &own | ||
| } | ||
|
|
||
| tokenResponse, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrgID}) | ||
| if tokenResponse.MfaEnabled { | ||
| i := 1 | ||
| for i < 6 { | ||
| mfaVerifyCode := askForMFACode(tokenResponse.MfaMethod) | ||
| if selectedOrgID == "" { | ||
| pickedOrgID, pickedSubOrgName, err := pickOrganization(httpClient, "Which Infisical organization would you like to select a project from?", userCreds.UserCredentials.Email) | ||
| if err != nil { | ||
| util.HandleError(err, "Unable to select organization") | ||
| } | ||
| selectedSubOrgName = pickedSubOrgName | ||
|
|
||
| httpClient, err := util.GetRestyClientWithCustomHeaders() | ||
| if err != nil { | ||
| util.HandleError(err, "Unable to get resty client with custom headers") | ||
| } | ||
| httpClient.SetAuthToken(tokenResponse.Token) | ||
| verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ | ||
| Email: userCreds.UserCredentials.Email, | ||
| MFAToken: mfaVerifyCode, | ||
| MFAMethod: tokenResponse.MfaMethod, | ||
| }) | ||
| if requestError != nil { | ||
| util.HandleError(err) | ||
| break | ||
| } else if mfaErrorResponse != nil { | ||
| if mfaErrorResponse.Context.Code == "mfa_invalid" { | ||
| msg := fmt.Sprintf("Incorrect, verification code. You have %v attempts left", 5-i) | ||
| util.PrintlnStderr(msg) | ||
| if i == 5 { | ||
| util.PrintErrorMessageAndExit("No tries left, please try again in a bit") | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if mfaErrorResponse.Context.Code == "mfa_expired" { | ||
| util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again") | ||
| break | ||
| } | ||
| i++ | ||
| } else { | ||
| httpClient.SetAuthToken(verifyMFAresponse.Token) | ||
| tokenResponse, err = api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrgID}) | ||
| break | ||
| } | ||
| newSessionToken, err := selectOrganizationToken(userCreds.UserCredentials.JTWToken, userCreds.UserCredentials.Email, pickedOrgID) | ||
| if err != nil { | ||
| util.HandleError(err, "Unable to select organization") | ||
| } | ||
| } | ||
|
|
||
| if err != nil { | ||
| util.HandleError(err, "Unable to select organization") | ||
| } | ||
| // The session token is now scoped to the selected organization; record | ||
| // it on the profile this invocation resolved to so later commands in | ||
| // this project don't have to ask again. | ||
| userCreds.UserCredentials.JTWToken = newSessionToken | ||
| orgID, subOrgID := util.ParseTokenOrgClaims(newSessionToken) | ||
| if orgID == "" { | ||
| orgID = pickedOrgID | ||
| } | ||
| orgInfo := util.DescribeSessionOrg(newSessionToken, orgID, subOrgID) | ||
|
|
||
| updatedProfile := userCreds.Profile | ||
| updatedProfile.OrganizationID = orgID | ||
| updatedProfile.SubOrganizationID = subOrgID | ||
| updatedProfile.OrganizationName = orgInfo.DisplayName() | ||
| updatedProfile.OrganizationSlug = orgInfo.Slug | ||
| selectedOrgID = updatedProfile.ScopedOrganizationID() | ||
|
|
||
| // Only move the global default when this invocation was using it; a | ||
| // terminal pinned via env var, flag, or directory scope must not switch | ||
| // other terminals. | ||
| makeActive := userCreds.ProfileSource == util.ProfileSourceDefault | ||
| err = util.PersistLoginProfile(updatedProfile, &userCreds.UserCredentials, makeActive) | ||
| httpClient.SetAuthToken(newSessionToken) | ||
|
|
||
| if err != nil { | ||
| util.HandleError(err, "Unable to store your user credentials") | ||
| } | ||
| } else { | ||
| orgDisplay := userCreds.OrganizationName | ||
| if orgDisplay == "" { | ||
| orgDisplay = selectedOrgID | ||
| } | ||
|
|
||
| // set the config jwt token to the new token | ||
| userCreds.UserCredentials.JTWToken = tokenResponse.Token | ||
| err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials) | ||
| httpClient.SetAuthToken(tokenResponse.Token) | ||
| // An --org override is per command, so a project linked under it | ||
| // would not resolve on later runs that use the profile's default. | ||
| // That would surface later as an unrelated-looking "project not | ||
| // found", and a warning here can be silenced, so refuse and point | ||
| // at the two ways to make the organization stick. | ||
| if userCreds.OrganizationSource != util.OrgSourceProfileDefault && userCreds.Profile.OrganizationID != "" && userCreds.OrganizationID != userCreds.Profile.ScopedOrganizationID() { | ||
| profileOrg := userCreds.Profile.OrganizationName | ||
| if profileOrg == "" { | ||
| profileOrg = userCreds.Profile.ScopedOrganizationID() | ||
| } | ||
| util.PrintErrorMessageAndExit( | ||
| fmt.Sprintf("Profile '%s' defaults to organization %s, so a project linked here under %s (selected via %s) would not be found by later commands unless they also pass --org.", userCreds.ProfileName, profileOrg, orgDisplay, userCreds.OrganizationSource), | ||
| fmt.Sprintf("Make %s the profile's default with [infisical profile set-org %s], or keep both organizations by running [infisical profile new <name> --org %s] and then [infisical profile bind <name>] in this directory.", orgDisplay, orgDisplay, orgDisplay)) | ||
| } | ||
|
Comment on lines
+129
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit concerned that this warning is not enough here. They can be silenced by --silent, and the errors from the command won't hint that the issue is this mismatch between the profile org and the project from another org. I think we should raise an error in this case.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. It is an error now, not a warning.
|
||
|
|
||
| if err != nil { | ||
| util.HandleError(err, "Unable to store your user credentials") | ||
| util.PrintlnStderr(fmt.Sprintf("Using organization %s from profile '%s'. Pass --org to pick a different one.", orgDisplay, userCreds.ProfileName)) | ||
| } | ||
|
|
||
| workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient) | ||
|
|
@@ -140,11 +158,48 @@ var initCmd = &cobra.Command{ | |
| util.HandleError(err) | ||
| } | ||
|
|
||
| offerDirectoryProfileBinding(userCreds.ProfileName) | ||
|
|
||
| Telemetry.CaptureEvent("cli-command:init", posthog.NewProperties().Set("version", util.CLI_VERSION)) | ||
|
|
||
| }, | ||
| } | ||
|
|
||
| // offerDirectoryProfileBinding asks (only when multiple profiles exist) | ||
| // whether this directory should always use the profile init just ran with, so | ||
| // commands run here pick the right tenant without flags or env vars. | ||
| func offerDirectoryProfileBinding(profileName string) { | ||
| configFile, err := util.GetMigratedConfigFile() | ||
| if err != nil || profileName == "" || len(configFile.Profiles) < 2 { | ||
| return | ||
| } | ||
|
|
||
| cwd, err := os.Getwd() | ||
| if err != nil { | ||
| return | ||
| } | ||
|
|
||
| if boundProfile, _, ok := util.FindGoverningDirectoryProfile(configFile, cwd); ok && boundProfile == profileName { | ||
| return | ||
| } | ||
|
|
||
| prompt := promptui.Select{ | ||
| Label: fmt.Sprintf("Bind this directory to profile '%s'? Commands run here will then select it automatically. Select[Yes/No]", profileName), | ||
| Items: []string{"No", "Yes"}, | ||
| } | ||
| _, result, err := prompt.Run() | ||
| if err != nil || result != "Yes" { | ||
| return | ||
| } | ||
|
|
||
| util.SetDirectoryProfile(&configFile, cwd, profileName) | ||
| if err := util.WriteConfigFile(&configFile); err != nil { | ||
| util.PrintWarning(fmt.Sprintf("Unable to save the directory profile binding [err=%s]", err)) | ||
| return | ||
| } | ||
| util.PrintlnStderr(fmt.Sprintf("Directory %s now uses profile '%s'. Manage bindings with [infisical profile bind] and [infisical profile unbind].", cwd, profileName)) | ||
| } | ||
|
|
||
| func init() { | ||
| RootCmd.AddCommand(initCmd) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to the file, but some considerations I had while testing this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All four are in now.
Slug, not name. Profiles are named
<email>--<org-slug>, e.g.scott@example.com--acme-x4k2. The slug is stable and already URL-safe. If an instance reports no slug we fall back to the slugified name, then to a short piece of the id.Org in the first name too. Every login gets the suffix now, including the first one, so you can tell the tenant from the name without listing.
Rename. Added
infisical profile rename <old> <new>. It moves the stored session and the cached org sessions, and updates the default profile and any directory bindings. A pinned terminal keeps pointing at the old name, so it tells you to pin again.login status. It now shows the profile and how it was selected, the org name next to its id, andOrganization viawhen--orgis in play. Same fields in--json(profile,profileSource,organizationName).One trade-off worth flagging: a profile that is not named after its email does not publish the legacy
loggedInUserEmailfield. An older CLI binary reading the same config will ask for a fresh login instead of loading the wrong profile's token. Scott and I decided that is the right call, but it does mean a downgrade after a fresh login is no longer seamless.