-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
acl: sso auth methods cli commands (#15322)
This PR implements CLI commands to interact with SSO auth methods. This PR is part of the SSO work captured under ☂️ ticket #13120.
- Loading branch information
1 parent
40b50b4
commit 2d83ce7
Showing
13 changed files
with
1,264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package command | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/hashicorp/nomad/api" | ||
"github.com/mitchellh/cli" | ||
) | ||
|
||
// Ensure ACLAuthMethodCommand satisfies the cli.Command interface. | ||
var _ cli.Command = &ACLAuthMethodCommand{} | ||
|
||
// ACLAuthMethodCommand implements cli.Command. | ||
type ACLAuthMethodCommand struct { | ||
Meta | ||
} | ||
|
||
// Help satisfies the cli.Command Help function. | ||
func (a *ACLAuthMethodCommand) Help() string { | ||
helpText := ` | ||
Usage: nomad acl auth-method <subcommand> [options] [args] | ||
This command groups subcommands for interacting with ACL auth methods. | ||
Create an ACL auth method: | ||
$ nomad acl auth-method create -name="name" -type="OIDC" -max-token-ttl="3600s" | ||
List all ACL auth methods: | ||
$ nomad acl auth-method list | ||
Lookup a specific ACL auth method: | ||
$ nomad acl auth-method info <acl_auth_method_name> | ||
Update an ACL auth method: | ||
$ nomad acl auth-method update -type="updated-type" <acl_auth_method_name> | ||
Delete an ACL auth method: | ||
$ nomad acl auth-method delete <acl_auth_method_name> | ||
Please see the individual subcommand help for detailed usage information. | ||
` | ||
return strings.TrimSpace(helpText) | ||
} | ||
|
||
// Synopsis satisfies the cli.Command Synopsis function. | ||
func (a *ACLAuthMethodCommand) Synopsis() string { return "Interact with ACL auth methods" } | ||
|
||
// Name returns the name of this command. | ||
func (a *ACLAuthMethodCommand) Name() string { return "acl auth-method" } | ||
|
||
// Run satisfies the cli.Command Run function. | ||
func (a *ACLAuthMethodCommand) Run(_ []string) int { return cli.RunResultHelp } | ||
|
||
// formatAuthMethod formats and converts the ACL auth method API object into a | ||
// string KV representation suitable for console output. | ||
func formatAuthMethod(authMethod *api.ACLAuthMethod) string { | ||
out := []string{ | ||
fmt.Sprintf("Name|%s", authMethod.Name), | ||
fmt.Sprintf("Type|%s", authMethod.Type), | ||
fmt.Sprintf("Locality|%s", authMethod.TokenLocality), | ||
fmt.Sprintf("MaxTokenTTL|%s", authMethod.MaxTokenTTL.String()), | ||
fmt.Sprintf("Default|%t", authMethod.Default), | ||
} | ||
|
||
if authMethod.Config != nil { | ||
out = append(out, formatAuthMethodConfig(authMethod.Config)...) | ||
} | ||
out = append(out, | ||
[]string{fmt.Sprintf("Create Index|%d", authMethod.CreateIndex), | ||
fmt.Sprintf("Modify Index|%d", authMethod.ModifyIndex), | ||
}..., | ||
) | ||
|
||
return formatKV(out) | ||
} | ||
|
||
func formatAuthMethodConfig(config *api.ACLAuthMethodConfig) []string { | ||
return []string{ | ||
fmt.Sprintf("OIDC Discovery URL|%s", config.OIDCDiscoveryURL), | ||
fmt.Sprintf("OIDC Client ID|%s", config.OIDCClientID), | ||
fmt.Sprintf("OIDC Client Secret|%s", config.OIDCClientSecret), | ||
fmt.Sprintf("Bound audiences|%s", strings.Join(config.BoundAudiences, ",")), | ||
fmt.Sprintf("Allowed redirects URIs|%s", strings.Join(config.AllowedRedirectURIs, ",")), | ||
fmt.Sprintf("Discovery CA pem|%s", strings.Join(config.DiscoveryCaPem, ",")), | ||
fmt.Sprintf("Signing algorithms|%s", strings.Join(config.SigningAlgs, ",")), | ||
fmt.Sprintf("Claim mappings|%s", formatMap(config.ClaimMappings)), | ||
fmt.Sprintf("List claim mappings|%s", formatMap(config.ListClaimMappings)), | ||
} | ||
} | ||
|
||
func formatMap(m map[string]string) string { | ||
out := []string{} | ||
for k, v := range m { | ||
out = append(out, fmt.Sprintf("%s/%s", k, v)) | ||
} | ||
return formatKV(out) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
package command | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"strings" | ||
"time" | ||
|
||
"github.com/hashicorp/nomad/api" | ||
"github.com/mitchellh/cli" | ||
"github.com/posener/complete" | ||
"golang.org/x/exp/slices" | ||
) | ||
|
||
// Ensure ACLAuthMethodCreateCommand satisfies the cli.Command interface. | ||
var _ cli.Command = &ACLAuthMethodCreateCommand{} | ||
|
||
// ACLAuthMethodCreateCommand implements cli.Command. | ||
type ACLAuthMethodCreateCommand struct { | ||
Meta | ||
|
||
name string | ||
methodType string | ||
tokenLocality string | ||
maxTokenTTL time.Duration | ||
isDefault bool | ||
config string | ||
|
||
testStdin io.Reader | ||
} | ||
|
||
// Help satisfies the cli.Command Help function. | ||
func (a *ACLAuthMethodCreateCommand) Help() string { | ||
helpText := ` | ||
Usage: nomad acl auth-method create [options] | ||
Create is used to create new ACL auth methods. Use requires a management token. | ||
General Options: | ||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + ` | ||
ACL Auth Method Create Options: | ||
-name | ||
Sets the human readable name for the ACL auth method. The name must be | ||
between 1-128 characters and is a required parameter. | ||
-type | ||
Sets the type of the auth method. Currently the only supported type is | ||
'OIDC'. | ||
-max-token-ttl | ||
Sets the duration of time all tokens created by this auth method should be | ||
valid for. | ||
-token-locality | ||
Defines the kind of token that this auth method should produce. This can be | ||
either 'local' or 'global'. | ||
-default | ||
Specifies whether this auth method should be treated as a default one in | ||
case no auth method is explicitly specified for a login command. | ||
-config | ||
Auth method configuration in JSON format. May be prefixed with '@' to | ||
indicate that the value is a file path to load the config from. '-' may | ||
also be given to indicate that the config is available on stdin. | ||
` | ||
return strings.TrimSpace(helpText) | ||
} | ||
|
||
func (a *ACLAuthMethodCreateCommand) AutocompleteFlags() complete.Flags { | ||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient), | ||
complete.Flags{ | ||
"-name": complete.PredictAnything, | ||
"-type": complete.PredictSet("OIDC"), | ||
"-max-token-ttl": complete.PredictAnything, | ||
"-token-locality": complete.PredictSet("local", "global"), | ||
"-default": complete.PredictSet("true", "false"), | ||
"-config": complete.PredictNothing, | ||
}) | ||
} | ||
|
||
func (a *ACLAuthMethodCreateCommand) AutocompleteArgs() complete.Predictor { | ||
return complete.PredictNothing | ||
} | ||
|
||
// Synopsis satisfies the cli.Command Synopsis function. | ||
func (a *ACLAuthMethodCreateCommand) Synopsis() string { return "Create a new ACL auth method" } | ||
|
||
// Name returns the name of this command. | ||
func (a *ACLAuthMethodCreateCommand) Name() string { return "acl auth-method create" } | ||
|
||
// Run satisfies the cli.Command Run function. | ||
func (a *ACLAuthMethodCreateCommand) Run(args []string) int { | ||
|
||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient) | ||
flags.Usage = func() { a.Ui.Output(a.Help()) } | ||
flags.StringVar(&a.name, "name", "", "") | ||
flags.StringVar(&a.methodType, "type", "", "") | ||
flags.StringVar(&a.tokenLocality, "token-locality", "", "") | ||
flags.DurationVar(&a.maxTokenTTL, "max-token-ttl", 0, "") | ||
flags.BoolVar(&a.isDefault, "default", false, "") | ||
flags.StringVar(&a.config, "config", "", "") | ||
if err := flags.Parse(args); err != nil { | ||
return 1 | ||
} | ||
|
||
// Check that we got no arguments. | ||
if len(flags.Args()) != 0 { | ||
a.Ui.Error("This command takes no arguments") | ||
a.Ui.Error(commandErrorText(a)) | ||
return 1 | ||
} | ||
|
||
// Perform some basic validation | ||
if a.name == "" { | ||
a.Ui.Error("ACL auth method name must be specified using the -name flag") | ||
return 1 | ||
} | ||
if !slices.Contains([]string{"global", "local"}, a.tokenLocality) { | ||
a.Ui.Error("Token locality must be set to either 'local' or 'global'") | ||
return 1 | ||
} | ||
if a.maxTokenTTL < 1 { | ||
a.Ui.Error("Max token TTL must be set to a value between min and max TTL configured for the server.") | ||
return 1 | ||
} | ||
if strings.ToUpper(a.methodType) != "OIDC" { | ||
a.Ui.Error("ACL auth method type must be set to 'OIDC'") | ||
return 1 | ||
} | ||
if len(a.config) == 0 { | ||
a.Ui.Error("Must provide ACL auth method config in JSON format") | ||
return 1 | ||
} | ||
|
||
config, err := loadDataSource(a.config, a.testStdin) | ||
if err != nil { | ||
a.Ui.Error(fmt.Sprintf("Error loading configuration: %v", err)) | ||
return 1 | ||
} | ||
|
||
configJSON := api.ACLAuthMethodConfig{} | ||
err = json.Unmarshal([]byte(config), &configJSON) | ||
if err != nil { | ||
a.Ui.Error(fmt.Sprintf("Unable to parse config: %v", err)) | ||
return 1 | ||
} | ||
|
||
// Set up the auth method with the passed parameters. | ||
authMethod := api.ACLAuthMethod{ | ||
Name: a.name, | ||
Type: strings.ToUpper(a.methodType), | ||
TokenLocality: a.tokenLocality, | ||
MaxTokenTTL: a.maxTokenTTL, | ||
Default: a.isDefault, | ||
Config: &configJSON, | ||
} | ||
|
||
// Get the HTTP client. | ||
client, err := a.Meta.Client() | ||
if err != nil { | ||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) | ||
return 1 | ||
} | ||
|
||
// Create the auth method via the API. | ||
_, err = client.ACLAuthMethods().Create(&authMethod, nil) | ||
if err != nil { | ||
a.Ui.Error(fmt.Sprintf("Error creating ACL auth method: %v", err)) | ||
return 1 | ||
} | ||
|
||
a.Ui.Output(fmt.Sprintf("Created ACL auth method %s", a.name)) | ||
return 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package command | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"testing" | ||
|
||
"github.com/hashicorp/nomad/ci" | ||
"github.com/hashicorp/nomad/command/agent" | ||
"github.com/hashicorp/nomad/testutil" | ||
"github.com/mitchellh/cli" | ||
"github.com/shoenig/test/must" | ||
) | ||
|
||
func TestACLAuthMethodCreateCommand_Run(t *testing.T) { | ||
ci.Parallel(t) | ||
|
||
// Build a test server with ACLs enabled. | ||
srv, _, url := testServer(t, false, func(c *agent.Config) { | ||
c.ACL.Enabled = true | ||
}) | ||
defer srv.Shutdown() | ||
|
||
// Wait for the server to start fully and ensure we have a bootstrap token. | ||
testutil.WaitForLeader(t, srv.Agent.RPC) | ||
rootACLToken := srv.RootToken | ||
must.NotNil(t, rootACLToken) | ||
|
||
ui := cli.NewMockUi() | ||
cmd := &ACLAuthMethodCreateCommand{ | ||
Meta: Meta{ | ||
Ui: ui, | ||
flagAddress: url, | ||
}, | ||
} | ||
|
||
// Test the basic validation on the command. | ||
must.Eq(t, 1, cmd.Run([]string{"-address=" + url, "this-command-does-not-take-args"})) | ||
must.StrContains(t, ui.ErrorWriter.String(), "This command takes no arguments") | ||
|
||
ui.OutputWriter.Reset() | ||
ui.ErrorWriter.Reset() | ||
|
||
must.Eq(t, 1, cmd.Run([]string{"-address=" + url})) | ||
must.StrContains(t, ui.ErrorWriter.String(), "ACL auth method name must be specified using the -name flag") | ||
|
||
ui.OutputWriter.Reset() | ||
ui.ErrorWriter.Reset() | ||
|
||
must.Eq(t, 1, cmd.Run([]string{"-address=" + url, "-name=foobar", "-token-locality=global", "-max-token-ttl=3600s"})) | ||
must.StrContains(t, ui.ErrorWriter.String(), "ACL auth method type must be set to 'OIDC'") | ||
|
||
ui.OutputWriter.Reset() | ||
ui.ErrorWriter.Reset() | ||
|
||
must.Eq(t, 1, cmd.Run([]string{"-address=" + url, "-name=foobar", "-type=OIDC", "-token-locality=global", "-max-token-ttl=3600s"})) | ||
must.StrContains(t, ui.ErrorWriter.String(), "Must provide ACL auth method config in JSON format") | ||
|
||
ui.OutputWriter.Reset() | ||
ui.ErrorWriter.Reset() | ||
|
||
// Create an auth method | ||
args := []string{ | ||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-name=acl-auth-method-cli-test", | ||
"-type=OIDC", "-token-locality=global", "-default=true", "-max-token-ttl=3600s", | ||
"-config={\"OIDCDiscoveryURL\":\"http://example.com\"}", | ||
} | ||
must.Eq(t, 0, cmd.Run(args)) | ||
s := ui.OutputWriter.String() | ||
must.StrContains(t, s, "acl-auth-method-cli-test") | ||
|
||
ui.OutputWriter.Reset() | ||
ui.ErrorWriter.Reset() | ||
|
||
// Create an auth method with a config from file | ||
configFile, err := os.CreateTemp("", "config.json") | ||
defer os.Remove(configFile.Name()) | ||
must.Nil(t, err) | ||
|
||
conf := map[string]interface{}{"OIDCDiscoveryURL": "http://example.com"} | ||
jsonData, err := json.Marshal(conf) | ||
must.Nil(t, err) | ||
|
||
_, err = configFile.Write(jsonData) | ||
must.Nil(t, err) | ||
|
||
args = []string{ | ||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-name=acl-auth-method-cli-test", | ||
"-type=OIDC", "-token-locality=global", "-default=true", "-max-token-ttl=3600s", | ||
fmt.Sprintf("-config=@%s", configFile.Name()), | ||
} | ||
must.Eq(t, 0, cmd.Run(args)) | ||
s = ui.OutputWriter.String() | ||
must.StrContains(t, s, "acl-auth-method-cli-test") | ||
|
||
ui.OutputWriter.Reset() | ||
ui.ErrorWriter.Reset() | ||
} |
Oops, something went wrong.