Skip to content

Commit

Permalink
acl: sso auth methods cli commands (#15322)
Browse files Browse the repository at this point in the history
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
pkazmierczak committed Nov 28, 2022
1 parent 40b50b4 commit 2d83ce7
Show file tree
Hide file tree
Showing 13 changed files with 1,264 additions and 0 deletions.
103 changes: 103 additions & 0 deletions command/acl_auth_method.go
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)
}
179 changes: 179 additions & 0 deletions command/acl_auth_method_create.go
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
}
99 changes: 99 additions & 0 deletions command/acl_auth_method_create_test.go
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()
}
Loading

0 comments on commit 2d83ce7

Please sign in to comment.