|
| 1 | +// Copyright 2022-2026 Salesforce, Inc. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +package sandbox |
| 16 | + |
| 17 | +import ( |
| 18 | + "context" |
| 19 | + "fmt" |
| 20 | + "slices" |
| 21 | + "strings" |
| 22 | + |
| 23 | + "github.com/slackapi/slack-cli/internal/iostreams" |
| 24 | + "github.com/slackapi/slack-cli/internal/shared" |
| 25 | + "github.com/slackapi/slack-cli/internal/shared/types" |
| 26 | + "github.com/slackapi/slack-cli/internal/slackerror" |
| 27 | + "github.com/slackapi/slack-cli/internal/style" |
| 28 | +) |
| 29 | + |
| 30 | +// getSandboxAuth returns the auth used for sandbox management. |
| 31 | +// Uses the global --token flag if present, otherwise delegates to resolveAuthForSandbox. |
| 32 | +func getSandboxAuth(ctx context.Context, clients *shared.ClientFactory) (string, *types.SlackAuth, error) { |
| 33 | + auth, err := resolveAuthForSandbox(ctx, clients) |
| 34 | + if err != nil { |
| 35 | + return "", nil, err |
| 36 | + } |
| 37 | + |
| 38 | + clients.Config.APIHostResolved = clients.Auth().ResolveAPIHost(ctx, clients.Config.APIHostFlag, auth) |
| 39 | + clients.Config.LogstashHostResolved = clients.Auth().ResolveLogstashHost(ctx, clients.Config.APIHostResolved, clients.CLIVersion) |
| 40 | + |
| 41 | + return auth.Token, auth, nil |
| 42 | +} |
| 43 | + |
| 44 | +// resolveAuthForSandbox determines what auth to use for sandbox operations. |
| 45 | +// If the global --token flag is set, we will use that. |
| 46 | +// Else if the global --team flag is set, we will use the associated token for that team. |
| 47 | +// Else if the user is only logged in to one team, we will default to that team's auth. |
| 48 | +// Else we will prompt the user to select a team to use for authentication. |
| 49 | +func resolveAuthForSandbox(ctx context.Context, clients *shared.ClientFactory) (*types.SlackAuth, error) { |
| 50 | + // Check for the global --token flag |
| 51 | + if clients.Config.TokenFlag != "" { |
| 52 | + auth, err := clients.Auth().AuthWithToken(ctx, clients.Config.TokenFlag) |
| 53 | + if err != nil { |
| 54 | + return nil, err |
| 55 | + } |
| 56 | + return &auth, nil |
| 57 | + } |
| 58 | + |
| 59 | + auths, err := clients.Auth().Auths(ctx) |
| 60 | + if err != nil { |
| 61 | + return nil, err |
| 62 | + } |
| 63 | + |
| 64 | + if len(auths) == 0 { |
| 65 | + return nil, slackerror.New(slackerror.ErrCredentialsNotFound). |
| 66 | + WithMessage("You must be logged in to manage sandboxes"). |
| 67 | + WithRemediation("Run 'slack login' to authenticate, or use --token for CI/CD") |
| 68 | + } |
| 69 | + |
| 70 | + // Check for the global --team flag |
| 71 | + if clients.Config.TeamFlag != "" { |
| 72 | + for _, auth := range auths { |
| 73 | + if auth.TeamID == clients.Config.TeamFlag || auth.TeamDomain == clients.Config.TeamFlag { |
| 74 | + return &auth, nil |
| 75 | + } |
| 76 | + } |
| 77 | + return nil, slackerror.New(slackerror.ErrTeamNotFound). |
| 78 | + WithMessage("No auth found for team: %s", clients.Config.TeamFlag). |
| 79 | + WithRemediation("Run 'slack auth list' to see your authorized workspaces") |
| 80 | + } |
| 81 | + |
| 82 | + // Prompt the user to select a team to use for authentication (if there are multiple auths), or default to the user's only auth |
| 83 | + if len(auths) == 1 { |
| 84 | + return &auths[0], nil |
| 85 | + } |
| 86 | + type authOption struct { |
| 87 | + auth types.SlackAuth |
| 88 | + label string |
| 89 | + } |
| 90 | + options := make([]authOption, 0, len(auths)) |
| 91 | + for _, a := range auths { |
| 92 | + options = append(options, authOption{ |
| 93 | + auth: a, |
| 94 | + label: fmt.Sprintf("%s %s", a.TeamDomain, style.Secondary(a.TeamID)), |
| 95 | + }) |
| 96 | + } |
| 97 | + slices.SortFunc(options, func(a, b authOption) int { |
| 98 | + if c := strings.Compare(a.auth.TeamDomain, b.auth.TeamDomain); c != 0 { |
| 99 | + return c |
| 100 | + } |
| 101 | + return strings.Compare(a.auth.TeamID, b.auth.TeamID) |
| 102 | + }) |
| 103 | + labels := make([]string, 0, len(options)) |
| 104 | + for _, opt := range options { |
| 105 | + labels = append(labels, opt.label) |
| 106 | + } |
| 107 | + selection, err := clients.IO.SelectPrompt(ctx, "Select a team for authentication", labels, iostreams.SelectPromptConfig{ |
| 108 | + Flag: clients.Config.Flags.Lookup("team"), |
| 109 | + Required: true, |
| 110 | + }) |
| 111 | + if err != nil { |
| 112 | + return nil, err |
| 113 | + } |
| 114 | + switch { |
| 115 | + case selection.Flag: |
| 116 | + for _, opt := range options { |
| 117 | + if opt.auth.TeamID == selection.Option || opt.auth.TeamDomain == selection.Option { |
| 118 | + return &opt.auth, nil |
| 119 | + } |
| 120 | + } |
| 121 | + return nil, slackerror.New(slackerror.ErrTeamNotFound). |
| 122 | + WithMessage("No auth found for team: %s", selection.Option). |
| 123 | + WithRemediation("Run 'slack auth list' to see your authorized workspaces") |
| 124 | + case selection.Prompt: |
| 125 | + return &options[selection.Index].auth, nil |
| 126 | + default: |
| 127 | + return nil, slackerror.New(slackerror.ErrInvalidAuth) |
| 128 | + } |
| 129 | +} |
0 commit comments