Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/18816.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
cli: Add `consul acl templated-policy` commands to read, list and preview templated policies.
```
12 changes: 3 additions & 9 deletions agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
28 changes: 15 additions & 13 deletions agent/structs/acl_templated_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
81 changes: 81 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
132 changes: 132 additions & 0 deletions command/acl/templatedpolicy/formatter.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions command/acl/templatedpolicy/formatter_ce_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
Loading