f9a43a1e2d
* ACL Authorizer overhaul To account for upcoming features every Authorization function can now take an extra *acl.EnterpriseAuthorizerContext. These are unused in OSS and will always be nil. Additionally the acl package has received some thorough refactoring to enable all of the extra Consul Enterprise specific authorizations including moving sentinel enforcement into the stubbed structs. The Authorizer funcs now return an acl.EnforcementDecision instead of a boolean. This improves the overall interface as it makes multiple Authorizers easily chainable as they now indicate whether they had an authoritative decision or should use some other defaults. A ChainedAuthorizer was added to handle this Authorizer enforcement chain and will never itself return a non-authoritative decision. * Include stub for extra enterprise rules in the global management policy * Allow for an upgrade of the global-management policy
1050 lines
27 KiB
Go
1050 lines
27 KiB
Go
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
)
|
|
|
|
// aclCreateResponse is used to wrap the ACL ID
|
|
type aclBootstrapResponse struct {
|
|
ID string
|
|
structs.ACLToken
|
|
}
|
|
|
|
// checkACLDisabled will return a standard response if ACLs are disabled. This
|
|
// returns true if they are disabled and we should not continue.
|
|
func (s *HTTPServer) checkACLDisabled(resp http.ResponseWriter, req *http.Request) bool {
|
|
if s.agent.delegate.ACLsEnabled() {
|
|
return false
|
|
}
|
|
|
|
resp.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprint(resp, "ACL support disabled")
|
|
return true
|
|
}
|
|
|
|
// ACLBootstrap is used to perform a one-time ACL bootstrap operation on
|
|
// a cluster to get the first management token.
|
|
func (s *HTTPServer) ACLBootstrap(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
args := structs.DCSpecificRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
}
|
|
|
|
legacy := false
|
|
legacyStr := req.URL.Query().Get("legacy")
|
|
if legacyStr != "" {
|
|
legacy, _ = strconv.ParseBool(legacyStr)
|
|
}
|
|
|
|
if legacy && s.agent.delegate.UseLegacyACLs() {
|
|
var out structs.ACL
|
|
err := s.agent.RPC("ACL.Bootstrap", &args, &out)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), structs.ACLBootstrapNotAllowedErr.Error()) {
|
|
resp.WriteHeader(http.StatusForbidden)
|
|
fmt.Fprint(resp, acl.PermissionDeniedError{Cause: err.Error()}.Error())
|
|
return nil, nil
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &aclBootstrapResponse{ID: out.ID}, nil
|
|
} else {
|
|
var out structs.ACLToken
|
|
err := s.agent.RPC("ACL.BootstrapTokens", &args, &out)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), structs.ACLBootstrapNotAllowedErr.Error()) {
|
|
resp.WriteHeader(http.StatusForbidden)
|
|
fmt.Fprint(resp, acl.PermissionDeniedError{Cause: err.Error()}.Error())
|
|
return nil, nil
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &aclBootstrapResponse{ID: out.SecretID, ACLToken: out}, nil
|
|
}
|
|
}
|
|
|
|
func (s *HTTPServer) ACLReplicationStatus(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
// 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("ACL.ReplicationStatus", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLRulesTranslate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
var token string
|
|
s.parseToken(req, &token)
|
|
rule, err := s.agent.resolveToken(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Should this require lesser permissions? Really the only reason to require authorization at all is
|
|
// to prevent external entities from DoS Consul with repeated rule translation requests
|
|
// TODO (namespaces) - pass through a real ent authz ctx
|
|
if rule != nil && rule.ACLRead(nil) != acl.Allow {
|
|
return nil, acl.ErrPermissionDenied
|
|
}
|
|
|
|
policyBytes, err := ioutil.ReadAll(req.Body)
|
|
if err != nil {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("Failed to read body: %v", err)}
|
|
}
|
|
|
|
translated, err := acl.TranslateLegacyRules(policyBytes)
|
|
if err != nil {
|
|
return nil, BadRequestError{Reason: err.Error()}
|
|
}
|
|
|
|
resp.Write(translated)
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLRulesTranslateLegacyToken(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
tokenID := strings.TrimPrefix(req.URL.Path, "/v1/acl/rules/translate/")
|
|
if tokenID == "" {
|
|
return nil, BadRequestError{Reason: "Missing token ID"}
|
|
}
|
|
|
|
args := structs.ACLTokenGetRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
TokenID: tokenID,
|
|
TokenIDType: structs.ACLTokenAccessor,
|
|
}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
// Do not allow blocking
|
|
args.QueryOptions.MinQueryIndex = 0
|
|
|
|
var out structs.ACLTokenResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("ACL.TokenRead", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.Token == nil {
|
|
return nil, acl.ErrNotFound
|
|
}
|
|
|
|
if out.Token.Rules == "" {
|
|
return nil, fmt.Errorf("The specified token does not have any rules set")
|
|
}
|
|
|
|
translated, err := acl.TranslateLegacyRules([]byte(out.Token.Rules))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse legacy rules: %v", err)
|
|
}
|
|
|
|
resp.Write(translated)
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLPolicyList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
var args structs.ACLPolicyListRequest
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
var out structs.ACLPolicyListResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("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 *HTTPServer) ACLPolicyCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
var fn func(resp http.ResponseWriter, req *http.Request, policyID string) (interface{}, error)
|
|
|
|
switch req.Method {
|
|
case "GET":
|
|
fn = s.ACLPolicyRead
|
|
|
|
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, BadRequestError{Reason: "Missing policy ID"}
|
|
}
|
|
|
|
return fn(resp, req, policyID)
|
|
}
|
|
|
|
func (s *HTTPServer) ACLPolicyRead(resp http.ResponseWriter, req *http.Request, policyID string) (interface{}, error) {
|
|
args := structs.ACLPolicyGetRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
PolicyID: policyID,
|
|
}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
var out structs.ACLPolicyResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("ACL.PolicyRead", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.Policy == nil {
|
|
// TODO(rb): should this return a normal 404?
|
|
return nil, acl.ErrNotFound
|
|
}
|
|
|
|
return out.Policy, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLPolicyCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
return s.aclPolicyWriteInternal(resp, req, "", true)
|
|
}
|
|
|
|
// fixTimeAndHashFields is used to help in decoding the ExpirationTTL, ExpirationTime, CreateTime, and Hash
|
|
// attributes from the ACL Token/Policy create/update requests. It is needed
|
|
// to help mapstructure decode things properly when decodeBody is used.
|
|
func fixTimeAndHashFields(raw interface{}) error {
|
|
rawMap, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if val, ok := rawMap["ExpirationTTL"]; ok {
|
|
if sval, ok := val.(string); ok {
|
|
d, err := time.ParseDuration(sval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rawMap["ExpirationTTL"] = d
|
|
}
|
|
}
|
|
|
|
if val, ok := rawMap["ExpirationTime"]; ok {
|
|
if sval, ok := val.(string); ok {
|
|
t, err := time.Parse(time.RFC3339, sval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rawMap["ExpirationTime"] = t
|
|
}
|
|
}
|
|
|
|
if val, ok := rawMap["CreateTime"]; ok {
|
|
if sval, ok := val.(string); ok {
|
|
t, err := time.Parse(time.RFC3339, sval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rawMap["CreateTime"] = t
|
|
}
|
|
}
|
|
|
|
if val, ok := rawMap["Hash"]; ok {
|
|
if sval, ok := val.(string); ok {
|
|
rawMap["Hash"] = []byte(sval)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLPolicyWrite(resp http.ResponseWriter, req *http.Request, policyID string) (interface{}, error) {
|
|
return s.aclPolicyWriteInternal(resp, req, policyID, false)
|
|
}
|
|
|
|
func (s *HTTPServer) 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 := decodeBody(req, &args.Policy, fixTimeAndHashFields); err != nil {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("Policy decoding failed: %v", err)}
|
|
}
|
|
|
|
args.Policy.Syntax = acl.SyntaxCurrent
|
|
|
|
if create {
|
|
if args.Policy.ID != "" {
|
|
return nil, BadRequestError{Reason: "Cannot specify the ID when creating a new policy"}
|
|
}
|
|
} else {
|
|
if args.Policy.ID != "" && args.Policy.ID != policyID {
|
|
return nil, BadRequestError{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("ACL.PolicySet", args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) 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)
|
|
|
|
var ignored string
|
|
if err := s.agent.RPC("ACL.PolicyDelete", args, &ignored); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
args := &structs.ACLTokenListRequest{
|
|
IncludeLocal: true,
|
|
IncludeGlobal: true,
|
|
}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
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")
|
|
|
|
var out structs.ACLTokenListResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("ACL.TokenList", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return out.Tokens, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
var fn func(resp http.ResponseWriter, req *http.Request, tokenID 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"}}
|
|
}
|
|
|
|
tokenID := strings.TrimPrefix(req.URL.Path, "/v1/acl/token/")
|
|
if strings.HasSuffix(tokenID, "/clone") && req.Method == "PUT" {
|
|
tokenID = tokenID[:len(tokenID)-6]
|
|
fn = s.ACLTokenClone
|
|
}
|
|
if tokenID == "" && req.Method != "PUT" {
|
|
return nil, BadRequestError{Reason: "Missing token ID"}
|
|
}
|
|
|
|
return fn(resp, req, tokenID)
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
args := structs.ACLTokenGetRequest{
|
|
TokenIDType: structs.ACLTokenSecret,
|
|
}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
// copy the token 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("ACL.TokenRead", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.Token == nil {
|
|
return nil, acl.ErrNotFound
|
|
}
|
|
|
|
return out.Token, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
return s.aclTokenSetInternal(resp, req, "", true)
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenGet(resp http.ResponseWriter, req *http.Request, tokenID string) (interface{}, error) {
|
|
args := structs.ACLTokenGetRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
TokenID: tokenID,
|
|
TokenIDType: structs.ACLTokenAccessor,
|
|
}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
var out structs.ACLTokenResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("ACL.TokenRead", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.Token == nil {
|
|
return nil, acl.ErrNotFound
|
|
}
|
|
|
|
return out.Token, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenSet(resp http.ResponseWriter, req *http.Request, tokenID string) (interface{}, error) {
|
|
return s.aclTokenSetInternal(resp, req, tokenID, false)
|
|
}
|
|
|
|
func (s *HTTPServer) aclTokenSetInternal(resp http.ResponseWriter, req *http.Request, tokenID string, create bool) (interface{}, error) {
|
|
args := structs.ACLTokenSetRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
Create: create,
|
|
}
|
|
s.parseToken(req, &args.Token)
|
|
|
|
if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)}
|
|
}
|
|
|
|
if !create {
|
|
if args.ACLToken.AccessorID != "" && args.ACLToken.AccessorID != tokenID {
|
|
return nil, BadRequestError{Reason: "Token Accessor ID in URL and payload do not match"}
|
|
} else if args.ACLToken.AccessorID == "" {
|
|
args.ACLToken.AccessorID = tokenID
|
|
}
|
|
}
|
|
|
|
var out structs.ACLToken
|
|
if err := s.agent.RPC("ACL.TokenSet", args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenDelete(resp http.ResponseWriter, req *http.Request, tokenID string) (interface{}, error) {
|
|
args := structs.ACLTokenDeleteRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
TokenID: tokenID,
|
|
}
|
|
s.parseToken(req, &args.Token)
|
|
|
|
var ignored string
|
|
if err := s.agent.RPC("ACL.TokenDelete", args, &ignored); err != nil {
|
|
return nil, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLTokenClone(resp http.ResponseWriter, req *http.Request, tokenID string) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
args := structs.ACLTokenSetRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
Create: true,
|
|
}
|
|
|
|
if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil && err.Error() != "EOF" {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)}
|
|
}
|
|
s.parseToken(req, &args.Token)
|
|
|
|
// Set this for the ID to clone
|
|
args.ACLToken.AccessorID = tokenID
|
|
|
|
var out structs.ACLToken
|
|
if err := s.agent.RPC("ACL.TokenClone", args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLRoleList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
var args structs.ACLRoleListRequest
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
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("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 *HTTPServer) ACLRoleCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
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, BadRequestError{Reason: "Missing role ID"}
|
|
}
|
|
|
|
return fn(resp, req, roleID)
|
|
}
|
|
|
|
func (s *HTTPServer) ACLRoleReadByName(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
roleName := strings.TrimPrefix(req.URL.Path, "/v1/acl/role/name/")
|
|
if roleName == "" {
|
|
return nil, BadRequestError{Reason: "Missing role Name"}
|
|
}
|
|
|
|
return s.ACLRoleRead(resp, req, "", roleName)
|
|
}
|
|
|
|
func (s *HTTPServer) ACLRoleReadByID(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
|
return s.ACLRoleRead(resp, req, roleID, "")
|
|
}
|
|
|
|
func (s *HTTPServer) 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 args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
var out structs.ACLRoleResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("ACL.RoleRead", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.Role == nil {
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
return nil, nil
|
|
}
|
|
|
|
return out.Role, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLRoleCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
return s.ACLRoleWrite(resp, req, "")
|
|
}
|
|
|
|
func (s *HTTPServer) 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 := decodeBody(req, &args.Role, fixTimeAndHashFields); err != nil {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("Role decoding failed: %v", err)}
|
|
}
|
|
|
|
if args.Role.ID != "" && args.Role.ID != roleID {
|
|
return nil, BadRequestError{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("ACL.RoleSet", args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) 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)
|
|
|
|
var ignored string
|
|
if err := s.agent.RPC("ACL.RoleDelete", args, &ignored); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLBindingRuleList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
var args structs.ACLBindingRuleListRequest
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
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("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 *HTTPServer) ACLBindingRuleCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
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, BadRequestError{Reason: "Missing binding rule ID"}
|
|
}
|
|
|
|
return fn(resp, req, bindingRuleID)
|
|
}
|
|
|
|
func (s *HTTPServer) 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 args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
var out structs.ACLBindingRuleResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("ACL.BindingRuleRead", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.BindingRule == nil {
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
return nil, nil
|
|
}
|
|
|
|
return out.BindingRule, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLBindingRuleCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
return s.ACLBindingRuleWrite(resp, req, "")
|
|
}
|
|
|
|
func (s *HTTPServer) ACLBindingRuleWrite(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
|
|
args := structs.ACLBindingRuleSetRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
}
|
|
s.parseToken(req, &args.Token)
|
|
|
|
if err := decodeBody(req, &args.BindingRule, fixTimeAndHashFields); err != nil {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("BindingRule decoding failed: %v", err)}
|
|
}
|
|
|
|
if args.BindingRule.ID != "" && args.BindingRule.ID != bindingRuleID {
|
|
return nil, BadRequestError{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("ACL.BindingRuleSet", args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLBindingRuleDelete(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
|
|
args := structs.ACLBindingRuleDeleteRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
BindingRuleID: bindingRuleID,
|
|
}
|
|
s.parseToken(req, &args.Token)
|
|
|
|
var ignored bool
|
|
if err := s.agent.RPC("ACL.BindingRuleDelete", args, &ignored); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLAuthMethodList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
var args structs.ACLAuthMethodListRequest
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
var out structs.ACLAuthMethodListResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("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 *HTTPServer) ACLAuthMethodCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
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, BadRequestError{Reason: "Missing auth method name"}
|
|
}
|
|
|
|
return fn(resp, req, methodName)
|
|
}
|
|
|
|
func (s *HTTPServer) 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 args.Datacenter == "" {
|
|
args.Datacenter = s.agent.config.Datacenter
|
|
}
|
|
|
|
var out structs.ACLAuthMethodResponse
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
if err := s.agent.RPC("ACL.AuthMethodRead", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out.AuthMethod == nil {
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
return nil, nil
|
|
}
|
|
|
|
fixupAuthMethodConfig(out.AuthMethod)
|
|
return out.AuthMethod, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLAuthMethodCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
return s.ACLAuthMethodWrite(resp, req, "")
|
|
}
|
|
|
|
func (s *HTTPServer) ACLAuthMethodWrite(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
|
|
args := structs.ACLAuthMethodSetRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
}
|
|
s.parseToken(req, &args.Token)
|
|
|
|
if err := decodeBody(req, &args.AuthMethod, fixTimeAndHashFields); err != nil {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("AuthMethod decoding failed: %v", err)}
|
|
}
|
|
|
|
if methodName != "" {
|
|
if args.AuthMethod.Name != "" && args.AuthMethod.Name != methodName {
|
|
return nil, BadRequestError{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("ACL.AuthMethodSet", args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fixupAuthMethodConfig(&out)
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLAuthMethodDelete(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
|
|
args := structs.ACLAuthMethodDeleteRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
AuthMethodName: methodName,
|
|
}
|
|
s.parseToken(req, &args.Token)
|
|
|
|
var ignored bool
|
|
if err := s.agent.RPC("ACL.AuthMethodDelete", args, &ignored); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLLogin(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
args := &structs.ACLLoginRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
}
|
|
s.parseDC(req, &args.Datacenter)
|
|
|
|
if err := decodeBody(req, &args.Auth, nil); err != nil {
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("Failed to decode request body:: %v", err)}
|
|
}
|
|
|
|
var out structs.ACLToken
|
|
if err := s.agent.RPC("ACL.Login", args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (s *HTTPServer) ACLLogout(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
if s.checkACLDisabled(resp, req) {
|
|
return nil, nil
|
|
}
|
|
|
|
args := structs.ACLLogoutRequest{
|
|
Datacenter: s.agent.config.Datacenter,
|
|
}
|
|
s.parseDC(req, &args.Datacenter)
|
|
s.parseToken(req, &args.Token)
|
|
|
|
if args.Token == "" {
|
|
return nil, acl.ErrNotFound
|
|
}
|
|
|
|
var ignored bool
|
|
if err := s.agent.RPC("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
|
|
}
|
|
}
|
|
}
|