consul/agent/acl_endpoint.go
hashicorp-copywrite[bot] 5fb9df1640
[COMPLIANCE] License changes (#18443)
* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

* Update copyright file headers to BUSL-1.1

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 09:12:13 -04:00

1135 lines
34 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package agent
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
)
// aclCreateResponse is used to wrap the ACL ID
type aclBootstrapResponse struct {
ID string
structs.ACLToken
}
var aclDisabled = HTTPError{StatusCode: http.StatusUnauthorized, Reason: "ACL support disabled"}
// checkACLDisabled will return a standard response if ACLs are disabled. This
// returns true if they are disabled and we should not continue.
func (s *HTTPHandlers) checkACLDisabled() bool {
if s.agent.config.ACLsEnabled {
return false
}
return true
}
// ACLBootstrap is used to perform a one-time ACL bootstrap operation on
// a cluster to get the first management token.
func (s *HTTPHandlers) ACLBootstrap(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
args := structs.ACLInitialTokenBootstrapRequest{
Datacenter: s.agent.config.Datacenter,
}
// Handle optional request body
if req.ContentLength > 0 {
var bootstrapSecretRequest api.BootstrapRequest
if err := lib.DecodeJSON(req.Body, &bootstrapSecretRequest); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Request decoding failed: %v", err)}
}
args.BootstrapSecret = bootstrapSecretRequest.BootstrapSecret
}
var out structs.ACLToken
err := s.agent.RPC(req.Context(), "ACL.BootstrapTokens", &args, &out)
if err != nil {
if strings.Contains(err.Error(), structs.ACLBootstrapNotAllowedErr.Error()) {
return nil, acl.PermissionDeniedError{Cause: err.Error()}
} else {
return nil, err
}
}
return &aclBootstrapResponse{ID: out.SecretID, ACLToken: out}, nil
}
func (s *HTTPHandlers) ACLReplicationStatus(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
// Note that we do not forward to the ACL DC here. This is a query for
// any DC that's doing replication.
args := structs.DCSpecificRequest{}
s.parseSource(req, &args.Source)
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
// Make the request.
var out structs.ACLReplicationStatus
if err := s.agent.RPC(req.Context(), "ACL.ReplicationStatus", &args, &out); err != nil {
return nil, err
}
return out, nil
}
func (s *HTTPHandlers) ACLPolicyList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var args structs.ACLPolicyListRequest
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLPolicyListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.PolicyList", &args, &out); err != nil {
return nil, err
}
// make sure we return an array and not nil
if out.Policies == nil {
out.Policies = make(structs.ACLPolicyListStubs, 0)
}
return out.Policies, nil
}
func (s *HTTPHandlers) ACLPolicyCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var fn func(resp http.ResponseWriter, req *http.Request, policyID string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLPolicyReadByID
case "PUT":
fn = s.ACLPolicyWrite
case "DELETE":
fn = s.ACLPolicyDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
policyID := strings.TrimPrefix(req.URL.Path, "/v1/acl/policy/")
if policyID == "" && req.Method != "PUT" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing policy ID"}
}
return fn(resp, req, policyID)
}
func (s *HTTPHandlers) ACLPolicyRead(resp http.ResponseWriter, req *http.Request, policyID, policyName string) (interface{}, error) {
// policy name needs to be unescaped in case there were `/` characters
policyName, err := url.QueryUnescape(policyName)
if err != nil {
return nil, err
}
args := structs.ACLPolicyGetRequest{
Datacenter: s.agent.config.Datacenter,
PolicyID: policyID,
PolicyName: policyName,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLPolicyResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.PolicyRead", &args, &out); err != nil {
// should return permission denied error if missing permissions
return nil, err
}
if out.Policy == nil {
// if no error was returned above, the policy does not exist
resp.WriteHeader(http.StatusNotFound)
msg := acl.ACLResourceNotExistError("policy", args.EnterpriseMeta)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: msg.Error()}
}
return out.Policy, nil
}
func (s *HTTPHandlers) ACLPolicyReadByName(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
policyName := strings.TrimPrefix(req.URL.Path, "/v1/acl/policy/name/")
if policyName == "" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing policy Name"}
}
return s.ACLPolicyRead(resp, req, "", policyName)
}
func (s *HTTPHandlers) ACLPolicyReadByID(resp http.ResponseWriter, req *http.Request, policyID string) (interface{}, error) {
return s.ACLPolicyRead(resp, req, policyID, "")
}
func (s *HTTPHandlers) ACLPolicyCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
return s.aclPolicyWriteInternal(resp, req, "", true)
}
func (s *HTTPHandlers) ACLPolicyWrite(resp http.ResponseWriter, req *http.Request, policyID string) (interface{}, error) {
return s.aclPolicyWriteInternal(resp, req, policyID, false)
}
func (s *HTTPHandlers) aclPolicyWriteInternal(_resp http.ResponseWriter, req *http.Request, policyID string, create bool) (interface{}, error) {
args := structs.ACLPolicySetRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.Policy.EnterpriseMeta); err != nil {
return nil, err
}
if err := s.rewordUnknownEnterpriseFieldError(lib.DecodeJSON(req.Body, &args.Policy)); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Policy decoding failed: %v", err)}
}
if create {
if args.Policy.ID != "" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Cannot specify the ID when creating a new policy"}
}
} else {
if args.Policy.ID != "" && args.Policy.ID != policyID {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Policy ID in URL and payload do not match"}
} else if args.Policy.ID == "" {
args.Policy.ID = policyID
}
}
var out structs.ACLPolicy
if err := s.agent.RPC(req.Context(), "ACL.PolicySet", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPHandlers) ACLPolicyDelete(resp http.ResponseWriter, req *http.Request, policyID string) (interface{}, error) {
args := structs.ACLPolicyDeleteRequest{
Datacenter: s.agent.config.Datacenter,
PolicyID: policyID,
}
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
var ignored string
if err := s.agent.RPC(req.Context(), "ACL.PolicyDelete", args, &ignored); err != nil {
if strings.Contains(err.Error(), acl.ErrNotFound.Error()) {
resp.WriteHeader(http.StatusNotFound)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "Cannot find policy to delete"}
}
return nil, err
}
return true, nil
}
func (s *HTTPHandlers) ACLTokenList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
args := &structs.ACLTokenListRequest{
IncludeLocal: true,
IncludeGlobal: true,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
args.Policy = req.URL.Query().Get("policy")
args.Role = req.URL.Query().Get("role")
args.AuthMethod = req.URL.Query().Get("authmethod")
if err := parseACLAuthMethodEnterpriseMeta(req, &args.ACLAuthMethodEnterpriseMeta); err != nil {
return nil, err
}
var out structs.ACLTokenListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.TokenList", &args, &out); err != nil {
return nil, err
}
return out.Tokens, nil
}
func (s *HTTPHandlers) ACLTokenCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var fn func(resp http.ResponseWriter, req *http.Request, tokenAccessorID string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLTokenGet
case "PUT":
fn = s.ACLTokenSet
case "DELETE":
fn = s.ACLTokenDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
tokenAccessorID := strings.TrimPrefix(req.URL.Path, "/v1/acl/token/")
if strings.HasSuffix(tokenAccessorID, "/clone") && req.Method == "PUT" {
tokenAccessorID = tokenAccessorID[:len(tokenAccessorID)-6]
fn = s.ACLTokenClone
}
if tokenAccessorID == "" && req.Method != "PUT" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing token AccessorID"}
}
return fn(resp, req, tokenAccessorID)
}
func (s *HTTPHandlers) ACLTokenSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
args := structs.ACLTokenGetRequest{
TokenIDType: structs.ACLTokenSecret,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
// copy the token secret parameter to the ID
args.TokenID = args.Token
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLTokenResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.TokenRead", &args, &out); err != nil {
// should return permission denied error if missing permissions
return nil, err
}
if out.Token == nil {
// if no error was returned above, the token does not exist
resp.WriteHeader(http.StatusNotFound)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "Supplied token does not exist"}
}
return out.Token, nil
}
func (s *HTTPHandlers) ACLTokenCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
return s.aclTokenSetInternal(req, "", true)
}
func (s *HTTPHandlers) ACLTokenGet(resp http.ResponseWriter, req *http.Request, tokenAccessorID string) (interface{}, error) {
args := structs.ACLTokenGetRequest{
Datacenter: s.agent.config.Datacenter,
TokenID: tokenAccessorID,
TokenIDType: structs.ACLTokenAccessor,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if _, ok := req.URL.Query()["expanded"]; ok {
args.Expanded = true
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLTokenResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.TokenRead", &args, &out); err != nil {
return nil, err
}
if out.Token == nil {
// if no error was returned above, the token does not exist
resp.WriteHeader(http.StatusNotFound)
msg := acl.ACLResourceNotExistError("token", args.EnterpriseMeta)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: msg.Error()}
}
if args.Expanded {
expanded := &structs.ACLTokenExpanded{
ACLToken: out.Token,
ExpandedTokenInfo: out.ExpandedTokenInfo,
}
return expanded, nil
}
return out.Token, nil
}
func (s *HTTPHandlers) ACLTokenSet(_ http.ResponseWriter, req *http.Request, tokenAccessorID string) (interface{}, error) {
return s.aclTokenSetInternal(req, tokenAccessorID, false)
}
func (s *HTTPHandlers) aclTokenSetInternal(req *http.Request, tokenAccessorID string, create bool) (interface{}, error) {
args := structs.ACLTokenSetRequest{
Datacenter: s.agent.config.Datacenter,
Create: create,
}
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.ACLToken.EnterpriseMeta); err != nil {
return nil, err
}
if err := s.rewordUnknownEnterpriseFieldError(lib.DecodeJSON(req.Body, &args.ACLToken)); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Token decoding failed: %v", err)}
}
if !create {
// NOTE: AccessorID in the request body is optional when not creating a new token.
// If not present in the body and only in the URL then it will be filled in by Consul.
if args.ACLToken.AccessorID == "" {
args.ACLToken.AccessorID = tokenAccessorID
}
if args.ACLToken.AccessorID != tokenAccessorID {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Token Accessor ID in URL and payload do not match"}
}
}
var out structs.ACLToken
if err := s.agent.RPC(req.Context(), "ACL.TokenSet", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPHandlers) ACLTokenDelete(resp http.ResponseWriter, req *http.Request, tokenAccessorID string) (interface{}, error) {
args := structs.ACLTokenDeleteRequest{
Datacenter: s.agent.config.Datacenter,
TokenID: tokenAccessorID,
}
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
var ignored string
if err := s.agent.RPC(req.Context(), "ACL.TokenDelete", args, &ignored); err != nil {
if strings.Contains(err.Error(), acl.ErrNotFound.Error()) {
resp.WriteHeader(http.StatusNotFound)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "Cannot find token to delete"}
}
return nil, err
}
return true, nil
}
func (s *HTTPHandlers) ACLTokenClone(resp http.ResponseWriter, req *http.Request, tokenAccessorID string) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
args := structs.ACLTokenSetRequest{
Datacenter: s.agent.config.Datacenter,
Create: true,
}
if err := s.parseEntMeta(req, &args.ACLToken.EnterpriseMeta); err != nil {
return nil, err
}
if err := s.rewordUnknownEnterpriseFieldError(lib.DecodeJSON(req.Body, &args.ACLToken)); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Token decoding failed: %v", err)}
}
s.parseToken(req, &args.Token)
// Set this for the ID to clone
args.ACLToken.AccessorID = tokenAccessorID
var out structs.ACLToken
if err := s.agent.RPC(req.Context(), "ACL.TokenClone", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPHandlers) ACLRoleList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var args structs.ACLRoleListRequest
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
args.Policy = req.URL.Query().Get("policy")
var out structs.ACLRoleListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.RoleList", &args, &out); err != nil {
return nil, err
}
// make sure we return an array and not nil
if out.Roles == nil {
out.Roles = make(structs.ACLRoles, 0)
}
return out.Roles, nil
}
func (s *HTTPHandlers) ACLRoleCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var fn func(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLRoleReadByID
case "PUT":
fn = s.ACLRoleWrite
case "DELETE":
fn = s.ACLRoleDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
roleID := strings.TrimPrefix(req.URL.Path, "/v1/acl/role/")
if roleID == "" && req.Method != "PUT" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing role ID"}
}
return fn(resp, req, roleID)
}
func (s *HTTPHandlers) ACLRoleReadByName(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
roleName := strings.TrimPrefix(req.URL.Path, "/v1/acl/role/name/")
if roleName == "" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing role Name"}
}
return s.ACLRoleRead(resp, req, "", roleName)
}
func (s *HTTPHandlers) ACLRoleReadByID(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
return s.ACLRoleRead(resp, req, roleID, "")
}
func (s *HTTPHandlers) ACLRoleRead(resp http.ResponseWriter, req *http.Request, roleID, roleName string) (interface{}, error) {
args := structs.ACLRoleGetRequest{
Datacenter: s.agent.config.Datacenter,
RoleID: roleID,
RoleName: roleName,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLRoleResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.RoleRead", &args, &out); err != nil {
// should return permission denied error if missing permissions
return nil, err
}
if out.Role == nil {
// if not permission denied error is returned above, role does not exist
resp.WriteHeader(http.StatusNotFound)
msg := acl.ACLResourceNotExistError("role", args.EnterpriseMeta)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: msg.Error()}
}
return out.Role, nil
}
func (s *HTTPHandlers) ACLRoleCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
return s.ACLRoleWrite(resp, req, "")
}
func (s *HTTPHandlers) ACLRoleWrite(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
args := structs.ACLRoleSetRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.Role.EnterpriseMeta); err != nil {
return nil, err
}
if err := s.rewordUnknownEnterpriseFieldError(lib.DecodeJSON(req.Body, &args.Role)); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Role decoding failed: %v", err)}
}
if args.Role.ID != "" && args.Role.ID != roleID {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Role ID in URL and payload do not match"}
} else if args.Role.ID == "" {
args.Role.ID = roleID
}
var out structs.ACLRole
if err := s.agent.RPC(req.Context(), "ACL.RoleSet", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPHandlers) ACLRoleDelete(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
args := structs.ACLRoleDeleteRequest{
Datacenter: s.agent.config.Datacenter,
RoleID: roleID,
}
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
var ignored string
if err := s.agent.RPC(req.Context(), "ACL.RoleDelete", args, &ignored); err != nil {
if strings.Contains(err.Error(), acl.ErrNotFound.Error()) {
resp.WriteHeader(http.StatusNotFound)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "Cannot find role to delete"}
}
return nil, err
}
return true, nil
}
func (s *HTTPHandlers) ACLBindingRuleList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var args structs.ACLBindingRuleListRequest
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
args.AuthMethod = req.URL.Query().Get("authmethod")
var out structs.ACLBindingRuleListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.BindingRuleList", &args, &out); err != nil {
return nil, err
}
// make sure we return an array and not nil
if out.BindingRules == nil {
out.BindingRules = make(structs.ACLBindingRules, 0)
}
return out.BindingRules, nil
}
func (s *HTTPHandlers) ACLBindingRuleCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var fn func(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLBindingRuleRead
case "PUT":
fn = s.ACLBindingRuleWrite
case "DELETE":
fn = s.ACLBindingRuleDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
bindingRuleID := strings.TrimPrefix(req.URL.Path, "/v1/acl/binding-rule/")
if bindingRuleID == "" && req.Method != "PUT" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing binding rule ID"}
}
return fn(resp, req, bindingRuleID)
}
func (s *HTTPHandlers) ACLBindingRuleRead(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
args := structs.ACLBindingRuleGetRequest{
Datacenter: s.agent.config.Datacenter,
BindingRuleID: bindingRuleID,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLBindingRuleResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.BindingRuleRead", &args, &out); err != nil {
// should return permission denied error if missing permissions
return nil, err
}
if out.BindingRule == nil {
// if no error was returned above, the binding rule does not exist
resp.WriteHeader(http.StatusNotFound)
msg := acl.ACLResourceNotExistError("binding rule", args.EnterpriseMeta)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: msg.Error()}
}
return out.BindingRule, nil
}
func (s *HTTPHandlers) ACLBindingRuleCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
return s.ACLBindingRuleWrite(resp, req, "")
}
func (s *HTTPHandlers) ACLBindingRuleWrite(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
args := structs.ACLBindingRuleSetRequest{}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.BindingRule.EnterpriseMeta); err != nil {
return nil, err
}
if err := s.rewordUnknownEnterpriseFieldError(lib.DecodeJSON(req.Body, &args.BindingRule)); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("BindingRule decoding failed: %v", err)}
}
if args.BindingRule.ID != "" && args.BindingRule.ID != bindingRuleID {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "BindingRule ID in URL and payload do not match"}
} else if args.BindingRule.ID == "" {
args.BindingRule.ID = bindingRuleID
}
var out structs.ACLBindingRule
if err := s.agent.RPC(req.Context(), "ACL.BindingRuleSet", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPHandlers) ACLBindingRuleDelete(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
args := structs.ACLBindingRuleDeleteRequest{
BindingRuleID: bindingRuleID,
}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
var ignored bool
if err := s.agent.RPC(req.Context(), "ACL.BindingRuleDelete", args, &ignored); err != nil {
if strings.Contains(err.Error(), acl.ErrNotFound.Error()) {
resp.WriteHeader(http.StatusNotFound)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "Cannot find binding rule to delete"}
}
return nil, err
}
return true, nil
}
func (s *HTTPHandlers) ACLAuthMethodList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var args structs.ACLAuthMethodListRequest
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLAuthMethodListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.AuthMethodList", &args, &out); err != nil {
return nil, err
}
// make sure we return an array and not nil
if out.AuthMethods == nil {
out.AuthMethods = make(structs.ACLAuthMethodListStubs, 0)
}
return out.AuthMethods, nil
}
func (s *HTTPHandlers) ACLAuthMethodCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
var fn func(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLAuthMethodRead
case "PUT":
fn = s.ACLAuthMethodWrite
case "DELETE":
fn = s.ACLAuthMethodDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
methodName := strings.TrimPrefix(req.URL.Path, "/v1/acl/auth-method/")
if methodName == "" && req.Method != "PUT" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing auth method name"}
}
return fn(resp, req, methodName)
}
func (s *HTTPHandlers) ACLAuthMethodRead(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
args := structs.ACLAuthMethodGetRequest{
Datacenter: s.agent.config.Datacenter,
AuthMethodName: methodName,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLAuthMethodResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC(req.Context(), "ACL.AuthMethodRead", &args, &out); err != nil {
// should return permission denied if missing permissions
return nil, err
}
if out.AuthMethod == nil {
// if no error was returned above, the auth method does not exist
resp.WriteHeader(http.StatusNotFound)
msg := acl.ACLResourceNotExistError("auth method", args.EnterpriseMeta)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: msg.Error()}
}
fixupAuthMethodConfig(out.AuthMethod)
return out.AuthMethod, nil
}
func (s *HTTPHandlers) ACLAuthMethodCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
return s.ACLAuthMethodWrite(resp, req, "")
}
func (s *HTTPHandlers) ACLAuthMethodWrite(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
args := structs.ACLAuthMethodSetRequest{}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.AuthMethod.EnterpriseMeta); err != nil {
return nil, err
}
if err := s.rewordUnknownEnterpriseFieldError(lib.DecodeJSON(req.Body, &args.AuthMethod)); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("AuthMethod decoding failed: %v", err)}
}
if methodName != "" {
if args.AuthMethod.Name != "" && args.AuthMethod.Name != methodName {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "AuthMethod Name in URL and payload do not match"}
} else if args.AuthMethod.Name == "" {
args.AuthMethod.Name = methodName
}
}
var out structs.ACLAuthMethod
if err := s.agent.RPC(req.Context(), "ACL.AuthMethodSet", args, &out); err != nil {
return nil, err
}
fixupAuthMethodConfig(&out)
return &out, nil
}
func (s *HTTPHandlers) ACLAuthMethodDelete(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
args := structs.ACLAuthMethodDeleteRequest{
AuthMethodName: methodName,
}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
var ignored bool
if err := s.agent.RPC(req.Context(), "ACL.AuthMethodDelete", args, &ignored); err != nil {
if strings.Contains(err.Error(), acl.ErrNotFound.Error()) {
resp.WriteHeader(http.StatusNotFound)
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "Cannot find auth method to delete"}
}
return nil, err
}
return true, nil
}
func (s *HTTPHandlers) ACLLogin(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
args := &structs.ACLLoginRequest{
Datacenter: s.agent.config.Datacenter,
Auth: &structs.ACLLoginParams{},
}
s.parseDC(req, &args.Datacenter)
if err := s.parseEntMeta(req, &args.Auth.EnterpriseMeta); err != nil {
return nil, err
}
if err := s.rewordUnknownEnterpriseFieldError(lib.DecodeJSON(req.Body, &args.Auth)); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Failed to decode request body: %v", err)}
}
var out structs.ACLToken
if err := s.agent.RPC(req.Context(), "ACL.Login", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPHandlers) ACLLogout(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled() {
return nil, aclDisabled
}
args := structs.ACLLogoutRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
if args.Token == "" {
return nil, HTTPError{StatusCode: http.StatusUnauthorized, Reason: "Supplied token does not exist"}
}
var ignored bool
if err := s.agent.RPC(req.Context(), "ACL.Logout", &args, &ignored); err != nil {
return nil, err
}
return true, nil
}
// A hack to fix up the config types inside of the map[string]interface{}
// so that they get formatted correctly during json.Marshal. Without this,
// string values that get converted to []uint8 end up getting output back
// to the user in base64-encoded form.
func fixupAuthMethodConfig(method *structs.ACLAuthMethod) {
for k, v := range method.Config {
if raw, ok := v.([]uint8); ok {
strVal := structs.Uint8ToString(raw)
method.Config[k] = strVal
}
}
}
func (s *HTTPHandlers) ACLAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// At first glance it may appear like this endpoint is going to leak security relevant information.
// There are a number of reason why this is okay.
//
// 1. The authorizations performed here are the same as what would be done if other HTTP APIs
// were used. This is just a way to see if it would be allowed. These authorization checks
// will be logged along with those from the real endpoints. In that respect, you can figure
// out if you have access just as easily by attempting to perform the requested operation.
// 2. In order to use this API you must have a valid ACL token secret.
// 3. Along with #2 you can use the ACL.GetPolicy RPC endpoint which will return a rolled up
// set of policy rules showing your tokens effective policy. This RPC endpoint exposes
// more information than this one and has been around since before v1.0.0. With that other
// endpoint you get to see all things possible rather than having to have a list of things
// you may want to do and to request authorizations for each one.
// 4. In addition to the legacy ACL.GetPolicy RPC endpoint we have an ACL.PolicyResolve and
// ACL.RoleResolve endpoints. These RPC endpoints allow reading roles and policies so long
// as the token used for the request is linked with them. This is needed to allow client
// agents to pull the policy and roles for a token that they are resolving. The only
// alternative to this style of access would be to make every agent use a token
// with acl:read privileges for all policy and role resolution requests. Once you have
// all the associated policies and roles it would be easy enough to recreate the effective
// policy.
const maxRequests = 64
if s.checkACLDisabled() {
return nil, aclDisabled
}
request := structs.RemoteACLAuthorizationRequest{
Datacenter: s.agent.config.Datacenter,
QueryOptions: structs.QueryOptions{
AllowStale: true,
RequireConsistent: false,
},
}
var responses []structs.ACLAuthorizationResponse
s.parseToken(req, &request.Token)
s.parseDC(req, &request.Datacenter)
if err := decodeBody(req.Body, &request.Requests); err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Failed to decode request body: %v", err)}
}
if len(request.Requests) > maxRequests {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Refusing to process more than %d authorizations at once", maxRequests)}
}
if len(request.Requests) == 0 {
return make([]structs.ACLAuthorizationResponse, 0), nil
}
if request.Datacenter != "" && request.Datacenter != s.agent.config.Datacenter {
// when we are targeting a datacenter other than our own then we must issue an RPC
// to perform the resolution as it may involve a local token
if err := s.agent.RPC(req.Context(), "ACL.Authorize", &request, &responses); err != nil {
return nil, err
}
} else {
authz, err := s.agent.delegate.ResolveTokenAndDefaultMeta(request.Token, nil, nil)
if err != nil {
return nil, err
}
responses, err = structs.CreateACLAuthorizationResponses(authz, request.Requests)
if err != nil {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: err.Error()}
}
}
if responses == nil {
responses = make([]structs.ACLAuthorizationResponse, 0)
}
return responses, nil
}