2017-08-08 06:02:03 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
import (
|
2022-09-21 01:20:04 +00:00
|
|
|
"fmt"
|
2017-08-08 06:02:03 +00:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
|
|
)
|
|
|
|
|
|
|
|
func (s *HTTPServer) ACLPoliciesRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
if req.Method != "GET" {
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.ACLPolicyListRequest{}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var out structs.ACLPolicyListResponse
|
|
|
|
if err := s.agent.RPC("ACL.ListPolicies", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &out.QueryMeta)
|
|
|
|
if out.Policies == nil {
|
|
|
|
out.Policies = make([]*structs.ACLPolicyListStub, 0)
|
|
|
|
}
|
|
|
|
return out.Policies, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) ACLPolicySpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
name := strings.TrimPrefix(req.URL.Path, "/v1/acl/policy/")
|
|
|
|
if len(name) == 0 {
|
|
|
|
return nil, CodedError(400, "Missing Policy Name")
|
|
|
|
}
|
|
|
|
switch req.Method {
|
|
|
|
case "GET":
|
|
|
|
return s.aclPolicyQuery(resp, req, name)
|
|
|
|
case "PUT", "POST":
|
|
|
|
return s.aclPolicyUpdate(resp, req, name)
|
|
|
|
case "DELETE":
|
|
|
|
return s.aclPolicyDelete(resp, req, name)
|
|
|
|
default:
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclPolicyQuery(resp http.ResponseWriter, req *http.Request,
|
|
|
|
policyName string) (interface{}, error) {
|
|
|
|
args := structs.ACLPolicySpecificRequest{
|
|
|
|
Name: policyName,
|
|
|
|
}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var out structs.SingleACLPolicyResponse
|
|
|
|
if err := s.agent.RPC("ACL.GetPolicy", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &out.QueryMeta)
|
|
|
|
if out.Policy == nil {
|
|
|
|
return nil, CodedError(404, "ACL policy not found")
|
|
|
|
}
|
|
|
|
return out.Policy, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclPolicyUpdate(resp http.ResponseWriter, req *http.Request,
|
|
|
|
policyName string) (interface{}, error) {
|
|
|
|
// Parse the policy
|
|
|
|
var policy structs.ACLPolicy
|
|
|
|
if err := decodeBody(req, &policy); err != nil {
|
2022-11-18 16:04:33 +00:00
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
2017-08-08 06:02:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the policy name matches
|
|
|
|
if policy.Name != policyName {
|
|
|
|
return nil, CodedError(400, "ACL policy name does not match request path")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format the request
|
|
|
|
args := structs.ACLPolicyUpsertRequest{
|
|
|
|
Policies: []*structs.ACLPolicy{&policy},
|
|
|
|
}
|
2017-09-07 23:56:15 +00:00
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
2017-08-08 06:02:03 +00:00
|
|
|
|
|
|
|
var out structs.GenericResponse
|
|
|
|
if err := s.agent.RPC("ACL.UpsertPolicies", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclPolicyDelete(resp http.ResponseWriter, req *http.Request,
|
|
|
|
policyName string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLPolicyDeleteRequest{
|
|
|
|
Names: []string{policyName},
|
|
|
|
}
|
2017-09-07 23:56:15 +00:00
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
2017-08-08 06:02:03 +00:00
|
|
|
|
|
|
|
var out structs.GenericResponse
|
|
|
|
if err := s.agent.RPC("ACL.DeletePolicies", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
return nil, nil
|
|
|
|
}
|
2017-08-12 23:08:00 +00:00
|
|
|
|
|
|
|
func (s *HTTPServer) ACLTokensRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
if req.Method != "GET" {
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.ACLTokenListRequest{}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var out structs.ACLTokenListResponse
|
|
|
|
if err := s.agent.RPC("ACL.ListTokens", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &out.QueryMeta)
|
|
|
|
if out.Tokens == nil {
|
|
|
|
out.Tokens = make([]*structs.ACLTokenListStub, 0)
|
|
|
|
}
|
|
|
|
return out.Tokens, nil
|
|
|
|
}
|
|
|
|
|
2017-08-21 01:31:22 +00:00
|
|
|
func (s *HTTPServer) ACLTokenBootstrap(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
// Ensure this is a PUT or POST
|
|
|
|
if !(req.Method == "PUT" || req.Method == "POST") {
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
2022-06-03 11:37:24 +00:00
|
|
|
var args structs.ACLTokenBootstrapRequest
|
|
|
|
|
|
|
|
if req.ContentLength != 0 {
|
|
|
|
if err := decodeBody(req, &args); err != nil {
|
2022-09-21 01:20:04 +00:00
|
|
|
return nil, CodedError(400, fmt.Sprintf("failed to decode request body: %s", err))
|
2022-06-03 11:37:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-07 23:56:15 +00:00
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
2017-08-21 01:31:22 +00:00
|
|
|
|
|
|
|
var out structs.ACLTokenUpsertResponse
|
|
|
|
if err := s.agent.RPC("ACL.Bootstrap", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
if len(out.Tokens) > 0 {
|
|
|
|
return out.Tokens[0], nil
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2017-08-12 23:08:00 +00:00
|
|
|
func (s *HTTPServer) ACLTokenSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2017-10-13 20:10:26 +00:00
|
|
|
path := req.URL.Path
|
2017-08-12 23:08:00 +00:00
|
|
|
|
2017-10-13 20:10:26 +00:00
|
|
|
switch path {
|
|
|
|
case "/v1/acl/token":
|
2017-08-12 23:08:00 +00:00
|
|
|
if !(req.Method == "PUT" || req.Method == "POST") {
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
return s.aclTokenUpdate(resp, req, "")
|
2017-10-13 20:10:26 +00:00
|
|
|
case "/v1/acl/token/self":
|
|
|
|
return s.aclTokenSelf(resp, req)
|
2017-08-12 23:08:00 +00:00
|
|
|
}
|
|
|
|
|
2017-10-13 20:10:26 +00:00
|
|
|
accessor := strings.TrimPrefix(path, "/v1/acl/token/")
|
|
|
|
return s.aclTokenCrud(resp, req, accessor)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclTokenCrud(resp http.ResponseWriter, req *http.Request,
|
|
|
|
tokenAccessor string) (interface{}, error) {
|
|
|
|
if tokenAccessor == "" {
|
2017-08-12 23:08:00 +00:00
|
|
|
return nil, CodedError(400, "Missing Token Accessor")
|
|
|
|
}
|
|
|
|
|
|
|
|
switch req.Method {
|
|
|
|
case "GET":
|
2017-10-13 20:10:26 +00:00
|
|
|
return s.aclTokenQuery(resp, req, tokenAccessor)
|
2017-08-12 23:08:00 +00:00
|
|
|
case "PUT", "POST":
|
2017-10-13 20:10:26 +00:00
|
|
|
return s.aclTokenUpdate(resp, req, tokenAccessor)
|
2017-08-12 23:08:00 +00:00
|
|
|
case "DELETE":
|
2017-10-13 20:10:26 +00:00
|
|
|
return s.aclTokenDelete(resp, req, tokenAccessor)
|
2017-08-12 23:08:00 +00:00
|
|
|
default:
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclTokenQuery(resp http.ResponseWriter, req *http.Request,
|
|
|
|
tokenAccessor string) (interface{}, error) {
|
|
|
|
args := structs.ACLTokenSpecificRequest{
|
|
|
|
AccessorID: tokenAccessor,
|
|
|
|
}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var out structs.SingleACLTokenResponse
|
|
|
|
if err := s.agent.RPC("ACL.GetToken", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &out.QueryMeta)
|
|
|
|
if out.Token == nil {
|
|
|
|
return nil, CodedError(404, "ACL token not found")
|
|
|
|
}
|
|
|
|
return out.Token, nil
|
|
|
|
}
|
|
|
|
|
2017-10-13 20:10:26 +00:00
|
|
|
func (s *HTTPServer) aclTokenSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
if req.Method != "GET" {
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
args := structs.ResolveACLTokenRequest{}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
args.SecretID = args.AuthToken
|
|
|
|
|
|
|
|
var out structs.ResolveACLTokenResponse
|
|
|
|
if err := s.agent.RPC("ACL.ResolveToken", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &out.QueryMeta)
|
|
|
|
if out.Token == nil {
|
|
|
|
return nil, CodedError(404, "ACL token not found")
|
|
|
|
}
|
|
|
|
return out.Token, nil
|
|
|
|
}
|
|
|
|
|
2017-08-12 23:08:00 +00:00
|
|
|
func (s *HTTPServer) aclTokenUpdate(resp http.ResponseWriter, req *http.Request,
|
|
|
|
tokenAccessor string) (interface{}, error) {
|
|
|
|
// Parse the token
|
|
|
|
var token structs.ACLToken
|
|
|
|
if err := decodeBody(req, &token); err != nil {
|
2022-11-18 16:04:33 +00:00
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
2017-08-12 23:08:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the token accessor matches
|
|
|
|
if tokenAccessor != "" && (token.AccessorID != tokenAccessor) {
|
|
|
|
return nil, CodedError(400, "ACL token accessor does not match request path")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format the request
|
|
|
|
args := structs.ACLTokenUpsertRequest{
|
|
|
|
Tokens: []*structs.ACLToken{&token},
|
|
|
|
}
|
2017-09-07 23:56:15 +00:00
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
2017-08-12 23:08:00 +00:00
|
|
|
|
2017-08-13 21:30:31 +00:00
|
|
|
var out structs.ACLTokenUpsertResponse
|
2017-08-12 23:08:00 +00:00
|
|
|
if err := s.agent.RPC("ACL.UpsertTokens", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
2017-08-13 21:30:31 +00:00
|
|
|
if len(out.Tokens) > 0 {
|
|
|
|
return out.Tokens[0], nil
|
|
|
|
}
|
2017-08-12 23:08:00 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclTokenDelete(resp http.ResponseWriter, req *http.Request,
|
|
|
|
tokenAccessor string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLTokenDeleteRequest{
|
|
|
|
AccessorIDs: []string{tokenAccessor},
|
|
|
|
}
|
2017-09-07 23:56:15 +00:00
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
2017-08-12 23:08:00 +00:00
|
|
|
|
|
|
|
var out structs.GenericResponse
|
|
|
|
if err := s.agent.RPC("ACL.DeleteTokens", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
return nil, nil
|
|
|
|
}
|
2021-02-25 21:41:00 +00:00
|
|
|
|
|
|
|
func (s *HTTPServer) UpsertOneTimeToken(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
// Ensure this is a PUT or POST
|
|
|
|
if !(req.Method == "PUT" || req.Method == "POST") {
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// the request body is empty but we need to parse to get the auth token
|
|
|
|
args := structs.OneTimeTokenUpsertRequest{}
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var out structs.OneTimeTokenUpsertResponse
|
|
|
|
if err := s.agent.RPC("ACL.UpsertOneTimeToken", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) ExchangeOneTimeToken(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
// Ensure this is a PUT or POST
|
|
|
|
if !(req.Method == "PUT" || req.Method == "POST") {
|
|
|
|
return nil, CodedError(405, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
var args structs.OneTimeTokenExchangeRequest
|
|
|
|
if err := decodeBody(req, &args); err != nil {
|
2022-11-18 16:04:33 +00:00
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
2021-02-25 21:41:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var out structs.OneTimeTokenExchangeResponse
|
|
|
|
if err := s.agent.RPC("ACL.ExchangeOneTimeToken", &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
return out, nil
|
|
|
|
}
|
2022-08-11 07:44:19 +00:00
|
|
|
|
|
|
|
// ACLRoleListRequest performs a listing of ACL roles and is callable via the
|
|
|
|
// /v1/acl/roles HTTP API.
|
|
|
|
func (s *HTTPServer) ACLRoleListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// The endpoint only supports GET requests.
|
|
|
|
if req.Method != http.MethodGet {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set up the request args and parse this to ensure the query options are
|
|
|
|
// set.
|
|
|
|
args := structs.ACLRolesListRequest{}
|
|
|
|
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform the RPC request.
|
|
|
|
var reply structs.ACLRolesListResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLListRolesRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &reply.QueryMeta)
|
|
|
|
|
|
|
|
if reply.ACLRoles == nil {
|
2022-08-22 15:20:23 +00:00
|
|
|
reply.ACLRoles = make([]*structs.ACLRoleListStub, 0)
|
2022-08-11 07:44:19 +00:00
|
|
|
}
|
|
|
|
return reply.ACLRoles, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ACLRoleRequest creates a new ACL role and is callable via the
|
|
|
|
// /v1/acl/role HTTP API.
|
|
|
|
func (s *HTTPServer) ACLRoleRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// // The endpoint only supports PUT or POST requests.
|
|
|
|
if !(req.Method == http.MethodPut || req.Method == http.MethodPost) {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use the generic upsert function without setting an ID as this will be
|
|
|
|
// handled by the Nomad leader.
|
|
|
|
return s.aclRoleUpsertRequest(resp, req, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ACLRoleSpecificRequest is callable via the /v1/acl/role/ HTTP API and
|
|
|
|
// handles read via both the role name and ID, updates, and deletions.
|
|
|
|
func (s *HTTPServer) ACLRoleSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// Grab the suffix of the request, so we can further understand it.
|
|
|
|
reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/acl/role/")
|
|
|
|
|
|
|
|
// Split the request suffix in order to identify whether this is a lookup
|
|
|
|
// of a service, or whether this includes a service and service identifier.
|
|
|
|
suffixParts := strings.Split(reqSuffix, "/")
|
|
|
|
|
|
|
|
switch len(suffixParts) {
|
|
|
|
case 1:
|
|
|
|
// Ensure the role ID is not an empty string which is possible if the
|
|
|
|
// caller requested "/v1/acl/role/"
|
|
|
|
if suffixParts[0] == "" {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "missing ACL role ID")
|
|
|
|
}
|
|
|
|
return s.aclRoleRequest(resp, req, suffixParts[0])
|
|
|
|
case 2:
|
|
|
|
// This endpoint only supports GET.
|
|
|
|
if req.Method != http.MethodGet {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that the path is correct, otherwise the call could use
|
|
|
|
// "/v1/acl/role/foobar/role-name" and successfully pass through here.
|
|
|
|
if suffixParts[0] != "name" {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "invalid URI")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the role name is not an empty string which is possible if the
|
|
|
|
// caller requested "/v1/acl/role/name/"
|
|
|
|
if suffixParts[1] == "" {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "missing ACL role name")
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.aclRoleGetByNameRequest(resp, req, suffixParts[1])
|
|
|
|
|
|
|
|
default:
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "invalid URI")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclRoleRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
|
|
|
|
|
|
|
// Identify the method which indicates which downstream function should be
|
|
|
|
// called.
|
|
|
|
switch req.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
return s.aclRoleGetByIDRequest(resp, req, roleID)
|
|
|
|
case http.MethodDelete:
|
|
|
|
return s.aclRoleDeleteRequest(resp, req, roleID)
|
|
|
|
case http.MethodPost, http.MethodPut:
|
|
|
|
return s.aclRoleUpsertRequest(resp, req, roleID)
|
|
|
|
default:
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclRoleGetByIDRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLRoleByIDRequest{
|
|
|
|
RoleID: roleID,
|
|
|
|
}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var reply structs.ACLRoleByIDResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLGetRoleByIDRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setMeta(resp, &reply.QueryMeta)
|
|
|
|
|
|
|
|
if reply.ACLRole == nil {
|
|
|
|
return nil, CodedError(http.StatusNotFound, "ACL role not found")
|
|
|
|
}
|
|
|
|
return reply.ACLRole, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclRoleDeleteRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLRolesDeleteByIDRequest{
|
|
|
|
ACLRoleIDs: []string{roleID},
|
|
|
|
}
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var reply structs.ACLRolesDeleteByIDResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLDeleteRolesByIDRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, reply.Index)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// aclRoleUpsertRequest handles upserting an ACL to the Nomad servers. It can
|
|
|
|
// handle both new creations, and updates to existing roles.
|
|
|
|
func (s *HTTPServer) aclRoleUpsertRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
|
|
|
|
|
|
|
// Decode the ACL role.
|
|
|
|
var aclRole structs.ACLRole
|
|
|
|
if err := decodeBody(req, &aclRole); err != nil {
|
2022-11-18 16:47:43 +00:00
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
2022-08-11 07:44:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the request path ID matches the ACL role ID that was decoded.
|
|
|
|
// Only perform this check on updates as a generic error on creation might
|
|
|
|
// be confusing to operators as there is no specific role request path.
|
|
|
|
if roleID != "" && roleID != aclRole.ID {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "ACL role ID does not match request path")
|
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.ACLRolesUpsertRequest{
|
|
|
|
ACLRoles: []*structs.ACLRole{&aclRole},
|
|
|
|
}
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var out structs.ACLRolesUpsertResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLUpsertRolesRPCMethod, &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
|
|
|
|
if len(out.ACLRoles) > 0 {
|
|
|
|
return out.ACLRoles[0], nil
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclRoleGetByNameRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, roleName string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLRoleByNameRequest{
|
|
|
|
RoleName: roleName,
|
|
|
|
}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var reply structs.ACLRoleByNameResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLGetRoleByNameRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setMeta(resp, &reply.QueryMeta)
|
|
|
|
|
|
|
|
if reply.ACLRole == nil {
|
|
|
|
return nil, CodedError(http.StatusNotFound, "ACL role not found")
|
|
|
|
}
|
|
|
|
return reply.ACLRole, nil
|
|
|
|
}
|
2022-11-23 08:38:02 +00:00
|
|
|
|
|
|
|
// ACLAuthMethodListRequest performs a listing of ACL auth-methods and is
|
|
|
|
// callable via the /v1/acl/auth-methods HTTP API.
|
|
|
|
func (s *HTTPServer) ACLAuthMethodListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// The endpoint only supports GET requests.
|
|
|
|
if req.Method != http.MethodGet {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set up the request args and parse this to ensure the query options are
|
|
|
|
// set.
|
|
|
|
args := structs.ACLAuthMethodListRequest{}
|
|
|
|
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform the RPC request.
|
|
|
|
var reply structs.ACLAuthMethodListResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLListAuthMethodsRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &reply.QueryMeta)
|
|
|
|
|
|
|
|
if reply.AuthMethods == nil {
|
|
|
|
reply.AuthMethods = make([]*structs.ACLAuthMethodStub, 0)
|
|
|
|
}
|
|
|
|
return reply.AuthMethods, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ACLAuthMethodRequest creates a new ACL auth-method and is callable via the
|
|
|
|
// /v1/acl/auth-method HTTP API.
|
|
|
|
func (s *HTTPServer) ACLAuthMethodRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// // The endpoint only supports PUT or POST requests.
|
|
|
|
if !(req.Method == http.MethodPut || req.Method == http.MethodPost) {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use the generic upsert function without setting an ID as this will be
|
|
|
|
// handled by the Nomad leader.
|
|
|
|
return s.aclAuthMethodUpsertRequest(resp, req, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ACLAuthMethodSpecificRequest is callable via the /v1/acl/auth-method/ HTTP
|
|
|
|
// API and handles reads, updates, and deletions of named methods.
|
|
|
|
func (s *HTTPServer) ACLAuthMethodSpecificRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// Grab the suffix of the request, so we can further understand it.
|
|
|
|
methodName := strings.TrimPrefix(req.URL.Path, "/v1/acl/auth-method/")
|
|
|
|
|
|
|
|
// Ensure the auth-method name is not an empty string which is possible if
|
|
|
|
// the caller requested "/v1/acl/role/auth-method/".
|
|
|
|
if methodName == "" {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "missing ACL auth-method name")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Identify the method which indicates which downstream function should be
|
|
|
|
// called.
|
|
|
|
switch req.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
return s.aclAuthMethodGetRequest(resp, req, methodName)
|
|
|
|
case http.MethodDelete:
|
|
|
|
return s.aclAuthMethodDeleteRequest(resp, req, methodName)
|
|
|
|
case http.MethodPost, http.MethodPut:
|
|
|
|
return s.aclAuthMethodUpsertRequest(resp, req, methodName)
|
|
|
|
default:
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// aclAuthMethodGetRequest is callable via the /v1/acl/auth-method/ HTTP API
|
|
|
|
// and is used for reading the named auth-method from state.
|
|
|
|
func (s *HTTPServer) aclAuthMethodGetRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLAuthMethodGetRequest{
|
|
|
|
MethodName: methodName,
|
|
|
|
}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var reply structs.ACLAuthMethodGetResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLGetAuthMethodRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setMeta(resp, &reply.QueryMeta)
|
|
|
|
|
|
|
|
if reply.AuthMethod == nil {
|
|
|
|
return nil, CodedError(http.StatusNotFound, "ACL auth-method not found")
|
|
|
|
}
|
|
|
|
return reply.AuthMethod, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// aclAuthMethodDeleteRequest is callable via the /v1/acl/auth-method/ HTTP API
|
|
|
|
// and is responsible for deleting the named auth-method from state.
|
|
|
|
func (s *HTTPServer) aclAuthMethodDeleteRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLAuthMethodDeleteRequest{
|
|
|
|
Names: []string{methodName},
|
|
|
|
}
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var reply structs.ACLAuthMethodDeleteResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLDeleteAuthMethodsRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, reply.Index)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// aclAuthMethodUpsertRequest handles upserting an ACL auth-method to the Nomad
|
|
|
|
// servers. It can handle both new creations, and updates to existing
|
|
|
|
// auth-methods.
|
|
|
|
func (s *HTTPServer) aclAuthMethodUpsertRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
|
|
|
|
|
|
|
|
// Decode the ACL auth-method.
|
|
|
|
var aclAuthMethod structs.ACLAuthMethod
|
|
|
|
if err := decodeBody(req, &aclAuthMethod); err != nil {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the request path name matches the ACL auth-method name that was
|
|
|
|
// decoded. Only perform this check on updates as a generic error on
|
|
|
|
// creation might be confusing to operators as there is no specific
|
|
|
|
// auth-method request path.
|
|
|
|
if methodName != "" && methodName != aclAuthMethod.Name {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "ACL auth-method name does not match request path")
|
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.ACLAuthMethodUpsertRequest{
|
|
|
|
AuthMethods: []*structs.ACLAuthMethod{&aclAuthMethod},
|
|
|
|
}
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var out structs.ACLAuthMethodUpsertResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLUpsertAuthMethodsRPCMethod, &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
2022-11-29 06:36:36 +00:00
|
|
|
|
|
|
|
if len(out.AuthMethods) > 0 {
|
|
|
|
return out.AuthMethods[0], nil
|
|
|
|
}
|
2022-11-23 08:38:02 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
2022-12-15 08:18:55 +00:00
|
|
|
|
|
|
|
// ACLBindingRuleListRequest performs a listing of ACL binding rules and is
|
|
|
|
// callable via the /v1/acl/binding-rules HTTP API.
|
|
|
|
func (s *HTTPServer) ACLBindingRuleListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// The endpoint only supports GET requests.
|
|
|
|
if req.Method != http.MethodGet {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set up the request args and parse this to ensure the query options are
|
|
|
|
// set.
|
|
|
|
args := structs.ACLBindingRulesListRequest{}
|
|
|
|
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform the RPC request.
|
|
|
|
var reply structs.ACLBindingRulesListResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLListBindingRulesRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
setMeta(resp, &reply.QueryMeta)
|
|
|
|
|
|
|
|
if reply.ACLBindingRules == nil {
|
|
|
|
reply.ACLBindingRules = make([]*structs.ACLBindingRuleListStub, 0)
|
|
|
|
}
|
|
|
|
return reply.ACLBindingRules, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ACLBindingRuleRequest creates a new ACL binding rule and is callable via the
|
|
|
|
// /v1/acl/binding-rule HTTP API.
|
|
|
|
func (s *HTTPServer) ACLBindingRuleRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// // The endpoint only supports PUT or POST requests.
|
|
|
|
if !(req.Method == http.MethodPut || req.Method == http.MethodPost) {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use the generic upsert function without setting an ID as this will be
|
|
|
|
// handled by the Nomad leader.
|
|
|
|
return s.aclBindingRuleUpsertRequest(resp, req, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ACLBindingRuleSpecificRequest is callable via the /v1/acl/binding-rule/ HTTP
|
|
|
|
// API and handles read via both the ID, updates, and deletions.
|
|
|
|
func (s *HTTPServer) ACLBindingRuleSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// Grab the suffix of the request, so we can further understand it.
|
|
|
|
reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/acl/binding-rule/")
|
|
|
|
|
|
|
|
// Ensure the binding rule ID is not an empty string which is possible if
|
|
|
|
// the caller requested "/v1/acl/role/binding-rule/".
|
|
|
|
if reqSuffix == "" {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "missing ACL binding rule ID")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Identify the HTTP method which indicates which downstream function
|
|
|
|
// should be called.
|
|
|
|
switch req.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
return s.aclBindingRuleGetRequest(resp, req, reqSuffix)
|
|
|
|
case http.MethodDelete:
|
|
|
|
return s.aclBindingRuleDeleteRequest(resp, req, reqSuffix)
|
|
|
|
case http.MethodPost, http.MethodPut:
|
|
|
|
return s.aclBindingRuleUpsertRequest(resp, req, reqSuffix)
|
|
|
|
default:
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclBindingRuleGetRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, ruleID string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLBindingRuleRequest{
|
|
|
|
ACLBindingRuleID: ruleID,
|
|
|
|
}
|
|
|
|
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var reply structs.ACLBindingRuleResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLGetBindingRuleRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setMeta(resp, &reply.QueryMeta)
|
|
|
|
|
|
|
|
if reply.ACLBindingRule == nil {
|
|
|
|
return nil, CodedError(http.StatusNotFound, "ACL binding rule not found")
|
|
|
|
}
|
|
|
|
return reply.ACLBindingRule, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServer) aclBindingRuleDeleteRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, ruleID string) (interface{}, error) {
|
|
|
|
|
|
|
|
args := structs.ACLBindingRulesDeleteRequest{
|
|
|
|
ACLBindingRuleIDs: []string{ruleID},
|
|
|
|
}
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var reply structs.ACLBindingRulesDeleteResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLDeleteBindingRulesRPCMethod, &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, reply.Index)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// aclBindingRuleUpsertRequest handles upserting an ACL binding rule to the
|
|
|
|
// Nomad servers. It can handle both new creations, and updates to existing
|
|
|
|
// rules.
|
|
|
|
func (s *HTTPServer) aclBindingRuleUpsertRequest(
|
|
|
|
resp http.ResponseWriter, req *http.Request, ruleID string) (interface{}, error) {
|
|
|
|
|
|
|
|
// Decode the ACL binding rule.
|
|
|
|
var aclBindingRule structs.ACLBindingRule
|
|
|
|
if err := decodeBody(req, &aclBindingRule); err != nil {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the request path includes an ID, ensure the payload has an ID if it
|
|
|
|
// has been left empty.
|
|
|
|
if ruleID != "" && aclBindingRule.ID == "" {
|
|
|
|
aclBindingRule.ID = ruleID
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the request path ID matches the ACL binding rule ID that was
|
|
|
|
// decoded. Only perform this check on updates as a generic error on
|
|
|
|
// creation might be confusing to operators as there is no specific binding
|
|
|
|
// rule request path.
|
|
|
|
if ruleID != "" && ruleID != aclBindingRule.ID {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, "ACL binding rule ID does not match request path")
|
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.ACLBindingRulesUpsertRequest{
|
|
|
|
ACLBindingRules: []*structs.ACLBindingRule{&aclBindingRule},
|
|
|
|
}
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
var out structs.ACLBindingRulesUpsertResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLUpsertBindingRulesRPCMethod, &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
|
|
|
|
if len(out.ACLBindingRules) > 0 {
|
|
|
|
return out.ACLBindingRules[0], nil
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|
2023-01-13 13:15:58 +00:00
|
|
|
|
|
|
|
// ACLOIDCAuthURLRequest starts the OIDC login workflow.
|
|
|
|
func (s *HTTPServer) ACLOIDCAuthURLRequest(_ http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// The endpoint only supports PUT or POST requests.
|
|
|
|
if req.Method != http.MethodPost && req.Method != http.MethodPut {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
var args structs.ACLOIDCAuthURLRequest
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
if err := decodeBody(req, &args); err != nil {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
var out structs.ACLOIDCAuthURLResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLOIDCAuthURLRPCMethod, &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ACLOIDCCompleteAuthRequest completes the OIDC login workflow.
|
|
|
|
func (s *HTTPServer) ACLOIDCCompleteAuthRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
|
|
|
|
// The endpoint only supports PUT or POST requests.
|
|
|
|
if req.Method != http.MethodPost && req.Method != http.MethodPut {
|
|
|
|
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
var args structs.ACLOIDCCompleteAuthRequest
|
|
|
|
s.parseWriteRequest(req, &args.WriteRequest)
|
|
|
|
|
|
|
|
if err := decodeBody(req, &args); err != nil {
|
|
|
|
return nil, CodedError(http.StatusBadRequest, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
var out structs.ACLOIDCCompleteAuthResponse
|
|
|
|
if err := s.agent.RPC(structs.ACLOIDCCompleteAuthRPCMethod, &args, &out); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
setIndex(resp, out.Index)
|
|
|
|
return out.ACLToken, nil
|
|
|
|
}
|