[NET-5334] Added CLI commands for templated policies (#18816)

This commit is contained in:
Ronald 2023-09-14 16:14:55 -04:00 committed by GitHub
parent 802122640b
commit 1afeb6e040
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1352 additions and 25 deletions

3
.changelog/18816.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
cli: Add `consul acl templated-policy` commands to read, list and preview templated policies.
```

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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)
})
}
}

View File

@ -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
`
)

View File

@ -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)
}

View File

@ -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
`
)

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -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)
})
}

View File

@ -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 <subcommand> [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.
`

View File

@ -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}"
}

View File

@ -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"
}

View File

@ -0,0 +1,4 @@
Name: builtin/dns
Input variables: None
Example usage:
consul acl token create -templated-policy builtin/dns

View File

@ -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}"
}

View File

@ -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"
}

View File

@ -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

View File

@ -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}"
}

View File

@ -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"
}

View File

@ -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

View File

@ -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}"
}
}

View File

@ -0,0 +1,3 @@
builtin/dns
builtin/node
builtin/service

View File

@ -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 }},