diff --git a/agent/acl_endpoint.go b/agent/acl_endpoint.go index afef4330e4..b98784e346 100644 --- a/agent/acl_endpoint.go +++ b/agent/acl_endpoint.go @@ -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 +} diff --git a/agent/acl_endpoint_test.go b/agent/acl_endpoint_test.go index a1aaba52a5..d8860931a4 100644 --- a/agent/acl_endpoint_test.go +++ b/agent/acl_endpoint_test.go @@ -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) { diff --git a/agent/http_register.go b/agent/http_register.go index bc2551ec00..340f7ef0fe 100644 --- a/agent/http_register.go +++ b/agent/http_register.go @@ -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) diff --git a/agent/structs/acl_templated_policy.go b/agent/structs/acl_templated_policy.go index 3cef447974..4e6fab5778 100644 --- a/agent/structs/acl_templated_policy.go +++ b/agent/structs/acl_templated_policy.go @@ -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 ©, 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 }