Merge pull request #7974 from influxdata/mr-4785-show-databases

Allow non-admin users to execute SHOW DATABASES
pull/7926/head^2
Mark Rushakoff 2017-02-13 15:04:00 -08:00 committed by GitHub
commit c762ab49ee
12 changed files with 303 additions and 15 deletions

View File

@ -2,10 +2,11 @@
### Features
- [#7776](https://github.com/influxdata/influxdb/issues/7776): Add system information to /debug/vars.
- [#7948](https://github.com/influxdata/influxdb/pull/7948): Reduce memory allocations by reusing gzip.Writers across requests
- [#7553](https://github.com/influxdata/influxdb/issues/7553): Add modulo operator to the query language.
- [#7977](https://github.com/influxdata/influxdb/issues/7977): Add chunked request processing back into the Go client v2
- [#7974](https://github.com/influxdata/influxdb/pull/7974): Allow non-admin users to execute SHOW DATABASES.
- [#7948](https://github.com/influxdata/influxdb/pull/7948): Reduce memory allocations by reusing gzip.Writers across requests
- [#7776](https://github.com/influxdata/influxdb/issues/7776): Add system information to /debug/vars.
- [#7553](https://github.com/influxdata/influxdb/issues/7553): Add modulo operator to the query language.
## v1.2.1 [unreleased]

View File

@ -293,6 +293,111 @@ func TestServer_DatabaseRetentionPolicyAutoCreate(t *testing.T) {
}
}
func TestServer_ShowDatabases_NoAuth(t *testing.T) {
t.Parallel()
s := OpenServer(NewConfig())
defer s.Close()
test := Test{
queries: []*Query{
&Query{
name: "create db1",
command: "CREATE DATABASE db1",
exp: `{"results":[{"statement_id":0}]}`,
},
&Query{
name: "create db2",
command: "CREATE DATABASE db2",
exp: `{"results":[{"statement_id":0}]}`,
},
&Query{
name: "show dbs",
command: "SHOW DATABASES",
exp: `{"results":[{"statement_id":0,"series":[{"name":"databases","columns":["name"],"values":[["db1"],["db2"]]}]}]}`,
},
},
}
for _, query := range test.queries {
if query.skip {
t.Logf("SKIP:: %s", query.name)
continue
}
if err := query.Execute(s); err != nil {
t.Error(fmt.Sprintf("command: %s - err: %s", query.command, query.Error(err)))
} else if !query.success() {
t.Error(query.failureMessage())
}
}
}
func TestServer_ShowDatabases_WithAuth(t *testing.T) {
t.Parallel()
c := NewConfig()
c.HTTPD.AuthEnabled = true
s := OpenServer(c)
defer s.Close()
adminParams := map[string][]string{"u": []string{"admin"}, "p": []string{"admin"}}
readerParams := map[string][]string{"u": []string{"reader"}, "p": []string{"r"}}
writerParams := map[string][]string{"u": []string{"writer"}, "p": []string{"w"}}
nobodyParams := map[string][]string{"u": []string{"nobody"}, "p": []string{"n"}}
test := Test{
queries: []*Query{
&Query{
name: "create admin",
command: `CREATE USER admin WITH PASSWORD 'admin' WITH ALL PRIVILEGES`,
exp: `{"results":[{"statement_id":0}]}`,
},
&Query{
name: "create databases",
command: "CREATE DATABASE dbR; CREATE DATABASE dbW",
params: adminParams,
exp: `{"results":[{"statement_id":0},{"statement_id":1}]}`,
},
&Query{
name: "show dbs as admin",
command: "SHOW DATABASES",
params: adminParams,
exp: `{"results":[{"statement_id":0,"series":[{"name":"databases","columns":["name"],"values":[["dbR"],["dbW"]]}]}]}`,
},
&Query{
name: "create users",
command: `CREATE USER reader WITH PASSWORD 'r'; GRANT READ ON "dbR" TO "reader"; CREATE USER writer WITH PASSWORD 'w'; GRANT WRITE ON "dbW" TO "writer"; CREATE USER nobody WITH PASSWORD 'n'`,
params: adminParams,
exp: `{"results":[{"statement_id":0},{"statement_id":1},{"statement_id":2},{"statement_id":3},{"statement_id":4}]}`,
},
&Query{
name: "show dbs as reader",
command: "SHOW DATABASES",
params: readerParams,
exp: `{"results":[{"statement_id":0,"series":[{"name":"databases","columns":["name"],"values":[["dbR"]]}]}]}`,
},
&Query{
name: "show dbs as writer",
command: "SHOW DATABASES",
params: writerParams,
exp: `{"results":[{"statement_id":0,"series":[{"name":"databases","columns":["name"],"values":[["dbW"]]}]}]}`,
},
&Query{
name: "show dbs as nobody",
command: "SHOW DATABASES",
params: nobodyParams,
exp: `{"results":[{"statement_id":0,"series":[{"name":"databases","columns":["name"]}]}]}`,
},
},
}
for _, query := range test.queries {
if err := query.Execute(s); err != nil {
t.Error(fmt.Sprintf("command: %s - err: %s", query.command, query.Error(err)))
} else if !query.success() {
t.Error(query.failureMessage())
}
}
}
// Ensure user commands work.
func TestServer_UserCommands(t *testing.T) {
t.Parallel()

View File

@ -156,7 +156,7 @@ func (e *StatementExecutor) ExecuteStatement(stmt influxql.Statement, ctx influx
case *influxql.ShowContinuousQueriesStatement:
rows, err = e.executeShowContinuousQueriesStatement(stmt)
case *influxql.ShowDatabasesStatement:
rows, err = e.executeShowDatabasesStatement(stmt)
rows, err = e.executeShowDatabasesStatement(stmt, &ctx)
case *influxql.ShowDiagnosticsStatement:
rows, err = e.executeShowDiagnosticsStatement(stmt)
case *influxql.ShowGrantsForUserStatement:
@ -620,12 +620,16 @@ func (e *StatementExecutor) executeShowContinuousQueriesStatement(stmt *influxql
return rows, nil
}
func (e *StatementExecutor) executeShowDatabasesStatement(q *influxql.ShowDatabasesStatement) (models.Rows, error) {
func (e *StatementExecutor) executeShowDatabasesStatement(q *influxql.ShowDatabasesStatement, ctx *influxql.ExecutionContext) (models.Rows, error) {
dis := e.MetaClient.Databases()
a := ctx.ExecutionOptions.Authorizer
row := &models.Row{Name: "databases", Columns: []string{"name"}}
for _, di := range dis {
row.Values = append(row.Values, []interface{}{di.Name})
// Only include databases that the user is authorized to read or write.
if a.AuthorizeDatabase(influxql.ReadPrivilege, di.Name) || a.AuthorizeDatabase(influxql.WritePrivilege, di.Name) {
row.Values = append(row.Values, []interface{}{di.Name})
}
}
return []*models.Row{row}, nil
}

View File

@ -193,6 +193,57 @@ func TestStatementExecutor_NormalizeDeleteSeries(t *testing.T) {
}
}
type mockAuthorizer struct {
AuthorizeDatabaseFn func(influxql.Privilege, string) bool
}
func (a *mockAuthorizer) AuthorizeDatabase(p influxql.Privilege, name string) bool {
return a.AuthorizeDatabaseFn(p, name)
}
func TestQueryExecutor_ExecuteQuery_ShowDatabases(t *testing.T) {
qe := influxql.NewQueryExecutor()
qe.StatementExecutor = &coordinator.StatementExecutor{
MetaClient: &internal.MetaClientMock{
DatabasesFn: func() []meta.DatabaseInfo {
return []meta.DatabaseInfo{
{Name: "db1"}, {Name: "db2"}, {Name: "db3"}, {Name: "db4"},
}
},
},
}
opt := influxql.ExecutionOptions{
Authorizer: &mockAuthorizer{
AuthorizeDatabaseFn: func(p influxql.Privilege, name string) bool {
return name == "db2" || name == "db4"
},
},
}
q, err := influxql.ParseQuery("SHOW DATABASES")
if err != nil {
t.Fatal(err)
}
results := ReadAllResults(qe.ExecuteQuery(q, opt, make(chan struct{})))
exp := []*influxql.Result{
{
StatementID: 0,
Series: []*models.Row{{
Name: "databases",
Columns: []string{"name"},
Values: [][]interface{}{
{"db2"}, {"db4"},
},
}},
},
}
if !reflect.DeepEqual(results, exp) {
t.Fatalf("unexpected results: exp %s, got %s", spew.Sdump(exp), spew.Sdump(results))
}
}
// QueryExecutor is a test wrapper for coordinator.QueryExecutor.
type QueryExecutor struct {
*influxql.QueryExecutor

View File

@ -2673,7 +2673,10 @@ func (s *ShowDatabasesStatement) String() string { return "SHOW DATABASES" }
// RequiredPrivileges returns the privilege required to execute a ShowDatabasesStatement.
func (s *ShowDatabasesStatement) RequiredPrivileges() (ExecutionPrivileges, error) {
return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}}, nil
// SHOW DATABASES is one of few statements that have no required privileges.
// Anyone is allowed to execute it, but the returned results depend on the user's
// individual database permissions.
return ExecutionPrivileges{{Admin: false, Name: "", Privilege: NoPrivileges}}, nil
}
// CreateContinuousQueryStatement represents a command for creating a continuous query.

View File

@ -1465,6 +1465,79 @@ func TestSelect_SubqueryPrivileges(t *testing.T) {
}
}
func TestShow_Privileges(t *testing.T) {
for _, c := range []struct {
stmt influxql.Statement
exp influxql.ExecutionPrivileges
}{
{
stmt: &influxql.ShowDatabasesStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.NoPrivileges}},
},
{
stmt: &influxql.ShowFieldKeysStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.ReadPrivilege}},
},
{
stmt: &influxql.ShowMeasurementsStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.ReadPrivilege}},
},
{
stmt: &influxql.ShowQueriesStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.ReadPrivilege}},
},
{
stmt: &influxql.ShowRetentionPoliciesStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.ReadPrivilege}},
},
{
stmt: &influxql.ShowSeriesStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.ReadPrivilege}},
},
{
stmt: &influxql.ShowShardGroupsStatement{},
exp: influxql.ExecutionPrivileges{{Admin: true, Privilege: influxql.AllPrivileges}},
},
{
stmt: &influxql.ShowShardsStatement{},
exp: influxql.ExecutionPrivileges{{Admin: true, Privilege: influxql.AllPrivileges}},
},
{
stmt: &influxql.ShowStatsStatement{},
exp: influxql.ExecutionPrivileges{{Admin: true, Privilege: influxql.AllPrivileges}},
},
{
stmt: &influxql.ShowSubscriptionsStatement{},
exp: influxql.ExecutionPrivileges{{Admin: true, Privilege: influxql.AllPrivileges}},
},
{
stmt: &influxql.ShowDiagnosticsStatement{},
exp: influxql.ExecutionPrivileges{{Admin: true, Privilege: influxql.AllPrivileges}},
},
{
stmt: &influxql.ShowTagKeysStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.ReadPrivilege}},
},
{
stmt: &influxql.ShowTagValuesStatement{},
exp: influxql.ExecutionPrivileges{{Admin: false, Privilege: influxql.ReadPrivilege}},
},
{
stmt: &influxql.ShowUsersStatement{},
exp: influxql.ExecutionPrivileges{{Admin: true, Privilege: influxql.AllPrivileges}},
},
} {
got, err := c.stmt.RequiredPrivileges()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(c.exp, got) {
t.Errorf("exp: %v, got: %v", c.exp, got)
}
}
}
func TestSources_Names(t *testing.T) {
sources := influxql.Sources([]influxql.Source{
&influxql.Measurement{

View File

@ -59,11 +59,30 @@ func ErrMaxConcurrentQueriesLimitExceeded(n, limit int) error {
return fmt.Errorf("max-concurrent-queries limit exceeded(%d, %d)", n, limit)
}
// Authorizer reports whether certain operations are authorized.
type Authorizer interface {
// AuthorizeDatabase indicates whether the given Privilege is authorized on the database with the given name.
AuthorizeDatabase(p Privilege, name string) bool
}
// OpenAuthorizer is the Authorizer used when authorization is disabled.
// It allows all operations.
type OpenAuthorizer struct{}
var _ Authorizer = OpenAuthorizer{}
// AuthorizeDatabase returns true to allow any operation on a database.
func (OpenAuthorizer) AuthorizeDatabase(Privilege, string) bool { return true }
// ExecutionOptions contains the options for executing a query.
type ExecutionOptions struct {
// The database the query is running against.
Database string
// How to determine whether the query is allowed to execute,
// what resources can be returned in SHOW queries, etc.
Authorizer Authorizer
// The requested maximum number of points to return in each result.
ChunkSize int

View File

@ -390,6 +390,14 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user *meta.
NodeID: nodeID,
}
if h.Config.AuthEnabled {
// The current user determines the authorized actions.
opts.Authorizer = user
} else {
// Auth is disabled, so allow everything.
opts.Authorizer = influxql.OpenAuthorizer{}
}
// Make sure if the client disconnects we signal the query to abort
var closing chan struct{}
if !async {

View File

@ -1409,15 +1409,24 @@ func (cqi *ContinuousQueryInfo) unmarshal(pb *internal.ContinuousQueryInfo) {
// UserInfo represents metadata about a user in the system.
type UserInfo struct {
Name string
Hash string
Admin bool
// User's name.
Name string
// Hashed password.
Hash string
// Whether the user is an admin, i.e. allowed to do everything.
Admin bool
// Map of database name to granted privilege.
Privileges map[string]influxql.Privilege
}
// Authorize returns true if the user is authorized and false if not.
func (ui *UserInfo) Authorize(privilege influxql.Privilege, database string) bool {
if ui.Admin {
var _ influxql.Authorizer = (*UserInfo)(nil)
// AuthorizeDatabase returns true if the user is authorized for the given privilege on the given database.
func (ui *UserInfo) AuthorizeDatabase(privilege influxql.Privilege, database string) bool {
if ui.Admin || privilege == influxql.NoPrivileges {
return true
}
p, ok := ui.Privileges[database]

View File

@ -109,3 +109,18 @@ func Test_Data_CreateRetentionPolicy(t *testing.T) {
t.Fatal(err)
}
}
func TestUserInfo_AuthorizeDatabase(t *testing.T) {
emptyUser := &meta.UserInfo{}
if !emptyUser.AuthorizeDatabase(influxql.NoPrivileges, "anydb") {
t.Fatal("expected NoPrivileges to be authorized but it wasn't")
}
if emptyUser.AuthorizeDatabase(influxql.ReadPrivilege, "anydb") {
t.Fatal("expected ReadPrivilege to prevent authorization, but it was authorized")
}
adminUser := &meta.UserInfo{Admin: true}
if !adminUser.AuthorizeDatabase(influxql.AllPrivileges, "anydb") {
t.Fatalf("expected admin to be authorized but it wasn't")
}
}

View File

@ -82,7 +82,7 @@ func (a *QueryAuthorizer) AuthorizeQuery(u *UserInfo, query *influxql.Query, dat
if db == "" {
db = database
}
if !u.Authorize(p.Privilege, db) {
if !u.AuthorizeDatabase(p.Privilege, db) {
return &ErrAuthorize{
Query: query,
User: u.Name,

View File

@ -19,7 +19,7 @@ func NewWriteAuthorizer(c *Client) *WriteAuthorizer {
// AuthorizeWrite returns nil if the user has permission to write to the database.
func (a WriteAuthorizer) AuthorizeWrite(username, database string) error {
u, err := a.Client.User(username)
if err != nil || u == nil || !u.Authorize(influxql.WritePrivilege, database) {
if err != nil || u == nil || !u.AuthorizeDatabase(influxql.WritePrivilege, database) {
return &ErrAuthorize{
Database: database,
Message: fmt.Sprintf("%s not authorized to write to %s", username, database),