From 7e5fdeb64b06c8577c9ff971cf9606120757b8c0 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 11:05:55 -0700 Subject: [PATCH 01/56] agent: Adding new ACL flags --- command/agent/config.go | 47 ++++++++++++++++++++++++++++++++++++ command/agent/config_test.go | 26 ++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/command/agent/config.go b/command/agent/config.go index 9a6a043f8a..5c14ae3641 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -194,6 +194,30 @@ type Config struct { CheckUpdateInterval time.Duration `mapstructure:"-"` CheckUpdateIntervalRaw string `mapstructure:"check_update_interval" json:"-"` + // ACLToken is the default token used to make requests if a per-request + // token is not provided. If not configured the 'anonymous' token is used. + ACLToken string `mapstructure:"acl_token" json:"-"` + + // ACLDatacenter is the central datacenter that holds authoritative + // ACL records. This must be the same for the entire cluster. + // If this is not set, ACLs are not enabled. Off by default. + ACLDatacenter string `mapstructure:"acl_datacenter"` + + // ACLCacheInterval is used to control how long ACLs are cached. This has + // a major impact on performance. By default, it is set to 30 seconds. + ACLCacheInterval time.Duration `mapstructure:"-"` + ACLCacheIntervalRaw string `mapstructure:"acl_cache_interval"` + + // ACLDownPolicy is used to control the ACL interaction when we cannot + // reach the ACLDatacenter and the token is not in the cache. + // There are two modes: + // * deny - Deny all requests + // * extend-cache - Ignore the cache expiration, and allow cached + // ACL's to be used to service requests. This + // is the default. If the ACL is not in the cache, + // this acts like deny. + ACLDownPolicy string `mapstructure:"acl_down_policy"` + // AEInterval controls the anti-entropy interval. This is how often // the agent attempts to reconcile it's local state with the server' // representation of our state. Defaults to every 60s. @@ -246,6 +270,8 @@ func DefaultConfig() *Config { Protocol: consul.ProtocolVersionMax, CheckUpdateInterval: 5 * time.Minute, AEInterval: time.Minute, + ACLCacheInterval: 30 * time.Second, + ACLDownPolicy: "extend-cache", } } @@ -341,6 +367,14 @@ func DecodeConfig(r io.Reader) (*Config, error) { result.CheckUpdateInterval = dur } + if raw := result.ACLCacheIntervalRaw; raw != "" { + dur, err := time.ParseDuration(raw) + if err != nil { + return nil, fmt.Errorf("ACLCacheInterval invalid: %v", err) + } + result.ACLCacheInterval = dur + } + return &result, nil } @@ -583,6 +617,19 @@ func MergeConfig(a, b *Config) *Config { if b.SyslogFacility != "" { result.SyslogFacility = b.SyslogFacility } + if b.ACLToken != "" { + result.ACLToken = b.ACLToken + } + if b.ACLDatacenter != "" { + result.ACLDatacenter = b.ACLDatacenter + } + if b.ACLCacheIntervalRaw != "" { + result.ACLCacheInterval = b.ACLCacheInterval + result.ACLCacheIntervalRaw = b.ACLCacheIntervalRaw + } + if b.ACLDownPolicy != "" { + result.ACLDownPolicy = b.ACLDownPolicy + } // Copy the start join addresses result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin)) diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 0c6db15e1c..17abee7e5b 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -356,6 +356,27 @@ func TestDecodeConfig(t *testing.T) { if config.CheckUpdateInterval != 10*time.Minute { t.Fatalf("bad: %#v", config) } + + // ACLs + input = `{"acl_token": "1234", "acl_datacenter": "dc2", + "acl_cache_interval": "60s", "acl_down_policy": "deny"}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if config.ACLToken != "1234" { + t.Fatalf("bad: %#v", config) + } + if config.ACLDatacenter != "dc2" { + t.Fatalf("bad: %#v", config) + } + if config.ACLCacheInterval != 60*time.Second { + t.Fatalf("bad: %#v", config) + } + if config.ACLDownPolicy != "deny" { + t.Fatalf("bad: %#v", config) + } } func TestDecodeConfig_Service(t *testing.T) { @@ -503,6 +524,11 @@ func TestMergeConfig(t *testing.T) { RejoinAfterLeave: true, CheckUpdateInterval: 8 * time.Minute, CheckUpdateIntervalRaw: "8m", + ACLToken: "1234", + ACLDatacenter: "dc2", + ACLCacheInterval: 15 * time.Second, + ACLCacheIntervalRaw: "15s", + ACLDownPolicy: "deny", } c := MergeConfig(a, b) From 9cd9a6bcc4b1a55ba743518fc292e679b98c935d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 15:03:47 -0700 Subject: [PATCH 02/56] agent: Changing ACL config names --- command/agent/config.go | 30 ++++++++++++++++++++---------- command/agent/config_test.go | 11 ++++++++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/command/agent/config.go b/command/agent/config.go index 5c14ae3641..3da8f8ff84 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -203,10 +203,16 @@ type Config struct { // If this is not set, ACLs are not enabled. Off by default. ACLDatacenter string `mapstructure:"acl_datacenter"` - // ACLCacheInterval is used to control how long ACLs are cached. This has + // ACLTTL is used to control the time-to-live of cached ACLs . This has // a major impact on performance. By default, it is set to 30 seconds. - ACLCacheInterval time.Duration `mapstructure:"-"` - ACLCacheIntervalRaw string `mapstructure:"acl_cache_interval"` + ACLTTL time.Duration `mapstructure:"-"` + ACLTTLRaw string `mapstructure:"acl_ttl"` + + // ACLDefaultPolicy is used to control the ACL interaction when + // there is no defined policy. This can be "allow" which means + // ACLs are used to black-list, or "deny" which means ACLs are + // white-lists. + ACLDefaultPolicy string `mapstructure:"acl_default_policy"` // ACLDownPolicy is used to control the ACL interaction when we cannot // reach the ACLDatacenter and the token is not in the cache. @@ -270,8 +276,9 @@ func DefaultConfig() *Config { Protocol: consul.ProtocolVersionMax, CheckUpdateInterval: 5 * time.Minute, AEInterval: time.Minute, - ACLCacheInterval: 30 * time.Second, + ACLTTL: 30 * time.Second, ACLDownPolicy: "extend-cache", + ACLDefaultPolicy: "allow", } } @@ -367,12 +374,12 @@ func DecodeConfig(r io.Reader) (*Config, error) { result.CheckUpdateInterval = dur } - if raw := result.ACLCacheIntervalRaw; raw != "" { + if raw := result.ACLTTLRaw; raw != "" { dur, err := time.ParseDuration(raw) if err != nil { - return nil, fmt.Errorf("ACLCacheInterval invalid: %v", err) + return nil, fmt.Errorf("ACL TTL invalid: %v", err) } - result.ACLCacheInterval = dur + result.ACLTTL = dur } return &result, nil @@ -623,13 +630,16 @@ func MergeConfig(a, b *Config) *Config { if b.ACLDatacenter != "" { result.ACLDatacenter = b.ACLDatacenter } - if b.ACLCacheIntervalRaw != "" { - result.ACLCacheInterval = b.ACLCacheInterval - result.ACLCacheIntervalRaw = b.ACLCacheIntervalRaw + if b.ACLTTLRaw != "" { + result.ACLTTL = b.ACLTTL + result.ACLTTLRaw = b.ACLTTLRaw } if b.ACLDownPolicy != "" { result.ACLDownPolicy = b.ACLDownPolicy } + if b.ACLDefaultPolicy != "" { + result.ACLDefaultPolicy = b.ACLDefaultPolicy + } // Copy the start join addresses result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin)) diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 17abee7e5b..c543e87005 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -359,7 +359,8 @@ func TestDecodeConfig(t *testing.T) { // ACLs input = `{"acl_token": "1234", "acl_datacenter": "dc2", - "acl_cache_interval": "60s", "acl_down_policy": "deny"}` + "acl_cache_interval": "60s", "acl_down_policy": "deny", + "acl_default_policy": "deny"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -377,6 +378,9 @@ func TestDecodeConfig(t *testing.T) { if config.ACLDownPolicy != "deny" { t.Fatalf("bad: %#v", config) } + if config.ACLDefaultPolicy != "deny" { + t.Fatalf("bad: %#v", config) + } } func TestDecodeConfig_Service(t *testing.T) { @@ -526,9 +530,10 @@ func TestMergeConfig(t *testing.T) { CheckUpdateIntervalRaw: "8m", ACLToken: "1234", ACLDatacenter: "dc2", - ACLCacheInterval: 15 * time.Second, - ACLCacheIntervalRaw: "15s", + ACLTTL: 15 * time.Second, + ACLTTLRaw: "15s", ACLDownPolicy: "deny", + ACLDefaultPolicy: "deny", } c := MergeConfig(a, b) From a8063457f85efe2b70b9f5df5ce08672ad14c717 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 15:20:35 -0700 Subject: [PATCH 03/56] consul: ACL setting passthrough --- command/agent/agent.go | 15 +++++++++++ command/agent/config_test.go | 4 +-- consul/client.go | 5 ++++ consul/config.go | 48 ++++++++++++++++++++++++++++++++++++ consul/server.go | 5 ++++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 0d8cecfdf8..fdc5536783 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -181,6 +181,21 @@ func (a *Agent) consulConfig() *consul.Config { if a.config.Protocol > 0 { base.ProtocolVersion = uint8(a.config.Protocol) } + if a.config.ACLToken != "" { + base.ACLToken = a.config.ACLToken + } + if a.config.ACLDatacenter != "" { + base.ACLDatacenter = a.config.ACLDatacenter + } + if a.config.ACLTTLRaw != "" { + base.ACLTTL = a.config.ACLTTL + } + if a.config.ACLDefaultPolicy != "" { + base.ACLDefaultPolicy = a.config.ACLDefaultPolicy + } + if a.config.ACLDownPolicy != "" { + base.ACLDownPolicy = a.config.ACLDownPolicy + } // Format the build string revision := a.config.Revision diff --git a/command/agent/config_test.go b/command/agent/config_test.go index c543e87005..973964dcd5 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -359,7 +359,7 @@ func TestDecodeConfig(t *testing.T) { // ACLs input = `{"acl_token": "1234", "acl_datacenter": "dc2", - "acl_cache_interval": "60s", "acl_down_policy": "deny", + "acl_ttl": "60s", "acl_down_policy": "deny", "acl_default_policy": "deny"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { @@ -372,7 +372,7 @@ func TestDecodeConfig(t *testing.T) { if config.ACLDatacenter != "dc2" { t.Fatalf("bad: %#v", config) } - if config.ACLCacheInterval != 60*time.Second { + if config.ACLTTL != 60*time.Second { t.Fatalf("bad: %#v", config) } if config.ACLDownPolicy != "deny" { diff --git a/consul/client.go b/consul/client.go index 92d9231959..70626d2de2 100644 --- a/consul/client.go +++ b/consul/client.go @@ -80,6 +80,11 @@ func NewClient(config *Config) (*Client, error) { return nil, fmt.Errorf("Config must provide a DataDir") } + // Sanity check the ACLs + if err := config.CheckACL(); err != nil { + return nil, err + } + // Ensure we have a log output if config.LogOutput == nil { config.LogOutput = os.Stderr diff --git a/consul/config.go b/consul/config.go index 8105e2e24d..92605b33ba 100644 --- a/consul/config.go +++ b/consul/config.go @@ -128,6 +128,33 @@ type Config struct { // operators track which versions are actively deployed Build string + // ACLToken is the default token to use when making a request. + // If not provided, the anonymous token is used. This enables + // backwards compatibility as well. + ACLToken string + + // ACLDatacenter provides the authoritative datacenter for ACL + // tokens. If not provided, ACL verification is disabled. + ACLDatacenter string + + // ACLTTL controls the time-to-live of cached ACL policies. + // It can be set to zero to disable caching, but this adds + // a substantial cost. + ACLTTL time.Duration + + // ACLDefaultPolicy is used to control the ACL interaction when + // there is no defined policy. This can be "allow" which means + // ACLs are used to black-list, or "deny" which means ACLs are + // white-lists. + ACLDefaultPolicy string + + // ACLDownPolicy controls the behavior of ACLs if the ACLDatacenter + // cannot be contacted. It can be either "deny" to deny all requests, + // or "extend-cache" which ignores the ACLCacheInterval and uses + // cached policies. If a policy is not in the cache, it acts like deny. + // "allow" can be used to allow all requests. This is not recommended. + ACLDownPolicy string + // ServerUp callback can be used to trigger a notification that // a Consul server is now up and known about. ServerUp func() @@ -145,6 +172,24 @@ func (c *Config) CheckVersion() error { return nil } +// CheckACL is used to sanity check the ACL configuration +func (c *Config) CheckACL() error { + switch c.ACLDefaultPolicy { + case "allow": + case "deny": + default: + return fmt.Errorf("Unsupported default ACL policy: %s", c.ACLDefaultPolicy) + } + switch c.ACLDownPolicy { + case "allow": + case "deny": + case "extend-cache": + default: + return fmt.Errorf("Unsupported down ACL policy: %s", c.ACLDownPolicy) + } + return nil +} + // AppendCA opens and parses the CA file and adds the certificates to // the provided CertPool. func (c *Config) AppendCA(pool *x509.CertPool) error { @@ -324,6 +369,9 @@ func DefaultConfig() *Config { SerfWANConfig: serf.DefaultConfig(), ReconcileInterval: 60 * time.Second, ProtocolVersion: ProtocolVersionMax, + ACLTTL: 30 * time.Second, + ACLDefaultPolicy: "allow", + ACLDownPolicy: "extend-cache", } // Increase our reap interval to 3 days instead of 24h. diff --git a/consul/server.go b/consul/server.go index af61dc94c7..927b7a74d9 100644 --- a/consul/server.go +++ b/consul/server.go @@ -140,6 +140,11 @@ func NewServer(config *Config) (*Server, error) { return nil, fmt.Errorf("Config must provide a DataDir") } + // Sanity check the ACLs + if err := config.CheckACL(); err != nil { + return nil, err + } + // Ensure we have a log output if config.LogOutput == nil { config.LogOutput = os.Stderr From cae4b421a319b6a85f2c73895419f13f28a5ffde Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 15:36:08 -0700 Subject: [PATCH 04/56] agent: Adding ACL master token --- command/agent/agent.go | 3 +++ command/agent/config.go | 8 ++++++++ command/agent/config_test.go | 6 +++++- consul/config.go | 5 +++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index fdc5536783..289637adcc 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -184,6 +184,9 @@ func (a *Agent) consulConfig() *consul.Config { if a.config.ACLToken != "" { base.ACLToken = a.config.ACLToken } + if a.config.ACLMasterToken != "" { + base.ACLMasterToken = a.config.ACLMasterToken + } if a.config.ACLDatacenter != "" { base.ACLDatacenter = a.config.ACLDatacenter } diff --git a/command/agent/config.go b/command/agent/config.go index 3da8f8ff84..87584e1a2b 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -198,6 +198,11 @@ type Config struct { // token is not provided. If not configured the 'anonymous' token is used. ACLToken string `mapstructure:"acl_token" json:"-"` + // ACLMasterToken is used to bootstrap the ACL system. It should be specified + // on the servers in the ACLDatacenter. When the leader comes online, it ensures + // that the Master token is available. This provides the initial token. + ACLMasterToken string `mapstructure:"acl_master_token" json:"-"` + // ACLDatacenter is the central datacenter that holds authoritative // ACL records. This must be the same for the entire cluster. // If this is not set, ACLs are not enabled. Off by default. @@ -627,6 +632,9 @@ func MergeConfig(a, b *Config) *Config { if b.ACLToken != "" { result.ACLToken = b.ACLToken } + if b.ACLMasterToken != "" { + result.ACLMasterToken = b.ACLMasterToken + } if b.ACLDatacenter != "" { result.ACLDatacenter = b.ACLDatacenter } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 973964dcd5..9bc67c69c0 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -360,7 +360,7 @@ func TestDecodeConfig(t *testing.T) { // ACLs input = `{"acl_token": "1234", "acl_datacenter": "dc2", "acl_ttl": "60s", "acl_down_policy": "deny", - "acl_default_policy": "deny"}` + "acl_default_policy": "deny", "acl_master_token": "2345"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -369,6 +369,9 @@ func TestDecodeConfig(t *testing.T) { if config.ACLToken != "1234" { t.Fatalf("bad: %#v", config) } + if config.ACLMasterToken != "2345" { + t.Fatalf("bad: %#v", config) + } if config.ACLDatacenter != "dc2" { t.Fatalf("bad: %#v", config) } @@ -529,6 +532,7 @@ func TestMergeConfig(t *testing.T) { CheckUpdateInterval: 8 * time.Minute, CheckUpdateIntervalRaw: "8m", ACLToken: "1234", + ACLMasterToken: "2345", ACLDatacenter: "dc2", ACLTTL: 15 * time.Second, ACLTTLRaw: "15s", diff --git a/consul/config.go b/consul/config.go index 92605b33ba..f49c8933a6 100644 --- a/consul/config.go +++ b/consul/config.go @@ -133,6 +133,11 @@ type Config struct { // backwards compatibility as well. ACLToken string + // ACLMasterToken is used to bootstrap the ACL system. It should be specified + // on the servers in the ACLDatacenter. When the leader comes online, it ensures + // that the Master token is available. This provides the initial token. + ACLMasterToken string + // ACLDatacenter provides the authoritative datacenter for ACL // tokens. If not provided, ACL verification is disabled. ACLDatacenter string From 3b4d8d58057d7acc1a9404afe2aefef4252b6780 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 15:48:28 -0700 Subject: [PATCH 05/56] consul: ACL structs --- consul/structs/structs.go | 66 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 56ec95c351..64995704e3 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -43,10 +43,15 @@ type RPCInfo interface { RequestDatacenter() string IsRead() bool AllowStaleRead() bool + ACLToken() string } // QueryOptions is used to specify various flags for read queries type QueryOptions struct { + // Token is the ACL token ID. If not provided, the 'anonymous' + // token is assumed for backwards compatibility. + Token string + // If set, wait until query exceeds given index. Must be provided // with MaxQueryTime. MinQueryIndex uint64 @@ -72,7 +77,15 @@ func (q QueryOptions) AllowStaleRead() bool { return q.AllowStale } -type WriteRequest struct{} +func (q QueryOptions) ACLToken() string { + return q.Token +} + +type WriteRequest struct { + // Token is the ACL token ID. If not provided, the 'anonymous' + // token is assumed for backwards compatibility. + Token string +} // WriteRequest only applies to writes, always false func (w WriteRequest) IsRead() bool { @@ -83,6 +96,10 @@ func (w WriteRequest) AllowStaleRead() bool { return false } +func (w WriteRequest) ACLToken() string { + return w.Token +} + // QueryMeta allows a query response to include potentially // useful metadata about a query type QueryMeta struct { @@ -396,6 +413,53 @@ type IndexedSessions struct { QueryMeta } +// ACL is used to represent a token and it's rules +type ACL struct { + CreateIndex uint64 + ModifyIndex uint64 + ID string + Name string + Type string + Rules string + TTL time.Duration +} +type ACLs []*ACL + +type ACLOp string + +const ( + ACLSet ACLOp = "set" + ACLDelete = "delete" +) + +// ACLRequest is used to create, update or delete an ACL +type ACLRequest struct { + Datacenter string + Op ACLOp + ACL ACL + WriteRequest +} + +func (r *ACLRequest) RequestDatacenter() string { + return r.Datacenter +} + +// ACLSpecificRequest is used to request an ACL by ID +type ACLSpecificRequest struct { + Datacenter string + ACL string + QueryOptions +} + +func (r *ACLSpecificRequest) RequestDatacenter() string { + return r.Datacenter +} + +type IndexedACLs struct { + ACLs ACLs + QueryMeta +} + // msgpackHandle is a shared handle for encoding/decoding of structs var msgpackHandle = &codec.MsgpackHandle{} From fea61d629b89c22cfe8ec441af69495b0950d2ba Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 16:43:57 -0700 Subject: [PATCH 06/56] consul: Adding ACLs to the state store --- consul/state_store.go | 147 +++++++++++++++++++++++++++- consul/state_store_test.go | 192 +++++++++++++++++++++++++++++++++++-- consul/structs/structs.go | 11 ++- 3 files changed, 340 insertions(+), 10 deletions(-) diff --git a/consul/state_store.go b/consul/state_store.go index f95b0554e0..39d79c2f2b 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -21,6 +21,7 @@ const ( dbKVS = "kvs" dbSessions = "sessions" dbSessionChecks = "sessionChecks" + dbACLs = "acls" dbMaxMapSize32bit uint64 = 512 * 1024 * 1024 // 512MB maximum size dbMaxMapSize64bit uint64 = 32 * 1024 * 1024 * 1024 // 32GB maximum size ) @@ -53,6 +54,7 @@ type StateStore struct { kvsTable *MDBTable sessionTable *MDBTable sessionCheckTable *MDBTable + aclTable *MDBTable tables MDBTables watch map[*MDBTable]*NotifyGroup queryTables map[string]MDBTables @@ -306,9 +308,26 @@ func (s *StateStore) initialize() error { }, } + s.aclTable = &MDBTable{ + Name: dbACLs, + Indexes: map[string]*MDBIndex{ + "id": &MDBIndex{ + Unique: true, + Fields: []string{"ID"}, + }, + }, + Decoder: func(buf []byte) interface{} { + out := new(structs.ACL) + if err := structs.Decode(buf, out); err != nil { + panic(err) + } + return out + }, + } + // Store the set of tables s.tables = []*MDBTable{s.nodeTable, s.serviceTable, s.checkTable, - s.kvsTable, s.sessionTable, s.sessionCheckTable} + s.kvsTable, s.sessionTable, s.sessionCheckTable, s.aclTable} for _, table := range s.tables { table.Env = s.env table.Encoder = encoder @@ -1249,8 +1268,8 @@ func (s *StateStore) SessionCreate(index uint64, session *structs.Session) error } // Generate a new session ID, verify uniqueness - session.ID = generateUUID() for { + session.ID = generateUUID() res, err = s.sessionTable.GetTxn(tx, "id", session.ID) if err != nil { return err @@ -1346,7 +1365,7 @@ func (s *StateStore) NodeSessions(node string) (uint64, []*structs.Session, erro return idx, out, err } -// SessionDelete is used to destroy a session. +// SessionDestroy is used to destroy a session. func (s *StateStore) SessionDestroy(index uint64, id string) error { tx, err := s.tables.StartTxn(false) if err != nil { @@ -1482,6 +1501,118 @@ func (s *StateStore) invalidateLocks(index uint64, tx *MDBTxn, return nil } +// ACLSet is used to create or update an ACL entry +func (s *StateStore) ACLSet(index uint64, acl *structs.ACL) error { + // Start a new txn + tx, err := s.tables.StartTxn(false) + if err != nil { + return err + } + defer tx.Abort() + + // Generate a new session ID + if acl.ID == "" { + for { + acl.ID = generateUUID() + res, err := s.aclTable.GetTxn(tx, "id", acl.ID) + if err != nil { + return err + } + // Quit if this ID is unique + if len(res) == 0 { + break + } + } + acl.CreateIndex = index + acl.ModifyIndex = index + + } else { + // Look for the existing node + res, err := s.aclTable.GetTxn(tx, "id", acl.ID) + if err != nil { + return err + } + + switch len(res) { + case 0: + return fmt.Errorf("Invalid ACL") + case 1: + exist := res[0].(*structs.ACL) + acl.CreateIndex = exist.CreateIndex + acl.ModifyIndex = index + default: + panic(fmt.Errorf("Duplicate ACL definition. Internal error")) + } + } + + // Insert the ACL + if err := s.aclTable.InsertTxn(tx, acl); err != nil { + return err + } + + // Trigger the update notifications + if err := s.aclTable.SetLastIndexTxn(tx, index); err != nil { + return err + } + tx.Defer(func() { s.watch[s.aclTable].Notify() }) + return tx.Commit() +} + +// ACLRestore is used to restore an ACL. It should only be used when +// doing a restore, otherwise ACLSet should be used. +func (s *StateStore) ACLRestore(acl *structs.ACL) error { + // Start a new txn + tx, err := s.aclTable.StartTxn(false, nil) + if err != nil { + return err + } + defer tx.Abort() + + if err := s.aclTable.InsertTxn(tx, acl); err != nil { + return err + } + return tx.Commit() +} + +// ACLGet is used to get an ACL by ID +func (s *StateStore) ACLGet(id string) (uint64, *structs.ACL, error) { + idx, res, err := s.aclTable.Get("id", id) + var d *structs.ACL + if len(res) > 0 { + d = res[0].(*structs.ACL) + } + return idx, d, err +} + +// ACLList is used to list all the acls +func (s *StateStore) ACLList() (uint64, []*structs.ACL, error) { + idx, res, err := s.aclTable.Get("id") + out := make([]*structs.ACL, len(res)) + for i, raw := range res { + out[i] = raw.(*structs.ACL) + } + return idx, out, err +} + +// ACLDelete is used to remove an ACL +func (s *StateStore) ACLDelete(index uint64, id string) error { + tx, err := s.tables.StartTxn(false) + if err != nil { + panic(fmt.Errorf("Failed to start txn: %v", err)) + } + defer tx.Abort() + + if n, err := s.aclTable.DeleteTxn(tx, "id", id); err != nil { + return err + } else if n > 0 { + if err := s.aclTable.SetLastIndexTxn(tx, index); err != nil { + return err + } + tx.Defer(func() { s.watch[s.aclTable].Notify() }) + } + return tx.Commit() +} + // Snapshot is used to create a point in time snapshot func (s *StateStore) Snapshot() (*StateSnapshot, error) { // Begin a new txn on all tables @@ -1555,3 +1686,13 @@ func (s *StateSnapshot) SessionList() ([]*structs.Session, error) { } return out, err } + +// ACLList is used to list all of the ACLs +func (s *StateSnapshot) ACLList() ([]*structs.ACL, error) { + res, err := s.store.aclTable.GetTxn(s.tx, "id") + out := make([]*structs.ACL, len(res)) + for i, raw := range res { + out[i] = raw.(*structs.ACL) + } + return out, err +} diff --git a/consul/state_store_test.go b/consul/state_store_test.go index a5130131a7..c1705f5df0 100644 --- a/consul/state_store_test.go +++ b/consul/state_store_test.go @@ -652,6 +652,22 @@ func TestStoreSnapshot(t *testing.T) { t.Fatalf("err: %v", err) } + a1 := &structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + } + if err := store.ACLSet(19, a1); err != nil { + t.Fatalf("err: %v", err) + } + + a2 := &structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + } + if err := store.ACLSet(20, a2); err != nil { + t.Fatalf("err: %v", err) + } + // Take a snapshot snap, err := store.Snapshot() if err != nil { @@ -660,7 +676,7 @@ func TestStoreSnapshot(t *testing.T) { defer snap.Close() // Check the last nodes - if idx := snap.LastIndex(); idx != 18 { + if idx := snap.LastIndex(); idx != 20 { t.Fatalf("bad: %v", idx) } @@ -724,14 +740,23 @@ func TestStoreSnapshot(t *testing.T) { t.Fatalf("missing sessions") } + // Check for an acl + acls, err := snap.ACLList() + if err != nil { + t.Fatalf("err: %v", err) + } + if len(acls) != 2 { + t.Fatalf("missing acls") + } + // Make some changes! - if err := store.EnsureService(19, "foo", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil { + if err := store.EnsureService(21, "foo", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil { t.Fatalf("err: %v", err) } - if err := store.EnsureService(20, "bar", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil { + if err := store.EnsureService(22, "bar", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil { t.Fatalf("err: %v", err) } - if err := store.EnsureNode(21, structs.Node{"baz", "127.0.0.3"}); err != nil { + if err := store.EnsureNode(23, structs.Node{"baz", "127.0.0.3"}); err != nil { t.Fatalf("err: %v", err) } checkAfter := &structs.HealthCheck{ @@ -741,11 +766,16 @@ func TestStoreSnapshot(t *testing.T) { Status: structs.HealthCritical, ServiceID: "db", } - if err := store.EnsureCheck(22, checkAfter); err != nil { + if err := store.EnsureCheck(24, checkAfter); err != nil { t.Fatalf("err: %v", err) } - if err := store.KVSDelete(23, "/web/b"); err != nil { + if err := store.KVSDelete(25, "/web/b"); err != nil { + t.Fatalf("err: %v", err) + } + + // Nuke an ACL + if err := store.ACLDelete(26, a1.ID); err != nil { t.Fatalf("err: %v", err) } @@ -807,6 +837,15 @@ func TestStoreSnapshot(t *testing.T) { if len(sessions) != 2 { t.Fatalf("missing sessions") } + + // Check for an acl + acls, err = snap.ACLList() + if err != nil { + t.Fatalf("err: %v", err) + } + if len(acls) != 2 { + t.Fatalf("missing acls") + } } func TestEnsureCheck(t *testing.T) { @@ -2117,3 +2156,144 @@ func TestSessionInvalidate_KeyUnlock(t *testing.T) { t.Fatalf("Bad: %v", expires) } } + +func TestACLSet_Get(t *testing.T) { + store, err := testStateStore() + if err != nil { + t.Fatalf("err: %v", err) + } + defer store.Close() + + idx, out, err := store.ACLGet("1234") + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 0 { + t.Fatalf("bad: %v", idx) + } + if out != nil { + t.Fatalf("bad: %v", out) + } + + a := &structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: "", + } + if err := store.ACLSet(50, a); err != nil { + t.Fatalf("err: %v", err) + } + if a.CreateIndex != 50 { + t.Fatalf("Bad: %v", a) + } + if a.ModifyIndex != 50 { + t.Fatalf("Bad: %v", a) + } + if a.ID == "" { + t.Fatalf("Bad: %v", a) + } + + idx, out, err = store.ACLGet(a.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 50 { + t.Fatalf("bad: %v", idx) + } + if !reflect.DeepEqual(out, a) { + t.Fatalf("bad: %v", out) + } + + // Update + a.Rules = "foo bar baz" + if err := store.ACLSet(52, a); err != nil { + t.Fatalf("err: %v", err) + } + if a.CreateIndex != 50 { + t.Fatalf("Bad: %v", a) + } + if a.ModifyIndex != 52 { + t.Fatalf("Bad: %v", a) + } + + idx, out, err = store.ACLGet(a.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 52 { + t.Fatalf("bad: %v", idx) + } + if !reflect.DeepEqual(out, a) { + t.Fatalf("bad: %v", out) + } +} + +func TestACLDelete(t *testing.T) { + store, err := testStateStore() + if err != nil { + t.Fatalf("err: %v", err) + } + defer store.Close() + + a := &structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: "", + } + if err := store.ACLSet(50, a); err != nil { + t.Fatalf("err: %v", err) + } + + if err := store.ACLDelete(52, a.ID); err != nil { + t.Fatalf("err: %v", err) + } + if err := store.ACLDelete(53, a.ID); err != nil { + t.Fatalf("err: %v", err) + } + + idx, out, err := store.ACLGet(a.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 52 { + t.Fatalf("bad: %v", idx) + } + if out != nil { + t.Fatalf("bad: %v", out) + } +} + +func TestACLList(t *testing.T) { + store, err := testStateStore() + if err != nil { + t.Fatalf("err: %v", err) + } + defer store.Close() + + a1 := &structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + } + if err := store.ACLSet(50, a1); err != nil { + t.Fatalf("err: %v", err) + } + + a2 := &structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + } + if err := store.ACLSet(51, a2); err != nil { + t.Fatalf("err: %v", err) + } + + idx, out, err := store.ACLList() + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 51 { + t.Fatalf("bad: %v", idx) + } + if len(out) != 2 { + t.Fatalf("bad: %v", out) + } +} diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 64995704e3..27cffd0b28 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -20,6 +20,7 @@ const ( DeregisterRequestType KVSRequestType SessionRequestType + ACLRequestType ) const ( @@ -32,6 +33,15 @@ const ( HealthCritical = "critical" ) +const ( + // Client tokens have rules applied + ACLTypeClient = "client" + + // Management tokens have an always allow policy. + // They are used for token management. + ACLTypeManagement = "management" +) + const ( // MaxLockDelay provides a maximum LockDelay value for // a session. Any value above this will not be respected. @@ -421,7 +431,6 @@ type ACL struct { Name string Type string Rules string - TTL time.Duration } type ACLs []*ACL From 70b84e44c925a4a51884e1251d31be7aeec981b7 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 16:55:58 -0700 Subject: [PATCH 07/56] consul: FSM support for ACLsg --- consul/fsm.go | 53 +++++++++++++++++++++++++++++ consul/fsm_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/consul/fsm.go b/consul/fsm.go index 8b4fd3d65a..4c82357e68 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -69,6 +69,8 @@ func (c *consulFSM) Apply(log *raft.Log) interface{} { return c.applyKVSOperation(buf[1:], log.Index) case structs.SessionRequestType: return c.applySessionOperation(buf[1:], log.Index) + case structs.ACLRequestType: + return c.applyACLOperation(buf[1:], log.Index) default: panic(fmt.Errorf("failed to apply request: %#v", buf)) } @@ -196,6 +198,27 @@ func (c *consulFSM) applySessionOperation(buf []byte, index uint64) interface{} return nil } +func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} { + var req structs.ACLRequest + if err := structs.Decode(buf, &req); err != nil { + panic(fmt.Errorf("failed to decode request: %v", err)) + } + switch req.Op { + case structs.ACLSet: + if err := c.state.ACLSet(index, &req.ACL); err != nil { + return err + } else { + return req.ACL.ID + } + case structs.ACLDelete: + return c.state.ACLDelete(index, req.ACL.ID) + default: + c.logger.Printf("[WARN] consul.fsm: Invalid ACL operation '%s'", req.Op) + return fmt.Errorf("Invalid ACL operation '%s'", req.Op) + } + return nil +} + func (c *consulFSM) Snapshot() (raft.FSMSnapshot, error) { defer func(start time.Time) { c.logger.Printf("[INFO] consul.fsm: snapshot created in %v", time.Now().Sub(start)) @@ -267,6 +290,15 @@ func (c *consulFSM) Restore(old io.ReadCloser) error { return err } + case structs.ACLRequestType: + var req structs.ACL + if err := dec.Decode(&req); err != nil { + return err + } + if err := c.state.ACLRestore(&req); err != nil { + return err + } + default: return fmt.Errorf("Unrecognized msg type: %v", msgType) } @@ -298,6 +330,11 @@ func (s *consulSnapshot) Persist(sink raft.SnapshotSink) error { return err } + if err := s.persistACLs(sink, encoder); err != nil { + sink.Cancel() + return err + } + if err := s.persistKV(sink, encoder); err != nil { sink.Cancel() return err @@ -364,6 +401,22 @@ func (s *consulSnapshot) persistSessions(sink raft.SnapshotSink, return nil } +func (s *consulSnapshot) persistACLs(sink raft.SnapshotSink, + encoder *codec.Encoder) error { + acls, err := s.state.ACLList() + if err != nil { + return err + } + + for _, s := range acls { + sink.Write([]byte{byte(structs.ACLRequestType)}) + if err := encoder.Encode(s); err != nil { + return err + } + } + return nil +} + func (s *consulSnapshot) persistKV(sink raft.SnapshotSink, encoder *codec.Encoder) error { streamCh := make(chan interface{}, 256) diff --git a/consul/fsm_test.go b/consul/fsm_test.go index 5e5d086d84..f47c006539 100644 --- a/consul/fsm_test.go +++ b/consul/fsm_test.go @@ -328,6 +328,8 @@ func TestFSM_SnapshotRestore(t *testing.T) { }) session := &structs.Session{Node: "foo"} fsm.state.SessionCreate(9, session) + acl := &structs.ACL{Name: "User Token"} + fsm.state.ACLSet(10, acl) // Snapshot snap, err := fsm.Snapshot() @@ -392,7 +394,16 @@ func TestFSM_SnapshotRestore(t *testing.T) { t.Fatalf("err: %v", err) } if s.Node != "foo" { - t.Fatalf("bad: %v", d) + t.Fatalf("bad: %v", s) + } + + // Verify ACL is restored + _, a, err := fsm.state.ACLGet(acl.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if a.Name != "User Token" { + t.Fatalf("bad: %v", a) } } @@ -767,3 +778,75 @@ func TestFSM_KVSUnlock(t *testing.T) { t.Fatalf("bad: %v", *d) } } + +func TestFSM_ACL_Set_Delete(t *testing.T) { + fsm, err := NewFSM(os.Stderr) + if err != nil { + t.Fatalf("err: %v", err) + } + defer fsm.Close() + + // Create a new ACL + req := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + }, + } + buf, err := structs.Encode(structs.ACLRequestType, req) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := fsm.Apply(makeLog(buf)) + if err, ok := resp.(error); ok { + t.Fatalf("resp: %v", err) + } + + // Get the ACL + id := resp.(string) + _, acl, err := fsm.state.ACLGet(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing") + } + + // Verify the ACL + if acl.ID != id { + t.Fatalf("bad: %v", *acl) + } + if acl.Name != "User token" { + t.Fatalf("bad: %v", *acl) + } + if acl.Type != structs.ACLTypeClient { + t.Fatalf("bad: %v", *acl) + } + + // Try to destroy + destroy := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLDelete, + ACL: structs.ACL{ + ID: id, + }, + } + buf, err = structs.Encode(structs.ACLRequestType, destroy) + if err != nil { + t.Fatalf("err: %v", err) + } + resp = fsm.Apply(makeLog(buf)) + if resp != nil { + t.Fatalf("resp: %v", resp) + } + + _, acl, err = fsm.state.ACLGet(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if acl != nil { + t.Fatalf("should be destroyed") + } +} From b53ee80acd32e1b3b483d55b9b3099f913cdb912 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 17:04:55 -0700 Subject: [PATCH 08/56] consul: register the ACL queries --- consul/state_store.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/consul/state_store.go b/consul/state_store.go index 39d79c2f2b..42c0d553fc 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -357,6 +357,8 @@ func (s *StateStore) initialize() error { "SessionGet": MDBTables{s.sessionTable}, "SessionList": MDBTables{s.sessionTable}, "NodeSessions": MDBTables{s.sessionTable}, + "ACLGet": MDBTables{s.aclTable}, + "ACLList": MDBTables{s.aclTable}, } return nil } From 7cbb2225af72da6b53b31a64c7aeae74d176fa4a Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 17:05:59 -0700 Subject: [PATCH 09/56] consul: Adding ACL endpoint --- consul/acl_endpoint.go | 91 ++++++++++++++++++++++++++++++++++++++++++ consul/server.go | 3 ++ 2 files changed, 94 insertions(+) create mode 100644 consul/acl_endpoint.go diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go new file mode 100644 index 0000000000..ac9b0cdab1 --- /dev/null +++ b/consul/acl_endpoint.go @@ -0,0 +1,91 @@ +package consul + +import ( + "fmt" + "github.com/armon/go-metrics" + "github.com/hashicorp/consul/consul/structs" + "time" +) + +// ACL endpoint is used to manipulate ACLs +type ACL struct { + srv *Server +} + +// Apply is used to apply a modifying request to the data store. This should +// only be used for operations that modify the data +func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error { + if done, err := a.srv.forward("ACL.Apply", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"consul", "acl", "apply"}, time.Now()) + + // Verify the args + switch args.ACL.Type { + case structs.ACLTypeClient: + case structs.ACLTypeManagement: + default: + return fmt.Errorf("Invalid ACL Type") + } + + // TODO: Verify ACL compiles... + if args.Op == structs.ACLSet { + } + + // Apply the update + resp, err := a.srv.raftApply(structs.ACLRequestType, args) + if err != nil { + a.srv.logger.Printf("[ERR] consul.acl: Apply failed: %v", err) + return err + } + if respErr, ok := resp.(error); ok { + return respErr + } + + // Check if the return type is a string + if respString, ok := resp.(string); ok { + *reply = respString + } + return nil +} + +// Get is used to retrieve a single ACL +func (a *ACL) Get(args *structs.ACLSpecificRequest, + reply *structs.IndexedACLs) error { + if done, err := a.srv.forward("ACL.Get", args, args, reply); done { + return err + } + + // Get the local state + state := a.srv.fsm.State() + return a.srv.blockingRPC(&args.QueryOptions, + &reply.QueryMeta, + state.QueryTables("ACLGet"), + func() error { + index, acl, err := state.ACLGet(args.ACL) + reply.Index = index + if acl != nil { + reply.ACLs = structs.ACLs{acl} + } + return err + }) +} + +// List is used to list all the ACLs +func (a *ACL) List(args *structs.DCSpecificRequest, + reply *structs.IndexedACLs) error { + if done, err := a.srv.forward("ACL.List", args, args, reply); done { + return err + } + + // Get the local state + state := a.srv.fsm.State() + return a.srv.blockingRPC(&args.QueryOptions, + &reply.QueryMeta, + state.QueryTables("ACLList"), + func() error { + var err error + reply.Index, reply.ACLs, err = state.ACLList() + return err + }) +} diff --git a/consul/server.go b/consul/server.go index 927b7a74d9..b07522e156 100644 --- a/consul/server.go +++ b/consul/server.go @@ -125,6 +125,7 @@ type endpoints struct { KVS *KVS Session *Session Internal *Internal + ACL *ACL } // NewServer is used to construct a new Consul server from the @@ -341,6 +342,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error { s.endpoints.KVS = &KVS{s} s.endpoints.Session = &Session{s} s.endpoints.Internal = &Internal{s} + s.endpoints.ACL = &ACL{s} // Register the handlers s.rpcServer.Register(s.endpoints.Status) @@ -349,6 +351,7 @@ func (s *Server) setupRPC(tlsConfig *tls.Config) error { s.rpcServer.Register(s.endpoints.KVS) s.rpcServer.Register(s.endpoints.Session) s.rpcServer.Register(s.endpoints.Internal) + s.rpcServer.Register(s.endpoints.ACL) list, err := net.ListenTCP("tcp", s.config.RPCAddr) if err != nil { From 1b6806872d4d0e0e7f439e32552ccb646f2ea6cf Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 17:24:48 -0700 Subject: [PATCH 10/56] consul: ACL Endpoint tests --- consul/acl_endpoint_test.go | 158 ++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 consul/acl_endpoint_test.go diff --git a/consul/acl_endpoint_test.go b/consul/acl_endpoint_test.go new file mode 100644 index 0000000000..7f08787972 --- /dev/null +++ b/consul/acl_endpoint_test.go @@ -0,0 +1,158 @@ +package consul + +import ( + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" + "os" + "testing" +) + +func TestACLEndpoint_Apply(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + }, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + id := out + + // Verify + state := s1.fsm.State() + _, s, err := state.ACLGet(out) + if err != nil { + t.Fatalf("err: %v", err) + } + if s == nil { + t.Fatalf("should not be nil") + } + if s.ID != out { + t.Fatalf("bad: %v", s) + } + if s.Name != "User token" { + t.Fatalf("bad: %v", s) + } + + // Do a delete + arg.Op = structs.ACLDelete + arg.ACL.ID = out + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Verify + _, s, err = state.ACLGet(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if s != nil { + t.Fatalf("bad: %v", s) + } +} + +func TestACLEndpoint_Get(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + }, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + getR := structs.ACLSpecificRequest{ + Datacenter: "dc1", + ACL: out, + } + var acls structs.IndexedACLs + if err := client.Call("ACL.Get", &getR, &acls); err != nil { + t.Fatalf("err: %v", err) + } + + if acls.Index == 0 { + t.Fatalf("Bad: %v", acls) + } + if len(acls.ACLs) != 1 { + t.Fatalf("Bad: %v", acls) + } + s := acls.ACLs[0] + if s.ID != out { + t.Fatalf("bad: %v", s) + } +} + +func TestACLEndpoint_List(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + ids := []string{} + for i := 0; i < 5; i++ { + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + }, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + ids = append(ids, out) + } + + getR := structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var acls structs.IndexedACLs + if err := client.Call("ACL.List", &getR, &acls); err != nil { + t.Fatalf("err: %v", err) + } + + if acls.Index == 0 { + t.Fatalf("Bad: %v", acls) + } + if len(acls.ACLs) != 5 { + t.Fatalf("Bad: %v", acls.ACLs) + } + for i := 0; i < len(acls.ACLs); i++ { + s := acls.ACLs[i] + if !strContains(ids, s.ID) { + t.Fatalf("bad: %v", s) + } + if s.Name != "User token" { + t.Fatalf("bad: %v", s) + } + } +} From 22658aa78150fbbc05f38d409792b7fe7a93718a Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 5 Aug 2014 17:50:36 -0700 Subject: [PATCH 11/56] agent: ACL endpoint --- command/agent/acl_endpoint.go | 171 ++++++++++++++++++++++++++++++++++ command/agent/http.go | 7 ++ 2 files changed, 178 insertions(+) create mode 100644 command/agent/acl_endpoint.go diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go new file mode 100644 index 0000000000..cfec2c8f13 --- /dev/null +++ b/command/agent/acl_endpoint.go @@ -0,0 +1,171 @@ +package agent + +import ( + "fmt" + "github.com/hashicorp/consul/consul/structs" + "net/http" + "strings" +) + +// aclCreateResponse is used to wrap the ACL ID +type aclCreateResponse struct { + ID string +} + +func (s *HTTPServer) ACLDelete(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + args := structs.ACLRequest{ + Op: structs.ACLDelete, + } + s.parseDC(req, &args.Datacenter) + + // Pull out the session id + args.ACL.ID = strings.TrimPrefix(req.URL.Path, "/v1/acl/delete/") + if args.ACL.ID == "" { + resp.WriteHeader(400) + resp.Write([]byte("Missing ACL")) + return nil, nil + } + + var out string + if err := s.agent.RPC("ACL.Apply", &args, &out); err != nil { + return nil, err + } + return true, nil +} + +func (s *HTTPServer) ACLCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return s.aclSet(resp, req, false) +} + +func (s *HTTPServer) ACLUpdate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return s.aclSet(resp, req, true) +} + +func (s *HTTPServer) aclSet(resp http.ResponseWriter, req *http.Request, update bool) (interface{}, error) { + // Mandate a PUT request + if req.Method != "PUT" { + resp.WriteHeader(405) + return nil, nil + } + + args := structs.ACLRequest{ + Op: structs.ACLSet, + ACL: structs.ACL{ + Type: structs.ACLTypeClient, + }, + } + s.parseDC(req, &args.Datacenter) + + // Handle optional request body + if req.ContentLength > 0 { + if err := decodeBody(req, &args.ACL, nil); err != nil { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err))) + return nil, nil + } + } + + // Ensure there is no ID set for create + if !update && args.ACL.ID != "" { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("ACL ID cannot be set"))) + return nil, nil + } + + // Ensure there is an ID set for update + if update && args.ACL.ID == "" { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("ACL ID must be set"))) + return nil, nil + } + + // Create the acl, get the ID + var out string + if err := s.agent.RPC("ACL.Apply", &args, &out); err != nil { + return nil, err + } + + // Format the response as a JSON object + return aclCreateResponse{out}, nil +} + +func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + args := structs.ACLSpecificRequest{} + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + // Pull out the session id + args.ACL = strings.TrimPrefix(req.URL.Path, "/v1/acl/clone/") + if args.ACL == "" { + resp.WriteHeader(400) + resp.Write([]byte("Missing ACL")) + return nil, nil + } + + var out structs.IndexedACLs + defer setMeta(resp, &out.QueryMeta) + if err := s.agent.RPC("ACL.Get", &args, &out); err != nil { + return nil, err + } + + // Bail if the ACL is not found + if len(out.ACLs) == 0 { + resp.WriteHeader(404) + resp.Write([]byte(fmt.Sprintf("Target ACL not found"))) + return nil, nil + } + + // Create a new ACL + createArgs := structs.ACLRequest{ + Datacenter: args.Datacenter, + Op: structs.ACLSet, + ACL: *out.ACLs[0], + } + createArgs.ACL.ID = "" + + // Create the acl, get the ID + var outID string + if err := s.agent.RPC("ACL.Apply", &createArgs, &outID); err != nil { + return nil, err + } + + // Format the response as a JSON object + return aclCreateResponse{outID}, nil +} + +func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + args := structs.ACLSpecificRequest{} + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + // Pull out the session id + args.ACL = strings.TrimPrefix(req.URL.Path, "/v1/acl/info/") + if args.ACL == "" { + resp.WriteHeader(400) + resp.Write([]byte("Missing ACL")) + return nil, nil + } + + var out structs.IndexedACLs + defer setMeta(resp, &out.QueryMeta) + if err := s.agent.RPC("ACL.Get", &args, &out); err != nil { + return nil, err + } + return out.ACLs, nil +} + +func (s *HTTPServer) ACLList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + args := structs.DCSpecificRequest{} + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + var out structs.IndexedACLs + defer setMeta(resp, &out.QueryMeta) + if err := s.agent.RPC("ACL.List", &args, &out); err != nil { + return nil, err + } + return out.ACLs, nil +} diff --git a/command/agent/http.go b/command/agent/http.go index a254ecf195..e40a883f82 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -99,6 +99,13 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/session/node/", s.wrap(s.SessionsForNode)) s.mux.HandleFunc("/v1/session/list", s.wrap(s.SessionList)) + s.mux.HandleFunc("/v1/acl/create", s.wrap(s.ACLCreate)) + s.mux.HandleFunc("/v1/acl/delete/", s.wrap(s.ACLDelete)) + s.mux.HandleFunc("/v1/acl/info/", s.wrap(s.ACLGet)) + s.mux.HandleFunc("/v1/acl/update/", s.wrap(s.ACLUpdate)) + s.mux.HandleFunc("/v1/acl/clone/", s.wrap(s.ACLClone)) + s.mux.HandleFunc("/v1/acl/list", s.wrap(s.ACLList)) + if enableDebug { s.mux.HandleFunc("/debug/pprof/", pprof.Index) s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) From 78049ad2400a750f803ffdd324139817ff4a1d43 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 6 Aug 2014 10:30:47 -0700 Subject: [PATCH 12/56] agent: ACL endpoint tests --- command/agent/acl_endpoint.go | 6 +- command/agent/acl_endpoint_test.go | 158 +++++++++++++++++++++++++++++ command/agent/http.go | 2 +- consul/acl_endpoint.go | 28 +++-- 4 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 command/agent/acl_endpoint_test.go diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index cfec2c8f13..0e291ebbcb 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -18,7 +18,7 @@ func (s *HTTPServer) ACLDelete(resp http.ResponseWriter, req *http.Request) (int } s.parseDC(req, &args.Datacenter) - // Pull out the session id + // Pull out the acl id args.ACL.ID = strings.TrimPrefix(req.URL.Path, "/v1/acl/delete/") if args.ACL.ID == "" { resp.WriteHeader(400) @@ -95,7 +95,7 @@ func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (inte return nil, nil } - // Pull out the session id + // Pull out the acl id args.ACL = strings.TrimPrefix(req.URL.Path, "/v1/acl/clone/") if args.ACL == "" { resp.WriteHeader(400) @@ -140,7 +140,7 @@ func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interf return nil, nil } - // Pull out the session id + // Pull out the acl id args.ACL = strings.TrimPrefix(req.URL.Path, "/v1/acl/info/") if args.ACL == "" { resp.WriteHeader(400) diff --git a/command/agent/acl_endpoint_test.go b/command/agent/acl_endpoint_test.go new file mode 100644 index 0000000000..fc7f29766f --- /dev/null +++ b/command/agent/acl_endpoint_test.go @@ -0,0 +1,158 @@ +package agent + +import ( + "bytes" + "encoding/json" + "github.com/hashicorp/consul/consul/structs" + "net/http" + "net/http/httptest" + "testing" +) + +func makeTestACL(t *testing.T, srv *HTTPServer) string { + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "Name": "User Token", + "Type": "client", + "Rules": "", + } + enc.Encode(raw) + + req, err := http.NewRequest("PUT", "/v1/acl/create", body) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := httptest.NewRecorder() + obj, err := srv.ACLCreate(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + aclResp := obj.(aclCreateResponse) + return aclResp.ID +} + +func TestACLUpdate(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + id := makeTestACL(t, srv) + + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "ID": id, + "Name": "User Token 2", + "Type": "client", + "Rules": "", + } + enc.Encode(raw) + + req, err := http.NewRequest("PUT", "/v1/acl/update", body) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := httptest.NewRecorder() + obj, err := srv.ACLUpdate(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + aclResp := obj.(aclCreateResponse) + if aclResp.ID != id { + t.Fatalf("bad: %v", aclResp) + } + }) +} + +func TestACLDelete(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + id := makeTestACL(t, srv) + req, err := http.NewRequest("PUT", "/v1/session/delete/"+id, nil) + resp := httptest.NewRecorder() + obj, err := srv.ACLDelete(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp := obj.(bool); !resp { + t.Fatalf("should work") + } + }) +} + +func TestACLClone(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + id := makeTestACL(t, srv) + + req, err := http.NewRequest("GET", + "/v1/acl/clone/"+id, nil) + resp := httptest.NewRecorder() + obj, err := srv.ACLClone(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + aclResp, ok := obj.(aclCreateResponse) + if !ok { + t.Fatalf("should work: %#v %#v", obj, resp) + } + if aclResp.ID == id { + t.Fatalf("bad id") + } + + req, err = http.NewRequest("GET", + "/v1/acl/info/"+aclResp.ID, nil) + resp = httptest.NewRecorder() + obj, err = srv.ACLGet(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.ACLs) + if !ok { + t.Fatalf("should work") + } + if len(respObj) != 1 { + t.Fatalf("bad: %v", respObj) + } + }) +} + +func TestACLGet(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + id := makeTestACL(t, srv) + + req, err := http.NewRequest("GET", + "/v1/acl/info/"+id, nil) + resp := httptest.NewRecorder() + obj, err := srv.ACLGet(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.ACLs) + if !ok { + t.Fatalf("should work") + } + if len(respObj) != 1 { + t.Fatalf("bad: %v", respObj) + } + }) +} + +func TestACLList(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + var ids []string + for i := 0; i < 10; i++ { + ids = append(ids, makeTestACL(t, srv)) + } + + req, err := http.NewRequest("GET", "/v1/acl/list", nil) + resp := httptest.NewRecorder() + obj, err := srv.ACLList(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.ACLs) + if !ok { + t.Fatalf("should work") + } + if len(respObj) != 10 { + t.Fatalf("bad: %v", respObj) + } + }) +} diff --git a/command/agent/http.go b/command/agent/http.go index e40a883f82..45443547b2 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -100,9 +100,9 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/session/list", s.wrap(s.SessionList)) s.mux.HandleFunc("/v1/acl/create", s.wrap(s.ACLCreate)) + s.mux.HandleFunc("/v1/acl/update", s.wrap(s.ACLUpdate)) s.mux.HandleFunc("/v1/acl/delete/", s.wrap(s.ACLDelete)) s.mux.HandleFunc("/v1/acl/info/", s.wrap(s.ACLGet)) - s.mux.HandleFunc("/v1/acl/update/", s.wrap(s.ACLUpdate)) s.mux.HandleFunc("/v1/acl/clone/", s.wrap(s.ACLClone)) s.mux.HandleFunc("/v1/acl/list", s.wrap(s.ACLList)) diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index ac9b0cdab1..89c7ee6d6c 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -20,16 +20,26 @@ func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error { } defer metrics.MeasureSince([]string{"consul", "acl", "apply"}, time.Now()) - // Verify the args - switch args.ACL.Type { - case structs.ACLTypeClient: - case structs.ACLTypeManagement: - default: - return fmt.Errorf("Invalid ACL Type") - } + switch args.Op { + case structs.ACLSet: + // Verify the ACL type + switch args.ACL.Type { + case structs.ACLTypeClient: + case structs.ACLTypeManagement: + default: + return fmt.Errorf("Invalid ACL Type") + } - // TODO: Verify ACL compiles... - if args.Op == structs.ACLSet { + // TODO: Validate the rules compile + // + + case structs.ACLDelete: + if args.ACL.ID == "" { + return fmt.Errorf("Missing ACL ID") + } + + default: + return fmt.Errorf("Invalid ACL Operation") } // Apply the update From 7a1d77847459cf6bcb271201f784a14e27330112 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 6 Aug 2014 15:08:17 -0700 Subject: [PATCH 13/56] acl: First pass --- acl/acl.go | 118 +++++++++++++++++++++++++++++++++++++ acl/acl_test.go | 142 +++++++++++++++++++++++++++++++++++++++++++++ acl/policy.go | 50 ++++++++++++++++ acl/policy_test.go | 52 +++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 acl/acl.go create mode 100644 acl/acl_test.go create mode 100644 acl/policy.go create mode 100644 acl/policy_test.go diff --git a/acl/acl.go b/acl/acl.go new file mode 100644 index 0000000000..51395948c3 --- /dev/null +++ b/acl/acl.go @@ -0,0 +1,118 @@ +package acl + +import ( + "fmt" + + "github.com/armon/go-radix" +) + +var ( + // allowAll is a singleton policy which allows all actions + allowAll ACL + + // denyAll is a singleton policy which denies all actions + denyAll ACL +) + +func init() { + // Setup the singletons + allowAll = &StaticACL{defaultAllow: true} + denyAll = &StaticACL{defaultAllow: false} +} + +// ACL is the interface for policy enforcement. +type ACL interface { + KeyRead(string) bool + KeyWrite(string) bool +} + +// StaticACL is used to implement a base ACL policy. It either +// allows or denies all requests. This can be used as a parent +// ACL to act in a blacklist or whitelist mode. +type StaticACL struct { + defaultAllow bool +} + +func (s *StaticACL) KeyRead(string) bool { + return s.defaultAllow +} + +func (s *StaticACL) KeyWrite(string) bool { + return s.defaultAllow +} + +// AllowAll returns an ACL rule that allows all operations +func AllowAll() ACL { + return allowAll +} + +// DenyAll returns an ACL rule that denies all operations +func DenyAll() ACL { + return denyAll +} + +// PolicyACL is used to wrap a set of ACL policies to provide +// the ACL interface. +type PolicyACL struct { + // parent is used to resolve policy if we have + // no matching rule. + parent ACL + + // keyRead contains the read policies + keyRead *radix.Tree + + // keyWrite contains the write policies + keyWrite *radix.Tree +} + +// New is used to construct a policy based ACL from a set of policies +// and a parent policy to resolve missing cases. +func New(parent ACL, policy *Policy) (*PolicyACL, error) { + p := &PolicyACL{ + parent: parent, + keyRead: radix.New(), + keyWrite: radix.New(), + } + + // Load the key policy + for _, kp := range policy.Keys { + switch kp.Policy { + case KeyPolicyDeny: + p.keyRead.Insert(kp.Prefix, false) + p.keyWrite.Insert(kp.Prefix, false) + case KeyPolicyRead: + p.keyRead.Insert(kp.Prefix, true) + p.keyWrite.Insert(kp.Prefix, false) + case KeyPolicyWrite: + p.keyRead.Insert(kp.Prefix, true) + p.keyWrite.Insert(kp.Prefix, true) + default: + return nil, fmt.Errorf("Invalid key policy: %#v", kp) + } + } + return p, nil +} + +// KeyRead returns if a key is allowed to be read +func (p *PolicyACL) KeyRead(key string) bool { + // Look for a matching rule + _, rule, ok := p.keyRead.LongestPrefix(key) + if ok { + return rule.(bool) + } + + // No matching rule, use the parent. + return p.parent.KeyRead(key) +} + +// KeyWrite returns if a key is allowed to be written +func (p *PolicyACL) KeyWrite(key string) bool { + // Look for a matching rule + _, rule, ok := p.keyWrite.LongestPrefix(key) + if ok { + return rule.(bool) + } + + // No matching rule, use the parent. + return p.parent.KeyWrite(key) +} diff --git a/acl/acl_test.go b/acl/acl_test.go new file mode 100644 index 0000000000..baf327e092 --- /dev/null +++ b/acl/acl_test.go @@ -0,0 +1,142 @@ +package acl + +import ( + "testing" +) + +func TestStaticACL(t *testing.T) { + all := AllowAll() + if _, ok := all.(*StaticACL); !ok { + t.Fatalf("expected static") + } + + none := DenyAll() + if _, ok := none.(*StaticACL); !ok { + t.Fatalf("expected static") + } + + if !all.KeyRead("foobar") { + t.Fatalf("should allow") + } + if !all.KeyWrite("foobar") { + t.Fatalf("should allow") + } + + if none.KeyRead("foobar") { + t.Fatalf("should not allow") + } + if none.KeyWrite("foobar") { + t.Fatalf("should not allow") + } +} + +func TestPolicyACL(t *testing.T) { + all := AllowAll() + policy := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "foo/", + Policy: KeyPolicyWrite, + }, + &KeyPolicy{ + Prefix: "foo/priv/", + Policy: KeyPolicyDeny, + }, + &KeyPolicy{ + Prefix: "bar/", + Policy: KeyPolicyDeny, + }, + &KeyPolicy{ + Prefix: "zip/", + Policy: KeyPolicyRead, + }, + }, + } + acl, err := New(all, policy) + if err != nil { + t.Fatalf("err: %v", err) + } + + type tcase struct { + inp string + read bool + write bool + } + cases := []tcase{ + {"other", true, true}, + {"foo/test", true, true}, + {"foo/priv/test", false, false}, + {"bar/any", false, false}, + {"zip/test", true, false}, + } + for _, c := range cases { + if c.read != acl.KeyRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.KeyWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } +} + +func TestPolicyACL_Parent(t *testing.T) { + deny := DenyAll() + policyRoot := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "foo/", + Policy: KeyPolicyWrite, + }, + &KeyPolicy{ + Prefix: "bar/", + Policy: KeyPolicyRead, + }, + }, + } + root, err := New(deny, policyRoot) + if err != nil { + t.Fatalf("err: %v", err) + } + + policy := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "foo/priv/", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "bar/", + Policy: KeyPolicyDeny, + }, + &KeyPolicy{ + Prefix: "zip/", + Policy: KeyPolicyRead, + }, + }, + } + acl, err := New(root, policy) + if err != nil { + t.Fatalf("err: %v", err) + } + + type tcase struct { + inp string + read bool + write bool + } + cases := []tcase{ + {"other", false, false}, + {"foo/test", true, true}, + {"foo/priv/test", true, false}, + {"bar/any", false, false}, + {"zip/test", true, false}, + } + for _, c := range cases { + if c.read != acl.KeyRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.KeyWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } +} diff --git a/acl/policy.go b/acl/policy.go new file mode 100644 index 0000000000..2abdc9812e --- /dev/null +++ b/acl/policy.go @@ -0,0 +1,50 @@ +package acl + +import ( + "fmt" + "github.com/hashicorp/hcl" +) + +// KeyPolicyType controls the various access levels for keys +type KeyPolicyType string + +const ( + KeyPolicyDeny = "deny" + KeyPolicyRead = "read" + KeyPolicyWrite = "write" +) + +// Policy is used to represent the policy specified by +// an ACL configuration. +type Policy struct { + Keys []*KeyPolicy `hcl:"key"` +} + +// KeyPolicy represents a policy for a key +type KeyPolicy struct { + Prefix string `hcl:",key"` + Policy KeyPolicyType +} + +// Parse is used to parse the specified ACL rules into an +// intermediary set of policies, before being compiled into +// the ACL +func Parse(rules string) (*Policy, error) { + // Decode the rules + p := &Policy{} + if err := hcl.Decode(p, rules); err != nil { + return nil, fmt.Errorf("Failed to parse ACL rules: %v", err) + } + + // Validate the key policy + for _, kp := range p.Keys { + switch kp.Policy { + case KeyPolicyDeny: + case KeyPolicyRead: + case KeyPolicyWrite: + default: + return nil, fmt.Errorf("Invalid key policy: %#v", kp) + } + } + return p, nil +} diff --git a/acl/policy_test.go b/acl/policy_test.go new file mode 100644 index 0000000000..c389f3b259 --- /dev/null +++ b/acl/policy_test.go @@ -0,0 +1,52 @@ +package acl + +import ( + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + inp := ` +key "" { + policy = "read" +} +key "foo/" { + policy = "write" +} +key "foo/bar/" { + policy = "read" +} +key "foo/bar/baz" { + polizy = "deny" +} + ` + exp := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "foo/", + Policy: KeyPolicyWrite, + }, + &KeyPolicy{ + Prefix: "foo/bar/", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "foo/bar/baz", + Policy: KeyPolicyDeny, + }, + }, + } + + out, err := Parse(inp) + if err != nil { + t.Fatalf("err: %v", err) + } + + if reflect.DeepEqual(out, exp) { + t.Fatalf("bad: %#v", out) + } +} From 6e9792dc3733ad4aae32b725a49efe95a7ea837e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 14:36:09 -0700 Subject: [PATCH 14/56] acl: Adding caching mechanism --- acl/cache.go | 89 +++++++++++++++++++++++++++++++ acl/cache_test.go | 130 ++++++++++++++++++++++++++++++++++++++++++++++ acl/policy.go | 5 ++ 3 files changed, 224 insertions(+) create mode 100644 acl/cache.go create mode 100644 acl/cache_test.go diff --git a/acl/cache.go b/acl/cache.go new file mode 100644 index 0000000000..ad744fb8f0 --- /dev/null +++ b/acl/cache.go @@ -0,0 +1,89 @@ +package acl + +import ( + "crypto/md5" + "fmt" + + "github.com/hashicorp/golang-lru" +) + +// FaultFunc is a function used to fault in the rules for an +// ACL given it's ID +type FaultFunc func(id string) (string, error) + +// Cache is used to implement policy and ACL caching +type Cache struct { + aclCache *lru.Cache + faultfn FaultFunc + parent ACL + policyCache *lru.Cache +} + +// NewCache contructs a new policy and ACL cache of a given size +func NewCache(size int, parent ACL, faultfn FaultFunc) (*Cache, error) { + if size <= 0 { + return nil, fmt.Errorf("Must provide positive cache size") + } + pc, _ := lru.New(size) + ac, _ := lru.New(size) + c := &Cache{ + aclCache: ac, + faultfn: faultfn, + parent: parent, + policyCache: pc, + } + return c, nil +} + +// GetPolicy is used to get a potentially cached policy set. +// If not cached, it will be parsed, and then cached. +func (c *Cache) GetPolicy(rules string) (*Policy, error) { + hash := fmt.Sprintf("%x", md5.Sum([]byte(rules))) + raw, ok := c.policyCache.Get(hash) + if ok { + return raw.(*Policy), nil + } + policy, err := Parse(rules) + if err != nil { + return nil, err + } + c.policyCache.Add(hash, policy) + return policy, nil +} + +// GetACL is used to get a potentially cached ACL policy. +// If not cached, it will be generated and then cached. +func (c *Cache) GetACL(id string) (ACL, error) { + // Look for the ACL directly + raw, ok := c.aclCache.Get(id) + if ok { + return raw.(ACL), nil + } + + // Get the rules + rules, err := c.faultfn(id) + if err != nil { + return nil, err + } + + // Get the policy + policy, err := c.GetPolicy(rules) + if err != nil { + return nil, err + } + + // Get the ACL + acl, err := New(c.parent, policy) + if err != nil { + return nil, err + } + + // Cache and return the ACL + c.aclCache.Add(id, acl) + return acl, nil +} + +// ClearACL is used to clear the ACL cache if any +func (c *Cache) ClearACL(id string) { + c.aclCache.Remove(id) +} diff --git a/acl/cache_test.go b/acl/cache_test.go new file mode 100644 index 0000000000..47602d6399 --- /dev/null +++ b/acl/cache_test.go @@ -0,0 +1,130 @@ +package acl + +import ( + "testing" +) + +func TestCache_GetPolicy(t *testing.T) { + c, err := NewCache(1, AllowAll(), nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + p, err := c.GetPolicy("") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Should get the same policy + p1, err := c.GetPolicy("") + if err != nil { + t.Fatalf("err: %v", err) + } + if p != p1 { + t.Fatalf("should be cached") + } + + // Cache a new policy + _, err = c.GetPolicy(testSimplePolicy) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Test invalidation of p + p3, err := c.GetPolicy("") + if err != nil { + t.Fatalf("err: %v", err) + } + if p == p3 { + t.Fatalf("should be not cached") + } +} + +func TestCache_GetACL(t *testing.T) { + policies := map[string]string{ + "foo": testSimplePolicy, + "bar": testSimplePolicy, + } + faultfn := func(id string) (string, error) { + return policies[id], nil + } + + c, err := NewCache(1, DenyAll(), faultfn) + if err != nil { + t.Fatalf("err: %v", err) + } + + acl, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if acl.KeyRead("bar/test") { + t.Fatalf("should deny") + } + if !acl.KeyRead("foo/test") { + t.Fatalf("should allow") + } + + acl2, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if acl != acl2 { + t.Fatalf("should be cached") + } + + // Invalidate cache + _, err = c.GetACL("bar") + if err != nil { + t.Fatalf("err: %v", err) + } + + acl3, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if acl == acl3 { + t.Fatalf("should not be cached") + } +} + +func TestCache_ClearACL(t *testing.T) { + policies := map[string]string{ + "foo": testSimplePolicy, + "bar": testSimplePolicy, + } + faultfn := func(id string) (string, error) { + return policies[id], nil + } + + c, err := NewCache(1, DenyAll(), faultfn) + if err != nil { + t.Fatalf("err: %v", err) + } + + acl, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Nuke the cache + c.ClearACL("foo") + + acl2, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if acl == acl2 { + t.Fatalf("should not be cached") + } +} + +var testSimplePolicy = ` +key "foo/" { + policy = "read" +} +` diff --git a/acl/policy.go b/acl/policy.go index 2abdc9812e..df4af9c065 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -32,6 +32,11 @@ type KeyPolicy struct { func Parse(rules string) (*Policy, error) { // Decode the rules p := &Policy{} + if rules == "" { + // Hot path for empty rules + return p, nil + } + if err := hcl.Decode(p, rules); err != nil { return nil, fmt.Errorf("Failed to parse ACL rules: %v", err) } From 1abfd6c0509134b01e092ad96ae268d6021ee693 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 15:25:11 -0700 Subject: [PATCH 15/56] acl: Adding cached policy fetch via ACL --- acl/cache.go | 51 ++++++++++++++++++++++++++++++++++++++++------ acl/cache_test.go | 42 ++++++++++++++++++++++++++++++++++++++ acl/policy_test.go | 2 +- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/acl/cache.go b/acl/cache.go index ad744fb8f0..76a493aa04 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -11,6 +11,12 @@ import ( // ACL given it's ID type FaultFunc func(id string) (string, error) +// aclEntry allows us to store the ACL with it's policy ID +type aclEntry struct { + ACL ACL + PolicyID string +} + // Cache is used to implement policy and ACL caching type Cache struct { aclCache *lru.Cache @@ -38,8 +44,13 @@ func NewCache(size int, parent ACL, faultfn FaultFunc) (*Cache, error) { // GetPolicy is used to get a potentially cached policy set. // If not cached, it will be parsed, and then cached. func (c *Cache) GetPolicy(rules string) (*Policy, error) { - hash := fmt.Sprintf("%x", md5.Sum([]byte(rules))) - raw, ok := c.policyCache.Get(hash) + return c.getPolicy(c.ruleID(rules), rules) +} + +// getPolicy is an internal method to get a cached policy, +// but it assumes a pre-computed ID +func (c *Cache) getPolicy(id, rules string) (*Policy, error) { + raw, ok := c.policyCache.Get(id) if ok { return raw.(*Policy), nil } @@ -47,8 +58,35 @@ func (c *Cache) GetPolicy(rules string) (*Policy, error) { if err != nil { return nil, err } - c.policyCache.Add(hash, policy) + c.policyCache.Add(id, policy) return policy, nil + +} + +// ruleID is used to generate an ID for a rule +func (c *Cache) ruleID(rules string) string { + return fmt.Sprintf("%x", md5.Sum([]byte(rules))) +} + +// GetACLPolicy is used to get the potentially cached ACL +// policy. If not cached, it will be generated and then cached. +func (c *Cache) GetACLPolicy(id string) (*Policy, error) { + // Check for a cached acl + if raw, ok := c.aclCache.Get(id); ok { + cached := raw.(aclEntry) + if raw, ok := c.policyCache.Get(cached.PolicyID); ok { + return raw.(*Policy), nil + } + } + + // Fault in the rules + rules, err := c.faultfn(id) + if err != nil { + return nil, err + } + + // Get cached + return c.GetPolicy(rules) } // GetACL is used to get a potentially cached ACL policy. @@ -57,7 +95,7 @@ func (c *Cache) GetACL(id string) (ACL, error) { // Look for the ACL directly raw, ok := c.aclCache.Get(id) if ok { - return raw.(ACL), nil + return raw.(aclEntry).ACL, nil } // Get the rules @@ -65,9 +103,10 @@ func (c *Cache) GetACL(id string) (ACL, error) { if err != nil { return nil, err } + ruleID := c.ruleID(rules) // Get the policy - policy, err := c.GetPolicy(rules) + policy, err := c.getPolicy(ruleID, rules) if err != nil { return nil, err } @@ -79,7 +118,7 @@ func (c *Cache) GetACL(id string) (ACL, error) { } // Cache and return the ACL - c.aclCache.Add(id, acl) + c.aclCache.Add(id, aclEntry{acl, ruleID}) return acl, nil } diff --git a/acl/cache_test.go b/acl/cache_test.go index 47602d6399..96b4bf0c88 100644 --- a/acl/cache_test.go +++ b/acl/cache_test.go @@ -123,6 +123,48 @@ func TestCache_ClearACL(t *testing.T) { } } +func TestCache_GetACLPolicy(t *testing.T) { + policies := map[string]string{ + "foo": testSimplePolicy, + "bar": testSimplePolicy, + } + faultfn := func(id string) (string, error) { + return policies[id], nil + } + c, err := NewCache(1, DenyAll(), faultfn) + if err != nil { + t.Fatalf("err: %v", err) + } + + p, err := c.GetPolicy(testSimplePolicy) + if err != nil { + t.Fatalf("err: %v", err) + } + + _, err = c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + p2, err := c.GetACLPolicy("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if p2 != p { + t.Fatalf("expected cached policy") + } + + p3, err := c.GetACLPolicy("bar") + if err != nil { + t.Fatalf("err: %v", err) + } + + if p3 != p { + t.Fatalf("expected cached policy") + } +} + var testSimplePolicy = ` key "foo/" { policy = "read" diff --git a/acl/policy_test.go b/acl/policy_test.go index c389f3b259..35f322e6a6 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -17,7 +17,7 @@ key "foo/bar/" { policy = "read" } key "foo/bar/baz" { - polizy = "deny" + policy = "deny" } ` exp := &Policy{ From 97a737b1ee33f4d0f729bff330e9836155a41bea Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 15:32:43 -0700 Subject: [PATCH 16/56] consul: Pulling in ACLs --- consul/acl.go | 79 +++++++++++++++++++++++++++++++++++++ consul/acl_endpoint.go | 20 ++++++++++ consul/acl_endpoint_test.go | 45 ++++++++++++++++++++- consul/server.go | 35 ++++++++++++++++ consul/structs/structs.go | 10 ++++- 5 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 consul/acl.go diff --git a/consul/acl.go b/consul/acl.go new file mode 100644 index 0000000000..29fe4c5f64 --- /dev/null +++ b/consul/acl.go @@ -0,0 +1,79 @@ +package consul + +import ( + "fmt" + "time" + + "github.com/hashicorp/consul/acl" +) + +// aclCacheEntry is used to cache non-authoritative ACL's +// If non-authoritative, then we must respect a TTL +type aclCacheEntry struct { + ACL acl.ACL + TTL time.Duration + Expires time.Time +} + +// aclFault is used to fault in the rules for an ACL if we take a miss +func (s *Server) aclFault(id string) (string, error) { + state := s.fsm.State() + _, acl, err := state.ACLGet(id) + if err != nil { + return "", err + } + if acl == nil { + return "", fmt.Errorf("ACL not found: %s", id) + } + return acl.Rules, nil +} + +// resolveToken is used to resolve an ACL is any is appropriate +func (s *Server) resolveToken(id string) (acl.ACL, error) { + // Check if there is no ACL datacenter (ACL's disabled) + authDC := s.config.ACLDatacenter + if authDC == "" { + return nil, nil + } + + // Check if we are the ACL datacenter and the leader, use the + // authoritative cache + if s.config.Datacenter == authDC && s.IsLeader() { + return s.aclAuthCache.GetACL(id) + } + + // Use our non-authoritative cache + return s.lookupACL(id) +} + +// lookupACL is used when we are non-authoritative, and need +// to resolve an ACL +func (s *Server) lookupACL(id string) (acl.ACL, error) { + // Check the cache for the ACL + var cached *aclCacheEntry + raw, ok := s.aclCache.Get(id) + if ok { + cached = raw.(*aclCacheEntry) + } + + // Check for live cache + if cached != nil && time.Now().Before(cached.Expires) { + return cached.ACL, nil + } + + // Attempt to refresh the policy + // TODO: GetPolicy... + + // Unable to refresh, apply the down policy + switch s.config.ACLDownPolicy { + case "allow": + return acl.AllowAll(), nil + case "extend-cache": + if cached != nil { + return cached.ACL, nil + } + fallthrough + default: + return acl.DenyAll(), nil + } +} diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index 89c7ee6d6c..908d2fcb53 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -81,6 +81,26 @@ func (a *ACL) Get(args *structs.ACLSpecificRequest, }) } +// GetPolicy is used to retrieve a compiled policy object with a TTL. Does not +// support a blocking query. +func (a *ACL) GetPolicy(args *structs.ACLSpecificRequest, reply *structs.ACLPolicy) error { + if done, err := a.srv.forward("ACL.GetPolicy", args, args, reply); done { + return err + } + + // Get the policy via the cache + policy, err := a.srv.aclAuthCache.GetACLPolicy(args.ACL) + if err != nil { + return err + } + + // Setup the response + reply.Policy = policy + reply.TTL = a.srv.config.ACLTTL + a.srv.setQueryMeta(&reply.QueryMeta) + return nil +} + // List is used to list all the ACLs func (a *ACL) List(args *structs.DCSpecificRequest, reply *structs.IndexedACLs) error { diff --git a/consul/acl_endpoint_test.go b/consul/acl_endpoint_test.go index 7f08787972..032df55206 100644 --- a/consul/acl_endpoint_test.go +++ b/consul/acl_endpoint_test.go @@ -1,10 +1,12 @@ package consul import ( - "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/consul/testutil" "os" "testing" + "time" + + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" ) func TestACLEndpoint_Apply(t *testing.T) { @@ -106,6 +108,45 @@ func TestACLEndpoint_Get(t *testing.T) { } } +func TestACLEndpoint_GetPolicy(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + }, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + getR := structs.ACLSpecificRequest{ + Datacenter: "dc1", + ACL: out, + } + var acls structs.ACLPolicy + if err := client.Call("ACL.GetPolicy", &getR, &acls); err != nil { + t.Fatalf("err: %v", err) + } + + if acls.Policy == nil { + t.Fatalf("Bad: %v", acls) + } + if acls.TTL != 30*time.Second { + t.Fatalf("bad: %v", acls) + } +} + func TestACLEndpoint_List(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) diff --git a/consul/server.go b/consul/server.go index b07522e156..5e0465213f 100644 --- a/consul/server.go +++ b/consul/server.go @@ -15,6 +15,8 @@ import ( "sync" "time" + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/golang-lru" "github.com/hashicorp/raft" "github.com/hashicorp/raft-mdb" "github.com/hashicorp/serf/serf" @@ -43,11 +45,21 @@ const ( // serverMaxStreams controsl how many idle streams we keep // open to a server serverMaxStreams = 64 + + // Maximum number of cached ACL entries + aclCacheSize = 256 ) // Server is Consul server which manages the service discovery, // health checking, DC forwarding, Raft, and multiple Serf pools. type Server struct { + // aclAuthCache is the authoritative ACL cache + aclAuthCache *acl.Cache + + // aclCache is a non-authoritative ACL cache + aclCache *lru.Cache + + // Consul configuration config *Config // Connection pool to other consul servers @@ -181,6 +193,29 @@ func NewServer(config *Config) (*Server, error) { shutdownCh: make(chan struct{}), } + // Determine the ACL root policy + var aclRoot acl.ACL + switch config.ACLDefaultPolicy { + case "allow": + aclRoot = acl.AllowAll() + case "deny": + aclRoot = acl.DenyAll() + } + + // Initialize the authoritative ACL cache + s.aclAuthCache, err = acl.NewCache(aclCacheSize, aclRoot, s.aclFault) + if err != nil { + s.Shutdown() + return nil, fmt.Errorf("Failed to create ACL cache: %v", err) + } + + // Initialize the non-authoritative ACL cache + s.aclCache, err = lru.New(aclCacheSize) + if err != nil { + s.Shutdown() + return nil, fmt.Errorf("Failed to create ACL cache: %v", err) + } + // Initialize the RPC layer if err := s.setupRPC(tlsConfig); err != nil { s.Shutdown() diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 27cffd0b28..3c9b78c9a8 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -3,8 +3,10 @@ package structs import ( "bytes" "fmt" - "github.com/ugorji/go/codec" "time" + + "github.com/hashicorp/consul/acl" + "github.com/ugorji/go/codec" ) var ( @@ -469,6 +471,12 @@ type IndexedACLs struct { QueryMeta } +type ACLPolicy struct { + Policy *acl.Policy + TTL time.Duration + QueryMeta +} + // msgpackHandle is a shared handle for encoding/decoding of structs var msgpackHandle = &codec.MsgpackHandle{} From 338f11c6cfe701c9e4c437ab3484907ab50fbf0e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 15:52:52 -0700 Subject: [PATCH 17/56] consul: Enable ACL lookup --- consul/acl.go | 58 +++++++++++++++++++++++++++++++++++---- consul/acl_endpoint.go | 4 ++- consul/structs/structs.go | 1 + 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index 29fe4c5f64..b35ace94f5 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -1,10 +1,17 @@ package consul import ( - "fmt" + "errors" + "strings" "time" "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/consul/structs" +) + +const ( + // aclNotFound indicates there is no matching ACL + aclNotFound = "ACL not found" ) // aclCacheEntry is used to cache non-authoritative ACL's @@ -23,7 +30,7 @@ func (s *Server) aclFault(id string) (string, error) { return "", err } if acl == nil { - return "", fmt.Errorf("ACL not found: %s", id) + return "", errors.New(aclNotFound) } return acl.Rules, nil } @@ -43,12 +50,12 @@ func (s *Server) resolveToken(id string) (acl.ACL, error) { } // Use our non-authoritative cache - return s.lookupACL(id) + return s.lookupACL(id, authDC) } // lookupACL is used when we are non-authoritative, and need // to resolve an ACL -func (s *Server) lookupACL(id string) (acl.ACL, error) { +func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { // Check the cache for the ACL var cached *aclCacheEntry raw, ok := s.aclCache.Get(id) @@ -62,7 +69,48 @@ func (s *Server) lookupACL(id string) (acl.ACL, error) { } // Attempt to refresh the policy - // TODO: GetPolicy... + args := structs.ACLSpecificRequest{ + Datacenter: authDC, + ACL: id, + } + var out structs.ACLPolicy + err := s.RPC("ACL.GetPolicy", &args, &out) + + // Handle the happy path + if err == nil { + // Determine the root + var root acl.ACL + switch out.Root { + case "allow": + root = acl.AllowAll() + default: + root = acl.DenyAll() + } + + // Compile the ACL + acl, err := acl.New(root, out.Policy) + if err != nil { + return nil, err + } + + // Cache the ACL + cached := &aclCacheEntry{ + ACL: acl, + TTL: out.TTL, + } + if out.TTL > 0 { + cached.Expires = time.Now().Add(out.TTL) + } + s.aclCache.Add(id, cached) + return acl, nil + } + + // Check for not-found + if strings.Contains(err.Error(), aclNotFound) { + return nil, errors.New(aclNotFound) + } else { + s.logger.Printf("[ERR] consul.acl: Failed to get policy for '%s': %v", id, err) + } // Unable to refresh, apply the down policy switch s.config.ACLDownPolicy { diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index 908d2fcb53..4c49f189aa 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -95,8 +95,10 @@ func (a *ACL) GetPolicy(args *structs.ACLSpecificRequest, reply *structs.ACLPoli } // Setup the response + conf := a.srv.config reply.Policy = policy - reply.TTL = a.srv.config.ACLTTL + reply.Root = conf.ACLDefaultPolicy + reply.TTL = conf.ACLTTL a.srv.setQueryMeta(&reply.QueryMeta) return nil } diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 3c9b78c9a8..317af5ee32 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -472,6 +472,7 @@ type IndexedACLs struct { } type ACLPolicy struct { + Root string Policy *acl.Policy TTL time.Duration QueryMeta From 50ba1f60670eda8a59f7d86604415640c89fd4df Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 15:57:28 -0700 Subject: [PATCH 18/56] acl: Change types --- acl/policy.go | 9 +++++---- acl/policy_test.go | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/acl/policy.go b/acl/policy.go index df4af9c065..5e337f94e8 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -5,9 +5,6 @@ import ( "github.com/hashicorp/hcl" ) -// KeyPolicyType controls the various access levels for keys -type KeyPolicyType string - const ( KeyPolicyDeny = "deny" KeyPolicyRead = "read" @@ -23,7 +20,11 @@ type Policy struct { // KeyPolicy represents a policy for a key type KeyPolicy struct { Prefix string `hcl:",key"` - Policy KeyPolicyType + Policy string +} + +func (k *KeyPolicy) GoString() string { + return fmt.Sprintf("%#v", *k) } // Parse is used to parse the specified ACL rules into an diff --git a/acl/policy_test.go b/acl/policy_test.go index 35f322e6a6..6db3317c08 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -46,7 +46,7 @@ key "foo/bar/baz" { t.Fatalf("err: %v", err) } - if reflect.DeepEqual(out, exp) { - t.Fatalf("bad: %#v", out) + if !reflect.DeepEqual(out, exp) { + t.Fatalf("bad: %#v %#v", out, exp) } } From b5c9e651753170047cc3a3efcd4c12c53685d47b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 16:00:32 -0700 Subject: [PATCH 19/56] consul: Verify compilation of rules --- consul/acl_endpoint.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index 4c49f189aa..eb6d4b20aa 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -2,9 +2,11 @@ package consul import ( "fmt" - "github.com/armon/go-metrics" - "github.com/hashicorp/consul/consul/structs" "time" + + "github.com/armon/go-metrics" + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/consul/structs" ) // ACL endpoint is used to manipulate ACLs @@ -30,8 +32,11 @@ func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error { return fmt.Errorf("Invalid ACL Type") } - // TODO: Validate the rules compile - // + // Validate the rules compile + _, err := acl.Parse(args.ACL.Rules) + if err != nil { + return fmt.Errorf("ACL rule compilation failed: %v", err) + } case structs.ACLDelete: if args.ACL.ID == "" { From 45f358e715744d5e2c1982f40cf8d5a490cac2f2 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 16:51:19 -0700 Subject: [PATCH 20/56] acl: Associate policy ID --- acl/cache.go | 1 + acl/policy.go | 1 + 2 files changed, 2 insertions(+) diff --git a/acl/cache.go b/acl/cache.go index 76a493aa04..a4aa61e717 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -58,6 +58,7 @@ func (c *Cache) getPolicy(id, rules string) (*Policy, error) { if err != nil { return nil, err } + policy.ID = id c.policyCache.Add(id, policy) return policy, nil diff --git a/acl/policy.go b/acl/policy.go index 5e337f94e8..ecc11e1daf 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -14,6 +14,7 @@ const ( // Policy is used to represent the policy specified by // an ACL configuration. type Policy struct { + ID string `hcl:"-"` Keys []*KeyPolicy `hcl:"key"` } From b5e22203fceae83bd886024a11d13247b11466d7 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 16:55:47 -0700 Subject: [PATCH 21/56] consul: Support conditional policy fetch --- consul/acl.go | 2 +- consul/acl_endpoint.go | 14 +++++++++++--- consul/acl_endpoint_test.go | 16 +++++++++++++++- consul/structs/structs.go | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index b35ace94f5..95d76bec32 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -69,7 +69,7 @@ func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { } // Attempt to refresh the policy - args := structs.ACLSpecificRequest{ + args := structs.ACLPolicyRequest{ Datacenter: authDC, ACL: id, } diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index eb6d4b20aa..8761426331 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -88,7 +88,7 @@ func (a *ACL) Get(args *structs.ACLSpecificRequest, // GetPolicy is used to retrieve a compiled policy object with a TTL. Does not // support a blocking query. -func (a *ACL) GetPolicy(args *structs.ACLSpecificRequest, reply *structs.ACLPolicy) error { +func (a *ACL) GetPolicy(args *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { if done, err := a.srv.forward("ACL.GetPolicy", args, args, reply); done { return err } @@ -99,12 +99,20 @@ func (a *ACL) GetPolicy(args *structs.ACLSpecificRequest, reply *structs.ACLPoli return err } - // Setup the response + // Generate an ETag conf := a.srv.config - reply.Policy = policy + etag := fmt.Sprintf("%s:%s", conf.ACLDefaultPolicy, policy.ID) + + // Setup the response + reply.ETag = etag reply.Root = conf.ACLDefaultPolicy reply.TTL = conf.ACLTTL a.srv.setQueryMeta(&reply.QueryMeta) + + // Only send the policy on an Etag mis-match + if args.ETag != etag { + reply.Policy = policy + } return nil } diff --git a/consul/acl_endpoint_test.go b/consul/acl_endpoint_test.go index 032df55206..f3daceccc9 100644 --- a/consul/acl_endpoint_test.go +++ b/consul/acl_endpoint_test.go @@ -130,7 +130,7 @@ func TestACLEndpoint_GetPolicy(t *testing.T) { t.Fatalf("err: %v", err) } - getR := structs.ACLSpecificRequest{ + getR := structs.ACLPolicyRequest{ Datacenter: "dc1", ACL: out, } @@ -145,6 +145,20 @@ func TestACLEndpoint_GetPolicy(t *testing.T) { if acls.TTL != 30*time.Second { t.Fatalf("bad: %v", acls) } + + // Do a conditional lookup with etag + getR.ETag = acls.ETag + var out2 structs.ACLPolicy + if err := client.Call("ACL.GetPolicy", &getR, &out2); err != nil { + t.Fatalf("err: %v", err) + } + + if out2.Policy != nil { + t.Fatalf("Bad: %v", out2) + } + if out2.TTL != 30*time.Second { + t.Fatalf("bad: %v", out2) + } } func TestACLEndpoint_List(t *testing.T) { diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 317af5ee32..b80145c436 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -466,12 +466,26 @@ func (r *ACLSpecificRequest) RequestDatacenter() string { return r.Datacenter } +// ACLPolicyRequest is used to request an ACL by ID, conditionally +// filtering on an ID +type ACLPolicyRequest struct { + Datacenter string + ACL string + ETag string + QueryOptions +} + +func (r *ACLPolicyRequest) RequestDatacenter() string { + return r.Datacenter +} + type IndexedACLs struct { ACLs ACLs QueryMeta } type ACLPolicy struct { + ETag string Root string Policy *acl.Policy TTL time.Duration From ef778699833b160a280b4a90d08c0dfc5f08e34d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 17:37:13 -0700 Subject: [PATCH 22/56] acl: Adding additional tier of caching --- acl/cache.go | 45 +++++++++++++++++++++++++++++---------------- acl/cache_test.go | 11 ++++++++++- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/acl/cache.go b/acl/cache.go index a4aa61e717..f5ef96fb8e 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -19,10 +19,11 @@ type aclEntry struct { // Cache is used to implement policy and ACL caching type Cache struct { - aclCache *lru.Cache + aclCache *lru.Cache // Cache id -> acl faultfn FaultFunc parent ACL - policyCache *lru.Cache + policyCache *lru.Cache // Cache policy -> acl + ruleCache *lru.Cache // Cache rules -> policy } // NewCache contructs a new policy and ACL cache of a given size @@ -30,6 +31,7 @@ func NewCache(size int, parent ACL, faultfn FaultFunc) (*Cache, error) { if size <= 0 { return nil, fmt.Errorf("Must provide positive cache size") } + rc, _ := lru.New(size) pc, _ := lru.New(size) ac, _ := lru.New(size) c := &Cache{ @@ -37,6 +39,7 @@ func NewCache(size int, parent ACL, faultfn FaultFunc) (*Cache, error) { faultfn: faultfn, parent: parent, policyCache: pc, + ruleCache: rc, } return c, nil } @@ -50,7 +53,7 @@ func (c *Cache) GetPolicy(rules string) (*Policy, error) { // getPolicy is an internal method to get a cached policy, // but it assumes a pre-computed ID func (c *Cache) getPolicy(id, rules string) (*Policy, error) { - raw, ok := c.policyCache.Get(id) + raw, ok := c.ruleCache.Get(id) if ok { return raw.(*Policy), nil } @@ -59,7 +62,7 @@ func (c *Cache) getPolicy(id, rules string) (*Policy, error) { return nil, err } policy.ID = id - c.policyCache.Add(id, policy) + c.ruleCache.Add(id, policy) return policy, nil } @@ -75,7 +78,7 @@ func (c *Cache) GetACLPolicy(id string) (*Policy, error) { // Check for a cached acl if raw, ok := c.aclCache.Get(id); ok { cached := raw.(aclEntry) - if raw, ok := c.policyCache.Get(cached.PolicyID); ok { + if raw, ok := c.ruleCache.Get(cached.PolicyID); ok { return raw.(*Policy), nil } } @@ -106,21 +109,31 @@ func (c *Cache) GetACL(id string) (ACL, error) { } ruleID := c.ruleID(rules) - // Get the policy - policy, err := c.getPolicy(ruleID, rules) - if err != nil { - return nil, err - } + // Check for a compiled ACL + var compiled ACL + if raw, ok := c.policyCache.Get(ruleID); ok { + compiled = raw.(ACL) + } else { + // Get the policy + policy, err := c.getPolicy(ruleID, rules) + if err != nil { + return nil, err + } - // Get the ACL - acl, err := New(c.parent, policy) - if err != nil { - return nil, err + // Compile the ACL + acl, err := New(c.parent, policy) + if err != nil { + return nil, err + } + + // Cache the compiled ACL + c.policyCache.Add(ruleID, acl) + compiled = acl } // Cache and return the ACL - c.aclCache.Add(id, aclEntry{acl, ruleID}) - return acl, nil + c.aclCache.Add(id, aclEntry{compiled, ruleID}) + return compiled, nil } // ClearACL is used to clear the ACL cache if any diff --git a/acl/cache_test.go b/acl/cache_test.go index 96b4bf0c88..341ad9c166 100644 --- a/acl/cache_test.go +++ b/acl/cache_test.go @@ -43,7 +43,7 @@ func TestCache_GetPolicy(t *testing.T) { func TestCache_GetACL(t *testing.T) { policies := map[string]string{ "foo": testSimplePolicy, - "bar": testSimplePolicy, + "bar": testSimplePolicy2, } faultfn := func(id string) (string, error) { return policies[id], nil @@ -113,6 +113,9 @@ func TestCache_ClearACL(t *testing.T) { // Nuke the cache c.ClearACL("foo") + // Clear the policy cache + c.policyCache.Remove(c.ruleID(testSimplePolicy)) + acl2, err := c.GetACL("foo") if err != nil { t.Fatalf("err: %v", err) @@ -170,3 +173,9 @@ key "foo/" { policy = "read" } ` + +var testSimplePolicy2 = ` +key "bar/" { + policy = "read" +} +` From 0c912f2c987c8d37d0025cc6ce07f385b438ea59 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 17:38:39 -0700 Subject: [PATCH 23/56] consul: Use Etag for policy caching --- consul/acl.go | 76 +++++++++++++++++++++++++++++++----------------- consul/server.go | 10 +++++++ 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index 95d76bec32..a6dd2db534 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -18,8 +18,8 @@ const ( // If non-authoritative, then we must respect a TTL type aclCacheEntry struct { ACL acl.ACL - TTL time.Duration Expires time.Time + ETag string } // aclFault is used to fault in the rules for an ACL if we take a miss @@ -78,31 +78,7 @@ func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { // Handle the happy path if err == nil { - // Determine the root - var root acl.ACL - switch out.Root { - case "allow": - root = acl.AllowAll() - default: - root = acl.DenyAll() - } - - // Compile the ACL - acl, err := acl.New(root, out.Policy) - if err != nil { - return nil, err - } - - // Cache the ACL - cached := &aclCacheEntry{ - ACL: acl, - TTL: out.TTL, - } - if out.TTL > 0 { - cached.Expires = time.Now().Add(out.TTL) - } - s.aclCache.Add(id, cached) - return acl, nil + return s.useACLPolicy(id, cached, &out) } // Check for not-found @@ -125,3 +101,51 @@ func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { return acl.DenyAll(), nil } } + +// useACLPolicy handles an ACLPolicy response +func (s *Server) useACLPolicy(id string, cached *aclCacheEntry, p *structs.ACLPolicy) (acl.ACL, error) { + // Check if we can used the cached policy + if cached != nil && cached.ETag == p.ETag { + if p.TTL > 0 { + cached.Expires = time.Now().Add(p.TTL) + } + return cached.ACL, nil + } + + // Check for a cached compiled policy + var compiled acl.ACL + raw, ok := s.aclPolicyCache.Get(cached.ETag) + if ok { + compiled = raw.(acl.ACL) + } else { + // Determine the root policy + var root acl.ACL + switch p.Root { + case "allow": + root = acl.AllowAll() + default: + root = acl.DenyAll() + } + + // Compile the ACL + acl, err := acl.New(root, p.Policy) + if err != nil { + return nil, err + } + + // Cache the policy + s.aclPolicyCache.Add(p.ETag, acl) + compiled = acl + } + + // Cache the ACL + cached = &aclCacheEntry{ + ACL: compiled, + ETag: p.ETag, + } + if p.TTL > 0 { + cached.Expires = time.Now().Add(p.TTL) + } + s.aclCache.Add(id, cached) + return acl, nil +} diff --git a/consul/server.go b/consul/server.go index 5e0465213f..dc30308293 100644 --- a/consul/server.go +++ b/consul/server.go @@ -59,6 +59,9 @@ type Server struct { // aclCache is a non-authoritative ACL cache aclCache *lru.Cache + // aclPolicyCache is a policy cache + aclPolicyCache *lru.Cache + // Consul configuration config *Config @@ -216,6 +219,13 @@ func NewServer(config *Config) (*Server, error) { return nil, fmt.Errorf("Failed to create ACL cache: %v", err) } + // Initialize the ACL policy cache + s.aclPolicyCache, err = lru.New(aclCacheSize) + if err != nil { + s.Shutdown() + return nil, fmt.Errorf("Failed to create ACL policy cache: %v", err) + } + // Initialize the RPC layer if err := s.setupRPC(tlsConfig); err != nil { s.Shutdown() From 3569082768d313d0dc29f012792c110c7c5d37b9 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 8 Aug 2014 17:44:23 -0700 Subject: [PATCH 24/56] acl: Adding cache purging --- acl/cache.go | 7 +++++++ acl/cache_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/acl/cache.go b/acl/cache.go index f5ef96fb8e..b61e32fc6f 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -140,3 +140,10 @@ func (c *Cache) GetACL(id string) (ACL, error) { func (c *Cache) ClearACL(id string) { c.aclCache.Remove(id) } + +// Purge is used to clear all the ACL caches. The +// rule and policy caches are not purged, since they +// are content-hashed anyways. +func (c *Cache) Purge() { + c.aclCache.Purge() +} diff --git a/acl/cache_test.go b/acl/cache_test.go index 341ad9c166..51ca5a2dfe 100644 --- a/acl/cache_test.go +++ b/acl/cache_test.go @@ -126,6 +126,39 @@ func TestCache_ClearACL(t *testing.T) { } } +func TestCache_Purge(t *testing.T) { + policies := map[string]string{ + "foo": testSimplePolicy, + "bar": testSimplePolicy, + } + faultfn := func(id string) (string, error) { + return policies[id], nil + } + + c, err := NewCache(1, DenyAll(), faultfn) + if err != nil { + t.Fatalf("err: %v", err) + } + + acl, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Nuke the cache + c.Purge() + c.policyCache.Purge() + + acl2, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if acl == acl2 { + t.Fatalf("should not be cached") + } +} + func TestCache_GetACLPolicy(t *testing.T) { policies := map[string]string{ "foo": testSimplePolicy, From 468c8c30130418bf1754bd63361645c006ce2321 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 10 Aug 2014 22:01:03 -0700 Subject: [PATCH 25/56] acl: Use only a single Radix tree per ACL --- acl/acl.go | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/acl/acl.go b/acl/acl.go index 51395948c3..f0e3c83552 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -1,8 +1,6 @@ package acl import ( - "fmt" - "github.com/armon/go-radix" ) @@ -58,11 +56,8 @@ type PolicyACL struct { // no matching rule. parent ACL - // keyRead contains the read policies - keyRead *radix.Tree - - // keyWrite contains the write policies - keyWrite *radix.Tree + // keyRules contains the key policies + keyRules *radix.Tree } // New is used to construct a policy based ACL from a set of policies @@ -70,25 +65,12 @@ type PolicyACL struct { func New(parent ACL, policy *Policy) (*PolicyACL, error) { p := &PolicyACL{ parent: parent, - keyRead: radix.New(), - keyWrite: radix.New(), + keyRules: radix.New(), } // Load the key policy for _, kp := range policy.Keys { - switch kp.Policy { - case KeyPolicyDeny: - p.keyRead.Insert(kp.Prefix, false) - p.keyWrite.Insert(kp.Prefix, false) - case KeyPolicyRead: - p.keyRead.Insert(kp.Prefix, true) - p.keyWrite.Insert(kp.Prefix, false) - case KeyPolicyWrite: - p.keyRead.Insert(kp.Prefix, true) - p.keyWrite.Insert(kp.Prefix, true) - default: - return nil, fmt.Errorf("Invalid key policy: %#v", kp) - } + p.keyRules.Insert(kp.Prefix, kp.Policy) } return p, nil } @@ -96,9 +78,16 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) { // KeyRead returns if a key is allowed to be read func (p *PolicyACL) KeyRead(key string) bool { // Look for a matching rule - _, rule, ok := p.keyRead.LongestPrefix(key) + _, rule, ok := p.keyRules.LongestPrefix(key) if ok { - return rule.(bool) + switch rule.(string) { + case KeyPolicyRead: + return true + case KeyPolicyWrite: + return true + default: + return false + } } // No matching rule, use the parent. @@ -108,9 +97,14 @@ func (p *PolicyACL) KeyRead(key string) bool { // KeyWrite returns if a key is allowed to be written func (p *PolicyACL) KeyWrite(key string) bool { // Look for a matching rule - _, rule, ok := p.keyWrite.LongestPrefix(key) + _, rule, ok := p.keyRules.LongestPrefix(key) if ok { - return rule.(bool) + switch rule.(string) { + case KeyPolicyWrite: + return true + default: + return false + } } // No matching rule, use the parent. From fe86c8c5ee4b09746b658da3da0db2ea688ac34b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 11 Aug 2014 14:01:45 -0700 Subject: [PATCH 26/56] consul: Testing ACL resolution --- consul/acl.go | 4 +- consul/acl_test.go | 223 ++++++++++++++++++++++++++++++++++++++++++ consul/server_test.go | 11 +++ 3 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 consul/acl_test.go diff --git a/consul/acl.go b/consul/acl.go index a6dd2db534..47767830c5 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -114,7 +114,7 @@ func (s *Server) useACLPolicy(id string, cached *aclCacheEntry, p *structs.ACLPo // Check for a cached compiled policy var compiled acl.ACL - raw, ok := s.aclPolicyCache.Get(cached.ETag) + raw, ok := s.aclPolicyCache.Get(p.ETag) if ok { compiled = raw.(acl.ACL) } else { @@ -147,5 +147,5 @@ func (s *Server) useACLPolicy(id string, cached *aclCacheEntry, p *structs.ACLPo cached.Expires = time.Now().Add(p.TTL) } s.aclCache.Add(id, cached) - return acl, nil + return compiled, nil } diff --git a/consul/acl_test.go b/consul/acl_test.go new file mode 100644 index 0000000000..bbf3ec0518 --- /dev/null +++ b/consul/acl_test.go @@ -0,0 +1,223 @@ +package consul + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" +) + +func TestACL_Disabled(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + acl, err := s1.resolveToken("does not exist") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl != nil { + t.Fatalf("got acl") + } +} + +func TestACL_Authority_NotFound(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + acl, err := s1.resolveToken("does not exist") + if err == nil || err.Error() != aclNotFound { + t.Fatalf("err: %v", err) + } + if acl != nil { + t.Fatalf("got acl") + } +} + +func TestACL_Authority_Found(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + // Create a new token + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testACLPolicy, + }, + } + var id string + if err := client.Call("ACL.Apply", &arg, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // Resolve the token + acl, err := s1.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing acl") + } + + // Check the policy + if acl.KeyRead("bar") { + t.Fatalf("unexpected read") + } + if !acl.KeyRead("foo/test") { + t.Fatalf("unexpected failed read") + } +} + +func TestACL_NonAuthority_NotFound(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.Bootstrap = false // Disable bootstrap + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + p1, _ := s1.raftPeers.Peers() + return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1)) + }, func(err error) { + t.Fatalf("should have 2 peers: %v", err) + }) + + client := rpcClient(t, s1) + defer client.Close() + testutil.WaitForLeader(t, client.Call, "dc1") + + // find the non-authoritative server + var nonAuth *Server + if !s1.IsLeader() { + nonAuth = s1 + } else { + nonAuth = s2 + } + + acl, err := nonAuth.resolveToken("does not exist") + if err == nil || err.Error() != aclNotFound { + t.Fatalf("err: %v", err) + } + if acl != nil { + t.Fatalf("got acl") + } +} + +func TestACL_NonAuthority_Found(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.Bootstrap = false // Disable bootstrap + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + p1, _ := s1.raftPeers.Peers() + return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1)) + }, func(err error) { + t.Fatalf("should have 2 peers: %v", err) + }) + testutil.WaitForLeader(t, client.Call, "dc1") + + // Create a new token + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testACLPolicy, + }, + } + var id string + if err := client.Call("ACL.Apply", &arg, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // find the non-authoritative server + var nonAuth *Server + if !s1.IsLeader() { + nonAuth = s1 + } else { + nonAuth = s2 + } + + // Token should resolve + acl, err := nonAuth.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing acl") + } + + // Check the policy + if acl.KeyRead("bar") { + t.Fatalf("unexpected read") + } + if !acl.KeyRead("foo/test") { + t.Fatalf("unexpected failed read") + } +} + +var testACLPolicy = ` +key "" { + policy = "deny" +} +key "foo/" { + policy = "write" +} +` diff --git a/consul/server_test.go b/consul/server_test.go index 70aa5811f6..76b7d4ed44 100644 --- a/consul/server_test.go +++ b/consul/server_test.go @@ -101,6 +101,17 @@ func testServerDCExpect(t *testing.T, dc string, expect int) (string, *Server) { return dir, server } +func testServerWithConfig(t *testing.T, cb func(c *Config)) (string, *Server) { + name := fmt.Sprintf("Node %d", getPort()) + dir, config := testServerConfig(t, name) + cb(config) + server, err := NewServer(config) + if err != nil { + t.Fatalf("err: %v", err) + } + return dir, server +} + func TestServer_StartStop(t *testing.T) { dir := tmpDir(t) defer os.RemoveAll(dir) From 01beaa60cc2b771ba8a54dde6c29805d77e80973 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 11 Aug 2014 14:18:51 -0700 Subject: [PATCH 27/56] consul: Testing down policies and multi-DC --- consul/acl_test.go | 287 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) diff --git a/consul/acl_test.go b/consul/acl_test.go index bbf3ec0518..061550da53 100644 --- a/consul/acl_test.go +++ b/consul/acl_test.go @@ -6,6 +6,7 @@ import ( "os" "testing" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/testutil" ) @@ -213,6 +214,292 @@ func TestACL_NonAuthority_Found(t *testing.T) { } } +func TestACL_DownPolicy_Deny(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLDownPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLDownPolicy = "deny" + c.Bootstrap = false // Disable bootstrap + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + p1, _ := s1.raftPeers.Peers() + return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1)) + }, func(err error) { + t.Fatalf("should have 2 peers: %v", err) + }) + testutil.WaitForLeader(t, client.Call, "dc1") + + // Create a new token + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testACLPolicy, + }, + } + var id string + if err := client.Call("ACL.Apply", &arg, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // find the non-authoritative server + var nonAuth *Server + var auth *Server + if !s1.IsLeader() { + nonAuth = s1 + auth = s2 + } else { + nonAuth = s2 + auth = s1 + } + + // Kill the authoritative server + auth.Shutdown() + + // Token should resolve into a DenyAll + aclR, err := nonAuth.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if aclR != acl.DenyAll() { + t.Fatalf("bad acl: %#v", aclR) + } +} + +func TestACL_DownPolicy_Allow(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLDownPolicy = "allow" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLDownPolicy = "allow" + c.Bootstrap = false // Disable bootstrap + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + p1, _ := s1.raftPeers.Peers() + return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1)) + }, func(err error) { + t.Fatalf("should have 2 peers: %v", err) + }) + testutil.WaitForLeader(t, client.Call, "dc1") + + // Create a new token + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testACLPolicy, + }, + } + var id string + if err := client.Call("ACL.Apply", &arg, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // find the non-authoritative server + var nonAuth *Server + var auth *Server + if !s1.IsLeader() { + nonAuth = s1 + auth = s2 + } else { + nonAuth = s2 + auth = s1 + } + + // Kill the authoritative server + auth.Shutdown() + + // Token should resolve into a AllowAll + aclR, err := nonAuth.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if aclR != acl.AllowAll() { + t.Fatalf("bad acl: %#v", aclR) + } +} + +func TestACL_DownPolicy_ExtendCache(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLTTL = 0 + c.ACLDownPolicy = "extend-cache" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLTTL = 0 + c.ACLDownPolicy = "extend-cache" + c.Bootstrap = false // Disable bootstrap + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + p1, _ := s1.raftPeers.Peers() + return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1)) + }, func(err error) { + t.Fatalf("should have 2 peers: %v", err) + }) + testutil.WaitForLeader(t, client.Call, "dc1") + + // Create a new token + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testACLPolicy, + }, + } + var id string + if err := client.Call("ACL.Apply", &arg, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // find the non-authoritative server + var nonAuth *Server + var auth *Server + if !s1.IsLeader() { + nonAuth = s1 + auth = s2 + } else { + nonAuth = s2 + auth = s1 + } + + // Warm the caches + aclR, err := nonAuth.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if aclR == nil { + t.Fatalf("bad acl: %#v", aclR) + } + + // Kill the authoritative server + auth.Shutdown() + + // Token should resolve into cached copy + aclR2, err := nonAuth.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if aclR2 != aclR { + t.Fatalf("bad acl: %#v", aclR) + } +} + +func TestACL_MultiDC_Found(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "dc2" + c.ACLDatacenter = "dc1" // Enable ACLs! + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfWANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinWAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForLeader(t, client.Call, "dc1") + testutil.WaitForLeader(t, client.Call, "dc2") + + // Create a new token + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testACLPolicy, + }, + } + var id string + if err := client.Call("ACL.Apply", &arg, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // Token should resolve + acl, err := s2.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing acl") + } + + // Check the policy + if acl.KeyRead("bar") { + t.Fatalf("unexpected read") + } + if !acl.KeyRead("foo/test") { + t.Fatalf("unexpected failed read") + } +} + var testACLPolicy = ` key "" { policy = "deny" From 827e7c9efa19d0324d5ea3b61f2167e9ca782ebb Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 11 Aug 2014 14:54:18 -0700 Subject: [PATCH 28/56] consul: Create anonymous and master tokens --- consul/acl.go | 11 +++++- consul/acl_test.go | 53 +++++++++++++++++++++++++++ consul/fsm.go | 8 ++++- consul/fsm_test.go | 2 +- consul/leader.go | 73 ++++++++++++++++++++++++++++++++++++++ consul/state_store.go | 10 ++++-- consul/state_store_test.go | 14 ++++---- consul/structs/structs.go | 5 +-- 8 files changed, 162 insertions(+), 14 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index 47767830c5..2909fea8f7 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -12,6 +12,10 @@ import ( const ( // aclNotFound indicates there is no matching ACL aclNotFound = "ACL not found" + + // anonymousToken is the token ID we re-write to if there + // is no token ID provided + anonymousToken = "anonymous" ) // aclCacheEntry is used to cache non-authoritative ACL's @@ -39,10 +43,15 @@ func (s *Server) aclFault(id string) (string, error) { func (s *Server) resolveToken(id string) (acl.ACL, error) { // Check if there is no ACL datacenter (ACL's disabled) authDC := s.config.ACLDatacenter - if authDC == "" { + if len(authDC) == 0 { return nil, nil } + // Handle the anonymous token + if len(id) == 0 { + id = anonymousToken + } + // Check if we are the ACL datacenter and the leader, use the // authoritative cache if s.config.Datacenter == authDC && s.IsLeader() { diff --git a/consul/acl_test.go b/consul/acl_test.go index 061550da53..b0dca39871 100644 --- a/consul/acl_test.go +++ b/consul/acl_test.go @@ -93,6 +93,59 @@ func TestACL_Authority_Found(t *testing.T) { } } +func TestACL_Authority_Anonymous_Found(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + // Resolve the token + acl, err := s1.resolveToken("") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing acl") + } + + // Check the policy, should allow all + if !acl.KeyRead("foo/test") { + t.Fatalf("unexpected failed read") + } +} + +func TestACL_Authority_Master_Found(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLMasterToken = "foobar" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + // Resolve the token + acl, err := s1.resolveToken("foobar") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing acl") + } + + // Check the policy, should allow all + if !acl.KeyRead("foo/test") { + t.Fatalf("unexpected failed read") + } +} + func TestACL_NonAuthority_NotFound(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" diff --git a/consul/fsm.go b/consul/fsm.go index 4c82357e68..9810a289be 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -205,7 +205,13 @@ func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} { } switch req.Op { case structs.ACLSet: - if err := c.state.ACLSet(index, &req.ACL); err != nil { + if err := c.state.ACLSet(index, &req.ACL, false); err != nil { + return err + } else { + return req.ACL.ID + } + case structs.ACLForceSet: + if err := c.state.ACLSet(index, &req.ACL, true); err != nil { return err } else { return req.ACL.ID diff --git a/consul/fsm_test.go b/consul/fsm_test.go index f47c006539..d188de9ffb 100644 --- a/consul/fsm_test.go +++ b/consul/fsm_test.go @@ -329,7 +329,7 @@ func TestFSM_SnapshotRestore(t *testing.T) { session := &structs.Session{Node: "foo"} fsm.state.SessionCreate(9, session) acl := &structs.ACL{Name: "User Token"} - fsm.state.ACLSet(10, acl) + fsm.state.ACLSet(10, acl, false) // Snapshot snap, err := fsm.Snapshot() diff --git a/consul/leader.go b/consul/leader.go index b63f7bbe80..60a6737f9b 100644 --- a/consul/leader.go +++ b/consul/leader.go @@ -1,6 +1,7 @@ package consul import ( + "fmt" "net" "strconv" "time" @@ -54,6 +55,11 @@ func (s *Server) leaderLoop(stopCh chan struct{}) { s.logger.Printf("[WARN] consul: failed to broadcast new leader event: %v", err) } + // Setup ACLs if we are the leader and need to + if err := s.initializeACL(); err != nil { + s.logger.Printf("[ERR] consul: ACL initialization failed: %v", err) + } + // Reconcile channel is only used once initial reconcile // has succeeded var reconcileCh chan serf.Member @@ -99,6 +105,73 @@ WAIT: } } +// initializeACL is used to setup the ACLs if we are the leader +// and need to do this. +func (s *Server) initializeACL() error { + // Bail if not configured or we are not authoritative + authDC := s.config.ACLDatacenter + if len(authDC) == 0 || authDC != s.config.Datacenter { + return nil + } + + // Purge the cache, since it could've changed while we + // were not the leader + s.aclAuthCache.Purge() + + // Look for the anonymous token + state := s.fsm.State() + _, acl, err := state.ACLGet(anonymousToken) + if err != nil { + return fmt.Errorf("failed to get anonymous token: %v", err) + } + + // Create anonymous token if missing + if acl == nil { + req := structs.ACLRequest{ + Datacenter: authDC, + Op: structs.ACLForceSet, + ACL: structs.ACL{ + ID: anonymousToken, + Name: "Anonymous Token", + Type: structs.ACLTypeClient, + }, + } + _, err := s.raftApply(structs.ACLRequestType, &req) + if err != nil { + return fmt.Errorf("failed to create anonymous token: %v", err) + } + } + + // Check for configured master token + master := s.config.ACLMasterToken + if len(master) == 0 { + return nil + } + + // Look for the master token + _, acl, err = state.ACLGet(master) + if err != nil { + return fmt.Errorf("failed to get master token: %v", err) + } + if acl == nil { + req := structs.ACLRequest{ + Datacenter: authDC, + Op: structs.ACLForceSet, + ACL: structs.ACL{ + ID: master, + Name: "Master Token", + Type: structs.ACLTypeManagement, + }, + } + _, err := s.raftApply(structs.ACLRequestType, &req) + if err != nil { + return fmt.Errorf("failed to create master token: %v", err) + } + + } + return nil +} + // reconcile is used to reconcile the differences between Serf // membership and what is reflected in our strongly consistent store. // Mainly we need to ensure all live nodes are registered, all failed diff --git a/consul/state_store.go b/consul/state_store.go index 42c0d553fc..6a43aa0270 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -1504,7 +1504,9 @@ func (s *StateStore) invalidateLocks(index uint64, tx *MDBTxn, } // ACLSet is used to create or update an ACL entry -func (s *StateStore) ACLSet(index uint64, acl *structs.ACL) error { +// allowCreate is used for initialization of the anonymous and master tokens, +// since it permits them to be created with a specified ID that does not exist. +func (s *StateStore) ACLSet(index uint64, acl *structs.ACL, allowCreate bool) error { // Start a new txn tx, err := s.tables.StartTxn(false) if err != nil { @@ -1537,7 +1539,11 @@ func (s *StateStore) ACLSet(index uint64, acl *structs.ACL) error { switch len(res) { case 0: - return fmt.Errorf("Invalid ACL") + if !allowCreate { + return fmt.Errorf("Invalid ACL") + } + acl.CreateIndex = index + acl.ModifyIndex = index case 1: exist := res[0].(*structs.ACL) acl.CreateIndex = exist.CreateIndex diff --git a/consul/state_store_test.go b/consul/state_store_test.go index c1705f5df0..f0cdae90f3 100644 --- a/consul/state_store_test.go +++ b/consul/state_store_test.go @@ -656,7 +656,7 @@ func TestStoreSnapshot(t *testing.T) { Name: "User token", Type: structs.ACLTypeClient, } - if err := store.ACLSet(19, a1); err != nil { + if err := store.ACLSet(19, a1, false); err != nil { t.Fatalf("err: %v", err) } @@ -664,7 +664,7 @@ func TestStoreSnapshot(t *testing.T) { Name: "User token", Type: structs.ACLTypeClient, } - if err := store.ACLSet(20, a2); err != nil { + if err := store.ACLSet(20, a2, false); err != nil { t.Fatalf("err: %v", err) } @@ -2180,7 +2180,7 @@ func TestACLSet_Get(t *testing.T) { Type: structs.ACLTypeClient, Rules: "", } - if err := store.ACLSet(50, a); err != nil { + if err := store.ACLSet(50, a, false); err != nil { t.Fatalf("err: %v", err) } if a.CreateIndex != 50 { @@ -2206,7 +2206,7 @@ func TestACLSet_Get(t *testing.T) { // Update a.Rules = "foo bar baz" - if err := store.ACLSet(52, a); err != nil { + if err := store.ACLSet(52, a, false); err != nil { t.Fatalf("err: %v", err) } if a.CreateIndex != 50 { @@ -2240,7 +2240,7 @@ func TestACLDelete(t *testing.T) { Type: structs.ACLTypeClient, Rules: "", } - if err := store.ACLSet(50, a); err != nil { + if err := store.ACLSet(50, a, false); err != nil { t.Fatalf("err: %v", err) } @@ -2274,7 +2274,7 @@ func TestACLList(t *testing.T) { Name: "User token", Type: structs.ACLTypeClient, } - if err := store.ACLSet(50, a1); err != nil { + if err := store.ACLSet(50, a1, false); err != nil { t.Fatalf("err: %v", err) } @@ -2282,7 +2282,7 @@ func TestACLList(t *testing.T) { Name: "User token", Type: structs.ACLTypeClient, } - if err := store.ACLSet(51, a2); err != nil { + if err := store.ACLSet(51, a2, false); err != nil { t.Fatalf("err: %v", err) } diff --git a/consul/structs/structs.go b/consul/structs/structs.go index b80145c436..3263a6ce28 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -439,8 +439,9 @@ type ACLs []*ACL type ACLOp string const ( - ACLSet ACLOp = "set" - ACLDelete = "delete" + ACLSet ACLOp = "set" + ACLForceSet = "force-set" + ACLDelete = "delete" ) // ACLRequest is used to create, update or delete an ACL From a82439c71378b0dcf05284f3293e5c5a85a7f194 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 11 Aug 2014 15:01:38 -0700 Subject: [PATCH 29/56] consul: Adding some metrics for ACL usage --- consul/acl.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/consul/acl.go b/consul/acl.go index 2909fea8f7..8921047dc6 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/armon/go-metrics" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/consul/structs" ) @@ -28,6 +29,7 @@ type aclCacheEntry struct { // aclFault is used to fault in the rules for an ACL if we take a miss func (s *Server) aclFault(id string) (string, error) { + defer metrics.MeasureSince([]string{"consul", "acl", "fault"}, time.Now()) state := s.fsm.State() _, acl, err := state.ACLGet(id) if err != nil { @@ -46,6 +48,7 @@ func (s *Server) resolveToken(id string) (acl.ACL, error) { if len(authDC) == 0 { return nil, nil } + defer metrics.MeasureSince([]string{"consul", "acl", "resolveToken"}, time.Now()) // Handle the anonymous token if len(id) == 0 { @@ -74,7 +77,10 @@ func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { // Check for live cache if cached != nil && time.Now().Before(cached.Expires) { + metrics.IncrCounter([]string{"consul", "acl", "cache_hit"}, 1) return cached.ACL, nil + } else { + metrics.IncrCounter([]string{"consul", "acl", "cache_miss"}, 1) } // Attempt to refresh the policy From 5c0da3a4d7dadc086466d750a459f83860cd7dd9 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 10:35:27 -0700 Subject: [PATCH 30/56] acl: Simplify parent ACL, adding root policies --- acl/acl.go | 12 ++++++++++ acl/acl_test.go | 12 ++++++++++ acl/cache.go | 29 +++++++++++++++--------- acl/cache_test.go | 57 ++++++++++++++++++++++++++++++++++++----------- 4 files changed, 86 insertions(+), 24 deletions(-) diff --git a/acl/acl.go b/acl/acl.go index f0e3c83552..e6d45d23e0 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -49,6 +49,18 @@ func DenyAll() ACL { return denyAll } +// RootACL returns a possible ACL if the ID matches a root policy +func RootACL(id string) ACL { + switch id { + case "allow": + return allowAll + case "deny": + return denyAll + default: + return nil + } +} + // PolicyACL is used to wrap a set of ACL policies to provide // the ACL interface. type PolicyACL struct { diff --git a/acl/acl_test.go b/acl/acl_test.go index baf327e092..444b3bd7fe 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -4,6 +4,18 @@ import ( "testing" ) +func TestRootACL(t *testing.T) { + if RootACL("allow") != AllowAll() { + t.Fatalf("Bad root") + } + if RootACL("deny") != DenyAll() { + t.Fatalf("Bad root") + } + if RootACL("foo") != nil { + t.Fatalf("bad root") + } +} + func TestStaticACL(t *testing.T) { all := AllowAll() if _, ok := all.(*StaticACL); !ok { diff --git a/acl/cache.go b/acl/cache.go index b61e32fc6f..2b1409ea98 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -7,9 +7,9 @@ import ( "github.com/hashicorp/golang-lru" ) -// FaultFunc is a function used to fault in the rules for an -// ACL given it's ID -type FaultFunc func(id string) (string, error) +// FaultFunc is a function used to fault in the parent, +// rules for an ACL given it's ID +type FaultFunc func(id string) (string, string, error) // aclEntry allows us to store the ACL with it's policy ID type aclEntry struct { @@ -19,15 +19,14 @@ type aclEntry struct { // Cache is used to implement policy and ACL caching type Cache struct { - aclCache *lru.Cache // Cache id -> acl faultfn FaultFunc - parent ACL + aclCache *lru.Cache // Cache id -> acl policyCache *lru.Cache // Cache policy -> acl ruleCache *lru.Cache // Cache rules -> policy } // NewCache contructs a new policy and ACL cache of a given size -func NewCache(size int, parent ACL, faultfn FaultFunc) (*Cache, error) { +func NewCache(size int, faultfn FaultFunc) (*Cache, error) { if size <= 0 { return nil, fmt.Errorf("Must provide positive cache size") } @@ -35,9 +34,8 @@ func NewCache(size int, parent ACL, faultfn FaultFunc) (*Cache, error) { pc, _ := lru.New(size) ac, _ := lru.New(size) c := &Cache{ - aclCache: ac, faultfn: faultfn, - parent: parent, + aclCache: ac, policyCache: pc, ruleCache: rc, } @@ -84,7 +82,7 @@ func (c *Cache) GetACLPolicy(id string) (*Policy, error) { } // Fault in the rules - rules, err := c.faultfn(id) + _, rules, err := c.faultfn(id) if err != nil { return nil, err } @@ -103,7 +101,7 @@ func (c *Cache) GetACL(id string) (ACL, error) { } // Get the rules - rules, err := c.faultfn(id) + parentID, rules, err := c.faultfn(id) if err != nil { return nil, err } @@ -120,8 +118,17 @@ func (c *Cache) GetACL(id string) (ACL, error) { return nil, err } + // Get the parent ACL + parent := RootACL(parentID) + if parent == nil { + parent, err = c.GetACL(parentID) + if err != nil { + return nil, err + } + } + // Compile the ACL - acl, err := New(c.parent, policy) + acl, err := New(parent, policy) if err != nil { return nil, err } diff --git a/acl/cache_test.go b/acl/cache_test.go index 51ca5a2dfe..96b06ba34b 100644 --- a/acl/cache_test.go +++ b/acl/cache_test.go @@ -5,7 +5,7 @@ import ( ) func TestCache_GetPolicy(t *testing.T) { - c, err := NewCache(1, AllowAll(), nil) + c, err := NewCache(1, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -45,11 +45,11 @@ func TestCache_GetACL(t *testing.T) { "foo": testSimplePolicy, "bar": testSimplePolicy2, } - faultfn := func(id string) (string, error) { - return policies[id], nil + faultfn := func(id string) (string, string, error) { + return "deny", policies[id], nil } - c, err := NewCache(1, DenyAll(), faultfn) + c, err := NewCache(1, faultfn) if err != nil { t.Fatalf("err: %v", err) } @@ -96,11 +96,11 @@ func TestCache_ClearACL(t *testing.T) { "foo": testSimplePolicy, "bar": testSimplePolicy, } - faultfn := func(id string) (string, error) { - return policies[id], nil + faultfn := func(id string) (string, string, error) { + return "deny", policies[id], nil } - c, err := NewCache(1, DenyAll(), faultfn) + c, err := NewCache(1, faultfn) if err != nil { t.Fatalf("err: %v", err) } @@ -131,11 +131,11 @@ func TestCache_Purge(t *testing.T) { "foo": testSimplePolicy, "bar": testSimplePolicy, } - faultfn := func(id string) (string, error) { - return policies[id], nil + faultfn := func(id string) (string, string, error) { + return "deny", policies[id], nil } - c, err := NewCache(1, DenyAll(), faultfn) + c, err := NewCache(1, faultfn) if err != nil { t.Fatalf("err: %v", err) } @@ -164,10 +164,10 @@ func TestCache_GetACLPolicy(t *testing.T) { "foo": testSimplePolicy, "bar": testSimplePolicy, } - faultfn := func(id string) (string, error) { - return policies[id], nil + faultfn := func(id string) (string, string, error) { + return "deny", policies[id], nil } - c, err := NewCache(1, DenyAll(), faultfn) + c, err := NewCache(1, faultfn) if err != nil { t.Fatalf("err: %v", err) } @@ -201,6 +201,37 @@ func TestCache_GetACLPolicy(t *testing.T) { } } +func TestCache_GetACL_Parent(t *testing.T) { + faultfn := func(id string) (string, string, error) { + switch id { + case "foo": + // Foo inherits from bar + return "bar", testSimplePolicy, nil + case "bar": + return "deny", testSimplePolicy2, nil + } + t.Fatalf("bad case") + return "", "", nil + } + + c, err := NewCache(1, faultfn) + if err != nil { + t.Fatalf("err: %v", err) + } + + acl, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if !acl.KeyRead("bar/test") { + t.Fatalf("should allow") + } + if !acl.KeyRead("foo/test") { + t.Fatalf("should allow") + } +} + var testSimplePolicy = ` key "foo/" { policy = "read" From ef171ca344dfefd96cf75f527cb59c6be7754a25 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 10:38:57 -0700 Subject: [PATCH 31/56] consul: Support management tokens --- consul/acl.go | 15 +++++++++++---- consul/acl_test.go | 28 ++++++++++++++++++++++++++++ consul/server.go | 11 +---------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index 8921047dc6..f9609611b8 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -28,17 +28,24 @@ type aclCacheEntry struct { } // aclFault is used to fault in the rules for an ACL if we take a miss -func (s *Server) aclFault(id string) (string, error) { +func (s *Server) aclFault(id string) (string, string, error) { defer metrics.MeasureSince([]string{"consul", "acl", "fault"}, time.Now()) state := s.fsm.State() _, acl, err := state.ACLGet(id) if err != nil { - return "", err + return "", "", err } if acl == nil { - return "", errors.New(aclNotFound) + return "", "", errors.New(aclNotFound) } - return acl.Rules, nil + + // Management tokens have no policy and inherit from allow + if acl.Type == structs.ACLTypeManagement { + return "allow", "", nil + } + + // Otherwise use the base policy + return s.config.ACLDefaultPolicy, acl.Rules, nil } // resolveToken is used to resolve an ACL is any is appropriate diff --git a/consul/acl_test.go b/consul/acl_test.go index b0dca39871..85bcb907c7 100644 --- a/consul/acl_test.go +++ b/consul/acl_test.go @@ -146,6 +146,34 @@ func TestACL_Authority_Master_Found(t *testing.T) { } } +func TestACL_Authority_Management(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLMasterToken = "foobar" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + // Resolve the token + acl, err := s1.resolveToken("foobar") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing acl") + } + + // Check the policy, should allow all + if !acl.KeyRead("foo/test") { + t.Fatalf("unexpected failed read") + } +} + func TestACL_NonAuthority_NotFound(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" diff --git a/consul/server.go b/consul/server.go index dc30308293..d3fe6da924 100644 --- a/consul/server.go +++ b/consul/server.go @@ -196,17 +196,8 @@ func NewServer(config *Config) (*Server, error) { shutdownCh: make(chan struct{}), } - // Determine the ACL root policy - var aclRoot acl.ACL - switch config.ACLDefaultPolicy { - case "allow": - aclRoot = acl.AllowAll() - case "deny": - aclRoot = acl.DenyAll() - } - // Initialize the authoritative ACL cache - s.aclAuthCache, err = acl.NewCache(aclCacheSize, aclRoot, s.aclFault) + s.aclAuthCache, err = acl.NewCache(aclCacheSize, s.aclFault) if err != nil { s.Shutdown() return nil, fmt.Errorf("Failed to create ACL cache: %v", err) From 2fe94709e66df2803ec9af8d18e5c9dbb0a99bc1 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 10:45:28 -0700 Subject: [PATCH 32/56] acl: Return the parent with GetACLPolicy --- acl/cache.go | 14 ++++++++------ acl/cache_test.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/acl/cache.go b/acl/cache.go index 2b1409ea98..22d13d7b9b 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -14,6 +14,7 @@ type FaultFunc func(id string) (string, string, error) // aclEntry allows us to store the ACL with it's policy ID type aclEntry struct { ACL ACL + Parent string PolicyID string } @@ -72,23 +73,24 @@ func (c *Cache) ruleID(rules string) string { // GetACLPolicy is used to get the potentially cached ACL // policy. If not cached, it will be generated and then cached. -func (c *Cache) GetACLPolicy(id string) (*Policy, error) { +func (c *Cache) GetACLPolicy(id string) (string, *Policy, error) { // Check for a cached acl if raw, ok := c.aclCache.Get(id); ok { cached := raw.(aclEntry) if raw, ok := c.ruleCache.Get(cached.PolicyID); ok { - return raw.(*Policy), nil + return cached.Parent, raw.(*Policy), nil } } // Fault in the rules - _, rules, err := c.faultfn(id) + parent, rules, err := c.faultfn(id) if err != nil { - return nil, err + return "", nil, err } // Get cached - return c.GetPolicy(rules) + policy, err := c.GetPolicy(rules) + return parent, policy, err } // GetACL is used to get a potentially cached ACL policy. @@ -139,7 +141,7 @@ func (c *Cache) GetACL(id string) (ACL, error) { } // Cache and return the ACL - c.aclCache.Add(id, aclEntry{compiled, ruleID}) + c.aclCache.Add(id, aclEntry{compiled, parentID, ruleID}) return compiled, nil } diff --git a/acl/cache_test.go b/acl/cache_test.go index 96b06ba34b..8502e44d3c 100644 --- a/acl/cache_test.go +++ b/acl/cache_test.go @@ -182,19 +182,25 @@ func TestCache_GetACLPolicy(t *testing.T) { t.Fatalf("err: %v", err) } - p2, err := c.GetACLPolicy("foo") + parent, p2, err := c.GetACLPolicy("foo") if err != nil { t.Fatalf("err: %v", err) } + if parent != "deny" { + t.Fatalf("bad: %v", parent) + } if p2 != p { t.Fatalf("expected cached policy") } - p3, err := c.GetACLPolicy("bar") + parent, p3, err := c.GetACLPolicy("bar") if err != nil { t.Fatalf("err: %v", err) } + if parent != "deny" { + t.Fatalf("bad: %v", parent) + } if p3 != p { t.Fatalf("expected cached policy") From 10db4c7c8f1c3c68d12ebf6b77dc01cf7e657e48 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 10:54:56 -0700 Subject: [PATCH 33/56] consul: Resolve parent ACLs --- consul/acl.go | 21 ++++++++------- consul/acl_endpoint.go | 6 ++--- consul/acl_test.go | 57 +++++++++++++++++++++++++++++++++++++++ consul/structs/structs.go | 2 +- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index f9609611b8..727ee0c5b0 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -100,7 +100,7 @@ func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { // Handle the happy path if err == nil { - return s.useACLPolicy(id, cached, &out) + return s.useACLPolicy(id, authDC, cached, &out) } // Check for not-found @@ -125,7 +125,7 @@ func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { } // useACLPolicy handles an ACLPolicy response -func (s *Server) useACLPolicy(id string, cached *aclCacheEntry, p *structs.ACLPolicy) (acl.ACL, error) { +func (s *Server) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *structs.ACLPolicy) (acl.ACL, error) { // Check if we can used the cached policy if cached != nil && cached.ETag == p.ETag { if p.TTL > 0 { @@ -140,17 +140,18 @@ func (s *Server) useACLPolicy(id string, cached *aclCacheEntry, p *structs.ACLPo if ok { compiled = raw.(acl.ACL) } else { - // Determine the root policy - var root acl.ACL - switch p.Root { - case "allow": - root = acl.AllowAll() - default: - root = acl.DenyAll() + // Resolve the parent policy + parent := acl.RootACL(p.Parent) + if parent == nil { + var err error + parent, err = s.lookupACL(p.Parent, authDC) + if err != nil { + return nil, err + } } // Compile the ACL - acl, err := acl.New(root, p.Policy) + acl, err := acl.New(parent, p.Policy) if err != nil { return nil, err } diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index 8761426331..67fec26c37 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -94,23 +94,23 @@ func (a *ACL) GetPolicy(args *structs.ACLPolicyRequest, reply *structs.ACLPolicy } // Get the policy via the cache - policy, err := a.srv.aclAuthCache.GetACLPolicy(args.ACL) + parent, policy, err := a.srv.aclAuthCache.GetACLPolicy(args.ACL) if err != nil { return err } // Generate an ETag conf := a.srv.config - etag := fmt.Sprintf("%s:%s", conf.ACLDefaultPolicy, policy.ID) + etag := fmt.Sprintf("%s:%s", parent, policy.ID) // Setup the response reply.ETag = etag - reply.Root = conf.ACLDefaultPolicy reply.TTL = conf.ACLTTL a.srv.setQueryMeta(&reply.QueryMeta) // Only send the policy on an Etag mis-match if args.ETag != etag { + reply.Parent = parent reply.Policy = policy } return nil diff --git a/consul/acl_test.go b/consul/acl_test.go index 85bcb907c7..7129a7e212 100644 --- a/consul/acl_test.go +++ b/consul/acl_test.go @@ -295,6 +295,63 @@ func TestACL_NonAuthority_Found(t *testing.T) { } } +func TestACL_NonAuthority_Management(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLMasterToken = "foobar" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLDefaultPolicy = "deny" + c.Bootstrap = false // Disable bootstrap + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + p1, _ := s1.raftPeers.Peers() + return len(p1) == 2, errors.New(fmt.Sprintf("%v", p1)) + }, func(err error) { + t.Fatalf("should have 2 peers: %v", err) + }) + testutil.WaitForLeader(t, client.Call, "dc1") + + // find the non-authoritative server + var nonAuth *Server + if !s1.IsLeader() { + nonAuth = s1 + } else { + nonAuth = s2 + } + + // Resolve the token + acl, err := nonAuth.resolveToken("foobar") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("missing acl") + } + + // Check the policy, should allow all + if !acl.KeyRead("foo/test") { + t.Fatalf("unexpected failed read") + } +} + func TestACL_DownPolicy_Deny(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 3263a6ce28..95f273f4fc 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -487,7 +487,7 @@ type IndexedACLs struct { type ACLPolicy struct { ETag string - Root string + Parent string Policy *acl.Policy TTL time.Duration QueryMeta From 2d5e869e69794cadd3017471bf03f1d62b6b4913 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 10:58:02 -0700 Subject: [PATCH 34/56] consul: Prevent resolution of root policy --- consul/acl.go | 5 +++++ consul/acl_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/consul/acl.go b/consul/acl.go index 727ee0c5b0..d45214cd33 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -14,6 +14,9 @@ const ( // aclNotFound indicates there is no matching ACL aclNotFound = "ACL not found" + // rootDenied is returned when attempting to resolve a root ACL + rootDenied = "Cannot resolve root ACL" + // anonymousToken is the token ID we re-write to if there // is no token ID provided anonymousToken = "anonymous" @@ -60,6 +63,8 @@ func (s *Server) resolveToken(id string) (acl.ACL, error) { // Handle the anonymous token if len(id) == 0 { id = anonymousToken + } else if acl.RootACL(id) != nil { + return nil, errors.New(rootDenied) } // Check if we are the ACL datacenter and the leader, use the diff --git a/consul/acl_test.go b/consul/acl_test.go index 7129a7e212..98def9574e 100644 --- a/consul/acl_test.go +++ b/consul/acl_test.go @@ -29,6 +29,30 @@ func TestACL_Disabled(t *testing.T) { } } +func TestACL_ResolveRootACL(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" // Enable ACLs! + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + acl, err := s1.resolveToken("allow") + if err == nil || err.Error() != rootDenied { + t.Fatalf("err: %v", err) + } + if acl != nil { + t.Fatalf("bad: %v", acl) + } + + acl, err = s1.resolveToken("deny") + if err == nil || err.Error() != rootDenied { + t.Fatalf("err: %v", err) + } + if acl != nil { + t.Fatalf("bad: %v", acl) + } +} + func TestACL_Authority_NotFound(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" // Enable ACLs! From fee3524deaa882b8fb2430c975c3536d70ed0bdb Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 11:34:58 -0700 Subject: [PATCH 35/56] agent: Special handler if ACL support is disabled --- command/agent/acl_endpoint.go | 7 +++++++ command/agent/http.go | 21 +++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 0e291ebbcb..815a867070 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -12,6 +12,13 @@ type aclCreateResponse struct { ID string } +// aclDisabled handles if ACL datacenter is not configured +func aclDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + resp.WriteHeader(401) + resp.Write([]byte("ACL support disabled")) + return nil, nil +} + func (s *HTTPServer) ACLDelete(resp http.ResponseWriter, req *http.Request) (interface{}, error) { args := structs.ACLRequest{ Op: structs.ACLDelete, diff --git a/command/agent/http.go b/command/agent/http.go index 45443547b2..905858e25c 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -99,12 +99,21 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/session/node/", s.wrap(s.SessionsForNode)) s.mux.HandleFunc("/v1/session/list", s.wrap(s.SessionList)) - s.mux.HandleFunc("/v1/acl/create", s.wrap(s.ACLCreate)) - s.mux.HandleFunc("/v1/acl/update", s.wrap(s.ACLUpdate)) - s.mux.HandleFunc("/v1/acl/delete/", s.wrap(s.ACLDelete)) - s.mux.HandleFunc("/v1/acl/info/", s.wrap(s.ACLGet)) - s.mux.HandleFunc("/v1/acl/clone/", s.wrap(s.ACLClone)) - s.mux.HandleFunc("/v1/acl/list", s.wrap(s.ACLList)) + if s.agent.config.ACLDatacenter != "" { + s.mux.HandleFunc("/v1/acl/create", s.wrap(s.ACLCreate)) + s.mux.HandleFunc("/v1/acl/update", s.wrap(s.ACLUpdate)) + s.mux.HandleFunc("/v1/acl/delete/", s.wrap(s.ACLDelete)) + s.mux.HandleFunc("/v1/acl/info/", s.wrap(s.ACLGet)) + s.mux.HandleFunc("/v1/acl/clone/", s.wrap(s.ACLClone)) + s.mux.HandleFunc("/v1/acl/list", s.wrap(s.ACLList)) + } else { + s.mux.HandleFunc("/v1/acl/create", s.wrap(aclDisabled)) + s.mux.HandleFunc("/v1/acl/update", s.wrap(aclDisabled)) + s.mux.HandleFunc("/v1/acl/delete/", s.wrap(aclDisabled)) + s.mux.HandleFunc("/v1/acl/info/", s.wrap(aclDisabled)) + s.mux.HandleFunc("/v1/acl/clone/", s.wrap(aclDisabled)) + s.mux.HandleFunc("/v1/acl/list", s.wrap(aclDisabled)) + } if enableDebug { s.mux.HandleFunc("/debug/pprof/", pprof.Index) From 88c2a9c9470a642ad1edbc3b02b075f93317651d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 11:35:22 -0700 Subject: [PATCH 36/56] agent: Adding token parsing --- command/agent/acl_endpoint.go | 32 ++++++++++++++++++++++---------- command/agent/http.go | 10 ++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 815a867070..07e0016ebe 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -21,9 +21,10 @@ func aclDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, erro func (s *HTTPServer) ACLDelete(resp http.ResponseWriter, req *http.Request) (interface{}, error) { args := structs.ACLRequest{ - Op: structs.ACLDelete, + Datacenter: s.agent.config.ACLDatacenter, + Op: structs.ACLDelete, } - s.parseDC(req, &args.Datacenter) + s.parseToken(req, &args.Token) // Pull out the acl id args.ACL.ID = strings.TrimPrefix(req.URL.Path, "/v1/acl/delete/") @@ -56,12 +57,13 @@ func (s *HTTPServer) aclSet(resp http.ResponseWriter, req *http.Request, update } args := structs.ACLRequest{ - Op: structs.ACLSet, + Datacenter: s.agent.config.ACLDatacenter, + Op: structs.ACLSet, ACL: structs.ACL{ Type: structs.ACLTypeClient, }, } - s.parseDC(req, &args.Datacenter) + s.parseToken(req, &args.Token) // Handle optional request body if req.ContentLength > 0 { @@ -97,8 +99,11 @@ func (s *HTTPServer) aclSet(resp http.ResponseWriter, req *http.Request, update } func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - args := structs.ACLSpecificRequest{} - if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + args := structs.ACLSpecificRequest{ + Datacenter: s.agent.config.ACLDatacenter, + } + var dc string + if done := s.parse(resp, req, &dc, &args.QueryOptions); done { return nil, nil } @@ -130,6 +135,7 @@ func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (inte ACL: *out.ACLs[0], } createArgs.ACL.ID = "" + createArgs.Token = args.Token // Create the acl, get the ID var outID string @@ -142,8 +148,11 @@ func (s *HTTPServer) ACLClone(resp http.ResponseWriter, req *http.Request) (inte } func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - args := structs.ACLSpecificRequest{} - if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + args := structs.ACLSpecificRequest{ + Datacenter: s.agent.config.ACLDatacenter, + } + var dc string + if done := s.parse(resp, req, &dc, &args.QueryOptions); done { return nil, nil } @@ -164,8 +173,11 @@ func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interf } func (s *HTTPServer) ACLList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - args := structs.DCSpecificRequest{} - if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + args := structs.DCSpecificRequest{ + Datacenter: s.agent.config.ACLDatacenter, + } + var dc string + if done := s.parse(resp, req, &dc, &args.QueryOptions); done { return nil, nil } diff --git a/command/agent/http.go b/command/agent/http.go index 905858e25c..905428f95c 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -289,10 +289,20 @@ func (s *HTTPServer) parseDC(req *http.Request, dc *string) { } } +// parseToken is used to parse the ?token query param +func (s *HTTPServer) parseToken(req *http.Request, token *string) { + if other := req.URL.Query().Get("token"); other != "" { + *token = other + } else if *token == "" { + *token = s.agent.config.ACLToken + } +} + // parse is a convenience method for endpoints that need // to use both parseWait and parseDC. func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, dc *string, b *structs.QueryOptions) bool { s.parseDC(req, dc) + s.parseToken(req, &b.Token) if parseConsistency(resp, req, b) { return true } From 7c5a39717eacdbcc6dde7f5b3d4ef5fefdfb42ef Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 14:48:36 -0700 Subject: [PATCH 37/56] agent: Fixing the ACL tests --- command/agent/acl_endpoint_test.go | 4 +++- command/agent/agent_test.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/command/agent/acl_endpoint_test.go b/command/agent/acl_endpoint_test.go index fc7f29766f..2ed53f7606 100644 --- a/command/agent/acl_endpoint_test.go +++ b/command/agent/acl_endpoint_test.go @@ -151,7 +151,9 @@ func TestACLList(t *testing.T) { if !ok { t.Fatalf("should work") } - if len(respObj) != 10 { + + // 10 + anonymous + if len(respObj) != 11 { t.Fatalf("bad: %v", respObj) } }) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index a9ca156949..d03596b38b 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -30,6 +30,7 @@ func nextConfig() *Config { conf.Ports.SerfWan = 18300 + idx conf.Ports.Server = 18100 + idx conf.Server = true + conf.ACLDatacenter = "dc1" cons := consul.DefaultConfig() conf.ConsulConfig = cons From c2153843c654f6bac11696993371d99eadff37f7 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 15:09:01 -0700 Subject: [PATCH 38/56] acl: Support ACL checks, adding new root policy --- acl/acl.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- acl/acl_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/acl/acl.go b/acl/acl.go index e6d45d23e0..071029603e 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -5,29 +5,47 @@ import ( ) var ( - // allowAll is a singleton policy which allows all actions + // allowAll is a singleton policy which allows all + // non-management actions allowAll ACL // denyAll is a singleton policy which denies all actions denyAll ACL + + // manageAll is a singleton policy which allows all + // actions, including management + manageAll ACL ) func init() { // Setup the singletons - allowAll = &StaticACL{defaultAllow: true} - denyAll = &StaticACL{defaultAllow: false} + allowAll = &StaticACL{ + allowManage: false, + defaultAllow: true, + } + denyAll = &StaticACL{ + allowManage: false, + defaultAllow: false, + } + manageAll = &StaticACL{ + allowManage: true, + defaultAllow: true, + } } // ACL is the interface for policy enforcement. type ACL interface { KeyRead(string) bool KeyWrite(string) bool + ACLList() bool + ACLModify() bool } // StaticACL is used to implement a base ACL policy. It either // allows or denies all requests. This can be used as a parent // ACL to act in a blacklist or whitelist mode. type StaticACL struct { + allowManage bool defaultAllow bool } @@ -39,6 +57,14 @@ func (s *StaticACL) KeyWrite(string) bool { return s.defaultAllow } +func (s *StaticACL) ACLList() bool { + return s.allowManage +} + +func (s *StaticACL) ACLModify() bool { + return s.allowManage +} + // AllowAll returns an ACL rule that allows all operations func AllowAll() ACL { return allowAll @@ -49,6 +75,11 @@ func DenyAll() ACL { return denyAll } +// ManageAll returns an ACL rule that can manage all resources +func ManageAll() ACL { + return manageAll +} + // RootACL returns a possible ACL if the ID matches a root policy func RootACL(id string) ACL { switch id { @@ -56,6 +87,8 @@ func RootACL(id string) ACL { return allowAll case "deny": return denyAll + case "manage": + return manageAll default: return nil } @@ -122,3 +155,13 @@ func (p *PolicyACL) KeyWrite(key string) bool { // No matching rule, use the parent. return p.parent.KeyWrite(key) } + +// ACLList checks if listing of ACLs is allowed +func (p *PolicyACL) ACLList() bool { + return p.ACLList() +} + +// ACLModify checks if modification of ACLs is allowed +func (p *PolicyACL) ACLModify() bool { + return p.ACLModify() +} diff --git a/acl/acl_test.go b/acl/acl_test.go index 444b3bd7fe..37cff4d079 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -11,6 +11,9 @@ func TestRootACL(t *testing.T) { if RootACL("deny") != DenyAll() { t.Fatalf("Bad root") } + if RootACL("manage") != ManageAll() { + t.Fatalf("Bad root") + } if RootACL("foo") != nil { t.Fatalf("bad root") } @@ -27,12 +30,23 @@ func TestStaticACL(t *testing.T) { t.Fatalf("expected static") } + manage := ManageAll() + if _, ok := none.(*StaticACL); !ok { + t.Fatalf("expected static") + } + if !all.KeyRead("foobar") { t.Fatalf("should allow") } if !all.KeyWrite("foobar") { t.Fatalf("should allow") } + if all.ACLList() { + t.Fatalf("should not allow") + } + if all.ACLModify() { + t.Fatalf("should not allow") + } if none.KeyRead("foobar") { t.Fatalf("should not allow") @@ -40,6 +54,25 @@ func TestStaticACL(t *testing.T) { if none.KeyWrite("foobar") { t.Fatalf("should not allow") } + if none.ACLList() { + t.Fatalf("should not noneow") + } + if none.ACLModify() { + t.Fatalf("should not noneow") + } + + if !manage.KeyRead("foobar") { + t.Fatalf("should allow") + } + if !manage.KeyWrite("foobar") { + t.Fatalf("should allow") + } + if !manage.ACLList() { + t.Fatalf("should allow") + } + if !manage.ACLModify() { + t.Fatalf("should allow") + } } func TestPolicyACL(t *testing.T) { From 78580a733ef89e3fc5c30a29245bf4a23af3a989 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 15:10:45 -0700 Subject: [PATCH 39/56] acl: Avoid infinite recursion... --- acl/acl.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acl/acl.go b/acl/acl.go index 071029603e..0b11094184 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -158,10 +158,10 @@ func (p *PolicyACL) KeyWrite(key string) bool { // ACLList checks if listing of ACLs is allowed func (p *PolicyACL) ACLList() bool { - return p.ACLList() + return p.parent.ACLList() } // ACLModify checks if modification of ACLs is allowed func (p *PolicyACL) ACLModify() bool { - return p.ACLModify() + return p.parent.ACLModify() } From 84488ed1f058cf0145abf37b0ce5ae6ea12a81fc Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 12 Aug 2014 15:32:44 -0700 Subject: [PATCH 40/56] consul: Starting token enforcement --- consul/acl.go | 16 ++++++- consul/acl_endpoint.go | 34 +++++++++++++++ consul/acl_endpoint_test.go | 84 +++++++++++++++++++++++++++++++++---- consul/acl_test.go | 12 ++++++ 4 files changed, 137 insertions(+), 9 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index d45214cd33..606e3c5602 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -17,11 +17,22 @@ const ( // rootDenied is returned when attempting to resolve a root ACL rootDenied = "Cannot resolve root ACL" + // permissionDenied is returned when an ACL based rejection happens + permissionDenied = "Permission denied" + + // aclDisabled is returned when ACL changes are not permitted + // since they are disabled. + aclDisabled = "ACL support disabled" + // anonymousToken is the token ID we re-write to if there // is no token ID provided anonymousToken = "anonymous" ) +var ( + permissionDeniedErr = errors.New(permissionDenied) +) + // aclCacheEntry is used to cache non-authoritative ACL's // If non-authoritative, then we must respect a TTL type aclCacheEntry struct { @@ -42,9 +53,10 @@ func (s *Server) aclFault(id string) (string, string, error) { return "", "", errors.New(aclNotFound) } - // Management tokens have no policy and inherit from allow + // Management tokens have no policy and inherit from the + // 'manage' root policy if acl.Type == structs.ACLTypeManagement { - return "allow", "", nil + return "manage", "", nil } // Otherwise use the base policy diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index 67fec26c37..efe431bd30 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -22,6 +22,18 @@ func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error { } defer metrics.MeasureSince([]string{"consul", "acl", "apply"}, time.Now()) + // Verify we are allowed to serve this request + if a.srv.config.ACLDatacenter != a.srv.config.Datacenter { + return fmt.Errorf(aclDisabled) + } + + // Verify token is permitted to list ACLs + if acl, err := a.srv.resolveToken(args.Token); err != nil { + return err + } else if acl == nil || !acl.ACLModify() { + return permissionDeniedErr + } + switch args.Op { case structs.ACLSet: // Verify the ACL type @@ -71,6 +83,11 @@ func (a *ACL) Get(args *structs.ACLSpecificRequest, return err } + // Verify we are allowed to serve this request + if a.srv.config.ACLDatacenter != a.srv.config.Datacenter { + return fmt.Errorf(aclDisabled) + } + // Get the local state state := a.srv.fsm.State() return a.srv.blockingRPC(&args.QueryOptions, @@ -93,6 +110,11 @@ func (a *ACL) GetPolicy(args *structs.ACLPolicyRequest, reply *structs.ACLPolicy return err } + // Verify we are allowed to serve this request + if a.srv.config.ACLDatacenter != a.srv.config.Datacenter { + return fmt.Errorf(aclDisabled) + } + // Get the policy via the cache parent, policy, err := a.srv.aclAuthCache.GetACLPolicy(args.ACL) if err != nil { @@ -123,6 +145,18 @@ func (a *ACL) List(args *structs.DCSpecificRequest, return err } + // Verify we are allowed to serve this request + if a.srv.config.ACLDatacenter != a.srv.config.Datacenter { + return fmt.Errorf(aclDisabled) + } + + // Verify token is permitted to list ACLs + if acl, err := a.srv.resolveToken(args.Token); err != nil { + return err + } else if acl == nil || !acl.ACLList() { + return permissionDeniedErr + } + // Get the local state state := a.srv.fsm.State() return a.srv.blockingRPC(&args.QueryOptions, diff --git a/consul/acl_endpoint_test.go b/consul/acl_endpoint_test.go index f3daceccc9..ba5da58f96 100644 --- a/consul/acl_endpoint_test.go +++ b/consul/acl_endpoint_test.go @@ -2,6 +2,7 @@ package consul import ( "os" + "strings" "testing" "time" @@ -10,7 +11,10 @@ import ( ) func TestACLEndpoint_Apply(t *testing.T) { - dir1, s1 := testServer(t) + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + }) defer os.RemoveAll(dir1) defer s1.Shutdown() client := rpcClient(t, s1) @@ -25,6 +29,7 @@ func TestACLEndpoint_Apply(t *testing.T) { Name: "User token", Type: structs.ACLTypeClient, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var out string if err := client.Call("ACL.Apply", &arg, &out); err != nil { @@ -65,8 +70,10 @@ func TestACLEndpoint_Apply(t *testing.T) { } } -func TestACLEndpoint_Get(t *testing.T) { - dir1, s1 := testServer(t) +func TestACLEndpoint_Apply_Denied(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + }) defer os.RemoveAll(dir1) defer s1.Shutdown() client := rpcClient(t, s1) @@ -83,6 +90,34 @@ func TestACLEndpoint_Get(t *testing.T) { }, } var out string + err := client.Call("ACL.Apply", &arg, &out) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("err: %v", err) + } +} + +func TestACLEndpoint_Get(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out string if err := client.Call("ACL.Apply", &arg, &out); err != nil { t.Fatalf("err: %v", err) } @@ -109,7 +144,10 @@ func TestACLEndpoint_Get(t *testing.T) { } func TestACLEndpoint_GetPolicy(t *testing.T) { - dir1, s1 := testServer(t) + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + }) defer os.RemoveAll(dir1) defer s1.Shutdown() client := rpcClient(t, s1) @@ -124,6 +162,7 @@ func TestACLEndpoint_GetPolicy(t *testing.T) { Name: "User token", Type: structs.ACLTypeClient, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var out string if err := client.Call("ACL.Apply", &arg, &out); err != nil { @@ -162,7 +201,10 @@ func TestACLEndpoint_GetPolicy(t *testing.T) { } func TestACLEndpoint_List(t *testing.T) { - dir1, s1 := testServer(t) + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + }) defer os.RemoveAll(dir1) defer s1.Shutdown() client := rpcClient(t, s1) @@ -179,6 +221,7 @@ func TestACLEndpoint_List(t *testing.T) { Name: "User token", Type: structs.ACLTypeClient, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var out string if err := client.Call("ACL.Apply", &arg, &out); err != nil { @@ -188,7 +231,8 @@ func TestACLEndpoint_List(t *testing.T) { } getR := structs.DCSpecificRequest{ - Datacenter: "dc1", + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: "root"}, } var acls structs.IndexedACLs if err := client.Call("ACL.List", &getR, &acls); err != nil { @@ -198,11 +242,16 @@ func TestACLEndpoint_List(t *testing.T) { if acls.Index == 0 { t.Fatalf("Bad: %v", acls) } - if len(acls.ACLs) != 5 { + + // 5 + anonymous + master + if len(acls.ACLs) != 7 { t.Fatalf("Bad: %v", acls.ACLs) } for i := 0; i < len(acls.ACLs); i++ { s := acls.ACLs[i] + if s.ID == anonymousToken || s.ID == "root" { + continue + } if !strContains(ids, s.ID) { t.Fatalf("bad: %v", s) } @@ -211,3 +260,24 @@ func TestACLEndpoint_List(t *testing.T) { } } } + +func TestACLEndpoint_List_Denied(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + getR := structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var acls structs.IndexedACLs + err := client.Call("ACL.List", &getR, &acls) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("err: %v", err) + } +} diff --git a/consul/acl_test.go b/consul/acl_test.go index 98def9574e..2fabcbee99 100644 --- a/consul/acl_test.go +++ b/consul/acl_test.go @@ -76,6 +76,7 @@ func TestACL_Authority_NotFound(t *testing.T) { func TestACL_Authority_Found(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" // Enable ACLs! + c.ACLMasterToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -93,6 +94,7 @@ func TestACL_Authority_Found(t *testing.T) { Type: structs.ACLTypeClient, Rules: testACLPolicy, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var id string if err := client.Call("ACL.Apply", &arg, &id); err != nil { @@ -250,6 +252,7 @@ func TestACL_NonAuthority_NotFound(t *testing.T) { func TestACL_NonAuthority_Found(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -287,6 +290,7 @@ func TestACL_NonAuthority_Found(t *testing.T) { Type: structs.ACLTypeClient, Rules: testACLPolicy, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var id string if err := client.Call("ACL.Apply", &arg, &id); err != nil { @@ -380,6 +384,7 @@ func TestACL_DownPolicy_Deny(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" c.ACLDownPolicy = "deny" + c.ACLMasterToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -418,6 +423,7 @@ func TestACL_DownPolicy_Deny(t *testing.T) { Type: structs.ACLTypeClient, Rules: testACLPolicy, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var id string if err := client.Call("ACL.Apply", &arg, &id); err != nil { @@ -452,6 +458,7 @@ func TestACL_DownPolicy_Allow(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" c.ACLDownPolicy = "allow" + c.ACLMasterToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -490,6 +497,7 @@ func TestACL_DownPolicy_Allow(t *testing.T) { Type: structs.ACLTypeClient, Rules: testACLPolicy, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var id string if err := client.Call("ACL.Apply", &arg, &id); err != nil { @@ -525,6 +533,7 @@ func TestACL_DownPolicy_ExtendCache(t *testing.T) { c.ACLDatacenter = "dc1" c.ACLTTL = 0 c.ACLDownPolicy = "extend-cache" + c.ACLMasterToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -564,6 +573,7 @@ func TestACL_DownPolicy_ExtendCache(t *testing.T) { Type: structs.ACLTypeClient, Rules: testACLPolicy, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var id string if err := client.Call("ACL.Apply", &arg, &id); err != nil { @@ -606,6 +616,7 @@ func TestACL_DownPolicy_ExtendCache(t *testing.T) { func TestACL_MultiDC_Found(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -638,6 +649,7 @@ func TestACL_MultiDC_Found(t *testing.T) { Type: structs.ACLTypeClient, Rules: testACLPolicy, }, + WriteRequest: structs.WriteRequest{Token: "root"}, } var id string if err := client.Call("ACL.Apply", &arg, &id); err != nil { From 614b0a14149da27051e189c948e055c2b06349a3 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 13 Aug 2014 10:42:10 -0700 Subject: [PATCH 41/56] consul: Helpers to filter on ACL rules --- consul/filter.go | 39 ++++++++++++++++++++++++++ consul/filter_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 consul/filter.go create mode 100644 consul/filter_test.go diff --git a/consul/filter.go b/consul/filter.go new file mode 100644 index 0000000000..9ccefc0797 --- /dev/null +++ b/consul/filter.go @@ -0,0 +1,39 @@ +package consul + +import ( + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/consul/structs" +) + +func FilterDirEnt(acl acl.ACL, ent structs.DirEntries) structs.DirEntries { + // Remove any keys blocked by ACLs + removed := 0 + for i := 0; i < len(ent); i++ { + if !acl.KeyRead(ent[i].Key) { + ent[i] = nil + removed++ + } + } + + // Compact the list + dst := 0 + src := 0 + n := len(ent) - removed + for dst < n { + for ent[src] == nil && src < n { + src++ + } + end := src + 1 + for ent[end] != nil && end < n { + end++ + } + span := end - src + copy(ent[dst:dst+span], ent[src:src+span]) + dst += span + src += span + } + + // Trim the entries + ent = ent[:n] + return ent +} diff --git a/consul/filter_test.go b/consul/filter_test.go new file mode 100644 index 0000000000..99c0398a62 --- /dev/null +++ b/consul/filter_test.go @@ -0,0 +1,65 @@ +package consul + +import ( + "reflect" + "testing" + + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/consul/structs" +) + +func TestFilterDirEnt(t *testing.T) { + policy, _ := acl.Parse(testFilterRules) + aclR, _ := acl.New(acl.DenyAll(), policy) + + type tcase struct { + in []string + out []string + } + cases := []tcase{ + tcase{ + in: []string{"foo/test", "foo/priv/nope", "foo/other", "zoo"}, + out: []string{"foo/test", "foo/other"}, + }, + tcase{ + in: []string{"abe", "lincoln"}, + out: nil, + }, + tcase{ + in: []string{"abe", "foo/1", "foo/2", "foo/3", "nope"}, + out: []string{"foo/1", "foo/2", "foo/3"}, + }, + } + + for _, tc := range cases { + ents := structs.DirEntries{} + for _, in := range tc.in { + ents = append(ents, &structs.DirEntry{Key: in}) + } + + ents = FilterDirEnt(aclR, ents) + var outL []string + for _, e := range ents { + outL = append(outL, e.Key) + } + + if !reflect.DeepEqual(outL, tc.out) { + t.Fatalf("bad: %#v %#v", outL, tc.out) + } + } +} + +var testFilterRules = ` +key "" { + policy = "deny" +} +key "foo/" { + policy = "read" +} +key "foo/priv/" { + policy = "deny" +} +key "zip/" { + policy = "read" +} +` From f49d34d0e347ca5ebe5be18a9f75018792223739 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 13 Aug 2014 11:31:23 -0700 Subject: [PATCH 42/56] consul: Filter keys, refactor to interface --- consul/filter.go | 86 ++++++++++++++++++++++++++++++++++--------- consul/filter_test.go | 31 ++++++++++++++++ 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/consul/filter.go b/consul/filter.go index 9ccefc0797..5577aa47a0 100644 --- a/consul/filter.go +++ b/consul/filter.go @@ -5,35 +5,85 @@ import ( "github.com/hashicorp/consul/consul/structs" ) -func FilterDirEnt(acl acl.ACL, ent structs.DirEntries) structs.DirEntries { - // Remove any keys blocked by ACLs - removed := 0 - for i := 0; i < len(ent); i++ { - if !acl.KeyRead(ent[i].Key) { - ent[i] = nil - removed++ - } - } +type dirEntFilter struct { + acl acl.ACL + ent structs.DirEntries +} +func (d *dirEntFilter) Len() int { + return len(d.ent) +} +func (d *dirEntFilter) Filter(i int) bool { + return !d.acl.KeyRead(d.ent[i].Key) +} +func (d *dirEntFilter) Move(dst, src, span int) { + copy(d.ent[dst:dst+span], d.ent[src:src+span]) +} + +// FilterDirEnt is used to filter a list of directory entries +// by applying an ACL policy +func FilterDirEnt(acl acl.ACL, ent structs.DirEntries) structs.DirEntries { + df := dirEntFilter{acl: acl, ent: ent} + return ent[:FilterEntries(&df)] +} + +type keyFilter struct { + acl acl.ACL + keys []string +} + +func (k *keyFilter) Len() int { + return len(k.keys) +} +func (k *keyFilter) Filter(i int) bool { + return !k.acl.KeyRead(k.keys[i]) +} + +func (k *keyFilter) Move(dst, src, span int) { + copy(k.keys[dst:dst+span], k.keys[src:src+span]) +} + +// FilterKeys is used to filter a list of keys by +// applying an ACL policy +func FilterKeys(acl acl.ACL, keys []string) []string { + kf := keyFilter{acl: acl, keys: keys} + return keys[:FilterEntries(&kf)] +} + +// Filter interfae is used with FilterEntries to do an +// in-place filter of a slice. +type Filter interface { + Len() int + Filter(int) bool + Move(dst, src, span int) +} + +// FilterEntries is used to do an inplace filter of +// a slice. This has cost proportional to the list length. +func FilterEntries(f Filter) int { // Compact the list dst := 0 src := 0 - n := len(ent) - removed + n := f.Len() for dst < n { - for ent[src] == nil && src < n { + for src < n && f.Filter(src) { src++ } + if src == n { + break + } end := src + 1 - for ent[end] != nil && end < n { + for end < n && !f.Filter(end) { end++ } span := end - src - copy(ent[dst:dst+span], ent[src:src+span]) - dst += span - src += span + if span > 0 { + f.Move(dst, src, span) + dst += span + src += span + } } - // Trim the entries - ent = ent[:n] - return ent + // Return the size of the slice + return dst } diff --git a/consul/filter_test.go b/consul/filter_test.go index 99c0398a62..15feb1e686 100644 --- a/consul/filter_test.go +++ b/consul/filter_test.go @@ -49,6 +49,37 @@ func TestFilterDirEnt(t *testing.T) { } } +func TestKeys(t *testing.T) { + policy, _ := acl.Parse(testFilterRules) + aclR, _ := acl.New(acl.DenyAll(), policy) + + type tcase struct { + in []string + out []string + } + cases := []tcase{ + tcase{ + in: []string{"foo/test", "foo/priv/nope", "foo/other", "zoo"}, + out: []string{"foo/test", "foo/other"}, + }, + tcase{ + in: []string{"abe", "lincoln"}, + out: nil, + }, + tcase{ + in: []string{"abe", "foo/1", "foo/2", "foo/3", "nope"}, + out: []string{"foo/1", "foo/2", "foo/3"}, + }, + } + + for _, tc := range cases { + out := FilterKeys(aclR, tc.in) + if !reflect.DeepEqual(out, tc.out) { + t.Fatalf("bad: %#v %#v", out, tc.out) + } + } +} + var testFilterRules = ` key "" { policy = "deny" From c7cb1f562bcafac5fd8b9d399818a9207045dbce Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 13 Aug 2014 12:09:39 -0700 Subject: [PATCH 43/56] consul: ACL enforcement for key reads --- consul/kvs_endpoint.go | 32 +++++- consul/kvs_endpoint_test.go | 219 ++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 3 deletions(-) diff --git a/consul/kvs_endpoint.go b/consul/kvs_endpoint.go index 91d8f3bdea..e271445c60 100644 --- a/consul/kvs_endpoint.go +++ b/consul/kvs_endpoint.go @@ -2,9 +2,10 @@ package consul import ( "fmt" + "time" + "github.com/armon/go-metrics" "github.com/hashicorp/consul/consul/structs" - "time" ) // KVS endpoint is used to manipulate the Key-Value store @@ -65,6 +66,11 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er return err } + acl, err := k.srv.resolveToken(args.Token) + if err != nil { + return err + } + // Get the local state state := k.srv.fsm.State() return k.srv.blockingRPC(&args.QueryOptions, @@ -75,6 +81,9 @@ func (k *KVS) Get(args *structs.KeyRequest, reply *structs.IndexedDirEntries) er if err != nil { return err } + if acl != nil && !acl.KeyRead(args.Key) { + ent = nil + } if ent == nil { // Must provide non-zero index to prevent blocking // Index 1 is impossible anyways (due to Raft internals) @@ -98,6 +107,11 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e return err } + acl, err := k.srv.resolveToken(args.Token) + if err != nil { + return err + } + // Get the local state state := k.srv.fsm.State() return k.srv.blockingRPC(&args.QueryOptions, @@ -108,6 +122,9 @@ func (k *KVS) List(args *structs.KeyRequest, reply *structs.IndexedDirEntries) e if err != nil { return err } + if acl != nil { + ent = FilterDirEnt(acl, ent) + } if len(ent) == 0 { // Must provide non-zero index to prevent blocking // Index 1 is impossible anyways (due to Raft internals) @@ -139,14 +156,23 @@ func (k *KVS) ListKeys(args *structs.KeyListRequest, reply *structs.IndexedKeyLi return err } + acl, err := k.srv.resolveToken(args.Token) + if err != nil { + return err + } + // Get the local state state := k.srv.fsm.State() return k.srv.blockingRPC(&args.QueryOptions, &reply.QueryMeta, state.QueryTables("KVSListKeys"), func() error { - var err error - reply.Index, reply.Keys, err = state.KVSListKeys(args.Prefix, args.Seperator) + index, keys, err := state.KVSListKeys(args.Prefix, args.Seperator) + reply.Index = index + if acl != nil { + keys = FilterKeys(acl, keys) + } + reply.Keys = keys return err }) } diff --git a/consul/kvs_endpoint_test.go b/consul/kvs_endpoint_test.go index c4131fdcbd..f4cfd0941e 100644 --- a/consul/kvs_endpoint_test.go +++ b/consul/kvs_endpoint_test.go @@ -111,6 +111,51 @@ func TestKVS_Get(t *testing.T) { } } +func TestKVS_Get_ACLDeny(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + arg := structs.KVSRequest{ + Datacenter: "dc1", + Op: structs.KVSSet, + DirEnt: structs.DirEntry{ + Key: "test", + Flags: 42, + Value: []byte("test"), + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out bool + if err := client.Call("KVS.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + getR := structs.KeyRequest{ + Datacenter: "dc1", + Key: "test", + } + var dirent structs.IndexedDirEntries + if err := client.Call("KVS.Get", &getR, &dirent); err != nil { + t.Fatalf("err: %v", err) + } + + if dirent.Index == 0 { + t.Fatalf("Bad: %v", dirent) + } + if len(dirent.Entries) != 0 { + t.Fatalf("Bad: %v", dirent) + } +} + func TestKVSEndpoint_List(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -170,6 +215,90 @@ func TestKVSEndpoint_List(t *testing.T) { } } +func TestKVSEndpoint_List_ACLDeny(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + keys := []string{ + "abe", + "bar", + "foo", + "test", + "zip", + } + + for _, key := range keys { + arg := structs.KVSRequest{ + Datacenter: "dc1", + Op: structs.KVSSet, + DirEnt: structs.DirEntry{ + Key: key, + Flags: 1, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out bool + if err := client.Call("KVS.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + } + + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testListRules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + id := out + + getR := structs.KeyRequest{ + Datacenter: "dc1", + Key: "", + QueryOptions: structs.QueryOptions{Token: id}, + } + var dirent structs.IndexedDirEntries + if err := client.Call("KVS.List", &getR, &dirent); err != nil { + t.Fatalf("err: %v", err) + } + + if dirent.Index == 0 { + t.Fatalf("Bad: %v", dirent) + } + if len(dirent.Entries) != 2 { + t.Fatalf("Bad: %v", dirent.Entries) + } + for i := 0; i < len(dirent.Entries); i++ { + d := dirent.Entries[i] + switch i { + case 0: + if d.Key != "foo" { + t.Fatalf("bad key") + } + case 1: + if d.Key != "test" { + t.Fatalf("bad key") + } + } + } +} + func TestKVSEndpoint_ListKeys(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -227,6 +356,84 @@ func TestKVSEndpoint_ListKeys(t *testing.T) { } } +func TestKVSEndpoint_ListKeys_ACLDeny(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + keys := []string{ + "abe", + "bar", + "foo", + "test", + "zip", + } + + for _, key := range keys { + arg := structs.KVSRequest{ + Datacenter: "dc1", + Op: structs.KVSSet, + DirEnt: structs.DirEntry{ + Key: key, + Flags: 1, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out bool + if err := client.Call("KVS.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + } + + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testListRules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + id := out + + getR := structs.KeyListRequest{ + Datacenter: "dc1", + Prefix: "", + Seperator: "/", + QueryOptions: structs.QueryOptions{Token: id}, + } + var dirent structs.IndexedKeyList + if err := client.Call("KVS.ListKeys", &getR, &dirent); err != nil { + t.Fatalf("err: %v", err) + } + + if dirent.Index == 0 { + t.Fatalf("Bad: %v", dirent) + } + if len(dirent.Keys) != 2 { + t.Fatalf("Bad: %v", dirent.Keys) + } + if dirent.Keys[0] != "foo" { + t.Fatalf("Bad: %v", dirent.Keys) + } + if dirent.Keys[1] != "test" { + t.Fatalf("Bad: %v", dirent.Keys) + } +} + func TestKVS_Apply_LockDelay(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -294,3 +501,15 @@ func TestKVS_Apply_LockDelay(t *testing.T) { t.Fatalf("should acquire") } } + +var testListRules = ` +key "" { + policy = "deny" +} +key "foo" { + policy = "read" +} +key "test" { + policy = "write" +} +` From 705c6cdb86536a65f4502a75213882358605ea72 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 14 Aug 2014 15:53:02 -0700 Subject: [PATCH 44/56] acl: Support checking write permissions on a prefix --- acl/acl.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ acl/acl_test.go | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/acl/acl.go b/acl/acl.go index 0b11094184..4428373402 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -35,9 +35,21 @@ func init() { // ACL is the interface for policy enforcement. type ACL interface { + // KeyRead checks for permission to read a given key KeyRead(string) bool + + // KeyWrite checks for permission to write a given key KeyWrite(string) bool + + // KeyWritePrefix checks for permission to write to an + // entire key prefix. This means there must be no sub-policies + // that deny a write. + KeyWritePrefix(string) bool + + // ACLList checks for permission to list all the ACLs ACLList() bool + + // ACLModify checks for permission to manipulate ACLs ACLModify() bool } @@ -57,6 +69,10 @@ func (s *StaticACL) KeyWrite(string) bool { return s.defaultAllow } +func (s *StaticACL) KeyWritePrefix(string) bool { + return s.defaultAllow +} + func (s *StaticACL) ACLList() bool { return s.allowManage } @@ -156,6 +172,39 @@ func (p *PolicyACL) KeyWrite(key string) bool { return p.parent.KeyWrite(key) } +// KeyWritePrefix returns if a prefix is allowed to be written +func (p *PolicyACL) KeyWritePrefix(prefix string) bool { + // Look for a matching rule that denies + _, rule, ok := p.keyRules.LongestPrefix(prefix) + if ok && rule.(string) != KeyPolicyWrite { + return false + } + + // Look if any of our children have a deny policy + deny := false + p.keyRules.WalkPrefix(prefix, func(path string, rule interface{}) bool { + // We have a rule to prevent a write in a sub-directory! + if rule.(string) != KeyPolicyWrite { + deny = true + return true + } + return false + }) + + // Deny the write if any sub-rules may be violated + if deny { + return false + } + + // If we had a matching rule, done + if ok { + return true + } + + // No matching rule, use the parent. + return p.parent.KeyWritePrefix(prefix) +} + // ACLList checks if listing of ACLs is allowed func (p *PolicyACL) ACLList() bool { return p.parent.ACLList() diff --git a/acl/acl_test.go b/acl/acl_test.go index 37cff4d079..9be0388db3 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -103,16 +103,19 @@ func TestPolicyACL(t *testing.T) { } type tcase struct { - inp string - read bool - write bool + inp string + read bool + write bool + writePrefix bool } cases := []tcase{ - {"other", true, true}, - {"foo/test", true, true}, - {"foo/priv/test", false, false}, - {"bar/any", false, false}, - {"zip/test", true, false}, + {"other", true, true, true}, + {"foo/test", true, true, true}, + {"foo/priv/test", false, false, false}, + {"bar/any", false, false, false}, + {"zip/test", true, false, false}, + {"foo/", true, true, false}, + {"", true, true, false}, } for _, c := range cases { if c.read != acl.KeyRead(c.inp) { @@ -121,6 +124,9 @@ func TestPolicyACL(t *testing.T) { if c.write != acl.KeyWrite(c.inp) { t.Fatalf("Write fail: %#v", c) } + if c.writePrefix != acl.KeyWritePrefix(c.inp) { + t.Fatalf("Write prefix fail: %#v", c) + } } } @@ -165,16 +171,17 @@ func TestPolicyACL_Parent(t *testing.T) { } type tcase struct { - inp string - read bool - write bool + inp string + read bool + write bool + writePrefix bool } cases := []tcase{ - {"other", false, false}, - {"foo/test", true, true}, - {"foo/priv/test", true, false}, - {"bar/any", false, false}, - {"zip/test", true, false}, + {"other", false, false, false}, + {"foo/test", true, true, true}, + {"foo/priv/test", true, false, false}, + {"bar/any", false, false, false}, + {"zip/test", true, false, false}, } for _, c := range cases { if c.read != acl.KeyRead(c.inp) { @@ -183,5 +190,8 @@ func TestPolicyACL_Parent(t *testing.T) { if c.write != acl.KeyWrite(c.inp) { t.Fatalf("Write fail: %#v", c) } + if c.writePrefix != acl.KeyWritePrefix(c.inp) { + t.Fatalf("Write prefix fail: %#v", c) + } } } From 25855b23622d4836f9422eb1b841782319f98ba7 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 14 Aug 2014 19:25:12 -0700 Subject: [PATCH 45/56] consul: ACL enforcement for KV updates --- consul/kvs_endpoint.go | 17 +++++++++ consul/kvs_endpoint_test.go | 75 +++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/consul/kvs_endpoint.go b/consul/kvs_endpoint.go index e271445c60..53ed238be1 100644 --- a/consul/kvs_endpoint.go +++ b/consul/kvs_endpoint.go @@ -26,6 +26,23 @@ func (k *KVS) Apply(args *structs.KVSRequest, reply *bool) error { return fmt.Errorf("Must provide key") } + // Apply the ACL policy if any + acl, err := k.srv.resolveToken(args.Token) + if err != nil { + return err + } else if acl != nil { + switch args.Op { + case structs.KVSDeleteTree: + if !acl.KeyWritePrefix(args.DirEnt.Key) { + return permissionDeniedErr + } + default: + if !acl.KeyWrite(args.DirEnt.Key) { + return permissionDeniedErr + } + } + } + // If this is a lock, we must check for a lock-delay. Since lock-delay // is based on wall-time, each peer expire the lock-delay at a slightly // different time. This means the enforcement of lock-delay cannot be done diff --git a/consul/kvs_endpoint_test.go b/consul/kvs_endpoint_test.go index f4cfd0941e..3a37698257 100644 --- a/consul/kvs_endpoint_test.go +++ b/consul/kvs_endpoint_test.go @@ -1,11 +1,13 @@ package consul import ( - "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/consul/testutil" "os" + "strings" "testing" "time" + + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" ) func TestKVS_Apply(t *testing.T) { @@ -64,6 +66,68 @@ func TestKVS_Apply(t *testing.T) { } } +func TestKVS_Apply_ACLDeny(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + // Create the ACL + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testListRules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + id := out + + // Try a write + argR := structs.KVSRequest{ + Datacenter: "dc1", + Op: structs.KVSSet, + DirEnt: structs.DirEntry{ + Key: "foo/bar", + Flags: 42, + Value: []byte("test"), + }, + WriteRequest: structs.WriteRequest{Token: id}, + } + var outR bool + err := client.Call("KVS.Apply", &argR, &outR) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("err: %v", err) + } + + // Try a recursive delete + argR = structs.KVSRequest{ + Datacenter: "dc1", + Op: structs.KVSDeleteTree, + DirEnt: structs.DirEntry{ + Key: "test", + }, + WriteRequest: structs.WriteRequest{Token: id}, + } + err = client.Call("KVS.Apply", &argR, &outR) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("err: %v", err) + } +} + func TestKVS_Get(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -128,7 +192,7 @@ func TestKVS_Get_ACLDeny(t *testing.T) { Datacenter: "dc1", Op: structs.KVSSet, DirEnt: structs.DirEntry{ - Key: "test", + Key: "zip", Flags: 42, Value: []byte("test"), }, @@ -141,7 +205,7 @@ func TestKVS_Get_ACLDeny(t *testing.T) { getR := structs.KeyRequest{ Datacenter: "dc1", - Key: "test", + Key: "zip", } var dirent structs.IndexedDirEntries if err := client.Call("KVS.Get", &getR, &dirent); err != nil { @@ -512,4 +576,7 @@ key "foo" { key "test" { policy = "write" } +key "test/priv" { + policy = "read" +} ` From 9bababf87274276516a439addf72efb3b483106a Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 14 Aug 2014 19:32:05 -0700 Subject: [PATCH 46/56] acl: Avoid shared cache with different parents --- acl/cache.go | 18 +++++++++++------ acl/cache_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/acl/cache.go b/acl/cache.go index 22d13d7b9b..6e7295562b 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -13,9 +13,9 @@ type FaultFunc func(id string) (string, string, error) // aclEntry allows us to store the ACL with it's policy ID type aclEntry struct { - ACL ACL - Parent string - PolicyID string + ACL ACL + Parent string + RuleID string } // Cache is used to implement policy and ACL caching @@ -71,13 +71,18 @@ func (c *Cache) ruleID(rules string) string { return fmt.Sprintf("%x", md5.Sum([]byte(rules))) } +// policyID returns the cache ID for a policy +func (c *Cache) policyID(parent, ruleID string) string { + return parent + ":" + ruleID +} + // GetACLPolicy is used to get the potentially cached ACL // policy. If not cached, it will be generated and then cached. func (c *Cache) GetACLPolicy(id string) (string, *Policy, error) { // Check for a cached acl if raw, ok := c.aclCache.Get(id); ok { cached := raw.(aclEntry) - if raw, ok := c.ruleCache.Get(cached.PolicyID); ok { + if raw, ok := c.ruleCache.Get(cached.RuleID); ok { return cached.Parent, raw.(*Policy), nil } } @@ -110,8 +115,9 @@ func (c *Cache) GetACL(id string) (ACL, error) { ruleID := c.ruleID(rules) // Check for a compiled ACL + policyID := c.policyID(parentID, ruleID) var compiled ACL - if raw, ok := c.policyCache.Get(ruleID); ok { + if raw, ok := c.policyCache.Get(policyID); ok { compiled = raw.(ACL) } else { // Get the policy @@ -136,7 +142,7 @@ func (c *Cache) GetACL(id string) (ACL, error) { } // Cache the compiled ACL - c.policyCache.Add(ruleID, acl) + c.policyCache.Add(policyID, acl) compiled = acl } diff --git a/acl/cache_test.go b/acl/cache_test.go index 8502e44d3c..f880bcaf4e 100644 --- a/acl/cache_test.go +++ b/acl/cache_test.go @@ -114,7 +114,7 @@ func TestCache_ClearACL(t *testing.T) { c.ClearACL("foo") // Clear the policy cache - c.policyCache.Remove(c.ruleID(testSimplePolicy)) + c.policyCache.Purge() acl2, err := c.GetACL("foo") if err != nil { @@ -238,6 +238,53 @@ func TestCache_GetACL_Parent(t *testing.T) { } } +func TestCache_GetACL_ParentCache(t *testing.T) { + // Same rules, different parent + faultfn := func(id string) (string, string, error) { + switch id { + case "foo": + return "allow", testSimplePolicy, nil + case "bar": + return "deny", testSimplePolicy, nil + } + t.Fatalf("bad case") + return "", "", nil + } + + c, err := NewCache(16, faultfn) + if err != nil { + t.Fatalf("err: %v", err) + } + + acl, err := c.GetACL("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + if !acl.KeyRead("bar/test") { + t.Fatalf("should allow") + } + if !acl.KeyRead("foo/test") { + t.Fatalf("should allow") + } + + acl2, err := c.GetACL("bar") + if err != nil { + t.Fatalf("err: %v", err) + } + + if acl == acl2 { + t.Fatalf("should not match") + } + + if acl2.KeyRead("bar/test") { + t.Fatalf("should not allow") + } + if !acl2.KeyRead("foo/test") { + t.Fatalf("should allow") + } +} + var testSimplePolicy = ` key "foo/" { policy = "read" From 0ff28a12fac9ae123b4c5c1aea573b2d60032a6e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 14 Aug 2014 19:34:50 -0700 Subject: [PATCH 47/56] agent: Copy token in KV PUT/DELETE --- command/agent/kvs_endpoint.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/command/agent/kvs_endpoint.go b/command/agent/kvs_endpoint.go index dfcce3025f..48d9ce19d5 100644 --- a/command/agent/kvs_endpoint.go +++ b/command/agent/kvs_endpoint.go @@ -145,6 +145,7 @@ func (s *HTTPServer) KVSPut(resp http.ResponseWriter, req *http.Request, args *s Value: nil, }, } + applyReq.Token = args.Token // Check for flags params := req.URL.Query() @@ -215,6 +216,7 @@ func (s *HTTPServer) KVSDelete(resp http.ResponseWriter, req *http.Request, args Key: args.Key, }, } + applyReq.Token = args.Token // Check for recurse params := req.URL.Query() From 8a0eeae26994a439f3f13280c05a1ed6f17aca18 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 14 Aug 2014 19:59:02 -0700 Subject: [PATCH 48/56] website: document configuration --- .../source/docs/agent/options.html.markdown | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index b436c90f0e..4e6d66590b 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -284,6 +284,38 @@ definitions support being updated during a reload. will not make use of TLS for outgoing connections. This applies to clients and servers, as both will make outgoing connections. +* `acl_datacenter` - Only used by servers. This designates the datacenter which + is authoritative for ACL information. It must be provided to enable ACLs. + All servers and datacenters must agree on the ACL datacenter. + +* `acl_token` - When provided, the agent will use this token when making requests + to the Consul servers. Clients can override this token on a per-request basis + by providing the ?token parameter. When not provided, the empty token is used + which maps to the 'anonymous' ACL policy. + +* `acl_master_token` - Only used for servers in the `acl_datacenter`. This token + will be created if it does not exist with management level permissions. It allows + operators to bootstrap the ACL system with a token ID that is well-known. + +* `acl_default_policy` - Either "allow" or "deny", defaults to "allow". The + default policy controls the behavior of a token when there is no matching + rule. In "allow" mode, ACLs are a blacklist: any operation not specifically + prohibited is allowed. In "deny" mode, ACLs are a whilelist: any operation not + specifically allowed is blocked. + +* `acl_down_policy` - Either "allow", "deny" or "extend-cache" which is the + default. In the case that the policy for a token cannot be read from the + `acl_datacenter` or leader node, the down policy is applied. In "allow" mode, + all actions are permitted, "deny" restricts all operations, and "extend-cache" + allows any cached ACLs to be used, ignoring their TTL values. If a non-cached + ACL is used, "extend-cache" acts like "deny". + +* `acl_ttl` - Used to control Time-To-Live caching of ACLs. By default this + is 30 seconds. This setting has a major performance impact: reducing it will + cause more frequent refreshes, while increasing it reduces the number of caches. + However, because the caches are not actively invalidated, ACL policy may be stale + up to the TTL value. + ## Ports Used Consul requires up to 5 different ports to work properly, some requiring From 34e018e47126ae27b484e034c793b0d07fcf02a6 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 11:36:16 -0700 Subject: [PATCH 49/56] acl: Updating for HCL changes --- acl/policy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acl/policy.go b/acl/policy.go index ecc11e1daf..014ef51ac8 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -15,7 +15,7 @@ const ( // an ACL configuration. type Policy struct { ID string `hcl:"-"` - Keys []*KeyPolicy `hcl:"key"` + Keys []*KeyPolicy `hcl:"key,expand"` } // KeyPolicy represents a policy for a key From 343f69504bc3b31a852c7100c0fc62bc09937bd7 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 12:05:01 -0700 Subject: [PATCH 50/56] agent: Rename acl delete to destroy --- command/agent/acl_endpoint.go | 4 ++-- command/agent/acl_endpoint_test.go | 6 +++--- command/agent/http.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 07e0016ebe..52db96fec2 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -19,7 +19,7 @@ func aclDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, erro return nil, nil } -func (s *HTTPServer) ACLDelete(resp http.ResponseWriter, req *http.Request) (interface{}, error) { +func (s *HTTPServer) ACLDestroy(resp http.ResponseWriter, req *http.Request) (interface{}, error) { args := structs.ACLRequest{ Datacenter: s.agent.config.ACLDatacenter, Op: structs.ACLDelete, @@ -27,7 +27,7 @@ func (s *HTTPServer) ACLDelete(resp http.ResponseWriter, req *http.Request) (int s.parseToken(req, &args.Token) // Pull out the acl id - args.ACL.ID = strings.TrimPrefix(req.URL.Path, "/v1/acl/delete/") + args.ACL.ID = strings.TrimPrefix(req.URL.Path, "/v1/acl/destroy/") if args.ACL.ID == "" { resp.WriteHeader(400) resp.Write([]byte("Missing ACL")) diff --git a/command/agent/acl_endpoint_test.go b/command/agent/acl_endpoint_test.go index 2ed53f7606..9db7971b47 100644 --- a/command/agent/acl_endpoint_test.go +++ b/command/agent/acl_endpoint_test.go @@ -62,12 +62,12 @@ func TestACLUpdate(t *testing.T) { }) } -func TestACLDelete(t *testing.T) { +func TestACLDestroy(t *testing.T) { httpTest(t, func(srv *HTTPServer) { id := makeTestACL(t, srv) - req, err := http.NewRequest("PUT", "/v1/session/delete/"+id, nil) + req, err := http.NewRequest("PUT", "/v1/session/destroy/"+id, nil) resp := httptest.NewRecorder() - obj, err := srv.ACLDelete(resp, req) + obj, err := srv.ACLDestroy(resp, req) if err != nil { t.Fatalf("err: %v", err) } diff --git a/command/agent/http.go b/command/agent/http.go index 905428f95c..43b8019e9d 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -102,14 +102,14 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { if s.agent.config.ACLDatacenter != "" { s.mux.HandleFunc("/v1/acl/create", s.wrap(s.ACLCreate)) s.mux.HandleFunc("/v1/acl/update", s.wrap(s.ACLUpdate)) - s.mux.HandleFunc("/v1/acl/delete/", s.wrap(s.ACLDelete)) + s.mux.HandleFunc("/v1/acl/destroy/", s.wrap(s.ACLDestroy)) s.mux.HandleFunc("/v1/acl/info/", s.wrap(s.ACLGet)) s.mux.HandleFunc("/v1/acl/clone/", s.wrap(s.ACLClone)) s.mux.HandleFunc("/v1/acl/list", s.wrap(s.ACLList)) } else { s.mux.HandleFunc("/v1/acl/create", s.wrap(aclDisabled)) s.mux.HandleFunc("/v1/acl/update", s.wrap(aclDisabled)) - s.mux.HandleFunc("/v1/acl/delete/", s.wrap(aclDisabled)) + s.mux.HandleFunc("/v1/acl/destroy/", s.wrap(aclDisabled)) s.mux.HandleFunc("/v1/acl/info/", s.wrap(aclDisabled)) s.mux.HandleFunc("/v1/acl/clone/", s.wrap(aclDisabled)) s.mux.HandleFunc("/v1/acl/list", s.wrap(aclDisabled)) From b802dd16eb6b3fb7376c147b3f7dbca19259dc30 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 12:16:17 -0700 Subject: [PATCH 51/56] website: Documenting ACL endpoints --- website/source/docs/agent/http.html.markdown | 151 ++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 980dd807fb..feff6bfb2a 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -17,6 +17,7 @@ All endpoints fall into one of several categories: * catalog - Manages nodes and services * health - Manages health checks * session - Session manipulation +* acl - ACL creations and management * status - Consul system status * internal - Internal APIs. Purposely undocumented, subject to change. @@ -85,6 +86,14 @@ By default, the output of all HTTP API requests return minimized JSON with all whitespace removed. By adding "?pretty" to the HTTP request URL, formatted JSON will be returned. +## ACLs + +Several endpoints in Consul use or require ACL tokens to operate. An agent +can be configured to use a default token in requests using the `acl_token` +configuration option. However, the token can also be specified per-request +by using the "?token=" query parameter. This will take precedence over the +default token. + ## KV The KV endpoint is used to expose a simple key/value store. This can be used @@ -99,7 +108,8 @@ are all supported. It is important to note that each datacenter has its own K/V store, and that there is no replication between datacenters. By default the datacenter of the agent is queried, however the dc can be provided using the "?dc=" query parameter. If a client wants to write -to all Datacenters, one request per datacenter must be made. +to all Datacenters, one request per datacenter must be made. The KV endpoint +supports the use of ACL tokens. ### GET Method @@ -1039,6 +1049,145 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. +## ACL + +The ACL endpoints are used to create, update, destroy and query ACL tokens. +The following endpoints are supported: + +* /v1/acl/create: Creates a new token with policy +* /v1/acl/update: Update the policy of a token +* /v1/acl/destroy/\: Destroys a given token +* /v1/acl/info/\: Queries the policy of a given token +* /v1/acl/clone/\: Creates a new token by cloning an existing token +* /v1/acl/list: Lists all the active tokens + +### /v1/acl/create + +The create endpoint is used to make a new token. A token has a name, +type, and a set of ACL rules. The name is opaque to Consul, and type +is either "client" or "management". A management token is effectively +like a root user, and has the ability to perform any action including +creating, modifying, and deleting ACLs. A client token can only perform +actions as permitted by the rules associated, and may never manage ACLs. +This means the request to this endpoint must be made with a management +token. + +In any Consul cluster, only a single datacenter is authoritative for ACLs, so +all requests are automatically routed to that datacenter regardless +of the agent that the request is made to. + +The create endpoint expects a JSON request body to be PUT. The request +body must look like: + + { + "Name": "my-app-token", + "Type": "client", + "Rules": "", + } + +None of the fields are mandatory, and in fact no body needs to be PUT +if the defaults are to be used. The `Name` and `Rules` default to being +blank, and the `Type` defaults to "client". The format of `Rules` is +[documented here](). + +The return code is 200 on success, along with a body like: + + {"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"} + +This is used to provide the ID of the newly created ACL token. + +### /v1/acl/update + +The update endpoint is used to modify the policy for a given +ACL token. It is very similar to the create endpoint, however +instead of generating a new token ID, the `ID` field must be +provided. Requests to this endpoint must be made with a management +token. + +In any Consul cluster, only a single datacenter is authoritative for ACLs, so +all requests are automatically routed to that datacenter regardless +of the agent that the request is made to. + +The update endpoint expects a JSON request body to be PUT. The request +body must look like: + + { + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" + "Name": "my-app-token-updated", + "Type": "client", + "Rules": "# New Rules", + } + +Only the `ID` field is mandatory, the other fields provide defaults. +The `Name` and `Rules` default to being blank, and the `Type` defaults to "client". +The format of `Rules` is [documented here](). + +The return code is 200 on success. + +### /v1/acl/destroy/\ + +The destroy endpoint is hit with a PUT and destroys the given ACL token. +The request is automatically routed to the authoritative ACL datacenter. +The token being destroyed must be provided after the slash, and requests +to the endpoint must be made with a management token. + +The return code is 200 on success. + +### /v1/acl/info/\ + +This endpoint is hit with a GET and returns the token information +by ID. All requests are routed to the authoritative ACL datacenter +The token being queried must be provided after the slash. + +It returns a JSON body like this: + + [ + { + "CreateIndex":3, + "ModifyIndex":3, + "ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05", + "Name":"Client Token", + "Type":"client", + "Rules":"..." + } + ] + +If the session is not found, null is returned instead of a JSON list. + +### /v1/acl/clone/\ + +The clone endpoint is hit with a PUT and returns a token ID that +is cloned from an existing token. This allows a token to serve +as a template for others, making it simple to generate new tokens +without complex rule management. The source token must be provided +after the slash. Requests to this endpoint require a management token. + +The return code is 200 on success, along with a body like: + + {"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"} + +This is used to provide the ID of the newly created ACL token. + +### /v1/acl/list + +The list endpoint is hit with a GET and lists all the active +ACL tokens. This is a privileged endpoint, and requires a +management token. + +It returns a JSON body like this: + + [ + { + "CreateIndex":3, + "ModifyIndex":3, + "ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05", + "Name":"Client Token", + "Type":"client", + "Rules":"..." + }, + ... + ] + ## Status The Status endpoints are used to get information about the status From 05900f35c2720cf23f6ebbb20fc0f7e9319ab2b1 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 14:54:52 -0700 Subject: [PATCH 52/56] acl: Test parsing JSON --- acl/policy_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/acl/policy_test.go b/acl/policy_test.go index 6db3317c08..0fc75e0fa9 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -50,3 +50,51 @@ key "foo/bar/baz" { t.Fatalf("bad: %#v %#v", out, exp) } } + +func TestParse_JSON(t *testing.T) { + inp := `{ + "key": { + "": { + "policy": "read" + }, + "foo/": { + "policy": "write" + }, + "foo/bar/": { + "policy": "read" + }, + "foo/bar/baz": { + "policy": "deny" + } + } +}` + exp := &Policy{ + Keys: []*KeyPolicy{ + &KeyPolicy{ + Prefix: "", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "foo/", + Policy: KeyPolicyWrite, + }, + &KeyPolicy{ + Prefix: "foo/bar/", + Policy: KeyPolicyRead, + }, + &KeyPolicy{ + Prefix: "foo/bar/baz", + Policy: KeyPolicyDeny, + }, + }, + } + + out, err := Parse(inp) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !reflect.DeepEqual(out, exp) { + t.Fatalf("bad: %#v %#v", out, exp) + } +} From 781ff2048d0e9a33af73d6ec6dda3fa1cfc001b1 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 15:05:11 -0700 Subject: [PATCH 53/56] website: ACL internals --- website/source/docs/agent/http.html.markdown | 4 +- .../source/docs/internals/acl.html.markdown | 112 ++++++++++++++++++ website/source/layouts/docs.erb | 4 + 3 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 website/source/docs/internals/acl.html.markdown diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index feff6bfb2a..2b59a2da98 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -1088,7 +1088,7 @@ body must look like: None of the fields are mandatory, and in fact no body needs to be PUT if the defaults are to be used. The `Name` and `Rules` default to being blank, and the `Type` defaults to "client". The format of `Rules` is -[documented here](). +[documented here](/docs/internals/acl.html). The return code is 200 on success, along with a body like: @@ -1120,7 +1120,7 @@ body must look like: Only the `ID` field is mandatory, the other fields provide defaults. The `Name` and `Rules` default to being blank, and the `Type` defaults to "client". -The format of `Rules` is [documented here](). +The format of `Rules` is [documented here](/docs/internals/acl.html). The return code is 200 on success. diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown new file mode 100644 index 0000000000..d5063ff2e7 --- /dev/null +++ b/website/source/docs/internals/acl.html.markdown @@ -0,0 +1,112 @@ +--- +layout: "docs" +page_title: "ACL System" +sidebar_current: "docs-internals-acl" +--- + +# ACL System + +Consul provides an optional Access Control List (ACL) system which can be used to control +access to data and APIs. The ACL system is an +[Object-Capability system](http://en.wikipedia.org/wiki/Object-capability_model) that relies +on tokens which can have fine grained rules applied to them. It is very similar to +[AWS IAM](http://aws.amazon.com/iam/) in many ways. + +## ACL Design + +The ACL system is designed to be easy to use, fast to enforce, flexible to new +policies, all while providing administrative insight. It has been modeled on +the AWS IAM system, as well as the more general object-capability model. The system +is modeled around "tokens". + +Every token has an ID, name, type and rule set. The ID is a randomly generated +UUID, making it unfeasible to guess. The name is opaque and human readable. +Lastly the type is either "client" meaning it cannot modify ACL rules, and +is restricted by the provided rules, or is "management" and is allowed to +perform all actions. + +The token ID is passed along with each RPC request to the servers. Agents +[can be configured](/docs/agent/options.html) with `acl_token` to provide a default token, +but the token can also be specified by a client on a [per-request basis](/docs/agent/http.html). +ACLs are new as of Consul 0.4, meaning versions prior do not provide a token. +This is handled by the special "anonymous" token. Anytime there is no token provided, +the rules defined by that token are automatically applied. This lets policy be enforced +on legacy clients. + +Enforcement is always done by the server nodes. All servers must be [configured +to provide](/docs/agent/options.html) an `acl_datacenter`, which enables +ACL enforcement but also specified the authoritative datacenter. Consul does not +replicate data cross-WAN, and instead relies on [RPC forwarding](/docs/internal/architecture.html) +to support Multi-Datacenter configurations. However, because requests can be +made across datacenter boundaries, ACL tokens must be valid globally. To avoid +replication issues, a single datacenter is considered authoritative and stores +all the tokens. + +When a request is made to any non-authoritative server with a token, it must +be resolved into the appropriate policy. This is done by reading the token +from the authoritative server and caching a configurable `acl_ttl`. The implication +of caching is that the cache TTL is an upper-bound on the staleness of policy +that is enforced. It is possible to set a zero TTL, but this has adverse +performance impacts, as every request requires refreshing the policy. + +Another possible issue is an outage of the `acl_datacenter` or networking +issues preventing access. In this case, it may be impossible for non-authoritative +servers to resolve tokens. Consul provides a number of configurable `acl_down_policy` +choices to tune behavior. It is possible to deny or permit all actions, or to ignore +cache TTLs and enter a fail-safe mode. + +ACLs can also act in either a whilelist or blacklist mode depending +on the configuration of `acl_default_policy`. If the default policy is +to deny all actions, then token rules can be set to allow or whitelist +actions. In the inverse, the allow all default behavior is a blacklist, +where rules are used to prohibit actions. + +Bootstrapping the ACL system is done by providing an initial `acl_master_token` +[configuration](/docs/agent/options.html), which will be created as a +"management" type token if it does not exist. + +## Rule Specification + +A core part of the ACL system is a rule language which is used +to describe the policy that must be enforced. We make use of +the [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl/) +to specify policy. This language is human readable and interoperable +with JSON making it easy to machine generate. + +As of Consul 0.4, it is only possible to specify policies for the +KV store. Specification in the HCL format looks like: + + # Default all keys to read-only + key "" { + policy = "read" + } + key "foo/" { + policy = "write" + } + key "foo/private/" { + # Deny access to the private dir + policy = "deny" + } + +This is equivalent to the following JSON input: + + { + "key": { + "": { + "policy": "read", + }, + "foo/": { + "policy": "write", + }, + "foo/private": { + "policy": "deny", + } + } + } + +Key policies provide both a prefix and a policy. The rules are enforced +using a longest-prefix match policy. This means we pick the most specific +policy possible. The policy is either "read", "write" or "deny". A "write" +policy implies "read", and there is no way to specify write-only. If there +is no applicable rule, the `acl_default_policy` is applied. + diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8ade7749c7..fcad0930ea 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -38,6 +38,10 @@ Sessions + > + ACLs + + > Security Model From 7cd10c807fd5fc1eb565121c8e22e7074364c19b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 15:08:43 -0700 Subject: [PATCH 54/56] website: rewording --- website/source/docs/internals/acl.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index d5063ff2e7..166094fa09 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -7,8 +7,8 @@ sidebar_current: "docs-internals-acl" # ACL System Consul provides an optional Access Control List (ACL) system which can be used to control -access to data and APIs. The ACL system is an -[Object-Capability system](http://en.wikipedia.org/wiki/Object-capability_model) that relies +access to data and APIs. The ACL system is a +[Capability-based system](http://en.wikipedia.org/wiki/Capability-based_security) that relies on tokens which can have fine grained rules applied to them. It is very similar to [AWS IAM](http://aws.amazon.com/iam/) in many ways. From e56007753d15e9f80483831e20fa31f62d0b35a2 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 15:20:21 -0700 Subject: [PATCH 55/56] consul: Provide ETag to avoid expensive policy fetch --- consul/acl.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/consul/acl.go b/consul/acl.go index 606e3c5602..abc011054c 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -112,6 +112,9 @@ func (s *Server) lookupACL(id, authDC string) (acl.ACL, error) { Datacenter: authDC, ACL: id, } + if cached != nil { + args.ETag = cached.ETag + } var out structs.ACLPolicy err := s.RPC("ACL.GetPolicy", &args, &out) From 43a7a20868050cfc3d113b5753c322645304a595 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 18 Aug 2014 15:23:02 -0700 Subject: [PATCH 56/56] consul: Ensure authoritative cache is purged after update --- consul/acl_endpoint.go | 5 +++ consul/acl_endpoint_test.go | 78 +++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/consul/acl_endpoint.go b/consul/acl_endpoint.go index efe431bd30..211a785741 100644 --- a/consul/acl_endpoint.go +++ b/consul/acl_endpoint.go @@ -69,6 +69,11 @@ func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error { return respErr } + // Clear the cache if applicable + if args.ACL.ID != "" { + a.srv.aclAuthCache.ClearACL(args.ACL.ID) + } + // Check if the return type is a string if respString, ok := resp.(string); ok { *reply = respString diff --git a/consul/acl_endpoint_test.go b/consul/acl_endpoint_test.go index ba5da58f96..18e1ddf383 100644 --- a/consul/acl_endpoint_test.go +++ b/consul/acl_endpoint_test.go @@ -70,6 +70,84 @@ func TestACLEndpoint_Apply(t *testing.T) { } } +func TestACLEndpoint_Update_PurgeCache(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + id := out + + // Resolve + acl1, err := s1.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if acl1 == nil { + t.Fatalf("should not be nil") + } + if !acl1.KeyRead("foo") { + t.Fatalf("should be allowed") + } + + // Do an update + arg.ACL.ID = out + arg.ACL.Rules = `{"key": {"": {"policy": "deny"}}}` + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Resolve again + acl2, err := s1.resolveToken(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if acl2 == nil { + t.Fatalf("should not be nil") + } + if acl2 == acl1 { + t.Fatalf("should not be cached") + } + if acl2.KeyRead("foo") { + t.Fatalf("should not be allowed") + } + + // Do a delete + arg.Op = structs.ACLDelete + arg.ACL.Rules = "" + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Resolve again + acl3, err := s1.resolveToken(id) + if err == nil || err.Error() != aclNotFound { + t.Fatalf("err: %v", err) + } + if acl3 != nil { + t.Fatalf("should be nil") + } +} + func TestACLEndpoint_Apply_Denied(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1"