From b702dbf3d9e411824cfdf9e657452e17a39f8c9b Mon Sep 17 00:00:00 2001 From: Shengzhe Yao Date: Mon, 13 Apr 2015 19:49:18 -0700 Subject: [PATCH] refactor tableacl 1. move simpleacl.go from go/vt/tableacl to go/vt/simpleacl 2. add an AclFactory interface. 3. move both ACL and AclFactory interfaces to go/vt/tableacl/acl 4. define a package level variable called "acls" in tableacl which contains all registered AclFactory. 5. enable simpleacl as the default tableacl mechanism. To plugin the specific tableacl implementation other than simpleacl, one could either modify all binaries (go/cmd/vtocc/vtocc.go and go/cmd/vttablet/vttablet.go) to call tableacl.Register(...) and then point tableacl.DefaultACL to the specific tableacl. Alternatively, one could also put the specific tableacl implementation under go/vt/tableacl dir and use the init function to finish the registeration. --- go/cmd/vtocc/vtocc.go | 2 + go/cmd/vttablet/vttablet.go | 2 + go/vt/tableacl/acl/acl.go | 21 +++++ go/vt/tableacl/simpleacl.go | 31 ------- go/vt/tableacl/simpleacl/simpleacl.go | 50 +++++++++++ go/vt/tableacl/tableacl.go | 71 ++++++++++++--- go/vt/tableacl/tableacl_test.go | 23 +++-- go/vt/tabletserver/query_executor_test.go | 100 ++++++++++++---------- go/vt/tabletserver/schema_info.go | 3 +- 9 files changed, 206 insertions(+), 97 deletions(-) create mode 100644 go/vt/tableacl/acl/acl.go delete mode 100644 go/vt/tableacl/simpleacl.go create mode 100644 go/vt/tableacl/simpleacl/simpleacl.go diff --git a/go/cmd/vtocc/vtocc.go b/go/cmd/vtocc/vtocc.go index 56c6bae871..53ba427ab9 100644 --- a/go/cmd/vtocc/vtocc.go +++ b/go/cmd/vtocc/vtocc.go @@ -17,6 +17,7 @@ import ( "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/tableacl" + "github.com/youtube/vitess/go/vt/tableacl/simpleacl" "github.com/youtube/vitess/go/vt/tabletserver" // import mysql to register mysql connection function @@ -76,6 +77,7 @@ func main() { log.Infof("schemaOverrides: %s\n", data) if *tableAclConfig != "" { + tableacl.Register("simpleacl", &simpleacl.Factory{}) tableacl.Init(*tableAclConfig) } qsc := tabletserver.NewQueryServiceControl() diff --git a/go/cmd/vttablet/vttablet.go b/go/cmd/vttablet/vttablet.go index a93acabeaa..5b9af0d79b 100644 --- a/go/cmd/vttablet/vttablet.go +++ b/go/cmd/vttablet/vttablet.go @@ -15,6 +15,7 @@ import ( "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/tableacl" + "github.com/youtube/vitess/go/vt/tableacl/simpleacl" "github.com/youtube/vitess/go/vt/tabletmanager" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/tabletserver" @@ -85,6 +86,7 @@ func main() { dbcfgs.App.EnableRowcache = *enableRowcache if *tableAclConfig != "" { + tableacl.Register("simpleacl", &simpleacl.Factory{}) tableacl.Init(*tableAclConfig) } diff --git a/go/vt/tableacl/acl/acl.go b/go/vt/tableacl/acl/acl.go new file mode 100644 index 0000000000..de5d523690 --- /dev/null +++ b/go/vt/tableacl/acl/acl.go @@ -0,0 +1,21 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package acl + +// ACL is an interface for Access Control List. +type ACL interface { + // IsMember checks the membership of a principal in this ACL. + IsMember(principal string) bool +} + +// Factory is responsible to create new ACL instance. +type Factory interface { + // New creates a new ACL instance. + New(entries []string) (ACL, error) + // All returns an ACL instance that contains all users. + All() ACL + // AllString returns a string representation of all users. + AllString() string +} diff --git a/go/vt/tableacl/simpleacl.go b/go/vt/tableacl/simpleacl.go deleted file mode 100644 index c238a9a37d..0000000000 --- a/go/vt/tableacl/simpleacl.go +++ /dev/null @@ -1,31 +0,0 @@ -package tableacl - -var allAcl simpleACL - -const ( - ALL = "*" -) - -// NewACL returns an ACL with the specified entries -func NewACL(entries []string) (ACL, error) { - a := simpleACL(map[string]bool{}) - for _, e := range entries { - a[e] = true - } - return a, nil -} - -// simpleACL keeps all entries in a unique in-memory list -type simpleACL map[string]bool - -// IsMember checks the membership of a principal in this ACL -func (a simpleACL) IsMember(principal string) bool { - return a[principal] || a[ALL] -} - -func all() ACL { - if allAcl == nil { - allAcl = simpleACL(map[string]bool{ALL: true}) - } - return allAcl -} diff --git a/go/vt/tableacl/simpleacl/simpleacl.go b/go/vt/tableacl/simpleacl/simpleacl.go new file mode 100644 index 0000000000..5bfd29e3b5 --- /dev/null +++ b/go/vt/tableacl/simpleacl/simpleacl.go @@ -0,0 +1,50 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package simpleacl + +import "github.com/youtube/vitess/go/vt/tableacl/acl" + +var allAcl SimpleAcl + +const all = "*" + +// SimpleAcl keeps all entries in a unique in-memory list +type SimpleAcl map[string]bool + +// IsMember checks the membership of a principal in this ACL +func (sacl SimpleAcl) IsMember(principal string) bool { + return sacl[principal] || sacl[all] +} + +// Factory is responsible to create new ACL instance. +type Factory struct{} + +// New creates a new ACL instance. +func (factory *Factory) New(entries []string) (acl.ACL, error) { + acl := SimpleAcl(map[string]bool{}) + for _, e := range entries { + acl[e] = true + } + return acl, nil +} + +// All returns an ACL instance that contains all users. +func (factory *Factory) All() acl.ACL { + if allAcl == nil { + allAcl = SimpleAcl(map[string]bool{all: true}) + } + return allAcl +} + +// AllString returns a string representation of all users. +func (factory *Factory) AllString() string { + return all +} + +// make sure SimpleAcl implements interface acl.ACL +var _ (acl.ACL) = (*SimpleAcl)(nil) + +// make sure Factory implements interface acl.AclFactory +var _ (acl.Factory) = (*Factory)(nil) diff --git a/go/vt/tableacl/tableacl.go b/go/vt/tableacl/tableacl.go index 08c362d118..1cb8d58b7d 100644 --- a/go/vt/tableacl/tableacl.go +++ b/go/vt/tableacl/tableacl.go @@ -1,3 +1,7 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package tableacl import ( @@ -7,15 +11,17 @@ import ( "log" "regexp" "strings" + "sync" + + "github.com/youtube/vitess/go/vt/tableacl/acl" ) -// ACL is an interface for Access Control List. -type ACL interface { - // IsMember checks the membership of a principal in this ACL - IsMember(principal string) bool -} +var mu sync.Mutex +var tableAcl map[*regexp.Regexp]map[Role]acl.ACL +var acls = make(map[string]acl.Factory) -var tableAcl map[*regexp.Regexp]map[Role]ACL +// DefaultACL tells the default ACL implementation to use. +var DefaultACL string // Init initiates table ACLs. func Init(configFile string) { @@ -42,19 +48,19 @@ func InitFromBytes(config []byte) (err error) { // : {"READER": "*", "WRITER": ",...","ADMIN": ""}, // : {"ADMIN": ""} //}`) -func load(config []byte) (map[*regexp.Regexp]map[Role]ACL, error) { +func load(config []byte) (map[*regexp.Regexp]map[Role]acl.ACL, error) { var contents map[string]map[string]string err := json.Unmarshal(config, &contents) if err != nil { return nil, err } - tableAcl := make(map[*regexp.Regexp]map[Role]ACL) + tableAcl := make(map[*regexp.Regexp]map[Role]acl.ACL) for tblPattern, accessMap := range contents { re, err := regexp.Compile(tblPattern) if err != nil { return nil, fmt.Errorf("regexp compile error %v: %v", tblPattern, err) } - tableAcl[re] = make(map[Role]ACL) + tableAcl[re] = make(map[Role]acl.ACL) entriesByRole := make(map[Role][]string) for i := READER; i < NumRoles; i++ { @@ -71,7 +77,7 @@ func load(config []byte) (map[*regexp.Regexp]map[Role]ACL, error) { } } for r, entries := range entriesByRole { - a, err := NewACL(entries) + a, err := newACL(entries) if err != nil { return nil, err } @@ -83,8 +89,8 @@ func load(config []byte) (map[*regexp.Regexp]map[Role]ACL, error) { } // Authorized returns the list of entities who have at least the -// minimum specified Role on a table. -func Authorized(table string, minRole Role) ACL { +// minimum specified Role on a tablel. +func Authorized(table string, minRole Role) acl.ACL { // If table ACL is disabled, return nil if tableAcl == nil { return nil @@ -98,3 +104,44 @@ func Authorized(table string, minRole Role) ACL { // No matching patterns for table, allow all access return all() } + +// Register registers a AclFactory. +func Register(name string, factory acl.Factory) { + mu.Lock() + defer mu.Unlock() + if _, ok := acls[name]; ok { + panic(fmt.Sprintf("register a registered key: %s", name)) + } + acls[name] = factory +} + +func newACL(entries []string) (acl.ACL, error) { + return getCurrentAclFactory().New(entries) +} + +func all() acl.ACL { + return getCurrentAclFactory().All() +} + +func allString() string { + return getCurrentAclFactory().AllString() +} + +func getCurrentAclFactory() acl.Factory { + mu.Lock() + defer mu.Unlock() + if DefaultACL == "" { + if len(acls) == 1 { + for _, aclFactory := range acls { + return aclFactory + } + } + panic("there are more than one AclFactory " + + "registered but no default has been given.") + } + aclFactory, ok := acls[DefaultACL] + if !ok { + panic(fmt.Sprintf("aclFactory for given default: %s is not found.", DefaultACL)) + } + return aclFactory +} diff --git a/go/vt/tableacl/tableacl_test.go b/go/vt/tableacl/tableacl_test.go index 541c2c332a..7025c193aa 100644 --- a/go/vt/tableacl/tableacl_test.go +++ b/go/vt/tableacl/tableacl_test.go @@ -4,12 +4,23 @@ package tableacl -import "testing" +import ( + "os" + "testing" + + "github.com/youtube/vitess/go/vt/tableacl/simpleacl" +) func currentUser() string { return "DummyUser" } +func TestMain(m *testing.M) { + name := "simpleacl-tableacl-test" + Register(name, &simpleacl.Factory{}) + os.Exit(m.Run()) +} + func TestParseInvalidJSON(t *testing.T) { checkLoad([]byte(`{1:2}`), false, t) checkLoad([]byte(`{"1":"2"}`), false, t) @@ -28,10 +39,10 @@ func TestValidConfigs(t *testing.T) { checkLoad([]byte(`{"table1":{"READER":"u1"}}`), true, t) checkLoad([]byte(`{"table1":{"READER":"u1,u2", "WRITER":"u3"}}`), true, t) checkLoad([]byte(`{"table[0-9]+":{"Reader":"u1,u2", "WRITER":"u3"}}`), true, t) - checkLoad([]byte(`{"table[0-9]+":{"Reader":"u1,`+ALL+`", "WRITER":"u3"}}`), true, t) + checkLoad([]byte(`{"table[0-9]+":{"Reader":"u1,`+allString()+`", "WRITER":"u3"}}`), true, t) checkLoad([]byte(`{ - "table[0-9]+":{"Reader":"u1,`+ALL+`", "WRITER":"u3"}, - "tbl[0-9]+":{"Reader":"u1,`+ALL+`", "WRITER":"u3", "ADMIN":"u4"} + "table[0-9]+":{"Reader":"u1,`+allString()+`", "WRITER":"u3"}, + "tbl[0-9]+":{"Reader":"u1,`+allString()+`", "WRITER":"u3", "ADMIN":"u4"} }`), true, t) } @@ -56,12 +67,12 @@ func TestAllowUnmatchedTable(t *testing.T) { } func TestAllUserReadAccess(t *testing.T) { - configData := []byte(`{"table[0-9]+":{"Reader":"` + ALL + `", "WRITER":"u3"}}`) + configData := []byte(`{"table[0-9]+":{"Reader":"` + allString() + `", "WRITER":"u3"}}`) checkAccess(configData, "table1", READER, t, true) } func TestAllUserWriteAccess(t *testing.T) { - configData := []byte(`{"table[0-9]+":{"Reader":"` + currentUser() + `", "WRITER":"` + ALL + `"}}`) + configData := []byte(`{"table[0-9]+":{"Reader":"` + currentUser() + `", "WRITER":"` + allString() + `"}}`) checkAccess(configData, "table1", WRITER, t, true) } diff --git a/go/vt/tabletserver/query_executor_test.go b/go/vt/tabletserver/query_executor_test.go index 017584f657..fd9557c529 100644 --- a/go/vt/tabletserver/query_executor_test.go +++ b/go/vt/tabletserver/query_executor_test.go @@ -15,6 +15,8 @@ import ( mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" "github.com/youtube/vitess/go/vt/callinfo" + "github.com/youtube/vitess/go/vt/tableacl" + "github.com/youtube/vitess/go/vt/tableacl/simpleacl" "github.com/youtube/vitess/go/vt/tabletserver/fakecacheservice" "github.com/youtube/vitess/go/vt/tabletserver/fakesqldb" "github.com/youtube/vitess/go/vt/tabletserver/planbuilder" @@ -567,53 +569,57 @@ func TestQueryExecutorPlanOther(t *testing.T) { checkEqual(t, expected, qre.Execute()) } -//func TestQueryExecutorTableAcl(t *testing.T) { -// db := setUpQueryExecutorTest() -// query := "select * from test_table limit 1000" -// expected := &mproto.QueryResult{ -// Fields: getTestTableFields(), -// RowsAffected: 0, -// Rows: [][]sqltypes.Value{}, -// } -// db.AddQuery(query, expected) -// db.AddQuery("select * from test_table where 1 != 1", &mproto.QueryResult{ -// Fields: getTestTableFields(), -// }) -// -// username := "u2" -// callInfo := &fakeCallInfo{ -// remoteAddr: "1.2.3.4", -// username: username, -// } -// ctx := callinfo.NewContext(context.Background(), callInfo) -// if err := tableacl.InitFromBytes( -// []byte(fmt.Sprintf(`{"test_table":{"READER":"%s"}}`, username))); err != nil { -// t.Fatalf("unable to load tableacl config, error: %v", err) -// } -// -// qre, sqlQuery := newTestQueryExecutor( -// query, ctx, enableRowCache|enableSchemaOverrides|enableStrict) -// checkPlanID(t, planbuilder.PLAN_PASS_SELECT, qre.plan.PlanId) -// checkEqual(t, expected, qre.Execute()) -// sqlQuery.disallowQueries() -// -// if err := tableacl.InitFromBytes([]byte(`{"test_table":{"READER":"superuser"}}`)); err != nil { -// t.Fatalf("unable to load tableacl config, error: %v", err) -// } -// // without enabling Config.StrictTableAcl -// qre, sqlQuery = newTestQueryExecutor( -// query, ctx, enableRowCache|enableSchemaOverrides|enableStrict) -// checkPlanID(t, planbuilder.PLAN_PASS_SELECT, qre.plan.PlanId) -// qre.Execute() -// sqlQuery.disallowQueries() -// // enable Config.StrictTableAcl -// qre, sqlQuery = newTestQueryExecutor( -// query, ctx, enableRowCache|enableSchemaOverrides|enableStrict|enableStrictTableAcl) -// defer sqlQuery.disallowQueries() -// checkPlanID(t, planbuilder.PLAN_PASS_SELECT, qre.plan.PlanId) -// defer handleAndVerifyTabletError(t, "query should fail because current user do not have read permissions", ErrFail) -// qre.Execute() -//} +func TestQueryExecutorTableAcl(t *testing.T) { + aclName := fmt.Sprintf("simpleacl-test-%d", rand.Int63()) + tableacl.Register(aclName, &simpleacl.Factory{}) + tableacl.DefaultACL = aclName + + db := setUpQueryExecutorTest() + query := "select * from test_table limit 1000" + expected := &mproto.QueryResult{ + Fields: getTestTableFields(), + RowsAffected: 0, + Rows: [][]sqltypes.Value{}, + } + db.AddQuery(query, expected) + db.AddQuery("select * from test_table where 1 != 1", &mproto.QueryResult{ + Fields: getTestTableFields(), + }) + + username := "u2" + callInfo := &fakeCallInfo{ + remoteAddr: "1.2.3.4", + username: username, + } + ctx := callinfo.NewContext(context.Background(), callInfo) + if err := tableacl.InitFromBytes( + []byte(fmt.Sprintf(`{"test_table":{"READER":"%s"}}`, username))); err != nil { + t.Fatalf("unable to load tableacl config, error: %v", err) + } + + qre, sqlQuery := newTestQueryExecutor( + query, ctx, enableRowCache|enableSchemaOverrides|enableStrict) + checkPlanID(t, planbuilder.PLAN_PASS_SELECT, qre.plan.PlanId) + checkEqual(t, expected, qre.Execute()) + sqlQuery.disallowQueries() + + if err := tableacl.InitFromBytes([]byte(`{"test_table":{"READER":"superuser"}}`)); err != nil { + t.Fatalf("unable to load tableacl config, error: %v", err) + } + // without enabling Config.StrictTableAcl + qre, sqlQuery = newTestQueryExecutor( + query, ctx, enableRowCache|enableSchemaOverrides|enableStrict) + checkPlanID(t, planbuilder.PLAN_PASS_SELECT, qre.plan.PlanId) + qre.Execute() + sqlQuery.disallowQueries() + // enable Config.StrictTableAcl + qre, sqlQuery = newTestQueryExecutor( + query, ctx, enableRowCache|enableSchemaOverrides|enableStrict|enableStrictTableAcl) + defer sqlQuery.disallowQueries() + checkPlanID(t, planbuilder.PLAN_PASS_SELECT, qre.plan.PlanId) + defer handleAndVerifyTabletError(t, "query should fail because current user do not have read permissions", ErrFail) + qre.Execute() +} func TestQueryExecutorBlacklistQRFail(t *testing.T) { db := setUpQueryExecutorTest() diff --git a/go/vt/tabletserver/schema_info.go b/go/vt/tabletserver/schema_info.go index 5b94aa6809..85972ed224 100644 --- a/go/vt/tabletserver/schema_info.go +++ b/go/vt/tabletserver/schema_info.go @@ -22,6 +22,7 @@ import ( "github.com/youtube/vitess/go/timer" "github.com/youtube/vitess/go/vt/schema" "github.com/youtube/vitess/go/vt/tableacl" + tacl "github.com/youtube/vitess/go/vt/tableacl/acl" "github.com/youtube/vitess/go/vt/tabletserver/planbuilder" "golang.org/x/net/context" ) @@ -44,7 +45,7 @@ type ExecPlan struct { TableInfo *TableInfo Fields []mproto.Field Rules *QueryRules - Authorized tableacl.ACL + Authorized tacl.ACL mu sync.Mutex QueryCount int64