Skip to content

Commit

Permalink
[NET-5334] Added CLI commands for templated policies (#18816)
Browse files Browse the repository at this point in the history
  • Loading branch information
roncodingenthusiast authored Sep 14, 2023
1 parent 8021226 commit 1afeb6e
Show file tree
Hide file tree
Showing 27 changed files with 1,352 additions and 25 deletions.
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

0 comments on commit 1afeb6e

Please sign in to comment.