diff --git a/.changelog/18816.txt b/.changelog/18816.txt new file mode 100644 index 0000000000..ef8989ee70 --- /dev/null +++ b/.changelog/18816.txt @@ -0,0 +1,3 @@ +```release-note:feature +cli: Add `consul acl templated-policy` commands to read, list and preview templated policies. +``` \ No newline at end of file diff --git a/agent/acl_endpoint.go b/agent/acl_endpoint.go index b98784e346..fb94862800 100644 --- a/agent/acl_endpoint.go +++ b/agent/acl_endpoint.go @@ -1134,12 +1134,6 @@ func (s *HTTPHandlers) ACLAuthorize(resp http.ResponseWriter, req *http.Request) return responses, nil } -type ACLTemplatedPolicyResponse struct { - TemplateName string - Schema string - Template string -} - func (s *HTTPHandlers) ACLTemplatedPoliciesList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { if s.checkACLDisabled() { return nil, aclDisabled @@ -1165,10 +1159,10 @@ func (s *HTTPHandlers) ACLTemplatedPoliciesList(resp http.ResponseWriter, req *h return nil, err } - templatedPolicies := make(map[string]ACLTemplatedPolicyResponse) + templatedPolicies := make(map[string]api.ACLTemplatedPolicyResponse) for tp, tmpBase := range structs.GetACLTemplatedPolicyList() { - templatedPolicies[tp] = ACLTemplatedPolicyResponse{ + templatedPolicies[tp] = api.ACLTemplatedPolicyResponse{ TemplateName: tmpBase.TemplateName, Schema: tmpBase.Schema, Template: tmpBase.Template, @@ -1213,7 +1207,7 @@ func (s *HTTPHandlers) ACLTemplatedPolicyRead(resp http.ResponseWriter, req *htt return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Invalid templated policy Name: %s", templateName)} } - return ACLTemplatedPolicyResponse{ + return api.ACLTemplatedPolicyResponse{ TemplateName: baseTemplate.TemplateName, Schema: baseTemplate.Schema, Template: baseTemplate.Template, diff --git a/agent/acl_endpoint_test.go b/agent/acl_endpoint_test.go index 0a135f38df..50b2ad20d4 100644 --- a/agent/acl_endpoint_test.go +++ b/agent/acl_endpoint_test.go @@ -1372,11 +1372,11 @@ func TestACL_HTTP(t *testing.T) { require.Equal(t, http.StatusOK, resp.Code) - var list map[string]ACLTemplatedPolicyResponse + var list map[string]api.ACLTemplatedPolicyResponse require.NoError(t, json.NewDecoder(resp.Body).Decode(&list)) require.Len(t, list, 3) - require.Equal(t, ACLTemplatedPolicyResponse{ + require.Equal(t, api.ACLTemplatedPolicyResponse{ TemplateName: api.ACLTemplatedPolicyServiceName, Schema: structs.ACLTemplatedPolicyIdentitiesSchema, Template: structs.ACLTemplatedPolicyService, @@ -1399,7 +1399,7 @@ func TestACL_HTTP(t *testing.T) { a.srv.h.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) - var templatedPolicy ACLTemplatedPolicyResponse + var templatedPolicy api.ACLTemplatedPolicyResponse require.NoError(t, json.NewDecoder(resp.Body).Decode(&templatedPolicy)) require.Equal(t, structs.ACLTemplatedPolicyDNSSchema, templatedPolicy.Schema) require.Equal(t, api.ACLTemplatedPolicyDNSName, templatedPolicy.TemplateName) diff --git a/agent/structs/acl_templated_policy.go b/agent/structs/acl_templated_policy.go index 4e6fab5778..1e62c984ce 100644 --- a/agent/structs/acl_templated_policy.go +++ b/agent/structs/acl_templated_policy.go @@ -23,19 +23,20 @@ type ACLTemplatedPolicies []*ACLTemplatedPolicy const ( ACLTemplatedPolicyNodeID = "00000000-0000-0000-0000-000000000004" ACLTemplatedPolicyServiceID = "00000000-0000-0000-0000-000000000003" - ACLTemplatedPolicyIdentitiesSchema = `{ - "type": "object", - "properties": { - "name": { "type": "string", "$ref": "#/definitions/min-length-one" } - }, - "required": ["name"], - "definitions": { - "min-length-one": { - "type": "string", - "minLength": 1 - } + ACLTemplatedPolicyIdentitiesSchema = ` +{ + "type": "object", + "properties": { + "name": { "type": "string", "$ref": "#/definitions/min-length-one" } + }, + "required": ["name"], + "definitions": { + "min-length-one": { + "type": "string", + "minLength": 1 } - }` + } +}` ACLTemplatedPolicyDNSID = "00000000-0000-0000-0000-000000000005" ACLTemplatedPolicyDNSSchema = "" // empty schema as it does not require variables @@ -51,8 +52,9 @@ type ACLTemplatedPolicyBase struct { } var ( - // TODO(Ronald): add other templates // This supports: node, service and dns templates + // Note: when adding a new builtin template, ensure you update `command/acl/templatedpolicy/formatter.go` + // to handle the new templates required variables and schema. aclTemplatedPoliciesList = map[string]*ACLTemplatedPolicyBase{ api.ACLTemplatedPolicyServiceName: { TemplateID: ACLTemplatedPolicyServiceID, diff --git a/api/acl.go b/api/acl.go index 31130cafc3..68d6f1f54c 100644 --- a/api/acl.go +++ b/api/acl.go @@ -166,6 +166,12 @@ type ACLTemplatedPolicy struct { Datacenters []string `json:",omitempty"` } +type ACLTemplatedPolicyResponse struct { + TemplateName string + Schema string + Template string +} + type ACLTemplatedPolicyVariables struct { Name string } @@ -1653,3 +1659,78 @@ func (a *ACL) OIDCCallback(auth *ACLOIDCCallbackParams, q *WriteOptions) (*ACLTo } return &out, wm, nil } + +// TemplatedPolicyReadByName retrieves the templated policy details (by name). Returns nil if not found. +func (a *ACL) TemplatedPolicyReadByName(templateName string, q *QueryOptions) (*ACLTemplatedPolicyResponse, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/acl/templated-policy/name/"+templateName) + r.setQueryOptions(q) + rtt, resp, err := a.c.doRequest(r) + if err != nil { + return nil, nil, err + } + defer closeResponseBody(resp) + found, resp, err := requireNotFoundOrOK(resp) + if err != nil { + return nil, nil, err + } + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + if !found { + return nil, qm, nil + } + + var out ACLTemplatedPolicyResponse + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, qm, nil +} + +// TemplatedPolicyList retrieves a listing of all templated policies. +func (a *ACL) TemplatedPolicyList(q *QueryOptions) (map[string]ACLTemplatedPolicyResponse, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/acl/templated-policies") + r.setQueryOptions(q) + rtt, resp, err := a.c.doRequest(r) + if err != nil { + return nil, nil, err + } + defer closeResponseBody(resp) + if err := requireOK(resp); err != nil { + return nil, nil, err + } + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var entries map[string]ACLTemplatedPolicyResponse + if err := decodeBody(resp, &entries); err != nil { + return nil, nil, err + } + return entries, qm, nil +} + +// TemplatedPolicyPreview is used to preview the policy rendered by the templated policy. +func (a *ACL) TemplatedPolicyPreview(tp *ACLTemplatedPolicy, q *WriteOptions) (*ACLPolicy, *WriteMeta, error) { + r := a.c.newRequest("POST", "/v1/acl/templated-policy/preview/"+tp.TemplateName) + r.setWriteOptions(q) + r.obj = tp.TemplateVariables + + rtt, resp, err := a.c.doRequest(r) + if err != nil { + return nil, nil, err + } + defer closeResponseBody(resp) + if err := requireOK(resp); err != nil { + return nil, nil, err + } + wm := &WriteMeta{RequestTime: rtt} + var out ACLPolicy + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, wm, nil +} diff --git a/command/acl/templatedpolicy/formatter.go b/command/acl/templatedpolicy/formatter.go new file mode 100644 index 0000000000..a3b18fddf7 --- /dev/null +++ b/command/acl/templatedpolicy/formatter.go @@ -0,0 +1,132 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicy + +import ( + "bytes" + "encoding/json" + "fmt" + "sort" + + "github.com/hashicorp/consul/api" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" + WhitespaceIndent = "\t" +) + +// Formatter defines methods provided by templated-policy command output formatter +type Formatter interface { + FormatTemplatedPolicy(policy api.ACLTemplatedPolicyResponse) (string, error) + FormatTemplatedPolicyList(policies map[string]api.ACLTemplatedPolicyResponse) (string, error) +} + +// GetSupportedFormats returns supported formats +func GetSupportedFormats() []string { + return []string{PrettyFormat, JSONFormat} +} + +// NewFormatter returns Formatter implementation +func NewFormatter(format string, showMeta bool) (formatter Formatter, err error) { + switch format { + case PrettyFormat: + formatter = newPrettyFormatter(showMeta) + case JSONFormat: + formatter = newJSONFormatter(showMeta) + default: + err = fmt.Errorf("unknown format: %q", format) + } + + return formatter, err +} + +func newPrettyFormatter(showMeta bool) Formatter { + return &prettyFormatter{showMeta} +} + +func newJSONFormatter(showMeta bool) Formatter { + return &jsonFormatter{showMeta} +} + +type prettyFormatter struct { + showMeta bool +} + +// FormatTemplatedPolicy displays template name, input variables and example usages. When +// showMeta is true, we display raw template code and schema. +// This implementation is a conscious choice as we know builtin variables we know every required/optional input variables +// so we can just hardcode this. +// In the future, when we implement user defined templated policies, we will move this to some sort of schema parsing. +// This implementation allows us to move forward without limiting ourselves when implementing user defined templated policies. +func (f *prettyFormatter) FormatTemplatedPolicy(templatedPolicy api.ACLTemplatedPolicyResponse) (string, error) { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("Name: %s\n", templatedPolicy.TemplateName)) + + buffer.WriteString("Input variables:") + switch templatedPolicy.TemplateName { + case api.ACLTemplatedPolicyServiceName: + buffer.WriteString(fmt.Sprintf("\n%sName: String - Required - The name of the service.\n", WhitespaceIndent)) + buffer.WriteString("Example usage:\n") + buffer.WriteString(WhitespaceIndent + "consul acl token create -templated-policy builtin/service -var name:api\n") + case api.ACLTemplatedPolicyNodeName: + buffer.WriteString(fmt.Sprintf("\n%sName: String - Required - The node name.\n", WhitespaceIndent)) + buffer.WriteString("Example usage:\n") + buffer.WriteString(fmt.Sprintf("%sconsul acl token create -templated-policy builtin/node -var name:node-1\n", WhitespaceIndent)) + case api.ACLTemplatedPolicyDNSName: + buffer.WriteString(" None\n") + buffer.WriteString("Example usage:\n") + buffer.WriteString(fmt.Sprintf("%sconsul acl token create -templated-policy builtin/dns\n", WhitespaceIndent)) + default: + buffer.WriteString(" None\n") + } + + if f.showMeta { + if templatedPolicy.Schema != "" { + buffer.WriteString(fmt.Sprintf("Schema:\n%s\n\n", templatedPolicy.Schema)) + } + buffer.WriteString(fmt.Sprintf("Raw Template:\n%s\n", templatedPolicy.Template)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) FormatTemplatedPolicyList(policies map[string]api.ACLTemplatedPolicyResponse) (string, error) { + var buffer bytes.Buffer + + templateNames := make([]string, 0, len(policies)) + for _, templatedPolicy := range policies { + templateNames = append(templateNames, templatedPolicy.TemplateName) + } + + //ensure the list is consistently sorted by strings + sort.Strings(templateNames) + for _, name := range templateNames { + buffer.WriteString(fmt.Sprintf("%s\n", name)) + } + + return buffer.String(), nil +} + +type jsonFormatter struct { + showMeta bool +} + +func (f *jsonFormatter) FormatTemplatedPolicy(templatedPolicy api.ACLTemplatedPolicyResponse) (string, error) { + b, err := json.MarshalIndent(templatedPolicy, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal templated policy: %v", err) + } + return string(b), nil +} + +func (f *jsonFormatter) FormatTemplatedPolicyList(templatedPolicies map[string]api.ACLTemplatedPolicyResponse) (string, error) { + b, err := json.MarshalIndent(templatedPolicies, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal templated policies: %v", err) + } + return string(b), nil +} diff --git a/command/acl/templatedpolicy/formatter_ce_test.go b/command/acl/templatedpolicy/formatter_ce_test.go new file mode 100644 index 0000000000..1d490afead --- /dev/null +++ b/command/acl/templatedpolicy/formatter_ce_test.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !consulent +// +build !consulent + +package templatedpolicy + +import "testing" + +func TestFormatTemplatedPolicy(t *testing.T) { + testFormatTemplatedPolicy(t, "FormatTemplatedPolicy/ce") +} + +func TestFormatTemplatedPolicyList(t *testing.T) { + testFormatTemplatedPolicyList(t, "FormatTemplatedPolicyList/ce") +} diff --git a/command/acl/templatedpolicy/formatter_test.go b/command/acl/templatedpolicy/formatter_test.go new file mode 100644 index 0000000000..d6e8fa4d0c --- /dev/null +++ b/command/acl/templatedpolicy/formatter_test.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicy + +import ( + "fmt" + "os" + "path" + "path/filepath" + "testing" + + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/require" +) + +// golden reads from the golden file returning the contents as a string. +func golden(t *testing.T, name string) string { + t.Helper() + + golden := filepath.Join("testdata", name+".golden") + expected, err := os.ReadFile(golden) + require.NoError(t, err) + + return string(expected) +} + +func testFormatTemplatedPolicy(t *testing.T, dirPath string) { + type testCase struct { + templatedPolicy api.ACLTemplatedPolicyResponse + } + + cases := map[string]testCase{ + "node-templated-policy": { + templatedPolicy: api.ACLTemplatedPolicyResponse{ + TemplateName: api.ACLTemplatedPolicyNodeName, + Schema: structs.ACLTemplatedPolicyIdentitiesSchema, + Template: structs.ACLTemplatedPolicyNode, + }, + }, + "dns-templated-policy": { + templatedPolicy: api.ACLTemplatedPolicyResponse{ + TemplateName: api.ACLTemplatedPolicyDNSName, + Schema: structs.ACLTemplatedPolicyDNSSchema, + Template: structs.ACLTemplatedPolicyDNS, + }, + }, + "service-templated-policy": { + templatedPolicy: api.ACLTemplatedPolicyResponse{ + TemplateName: api.ACLTemplatedPolicyServiceName, + Schema: structs.ACLTemplatedPolicyIdentitiesSchema, + Template: structs.ACLTemplatedPolicyService, + }, + }, + } + + formatters := map[string]Formatter{ + "pretty": newPrettyFormatter(false), + "pretty-meta": newPrettyFormatter(true), + // the JSON formatter ignores the showMeta + "json": newJSONFormatter(false), + } + + for name, tcase := range cases { + t.Run(name, func(t *testing.T) { + for fmtName, formatter := range formatters { + t.Run(fmtName, func(t *testing.T) { + actual, err := formatter.FormatTemplatedPolicy(tcase.templatedPolicy) + require.NoError(t, err) + + gName := fmt.Sprintf("%s.%s", name, fmtName) + + expected := golden(t, path.Join(dirPath, gName)) + require.Equal(t, expected, actual) + }) + } + }) + } +} + +func testFormatTemplatedPolicyList(t *testing.T, dirPath string) { + // we don't consider the showMeta field for policy list + formatters := map[string]Formatter{ + "pretty": newPrettyFormatter(false), + "json": newJSONFormatter(false), + } + + policies := map[string]api.ACLTemplatedPolicyResponse{ + "builtin/node": { + TemplateName: api.ACLTemplatedPolicyNodeName, + Schema: structs.ACLTemplatedPolicyIdentitiesSchema, + Template: structs.ACLTemplatedPolicyNode, + }, + "builtin/dns": { + TemplateName: api.ACLTemplatedPolicyDNSName, + Schema: structs.ACLTemplatedPolicyDNSSchema, + Template: structs.ACLTemplatedPolicyDNS, + }, + "builtin/service": { + TemplateName: api.ACLTemplatedPolicyServiceName, + Schema: structs.ACLTemplatedPolicyIdentitiesSchema, + Template: structs.ACLTemplatedPolicyService, + }, + } + + for fmtName, formatter := range formatters { + t.Run(fmtName, func(t *testing.T) { + actual, err := formatter.FormatTemplatedPolicyList(policies) + require.NoError(t, err) + + gName := fmt.Sprintf("list.%s", fmtName) + + expected := golden(t, path.Join(dirPath, gName)) + require.Equal(t, expected, actual) + }) + } +} diff --git a/command/acl/templatedpolicy/list/templated_policy_list.go b/command/acl/templatedpolicy/list/templated_policy_list.go new file mode 100644 index 0000000000..3e5e30570f --- /dev/null +++ b/command/acl/templatedpolicy/list/templated_policy_list.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicylist + +import ( + "flag" + "fmt" + "strings" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/command/acl/templatedpolicy" + "github.com/hashicorp/consul/command/flags" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.flags.StringVar( + &c.format, + "format", + templatedpolicy.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(templatedpolicy.GetSupportedFormats(), "|")), + ) + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.MultiTenancyFlags()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + tps, _, err := client.ACL().TemplatedPolicyList(nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to retrieve the templated policies list: %v", err)) + return 1 + } + + formatter, err := templatedpolicy.NewFormatter(c.format, false) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatTemplatedPolicyList(tps) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Lists ACL templated policies" + help = ` +Usage: consul acl templated-policy list [options] + + Lists all the ACL templated policies. + + Example: + + $ consul acl templated-policy list +` +) diff --git a/command/acl/templatedpolicy/list/templated_policy_list_test.go b/command/acl/templatedpolicy/list/templated_policy_list_test.go new file mode 100644 index 0000000000..bdde3782d7 --- /dev/null +++ b/command/acl/templatedpolicy/list/templated_policy_list_test.go @@ -0,0 +1,102 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicylist + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTemplatedPolicyListCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestTemplatedPolicyListCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + initial_management = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1", testrpc.WithToken("root")) + + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 0) + assert.Empty(t, ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + require.Contains(t, output, api.ACLTemplatedPolicyServiceName) + require.Contains(t, output, api.ACLTemplatedPolicyDNSName) +} + +func TestTemplatedPolicyListCommand_JSON(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + initial_management = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1", testrpc.WithToken("root")) + + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 0) + assert.Empty(t, ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + require.Contains(t, output, api.ACLTemplatedPolicyServiceName) + require.Contains(t, output, api.ACLTemplatedPolicyDNSName) + + var jsonOutput map[string]api.ACLTemplatedPolicyResponse + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + outputTemplate := jsonOutput[api.ACLTemplatedPolicyDNSName] + assert.Equal(t, structs.ACLTemplatedPolicyDNSSchema, outputTemplate.Schema) +} diff --git a/command/acl/templatedpolicy/preview/templated_policy_preview.go b/command/acl/templatedpolicy/preview/templated_policy_preview.go new file mode 100644 index 0000000000..c2dc706f90 --- /dev/null +++ b/command/acl/templatedpolicy/preview/templated_policy_preview.go @@ -0,0 +1,134 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicylist + +import ( + "flag" + "fmt" + "strings" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/policy" + "github.com/hashicorp/consul/command/acl/templatedpolicy" + "github.com/hashicorp/consul/command/flags" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + templatedPolicyName string + templatedPolicyFile string + templatedPolicyVariables []string + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.flags.StringVar( + &c.format, + "format", + templatedpolicy.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(templatedpolicy.GetSupportedFormats(), "|")), + ) + c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+ + " Must be used in combination with -name flag to specify required variables."+ + " May be specified multiple times with different variables."+ + " Format is VariableName:Value") + c.flags.StringVar(&c.templatedPolicyName, "name", "", "The templated policy name. Use -var flag to specify variables when required.") + c.flags.StringVar(&c.templatedPolicyFile, "file", "", "Path to a file containing templated policies and variables.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.MultiTenancyFlags()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if len(c.templatedPolicyName) == 0 && len(c.templatedPolicyFile) == 0 { + c.UI.Error("Cannot preview a templated policy without specifying -name or -file") + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.templatedPolicyName, c.templatedPolicyFile, c.templatedPolicyVariables) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if !(len(parsedTemplatedPolicies) == 1) { + c.UI.Error("Can only preview a single templated policy at a time.") + return 1 + } + + syntheticPolicy, _, err := client.ACL().TemplatedPolicyPreview(parsedTemplatedPolicies[0], nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to generate the templated policy preview: %v", err)) + return 1 + } + + formatter, err := policy.NewFormatter(c.format, false) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatPolicy(syntheticPolicy) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Preview the policy rendered by the ACL templated policy" + help = ` +Usage: consul acl templated-policy preview [options] + + Preview the policy rendered by the ACL templated policy. + + Example: + + $ consul acl templated-policy preview -name "builtin/service" -var "name:api" + + Preview a templated policy using a file. + + Example: + + $ consul acl templated-policy preview -file templated-policy-file.hcl +` +) diff --git a/command/acl/templatedpolicy/preview/templated_policy_preview_test.go b/command/acl/templatedpolicy/preview/templated_policy_preview_test.go new file mode 100644 index 0000000000..706e1d270f --- /dev/null +++ b/command/acl/templatedpolicy/preview/templated_policy_preview_test.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicylist + +import ( + "encoding/json" + "os" + "strings" + "testing" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/consul/testrpc" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTemplatedPolicyPreviewCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestTemplatedPolicyPreviewCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + testDir := testutil.TempDir(t, "acl") + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + initial_management = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1", testrpc.WithToken("root")) + + t.Run("missing name and file flags", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 1) + assert.Contains(t, ui.ErrorWriter.String(), "Cannot preview a templated policy without specifying -name or -file") + }) + + t.Run("missing required template variables", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=builtin/node", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 1) + assert.Contains(t, ui.ErrorWriter.String(), "Failed to generate the templated policy preview") + }) + + t.Run("correct input", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=builtin/node", + "-var=name:api", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 0) + assert.Empty(t, ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + require.Contains(t, output, "synthetic policy generated from templated policy: builtin/node") + }) + + t.Run("correct input with file", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-file=" + testDir + "/templated-policy.hcl", + } + + templatedPolicy := []byte("TemplatedPolicy \"builtin/service\" { Name = \"web\"}") + err := os.WriteFile(testDir+"/templated-policy.hcl", templatedPolicy, 0644) + require.NoError(t, err) + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 0) + assert.Empty(t, ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + require.Contains(t, output, "synthetic policy generated from templated policy: builtin/service") + }) + + t.Run("multiple templated policies input in file", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-file=" + testDir + "/templated-policy.hcl", + } + + templatedPolicy := []byte(` + TemplatedPolicy "builtin/service" { Name = "web"} + TemplatedPolicy "builtin/node" { Name = "api"} + `) + err := os.WriteFile(testDir+"/templated-policy.hcl", templatedPolicy, 0644) + require.NoError(t, err) + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 1) + assert.Contains(t, ui.ErrorWriter.String(), "Can only preview a single templated policy at a time.") + }) +} + +func TestTemplatedPolicyPreviewCommand_JSON(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + initial_management = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1", testrpc.WithToken("root")) + + t.Run("missing templated-policy flags", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 1) + assert.Contains(t, ui.ErrorWriter.String(), "Cannot preview a templated policy without specifying -name or -file") + }) + + t.Run("missing required template variables", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=builtin/node", + "-format=json", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 1) + assert.Contains(t, ui.ErrorWriter.String(), "Failed to generate the templated policy preview") + }) + + t.Run("correct input", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=builtin/node", + "-var=name:api", + "-format=json", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 0) + assert.Empty(t, ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + require.Contains(t, output, "synthetic policy generated from templated policy: builtin/node") + + // ensure valid json + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) +} diff --git a/command/acl/templatedpolicy/read/templated_policy_read.go b/command/acl/templatedpolicy/read/templated_policy_read.go new file mode 100644 index 0000000000..0d3955fb00 --- /dev/null +++ b/command/acl/templatedpolicy/read/templated_policy_read.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicyread + +import ( + "flag" + "fmt" + "strings" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/acl/templatedpolicy" + "github.com/hashicorp/consul/command/flags" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" + synopsis = "Read an ACL Templated Policy" + help = ` +Usage: consul acl templated-policy read [options] TEMPLATED_POLICY + + This command will retrieve and print out the details of a single templated policy. + + Example: + + $ consul acl templated-policy read -name templated-policy-name +` +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + templateName string + format string + showMeta bool +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.flags.StringVar(&c.templateName, "name", "", "The name of the templated policy to read.") + c.flags.StringVar( + &c.format, + "format", + templatedpolicy.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(templatedpolicy.GetSupportedFormats(), "|")), + ) + c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that templated policy metadata such "+ + "as the schema and template code should be shown for each entry.") + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + flags.Merge(c.flags, c.http.MultiTenancyFlags()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.templateName == "" { + c.UI.Error("Must specify the -name parameter") + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + var tp *api.ACLTemplatedPolicyResponse + + tp, _, err = client.ACL().TemplatedPolicyReadByName(c.templateName, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading templated policy %q: %v", c.templateName, err)) + return 1 + } else if tp == nil { + c.UI.Error(fmt.Sprintf("Templated policy not found with name %q", c.templateName)) + return 1 + } + + formatter, err := templatedpolicy.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatTemplatedPolicy(*tp) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if out != "" { + c.UI.Info(out) + } + + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} diff --git a/command/acl/templatedpolicy/read/templated_policy_read_test.go b/command/acl/templatedpolicy/read/templated_policy_read_test.go new file mode 100644 index 0000000000..9059ed99c1 --- /dev/null +++ b/command/acl/templatedpolicy/read/templated_policy_read_test.go @@ -0,0 +1,134 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicyread + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTemplatedPolicyReadCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestTemplatedPolicyReadCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + initial_management = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1", testrpc.WithToken("root")) + + t.Run("missing name flag", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 1) + assert.Contains(t, ui.ErrorWriter.String(), "Must specify the -name parameter") + }) + + t.Run("correct input", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + api.ACLTemplatedPolicyNodeName, + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 0) + assert.Empty(t, ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + require.Contains(t, output, "Name: String - Required - The node name.") + require.Contains(t, output, "consul acl token create -templated-policy builtin/node -var name:node-1") + }) +} + +func TestTemplatedPolicyReadCommand_JSON(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + initial_management = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1", testrpc.WithToken("root")) + + t.Run("missing name flag", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 1) + assert.Contains(t, ui.ErrorWriter.String(), "Must specify the -name parameter") + }) + + t.Run("correct input", func(t *testing.T) { + ui := cli.NewMockUi() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + api.ACLTemplatedPolicyNodeName, + "-format=json", + } + + cmd := New(ui) + code := cmd.Run(args) + assert.Equal(t, code, 0) + assert.Empty(t, ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + var templatedPolicy api.ACLTemplatedPolicyResponse + err := json.Unmarshal([]byte(output), &templatedPolicy) + + assert.NoError(t, err) + assert.Equal(t, structs.ACLTemplatedPolicyIdentitiesSchema, templatedPolicy.Schema) + assert.Equal(t, api.ACLTemplatedPolicyNodeName, templatedPolicy.TemplateName) + }) +} diff --git a/command/acl/templatedpolicy/templated_policy.go b/command/acl/templatedpolicy/templated_policy.go new file mode 100644 index 0000000000..b6951a075c --- /dev/null +++ b/command/acl/templatedpolicy/templated_policy.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package templatedpolicy + +import ( + "github.com/hashicorp/consul/command/flags" + "github.com/mitchellh/cli" +) + +func New() *cmd { + return &cmd{} +} + +type cmd struct{} + +func (c *cmd) Run(args []string) int { + return cli.RunResultHelp +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(help, nil) +} + +const synopsis = "Manage Consul's ACL templated policies" +const help = ` +Usage: consul acl templated-policy [options] [args] + + This command has subcommands for managing Consul ACL templated policies. + Here are some simple examples, and more detailed examples are available + in the subcommands or the documentation. + + List all templated policies: + + $ consul acl templated-policy list + + Preview the policy rendered by the ACL templated policy: + + $ consul acl templated-policy preview -name "builtin/service" -var "name:api" + + Read a templated policy with name: + + $ consul acl templated-policy read -name "builtin/service" + + For more examples, ask for subcommand help or view the documentation. +` diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.json.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.json.golden new file mode 100644 index 0000000000..36682729f1 --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.json.golden @@ -0,0 +1,5 @@ +{ + "TemplateName": "builtin/dns", + "Schema": "", + "Template": "\nnode_prefix \"\" {\n\tpolicy = \"read\"\n}\nservice_prefix \"\" {\n\tpolicy = \"read\"\n}\nquery_prefix \"\" {\n\tpolicy = \"read\"\n}" +} \ No newline at end of file diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.pretty-meta.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.pretty-meta.golden new file mode 100644 index 0000000000..4b63841632 --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.pretty-meta.golden @@ -0,0 +1,15 @@ +Name: builtin/dns +Input variables: None +Example usage: + consul acl token create -templated-policy builtin/dns +Raw Template: + +node_prefix "" { + policy = "read" +} +service_prefix "" { + policy = "read" +} +query_prefix "" { + policy = "read" +} diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.pretty.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.pretty.golden new file mode 100644 index 0000000000..012987d039 --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/dns-templated-policy.pretty.golden @@ -0,0 +1,4 @@ +Name: builtin/dns +Input variables: None +Example usage: + consul acl token create -templated-policy builtin/dns diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.json.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.json.golden new file mode 100644 index 0000000000..2643a2b9ee --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.json.golden @@ -0,0 +1,5 @@ +{ + "TemplateName": "builtin/node", + "Schema": "\n{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"name\": { \"type\": \"string\", \"$ref\": \"#/definitions/min-length-one\" }\n\t},\n\t\"required\": [\"name\"],\n\t\"definitions\": {\n\t\t\"min-length-one\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"minLength\": 1\n\t\t}\n\t}\n}", + "Template": "\nnode \"{{.Name}}\" {\n\tpolicy = \"write\"\n}\nservice_prefix \"\" {\n\tpolicy = \"read\"\n}" +} \ No newline at end of file diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.pretty-meta.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.pretty-meta.golden new file mode 100644 index 0000000000..4547a282d7 --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.pretty-meta.golden @@ -0,0 +1,29 @@ +Name: builtin/node +Input variables: + Name: String - Required - The node name. +Example usage: + consul acl token create -templated-policy builtin/node -var name:node-1 +Schema: + +{ + "type": "object", + "properties": { + "name": { "type": "string", "$ref": "#/definitions/min-length-one" } + }, + "required": ["name"], + "definitions": { + "min-length-one": { + "type": "string", + "minLength": 1 + } + } +} + +Raw Template: + +node "{{.Name}}" { + policy = "write" +} +service_prefix "" { + policy = "read" +} diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.pretty.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.pretty.golden new file mode 100644 index 0000000000..be80ef625c --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/node-templated-policy.pretty.golden @@ -0,0 +1,5 @@ +Name: builtin/node +Input variables: + Name: String - Required - The node name. +Example usage: + consul acl token create -templated-policy builtin/node -var name:node-1 diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.json.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.json.golden new file mode 100644 index 0000000000..a23b5f8c7c --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.json.golden @@ -0,0 +1,5 @@ +{ + "TemplateName": "builtin/service", + "Schema": "\n{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"name\": { \"type\": \"string\", \"$ref\": \"#/definitions/min-length-one\" }\n\t},\n\t\"required\": [\"name\"],\n\t\"definitions\": {\n\t\t\"min-length-one\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"minLength\": 1\n\t\t}\n\t}\n}", + "Template": "\nservice \"{{.Name}}\" {\n\tpolicy = \"write\"\n}\nservice \"{{.Name}}-sidecar-proxy\" {\n\tpolicy = \"write\"\n}\nservice_prefix \"\" {\n\tpolicy = \"read\"\n}\nnode_prefix \"\" {\n\tpolicy = \"read\"\n}" +} \ No newline at end of file diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.pretty-meta.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.pretty-meta.golden new file mode 100644 index 0000000000..d551c2857d --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.pretty-meta.golden @@ -0,0 +1,35 @@ +Name: builtin/service +Input variables: + Name: String - Required - The name of the service. +Example usage: + consul acl token create -templated-policy builtin/service -var name:api +Schema: + +{ + "type": "object", + "properties": { + "name": { "type": "string", "$ref": "#/definitions/min-length-one" } + }, + "required": ["name"], + "definitions": { + "min-length-one": { + "type": "string", + "minLength": 1 + } + } +} + +Raw Template: + +service "{{.Name}}" { + policy = "write" +} +service "{{.Name}}-sidecar-proxy" { + policy = "write" +} +service_prefix "" { + policy = "read" +} +node_prefix "" { + policy = "read" +} diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.pretty.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.pretty.golden new file mode 100644 index 0000000000..ab10d27b25 --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicy/ce/service-templated-policy.pretty.golden @@ -0,0 +1,5 @@ +Name: builtin/service +Input variables: + Name: String - Required - The name of the service. +Example usage: + consul acl token create -templated-policy builtin/service -var name:api diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicyList/ce/list.json.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicyList/ce/list.json.golden new file mode 100644 index 0000000000..1cb724df8b --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicyList/ce/list.json.golden @@ -0,0 +1,17 @@ +{ + "builtin/dns": { + "TemplateName": "builtin/dns", + "Schema": "", + "Template": "\nnode_prefix \"\" {\n\tpolicy = \"read\"\n}\nservice_prefix \"\" {\n\tpolicy = \"read\"\n}\nquery_prefix \"\" {\n\tpolicy = \"read\"\n}" + }, + "builtin/node": { + "TemplateName": "builtin/node", + "Schema": "\n{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"name\": { \"type\": \"string\", \"$ref\": \"#/definitions/min-length-one\" }\n\t},\n\t\"required\": [\"name\"],\n\t\"definitions\": {\n\t\t\"min-length-one\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"minLength\": 1\n\t\t}\n\t}\n}", + "Template": "\nnode \"{{.Name}}\" {\n\tpolicy = \"write\"\n}\nservice_prefix \"\" {\n\tpolicy = \"read\"\n}" + }, + "builtin/service": { + "TemplateName": "builtin/service", + "Schema": "\n{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"name\": { \"type\": \"string\", \"$ref\": \"#/definitions/min-length-one\" }\n\t},\n\t\"required\": [\"name\"],\n\t\"definitions\": {\n\t\t\"min-length-one\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"minLength\": 1\n\t\t}\n\t}\n}", + "Template": "\nservice \"{{.Name}}\" {\n\tpolicy = \"write\"\n}\nservice \"{{.Name}}-sidecar-proxy\" {\n\tpolicy = \"write\"\n}\nservice_prefix \"\" {\n\tpolicy = \"read\"\n}\nnode_prefix \"\" {\n\tpolicy = \"read\"\n}" + } +} \ No newline at end of file diff --git a/command/acl/templatedpolicy/testdata/FormatTemplatedPolicyList/ce/list.pretty.golden b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicyList/ce/list.pretty.golden new file mode 100644 index 0000000000..e6080847d4 --- /dev/null +++ b/command/acl/templatedpolicy/testdata/FormatTemplatedPolicyList/ce/list.pretty.golden @@ -0,0 +1,3 @@ +builtin/dns +builtin/node +builtin/service diff --git a/command/registry.go b/command/registry.go index fceb708607..559189b6f9 100644 --- a/command/registry.go +++ b/command/registry.go @@ -36,6 +36,10 @@ import ( aclrlist "github.com/hashicorp/consul/command/acl/role/list" aclrread "github.com/hashicorp/consul/command/acl/role/read" aclrupdate "github.com/hashicorp/consul/command/acl/role/update" + acltp "github.com/hashicorp/consul/command/acl/templatedpolicy" + acltplist "github.com/hashicorp/consul/command/acl/templatedpolicy/list" + acltppreview "github.com/hashicorp/consul/command/acl/templatedpolicy/preview" + acltpread "github.com/hashicorp/consul/command/acl/templatedpolicy/read" acltoken "github.com/hashicorp/consul/command/acl/token" acltclone "github.com/hashicorp/consul/command/acl/token/clone" acltcreate "github.com/hashicorp/consul/command/acl/token/create" @@ -178,6 +182,10 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory { entry{"acl binding-rule read", func(ui cli.Ui) (cli.Command, error) { return aclbrread.New(ui), nil }}, entry{"acl binding-rule update", func(ui cli.Ui) (cli.Command, error) { return aclbrupdate.New(ui), nil }}, entry{"acl binding-rule delete", func(ui cli.Ui) (cli.Command, error) { return aclbrdelete.New(ui), nil }}, + entry{"acl templated-policy", func(cli.Ui) (cli.Command, error) { return acltp.New(), nil }}, + entry{"acl templated-policy list", func(ui cli.Ui) (cli.Command, error) { return acltplist.New(ui), nil }}, + entry{"acl templated-policy read", func(ui cli.Ui) (cli.Command, error) { return acltpread.New(ui), nil }}, + entry{"acl templated-policy preview", func(ui cli.Ui) (cli.Command, error) { return acltppreview.New(ui), nil }}, entry{"agent", func(ui cli.Ui) (cli.Command, error) { return agent.New(ui), nil }}, entry{"catalog", func(cli.Ui) (cli.Command, error) { return catalog.New(), nil }}, entry{"catalog datacenters", func(ui cli.Ui) (cli.Command, error) { return catlistdc.New(ui), nil }},