Skip to content

Commit

Permalink
[NET-5333] Add api to read/list and preview templated policies (#18748)
Browse files Browse the repository at this point in the history
  • Loading branch information
roncodingenthusiast authored Sep 11, 2023
1 parent b1688ad commit 9776c10
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 2 deletions.
147 changes: 147 additions & 0 deletions agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1133,3 +1133,150 @@ 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
}

var token string
s.parseToken(req, &token)

var entMeta acl.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}

s.defaultMetaPartitionToAgent(&entMeta)
var authzContext acl.AuthorizerContext
authz, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, &entMeta, &authzContext)
if err != nil {
return nil, err
}

// Only ACLRead privileges are required to list templated policies
if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return nil, err
}

templatedPolicies := make(map[string]ACLTemplatedPolicyResponse)

for tp, tmpBase := range structs.GetACLTemplatedPolicyList() {
templatedPolicies[tp] = ACLTemplatedPolicyResponse{
TemplateName: tmpBase.TemplateName,
Schema: tmpBase.Schema,
Template: tmpBase.Template,
}
}

return templatedPolicies, nil
}

func (s *HTTPHandlers) ACLTemplatedPolicyRead(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}

templateName := strings.TrimPrefix(req.URL.Path, "/v1/acl/templated-policy/name/")
if templateName == "" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing templated policy Name"}
}

var token string
s.parseToken(req, &token)

var entMeta acl.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}

s.defaultMetaPartitionToAgent(&entMeta)
var authzContext acl.AuthorizerContext
authz, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, &entMeta, &authzContext)
if err != nil {
return nil, err
}

// Only ACLRead privileges are required to read templated policies
if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return nil, err
}

baseTemplate, ok := structs.GetACLTemplatedPolicyBase(templateName)
if !ok {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Invalid templated policy Name: %s", templateName)}
}

return ACLTemplatedPolicyResponse{
TemplateName: baseTemplate.TemplateName,
Schema: baseTemplate.Schema,
Template: baseTemplate.Template,
}, nil
}

func (s *HTTPHandlers) ACLTemplatedPolicyPreview(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}

templateName := strings.TrimPrefix(req.URL.Path, "/v1/acl/templated-policy/preview/")
if templateName == "" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing templated policy Name"}
}

var token string
s.parseToken(req, &token)

var entMeta acl.EnterpriseMeta
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err
}

s.defaultMetaPartitionToAgent(&entMeta)
var authzContext acl.AuthorizerContext
authz, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, &entMeta, &authzContext)
if err != nil {
return nil, err
}

// Only ACLRead privileges are required to read/preview templated policies
if err := authz.ToAllowAuthorizer().ACLReadAllowed(&authzContext); err != nil {
return nil, err
}

baseTemplate, ok := structs.GetACLTemplatedPolicyBase(templateName)
if !ok {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("templated policy %q does not exist", templateName)}
}

var tpRequest structs.ACLTemplatedPolicyVariables

if err := decodeBody(req.Body, &tpRequest); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Failed to decode request body: %s", err.Error())}
}

templatedPolicy := structs.ACLTemplatedPolicy{
TemplateID: baseTemplate.TemplateID,
TemplateName: baseTemplate.TemplateName,
TemplateVariables: &tpRequest,
}

err = templatedPolicy.ValidateTemplatedPolicy(baseTemplate.Schema)
if err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("validation error for templated policy: %q: %s", templatedPolicy.TemplateName, err.Error())}
}

renderedPolicy, err := templatedPolicy.SyntheticPolicy(&entMeta)

if err != nil {
return nil, HTTPError{StatusCode: http.StatusInternalServerError, Reason: fmt.Sprintf("Failed to generate synthetic policy: %q: %s", templatedPolicy.TemplateName, err.Error())}
}

return renderedPolicy, nil
}
83 changes: 83 additions & 0 deletions agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
Expand Down Expand Up @@ -1361,6 +1362,88 @@ func TestACL_HTTP(t *testing.T) {
require.Equal(t, "sn1", token.ServiceIdentities[0].ServiceName)
})
})

t.Run("ACLTemplatedPolicy", func(t *testing.T) {
t.Run("List", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/templated-policies", nil)
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)

require.Equal(t, http.StatusOK, resp.Code)

var list map[string]ACLTemplatedPolicyResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&list))
require.Len(t, list, 3)

require.Equal(t, ACLTemplatedPolicyResponse{
TemplateName: api.ACLTemplatedPolicyServiceName,
Schema: structs.ACLTemplatedPolicyIdentitiesSchema,
Template: structs.ACLTemplatedPolicyService,
}, list[api.ACLTemplatedPolicyServiceName])
})
t.Run("Read", func(t *testing.T) {
t.Run("With non existing templated policy", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/templated-policy/name/fake", nil)
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusBadRequest, resp.Code)
})

t.Run("With existing templated policy", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/templated-policy/name/"+api.ACLTemplatedPolicyDNSName, nil)
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()

a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)

var templatedPolicy ACLTemplatedPolicyResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&templatedPolicy))
require.Equal(t, structs.ACLTemplatedPolicyDNSSchema, templatedPolicy.Schema)
require.Equal(t, api.ACLTemplatedPolicyDNSName, templatedPolicy.TemplateName)
require.Equal(t, structs.ACLTemplatedPolicyDNS, templatedPolicy.Template)
})
})
t.Run("preview", func(t *testing.T) {
t.Run("When missing required variables", func(t *testing.T) {
previewInput := &structs.ACLTemplatedPolicyVariables{}
req, _ := http.NewRequest(
"POST",
fmt.Sprintf("/v1/acl/templated-policy/preview/%s", api.ACLTemplatedPolicyServiceName),
jsonBody(previewInput),
)
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()

a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusBadRequest, resp.Code)
})

t.Run("Correct input", func(t *testing.T) {
previewInput := &structs.ACLTemplatedPolicyVariables{Name: "web"}
req, _ := http.NewRequest(
"POST",
fmt.Sprintf("/v1/acl/templated-policy/preview/%s", api.ACLTemplatedPolicyServiceName),
jsonBody(previewInput),
)
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()

a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)

var syntheticPolicy *structs.ACLPolicy
require.NoError(t, json.NewDecoder(resp.Body).Decode(&syntheticPolicy))

require.NotEmpty(t, syntheticPolicy.ID)
require.NotEmpty(t, syntheticPolicy.Hash)
require.Equal(t, "synthetic policy generated from templated policy: builtin/service", syntheticPolicy.Description)
require.Contains(t, syntheticPolicy.Name, "synthetic-policy-")
})
})
})
}

func TestACL_LoginProcedure_HTTP(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions agent/http_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func init() {
registerEndpoint("/v1/acl/token", []string{"PUT"}, (*HTTPHandlers).ACLTokenCreate)
registerEndpoint("/v1/acl/token/self", []string{"GET"}, (*HTTPHandlers).ACLTokenSelf)
registerEndpoint("/v1/acl/token/", []string{"GET", "PUT", "DELETE"}, (*HTTPHandlers).ACLTokenCRUD)
registerEndpoint("/v1/acl/templated-policies", []string{"GET"}, (*HTTPHandlers).ACLTemplatedPoliciesList)
registerEndpoint("/v1/acl/templated-policy/name/", []string{"GET"}, (*HTTPHandlers).ACLTemplatedPolicyRead)
registerEndpoint("/v1/acl/templated-policy/preview/", []string{"POST"}, (*HTTPHandlers).ACLTemplatedPolicyPreview)
registerEndpoint("/v1/agent/token/", []string{"PUT"}, (*HTTPHandlers).AgentToken)
registerEndpoint("/v1/agent/self", []string{"GET"}, (*HTTPHandlers).AgentSelf)
registerEndpoint("/v1/agent/host", []string{"GET"}, (*HTTPHandlers).AgentHost)
Expand Down
16 changes: 14 additions & 2 deletions agent/structs/acl_templated_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,19 @@ func (tps ACLTemplatedPolicies) Deduplicate() ACLTemplatedPolicies {
}

func GetACLTemplatedPolicyBase(templateName string) (*ACLTemplatedPolicyBase, bool) {
baseTemplate, found := aclTemplatedPoliciesList[templateName]
if orig, found := aclTemplatedPoliciesList[templateName]; found {
copy := *orig
return &copy, found
}

return nil, false
}

func GetACLTemplatedPolicyList() map[string]*ACLTemplatedPolicyBase {
m := make(map[string]*ACLTemplatedPolicyBase, len(aclTemplatedPoliciesList))
for k, v := range aclTemplatedPoliciesList {
m[k] = v
}

return baseTemplate, found
return m
}

0 comments on commit 9776c10

Please sign in to comment.