[NET-5333] Add api to read/list and preview templated policies (#18748)

This commit is contained in:
Ronald 2023-09-11 14:11:37 -04:00 committed by GitHub
parent b1688ad856
commit 9776c10efb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 247 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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