Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add node/service/check operations to transaction api #4869

Merged
merged 11 commits into from
Jan 22, 2019
96 changes: 96 additions & 0 deletions agent/consul/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1351,3 +1351,99 @@ func vetDeregisterWithACL(rule acl.Authorizer, subj *structs.DeregisterRequest,

return nil
}

// vetNodeTxnOp applies the given ACL policy to a node transaction operation.
func vetNodeTxnOp(op *structs.TxnNodeOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}

node := op.Node

n := &api.Node{
Node: node.Node,
ID: string(node.ID),
Address: node.Address,
Datacenter: node.Datacenter,
TaggedAddresses: node.TaggedAddresses,
Meta: node.Meta,
}

// Sentinel doesn't apply to deletes, only creates/updates, so we don't need the scopeFn.
var scope func() map[string]interface{}
if op.Verb != api.NodeDelete && op.Verb != api.NodeDeleteCAS {
scope = func() map[string]interface{} {
return sentinel.ScopeCatalogUpsert(n, nil)
}
}

if rule != nil && !rule.NodeWrite(node.Node, scope) {
return acl.ErrPermissionDenied
}

return nil
}

// vetServiceTxnOp applies the given ACL policy to a service transaction operation.
func vetServiceTxnOp(op *structs.TxnServiceOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}

service := op.Service

n := &api.Node{Node: op.Node}
svc := &api.AgentService{
ID: service.ID,
Service: service.Service,
Tags: service.Tags,
Meta: service.Meta,
Address: service.Address,
Port: service.Port,
EnableTagOverride: service.EnableTagOverride,
}
scope := func() map[string]interface{} {
kyhavlov marked this conversation as resolved.
Show resolved Hide resolved
return sentinel.ScopeCatalogUpsert(n, svc)
}
if !rule.ServiceWrite(service.Service, scope) {
return acl.ErrPermissionDenied
}

return nil
}

// vetCheckTxnOp applies the given ACL policy to a check transaction operation.
func vetCheckTxnOp(op *structs.TxnCheckOp, rule acl.Authorizer) error {
// Fast path if ACLs are not enabled.
if rule == nil {
return nil
}

n := &api.Node{Node: op.Check.Node}
svc := &api.AgentService{
ID: op.Check.ServiceID,
Service: op.Check.ServiceID,
Tags: op.Check.ServiceTags,
}
if op.Check.ServiceID == "" {
// Node-level check.
scope := func() map[string]interface{} {
kyhavlov marked this conversation as resolved.
Show resolved Hide resolved
return sentinel.ScopeCatalogUpsert(n, svc)
}
if !rule.NodeWrite(op.Check.Node, scope) {
return acl.ErrPermissionDenied
}
} else {
// Service-level check.
scope := func() map[string]interface{} {
kyhavlov marked this conversation as resolved.
Show resolved Hide resolved
return sentinel.ScopeCatalogUpsert(n, svc)
}
if !rule.ServiceWrite(op.Check.ServiceName, scope) {
return acl.ErrPermissionDenied
}
}

return nil
}
128 changes: 75 additions & 53 deletions agent/consul/catalog_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,74 +20,98 @@ type Catalog struct {
srv *Server
}

// Register is used register that a node is providing a given service.
func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error {
if done, err := c.srv.forward("Catalog.Register", args, args, reply); done {
// nodePreApply does the verification of a node before it is applied to Raft.
func nodePreApply(nodeName, nodeID string) error {
mkeeler marked this conversation as resolved.
Show resolved Hide resolved
if nodeName == "" {
return fmt.Errorf("Must provide node")
}
if nodeID != "" {
if _, err := uuid.ParseUUID(nodeID); err != nil {
return fmt.Errorf("Bad node ID: %v", err)
}
}

return nil
}

func servicePreApply(service *structs.NodeService, rule acl.Authorizer) error {
// Validate the service. This is in addition to the below since
// the above just hasn't been moved over yet. We should move it over
// in time.
if err := service.Validate(); err != nil {
return err
}
defer metrics.MeasureSince([]string{"catalog", "register"}, time.Now())

// Verify the args.
if args.Node == "" {
return fmt.Errorf("Must provide node")
// If no service id, but service name, use default
if service.ID == "" && service.Service != "" {
service.ID = service.Service
}
if args.Address == "" && !args.SkipNodeUpdate {
return fmt.Errorf("Must provide address if SkipNodeUpdate is not set")

// Verify ServiceName provided if ID.
if service.ID != "" && service.Service == "" {
return fmt.Errorf("Must provide service name with ID")
}
if args.ID != "" {
if _, err := uuid.ParseUUID(string(args.ID)); err != nil {
return fmt.Errorf("Bad node ID: %v", err)

// Check the service address here and in the agent endpoint
// since service registration isn't synchronous.
if ipaddr.IsAny(service.Address) {
return fmt.Errorf("Invalid service address")
}

// Apply the ACL policy if any. The 'consul' service is excluded
// since it is managed automatically internally (that behavior
// is going away after version 0.8). We check this same policy
// later if version 0.8 is enabled, so we can eventually just
// delete this and do all the ACL checks down there.
if service.Service != structs.ConsulServiceName {
if rule != nil && !rule.ServiceWrite(service.Service, nil) {
return acl.ErrPermissionDenied
}
}

// Proxies must have write permission on their destination
if service.Kind == structs.ServiceKindConnectProxy {
if rule != nil && !rule.ServiceWrite(service.Proxy.DestinationServiceName, nil) {
return acl.ErrPermissionDenied
}
}

return nil
}

// checkPreApply does the verification of a check before it is applied to Raft.
func checkPreApply(check *structs.HealthCheck) {
if check.CheckID == "" && check.Name != "" {
mkeeler marked this conversation as resolved.
Show resolved Hide resolved
check.CheckID = types.CheckID(check.Name)
}
}

// Register is used register that a node is providing a given service.
func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error {
if done, err := c.srv.forward("Catalog.Register", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"catalog", "register"}, time.Now())

// Fetch the ACL token, if any.
rule, err := c.srv.ResolveToken(args.Token)
if err != nil {
return err
}

// Verify the args.
if err := nodePreApply(args.Node, string(args.ID)); err != nil {
return err
}
if args.Address == "" && !args.SkipNodeUpdate {
return fmt.Errorf("Must provide address if SkipNodeUpdate is not set")
}

// Handle a service registration.
if args.Service != nil {
// Validate the service. This is in addition to the below since
// the above just hasn't been moved over yet. We should move it over
// in time.
if err := args.Service.Validate(); err != nil {
if err := servicePreApply(args.Service, rule); err != nil {
return err
}

// If no service id, but service name, use default
if args.Service.ID == "" && args.Service.Service != "" {
args.Service.ID = args.Service.Service
}

// Verify ServiceName provided if ID.
if args.Service.ID != "" && args.Service.Service == "" {
return fmt.Errorf("Must provide service name with ID")
}

// Check the service address here and in the agent endpoint
// since service registration isn't synchronous.
if ipaddr.IsAny(args.Service.Address) {
return fmt.Errorf("Invalid service address")
}

// Apply the ACL policy if any. The 'consul' service is excluded
// since it is managed automatically internally (that behavior
// is going away after version 0.8). We check this same policy
// later if version 0.8 is enabled, so we can eventually just
// delete this and do all the ACL checks down there.
if args.Service.Service != structs.ConsulServiceName {
if rule != nil && !rule.ServiceWrite(args.Service.Service, nil) {
return acl.ErrPermissionDenied
}
}

// Proxies must have write permission on their destination
if args.Service.Kind == structs.ServiceKindConnectProxy {
if rule != nil && !rule.ServiceWrite(args.Service.Proxy.DestinationServiceName, nil) {
return acl.ErrPermissionDenied
}
}
}

// Move the old format single check into the slice, and fixup IDs.
Expand All @@ -96,12 +120,10 @@ func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error
args.Check = nil
}
for _, check := range args.Checks {
if check.CheckID == "" && check.Name != "" {
check.CheckID = types.CheckID(check.Name)
}
if check.Node == "" {
check.Node = args.Node
}
checkPreApply(check)
}

// Check the complete register request against the given ACL policy.
Expand Down
12 changes: 11 additions & 1 deletion agent/consul/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,18 @@ func (t *txnResultsFilter) Len() int {

func (t *txnResultsFilter) Filter(i int) bool {
result := t.results[i]
if result.KV != nil {
switch {
case result.KV != nil:
return !t.authorizer.KeyRead(result.KV.Key)
case result.Node != nil:
return !t.authorizer.NodeRead(result.Node.Node)
case result.Service != nil:
return !t.authorizer.ServiceRead(result.Service.Service)
case result.Check != nil:
if result.Check.ServiceName != "" {
return !t.authorizer.ServiceRead(result.Check.ServiceName)
}
return !t.authorizer.NodeRead(result.Check.Node)
}
return false
}
Expand Down
Loading