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-5333] Add api to read/list and preview templated policies #18748

Merged
merged 1 commit into from
Sep 11, 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
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
}