diff --git a/go/vt/tabletserver/query_engine.go b/go/vt/tabletserver/query_engine.go index a221a6807e5..2c66fbc170d 100644 --- a/go/vt/tabletserver/query_engine.go +++ b/go/vt/tabletserver/query_engine.go @@ -62,9 +62,16 @@ type QueryEngine struct { maxResultSize sync2.AtomicInt64 maxDMLRows sync2.AtomicInt64 streamBufferSize sync2.AtomicInt64 - strictTableAcl bool - enableAutoCommit bool - exemptACL string + // tableaclExemptCount count the number of accesses allowed + // based on membership in the superuser ACL + tableaclExemptCount sync2.AtomicInt64 + tableaclAllowed *stats.MultiCounters + tableaclDenied *stats.MultiCounters + tableaclPseudoDenied *stats.MultiCounters + strictTableAcl bool + enableAutoCommit bool + enableTableAclDryRun bool + exemptACL string // Loggers accessCheckerLogger *logutil.ThrottledLogger @@ -166,6 +173,7 @@ func NewQueryEngine(config Config) *QueryEngine { qe.strictMode.Set(1) } qe.strictTableAcl = config.StrictTableAcl + qe.enableTableAclDryRun = config.EnableTableAclDryRun qe.exemptACL = config.TableAclExemptACL qe.maxResultSize = sync2.AtomicInt64(config.MaxResultSize) qe.maxDMLRows = sync2.AtomicInt64(config.MaxDMLRows) @@ -174,6 +182,9 @@ func NewQueryEngine(config Config) *QueryEngine { // Loggers qe.accessCheckerLogger = logutil.NewThrottledLogger("accessChecker", 1*time.Second) + var tableACLAllowedName string + var tableACLDeniedName string + var tableACLPseudoDeniedName string // Stats if config.EnablePublishStats { stats.Publish(config.StatsPrefix+"MaxResultSize", stats.IntFunc(qe.maxResultSize.Get)) @@ -183,7 +194,16 @@ func NewQueryEngine(config Config) *QueryEngine { stats.Publish(config.StatsPrefix+"RowcacheSpotCheckRatio", stats.FloatFunc(func() float64 { return float64(qe.spotCheckFreq.Get()) / spotCheckMultiplier })) + stats.Publish(config.StatsPrefix+"TableACLExemptCount", stats.IntFunc(qe.tableaclExemptCount.Get)) + tableACLAllowedName = "TableACLAllowed" + tableACLDeniedName = "TableACLDenied" + tableACLPseudoDeniedName = "TableACLPseudoDenied" } + + qe.tableaclAllowed = stats.NewMultiCounters(tableACLAllowedName, []string{"TableName", "TableGroup", "PlanID", "Username"}) + qe.tableaclDenied = stats.NewMultiCounters(tableACLDeniedName, []string{"TableName", "TableGroup", "PlanID", "Username"}) + qe.tableaclPseudoDenied = stats.NewMultiCounters(tableACLPseudoDeniedName, []string{"TableName", "TableGroup", "PlanID", "Username"}) + return qe } diff --git a/go/vt/tabletserver/query_executor.go b/go/vt/tabletserver/query_executor.go index 592b169f67e..d1e69c7b968 100644 --- a/go/vt/tabletserver/query_executor.go +++ b/go/vt/tabletserver/query_executor.go @@ -186,6 +186,7 @@ func (qre *QueryExecutor) execDmlAutoCommit() (reply *mproto.QueryResult, err er return reply, err } +// checkPermissions func (qre *QueryExecutor) checkPermissions() error { // Skip permissions check if we have a background context. if qre.ctx == context.Background() { @@ -207,19 +208,38 @@ func (qre *QueryExecutor) checkPermissions() error { case QR_FAIL_RETRY: return NewTabletError(ErrRetry, "Query disallowed due to rule: %s", desc) } - // a superuser that exempts from table ACL checking + + // a superuser that exempts from table ACL checking. if qre.qe.exemptACL == username { + qre.qe.tableaclExemptCount.Add(1) return nil } - // Perform table ACL check if it is enabled - if qre.plan.Authorized != nil && !qre.plan.Authorized.IsMember(username) { + tableACLStatsKey := []string{ + qre.plan.TableName, + // TODO(shengzhe): use table group instead of username. + username, + qre.plan.PlanId.String(), + username, + } + if qre.plan.Authorized == nil { + return NewTabletError(ErrFail, "table acl error: nil acl") + } + // perform table ACL check if it is enabled. + if !qre.plan.Authorized.IsMember(username) { + if qre.qe.enableTableAclDryRun { + qre.qe.tableaclPseudoDenied.Add(tableACLStatsKey, 1) + return nil + } errStr := fmt.Sprintf("table acl error: %q cannot run %v on table %q", username, qre.plan.PlanId, qre.plan.TableName) - // Raise error if in strictTableAcl mode, else just log an error + // raise error if in strictTableAcl mode, else just log an error. if qre.qe.strictTableAcl { + qre.qe.tableaclDenied.Add(tableACLStatsKey, 1) return NewTabletError(ErrFail, "%s", errStr) } qre.qe.accessCheckerLogger.Errorf("%s", errStr) + return nil } + qre.qe.tableaclAllowed.Add(tableACLStatsKey, 1) return nil } diff --git a/go/vt/tabletserver/query_executor_test.go b/go/vt/tabletserver/query_executor_test.go index 81d406d0804..6a746e7be91 100644 --- a/go/vt/tabletserver/query_executor_test.go +++ b/go/vt/tabletserver/query_executor_test.go @@ -1052,6 +1052,65 @@ func TestQueryExecutorTableAclExemptACL(t *testing.T) { } } +func TestQueryExecutorTableAclDryRun(t *testing.T) { + aclName := fmt.Sprintf("simpleacl-test-%d", rand.Int63()) + tableacl.Register(aclName, &simpleacl.Factory{}) + tableacl.SetDefaultACL(aclName) + db := setUpQueryExecutorTest() + query := "select * from test_table limit 1000" + want := &mproto.QueryResult{ + Fields: getTestTableFields(), + RowsAffected: 0, + Rows: [][]sqltypes.Value{}, + } + db.AddQuery(query, want) + 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) + + config := &tableaclpb.Config{ + TableGroups: []*tableaclpb.TableGroupSpec{{ + Name: "group02", + TableNamesOrPrefixes: []string{"test_table"}, + Readers: []string{"u1"}, + }}, + } + + if err := tableacl.InitFromProto(config); err != nil { + t.Fatalf("unable to load tableacl config, error: %v", err) + } + + tableACLStatsKey := strings.Join([]string{ + "test_table", + username, + planbuilder.PLAN_PASS_SELECT.String(), + username, + }, ".") + // enable Config.StrictTableAcl + sqlQuery := newTestSQLQuery(ctx, enableRowCache|enableSchemaOverrides|enableStrict|enableStrictTableAcl) + sqlQuery.qe.enableTableAclDryRun = true + qre := newTestQueryExecutor(ctx, sqlQuery, query, 0) + defer sqlQuery.disallowQueries() + checkPlanID(t, planbuilder.PLAN_PASS_SELECT, qre.plan.PlanId) + beforeCount := sqlQuery.qe.tableaclPseudoDenied.Counters.Counts()[tableACLStatsKey] + // query should fail because current user do not have read permissions + _, err := qre.Execute() + if err != nil { + t.Fatalf("qre.Execute() = %v, want: nil", err) + } + afterCount := sqlQuery.qe.tableaclPseudoDenied.Counters.Counts()[tableACLStatsKey] + if afterCount-beforeCount != 1 { + t.Fatalf("table acl pseudo denied count should increase by one. got: %d, want: %d", afterCount, beforeCount+1) + } +} + func TestQueryExecutorBlacklistQRFail(t *testing.T) { db := setUpQueryExecutorTest() query := "select * from test_table where name = 1 limit 1000" diff --git a/go/vt/tabletserver/queryctl.go b/go/vt/tabletserver/queryctl.go index ecfc5b48ddb..00d1cc1e4fb 100644 --- a/go/vt/tabletserver/queryctl.go +++ b/go/vt/tabletserver/queryctl.go @@ -50,6 +50,7 @@ func init() { flag.BoolVar(&qsConfig.StrictMode, "queryserver-config-strict-mode", DefaultQsConfig.StrictMode, "allow only predictable DMLs and enforces MySQL's STRICT_TRANS_TABLES") // tableacl related configurations. flag.BoolVar(&qsConfig.StrictTableAcl, "queryserver-config-strict-table-acl", DefaultQsConfig.StrictTableAcl, "only allow queries that pass table acl checks") + flag.BoolVar(&qsConfig.EnableTableAclDryRun, "queryserver-config-enable-table-acl-dry-run", DefaultQsConfig.EnableTableAclDryRun, "If this flag is enabled, tabletserver will emit monitoring metrics and let the request pass regardless of table acl check results") flag.StringVar(&qsConfig.TableAclExemptACL, "queryserver-config-acl-exempt-acl", DefaultQsConfig.TableAclExemptACL, "an acl that exempt from table acl checking (this acl is free to access any vitess tables).") flag.BoolVar(&qsConfig.TerseErrors, "queryserver-config-terse-errors", DefaultQsConfig.TerseErrors, "prevent bind vars from escaping in returned errors") flag.BoolVar(&qsConfig.EnablePublishStats, "queryserver-config-enable-publish-stats", DefaultQsConfig.EnablePublishStats, "set this flag to true makes queryservice publish monitoring stats") @@ -103,29 +104,30 @@ func (c *RowCacheConfig) GetSubprocessFlags(socket string) []string { // Config contains all the configuration for query service type Config struct { - PoolSize int - StreamPoolSize int - TransactionCap int - TransactionTimeout float64 - MaxResultSize int - MaxDMLRows int - StreamBufferSize int - QueryCacheSize int - SchemaReloadTime float64 - QueryTimeout float64 - TxPoolTimeout float64 - IdleTimeout float64 - RowCache RowCacheConfig - SpotCheckRatio float64 - StrictMode bool - StrictTableAcl bool - TerseErrors bool - EnablePublishStats bool - EnableAutoCommit bool - StatsPrefix string - DebugURLPrefix string - PoolNamePrefix string - TableAclExemptACL string + PoolSize int + StreamPoolSize int + TransactionCap int + TransactionTimeout float64 + MaxResultSize int + MaxDMLRows int + StreamBufferSize int + QueryCacheSize int + SchemaReloadTime float64 + QueryTimeout float64 + TxPoolTimeout float64 + IdleTimeout float64 + RowCache RowCacheConfig + SpotCheckRatio float64 + StrictMode bool + StrictTableAcl bool + TerseErrors bool + EnablePublishStats bool + EnableAutoCommit bool + EnableTableAclDryRun bool + StatsPrefix string + DebugURLPrefix string + PoolNamePrefix string + TableAclExemptACL string } // DefaultQSConfig is the default value for the query service config. @@ -137,29 +139,30 @@ type Config struct { // great (the overhead makes the final packets on the wire about twice // bigger than this). var DefaultQsConfig = Config{ - PoolSize: 16, - StreamPoolSize: 750, - TransactionCap: 20, - TransactionTimeout: 30, - MaxResultSize: 10000, - MaxDMLRows: 500, - QueryCacheSize: 5000, - SchemaReloadTime: 30 * 60, - QueryTimeout: 0, - TxPoolTimeout: 1, - IdleTimeout: 30 * 60, - StreamBufferSize: 32 * 1024, - RowCache: RowCacheConfig{Memory: -1, Connections: -1, Threads: -1}, - SpotCheckRatio: 0, - StrictMode: true, - StrictTableAcl: false, - TerseErrors: false, - EnablePublishStats: true, - EnableAutoCommit: false, - StatsPrefix: "", - DebugURLPrefix: "/debug", - PoolNamePrefix: "", - TableAclExemptACL: "", + PoolSize: 16, + StreamPoolSize: 750, + TransactionCap: 20, + TransactionTimeout: 30, + MaxResultSize: 10000, + MaxDMLRows: 500, + QueryCacheSize: 5000, + SchemaReloadTime: 30 * 60, + QueryTimeout: 0, + TxPoolTimeout: 1, + IdleTimeout: 30 * 60, + StreamBufferSize: 32 * 1024, + RowCache: RowCacheConfig{Memory: -1, Connections: -1, Threads: -1}, + SpotCheckRatio: 0, + StrictMode: true, + StrictTableAcl: false, + TerseErrors: false, + EnablePublishStats: true, + EnableAutoCommit: false, + EnableTableAclDryRun: false, + StatsPrefix: "", + DebugURLPrefix: "/debug", + PoolNamePrefix: "", + TableAclExemptACL: "", } var qsConfig Config