From 9ba37325f62881d0174351a1d389ed854d09e6d1 Mon Sep 17 00:00:00 2001 From: gunnaraasen Date: Fri, 10 Jul 2015 14:39:33 -0600 Subject: [PATCH] Fixes authorization. Adds GRANT and REVOKE statements for admin privilege. Adds authorization to the query endpoint. --- CHANGELOG.md | 2 + influxql/ast.go | 144 ++++++++++++++++++++------------ influxql/parser.go | 132 ++++++++++++++++++++++------- influxql/parser_test.go | 93 +++++++++++++++++---- meta/data.go | 35 +++++++- meta/errors.go | 15 ---- meta/internal/meta.pb.go | 37 ++++++++ meta/internal/meta.proto | 8 ++ meta/statement_executor.go | 35 ++++++-- meta/statement_executor_test.go | 72 ++++++++++++++++ meta/store.go | 34 ++++++++ services/httpd/handler.go | 33 ++++---- services/httpd/handler_test.go | 45 ++++------ tsdb/query_executor.go | 78 +++++++++-------- 14 files changed, 558 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f92d40e4de..e543ff804ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - [#2761](https://github.com/influxdb/influxdb/issues/2761): Make SHOW RETENTION POLICIES consistent with other queries. - [#3356](https://github.com/influxdb/influxdb/pull/3356): Disregard semicolons after database name in use command. Thanks @timraymond. - [#3351](https://github.com/influxdb/influxdb/pull/3351): Handle malformed regex comparisons during parsing. Thanks @rnubel +- [#3244](https://github.com/influxdb/influxdb/pull/3244): Wire up admin privilege grant and revoke. +- [#3259](https://github.com/influxdb/influxdb/issues/3259): Respect privileges for queries. ## v0.9.1 [2015-07-02] diff --git a/influxql/ast.go b/influxql/ast.go index 9cc83b20ddc..36803d5b238 100644 --- a/influxql/ast.go +++ b/influxql/ast.go @@ -92,7 +92,9 @@ func (*DropRetentionPolicyStatement) node() {} func (*DropSeriesStatement) node() {} func (*DropUserStatement) node() {} func (*GrantStatement) node() {} +func (*GrantAdminStatement) node() {} func (*RevokeStatement) node() {} +func (*RevokeAdminStatement) node() {} func (*SelectStatement) node() {} func (*SetPasswordUserStatement) node() {} func (*ShowContinuousQueriesStatement) node() {} @@ -169,11 +171,13 @@ type HasDefaultDatabase interface { // ExecutionPrivilege is a privilege required for a user to execute // a statement on a database or resource. type ExecutionPrivilege struct { - // Name of the database or resource. - // If "", then the resource is the cluster. + // Admin privilege required. + Admin bool + + // Name of the database. Name string - // Privilege required. + // Database privilege required. Privilege Privilege } @@ -193,6 +197,7 @@ func (*DropRetentionPolicyStatement) stmt() {} func (*DropSeriesStatement) stmt() {} func (*DropUserStatement) stmt() {} func (*GrantStatement) stmt() {} +func (*GrantAdminStatement) stmt() {} func (*ShowContinuousQueriesStatement) stmt() {} func (*ShowGrantsForUserStatement) stmt() {} func (*ShowServersStatement) stmt() {} @@ -207,6 +212,7 @@ func (*ShowTagKeysStatement) stmt() {} func (*ShowTagValuesStatement) stmt() {} func (*ShowUsersStatement) stmt() {} func (*RevokeStatement) stmt() {} +func (*RevokeAdminStatement) stmt() {} func (*SelectStatement) stmt() {} func (*SetPasswordUserStatement) stmt() {} @@ -302,7 +308,7 @@ func (s *CreateDatabaseStatement) String() string { // RequiredPrivileges returns the privilege required to execute a CreateDatabaseStatement. func (s *CreateDatabaseStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // DropDatabaseStatement represents a command to drop a database. @@ -321,7 +327,7 @@ func (s *DropDatabaseStatement) String() string { // RequiredPrivileges returns the privilege required to execute a DropDatabaseStatement. func (s *DropDatabaseStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // DropRetentionPolicyStatement represents a command to drop a retention policy from a database. @@ -345,7 +351,7 @@ func (s *DropRetentionPolicyStatement) String() string { // RequiredPrivileges returns the privilege required to execute a DropRetentionPolicyStatement. func (s *DropRetentionPolicyStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: s.Database, Privilege: WritePrivilege}} + return ExecutionPrivileges{{Admin: false, Name: s.Database, Privilege: WritePrivilege}} } // CreateUserStatement represents a command for creating a new user. @@ -353,11 +359,11 @@ type CreateUserStatement struct { // Name of the user to be created. Name string - // User's password + // User's password. Password string - // User's privilege level. - Privilege *Privilege + // User's admin privilege. + Admin bool } // String returns a string representation of the create user statement. @@ -367,18 +373,15 @@ func (s *CreateUserStatement) String() string { _, _ = buf.WriteString(s.Name) _, _ = buf.WriteString(" WITH PASSWORD ") _, _ = buf.WriteString(s.Password) - - if s.Privilege != nil { - _, _ = buf.WriteString(" WITH ") - _, _ = buf.WriteString(s.Privilege.String()) + if s.Admin { + _, _ = buf.WriteString(" WITH ALL PRIVILEGES") } - return buf.String() } // RequiredPrivileges returns the privilege(s) required to execute a CreateUserStatement. func (s *CreateUserStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // DropUserStatement represents a command for dropping a user. @@ -397,7 +400,7 @@ func (s *DropUserStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a DropUserStatement. func (s *DropUserStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // Privilege is a type of action a user can be granted the right to use. @@ -437,7 +440,7 @@ type GrantStatement struct { // The privilege to be granted. Privilege Privilege - // Thing to grant privilege on (e.g., a DB). + // Database to grant the privilege to. On string // Who to grant the privilege to. @@ -449,10 +452,8 @@ func (s *GrantStatement) String() string { var buf bytes.Buffer _, _ = buf.WriteString("GRANT ") _, _ = buf.WriteString(s.Privilege.String()) - if s.On != "" { - _, _ = buf.WriteString(" ON ") - _, _ = buf.WriteString(s.On) - } + _, _ = buf.WriteString(" ON ") + _, _ = buf.WriteString(s.On) _, _ = buf.WriteString(" TO ") _, _ = buf.WriteString(s.User) return buf.String() @@ -460,7 +461,26 @@ func (s *GrantStatement) String() string { // RequiredPrivileges returns the privilege required to execute a GrantStatement. func (s *GrantStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} +} + +// GrantAdminStatement represents a command for granting admin privilege. +type GrantAdminStatement struct { + // Who to grant the privilege to. + User string +} + +// String returns a string representation of the grant admin statement. +func (s *GrantAdminStatement) String() string { + var buf bytes.Buffer + _, _ = buf.WriteString("GRANT ALL PRIVILEGES TO ") + _, _ = buf.WriteString(s.User) + return buf.String() +} + +// RequiredPrivileges returns the privilege required to execute a GrantAdminStatement. +func (s *GrantAdminStatement) RequiredPrivileges() ExecutionPrivileges { + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // SetPasswordUserStatement represents a command for changing user password. @@ -482,17 +502,17 @@ func (s *SetPasswordUserStatement) String() string { return buf.String() } -// RequiredPrivileges returns the privilege required to execute a GrantStatement. +// RequiredPrivileges returns the privilege required to execute a SetPasswordUserStatement. func (s *SetPasswordUserStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // RevokeStatement represents a command to revoke a privilege from a user. type RevokeStatement struct { - // Privilege to be revoked. + // The privilege to be revoked. Privilege Privilege - // Thing to revoke privilege to (e.g., a DB) + // Database to revoke the privilege from. On string // Who to revoke privilege from. @@ -504,10 +524,8 @@ func (s *RevokeStatement) String() string { var buf bytes.Buffer _, _ = buf.WriteString("REVOKE ") _, _ = buf.WriteString(s.Privilege.String()) - if s.On != "" { - _, _ = buf.WriteString(" ON ") - _, _ = buf.WriteString(s.On) - } + _, _ = buf.WriteString(" ON ") + _, _ = buf.WriteString(s.On) _, _ = buf.WriteString(" FROM ") _, _ = buf.WriteString(s.User) return buf.String() @@ -515,7 +533,26 @@ func (s *RevokeStatement) String() string { // RequiredPrivileges returns the privilege required to execute a RevokeStatement. func (s *RevokeStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} +} + +// RevokeAdminStatement represents a command to revoke admin privilege from a user. +type RevokeAdminStatement struct { + // Who to revoke admin privilege from. + User string +} + +// String returns a string representation of the revoke admin statement. +func (s *RevokeAdminStatement) String() string { + var buf bytes.Buffer + _, _ = buf.WriteString("REVOKE ALL PRIVILEGES FROM ") + _, _ = buf.WriteString(s.User) + return buf.String() +} + +// RequiredPrivileges returns the privilege required to execute a RevokeAdminStatement. +func (s *RevokeAdminStatement) RequiredPrivileges() ExecutionPrivileges { + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // CreateRetentionPolicyStatement represents a command to create a retention policy. @@ -555,7 +592,7 @@ func (s *CreateRetentionPolicyStatement) String() string { // RequiredPrivileges returns the privilege required to execute a CreateRetentionPolicyStatement. func (s *CreateRetentionPolicyStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // AlterRetentionPolicyStatement represents a command to alter an existing retention policy. @@ -603,7 +640,7 @@ func (s *AlterRetentionPolicyStatement) String() string { // RequiredPrivileges returns the privilege required to execute an AlterRetentionPolicyStatement. func (s *AlterRetentionPolicyStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } type FillOption int @@ -857,10 +894,10 @@ func (s *SelectStatement) String() string { // RequiredPrivileges returns the privilege required to execute the SelectStatement. func (s *SelectStatement) RequiredPrivileges() ExecutionPrivileges { - ep := ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + ep := ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} if s.Target != nil { - p := ExecutionPrivilege{Name: s.Target.Measurement.Database, Privilege: WritePrivilege} + p := ExecutionPrivilege{Admin: false, Name: s.Target.Measurement.Database, Privilege: WritePrivilege} ep = append(ep, p) } return ep @@ -1428,7 +1465,7 @@ func (s *DeleteStatement) String() string { // RequiredPrivileges returns the privilege required to execute a DeleteStatement. func (s *DeleteStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: WritePrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: WritePrivilege}} } // ShowSeriesStatement represents a command for listing series in the database. @@ -1481,7 +1518,7 @@ func (s *ShowSeriesStatement) String() string { // RequiredPrivileges returns the privilege required to execute a ShowSeriesStatement. func (s *ShowSeriesStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} } // DropSeriesStatement represents a command for removing a series from the database. @@ -1512,7 +1549,7 @@ func (s *DropSeriesStatement) String() string { // RequiredPrivileges returns the privilege required to execute a DropSeriesStatement. func (s DropSeriesStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: WritePrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: WritePrivilege}} } // ShowContinuousQueriesStatement represents a command for listing continuous queries. @@ -1523,7 +1560,7 @@ func (s *ShowContinuousQueriesStatement) String() string { return "SHOW CONTINUO // RequiredPrivileges returns the privilege required to execute a ShowContinuousQueriesStatement. func (s *ShowContinuousQueriesStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} } // ShowGrantsForUserStatement represents a command for listing user privileges. @@ -1543,7 +1580,7 @@ func (s *ShowGrantsForUserStatement) String() string { // RequiredPrivileges returns the privilege required to execute a ShowGrantsForUserStatement func (s *ShowGrantsForUserStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // ShowServersStatement represents a command for listing all servers. @@ -1554,7 +1591,7 @@ func (s *ShowServersStatement) String() string { return "SHOW SERVERS" } // RequiredPrivileges returns the privilege required to execute a ShowServersStatement func (s *ShowServersStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // ShowDatabasesStatement represents a command for listing all databases in the cluster. @@ -1565,7 +1602,7 @@ func (s *ShowDatabasesStatement) String() string { return "SHOW DATABASES" } // RequiredPrivileges returns the privilege required to execute a ShowDatabasesStatement func (s *ShowDatabasesStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // CreateContinuousQueryStatement represents a command for creating a continuous query. @@ -1592,7 +1629,7 @@ func (s *CreateContinuousQueryStatement) DefaultDatabase() string { // RequiredPrivileges returns the privilege required to execute a CreateContinuousQueryStatement. func (s *CreateContinuousQueryStatement) RequiredPrivileges() ExecutionPrivileges { - ep := ExecutionPrivileges{{Name: s.Database, Privilege: ReadPrivilege}} + ep := ExecutionPrivileges{{Admin: false, Name: s.Database, Privilege: ReadPrivilege}} // Selecting into a database that's different from the source? if s.Source.Target.Measurement.Database != "" { @@ -1601,6 +1638,7 @@ func (s *CreateContinuousQueryStatement) RequiredPrivileges() ExecutionPrivilege // Add destination database privilege requirement and set it to write. p := ExecutionPrivilege{ + Admin: false, Name: s.Source.Target.Measurement.Database, Privilege: WritePrivilege, } @@ -1623,7 +1661,7 @@ func (s *DropContinuousQueryStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a DropContinuousQueryStatement func (s *DropContinuousQueryStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: WritePrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: WritePrivilege}} } // ShowMeasurementsStatement represents a command for listing measurements. @@ -1668,7 +1706,7 @@ func (s *ShowMeasurementsStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a ShowMeasurementsStatement func (s *ShowMeasurementsStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} } // DropMeasurementStatement represents a command to drop a measurement. @@ -1687,7 +1725,7 @@ func (s *DropMeasurementStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a DropMeasurementStatement func (s *DropMeasurementStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // ShowRetentionPoliciesStatement represents a command for listing retention policies. @@ -1706,7 +1744,7 @@ func (s *ShowRetentionPoliciesStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a ShowRetentionPoliciesStatement func (s *ShowRetentionPoliciesStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} } // ShowRetentionPoliciesStatement represents a command for displaying stats for a given server. @@ -1727,7 +1765,7 @@ func (s *ShowStatsStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a ShowStatsStatement func (s *ShowStatsStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // ShowDiagnosticsStatement represents a command for show node diagnostics. @@ -1738,7 +1776,7 @@ func (s *ShowDiagnosticsStatement) String() string { return "SHOW DIAGNOSTICS" } // RequiredPrivileges returns the privilege required to execute a ShowDiagnosticsStatement func (s *ShowDiagnosticsStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // ShowTagKeysStatement represents a command for listing tag keys. @@ -1790,7 +1828,7 @@ func (s *ShowTagKeysStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a ShowTagKeysStatement func (s *ShowTagKeysStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} } // ShowTagValuesStatement represents a command for listing tag values. @@ -1845,7 +1883,7 @@ func (s *ShowTagValuesStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a ShowTagValuesStatement func (s *ShowTagValuesStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} } // ShowUsersStatement represents a command for listing users. @@ -1858,7 +1896,7 @@ func (s *ShowUsersStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a ShowUsersStatement func (s *ShowUsersStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: AllPrivileges}} + return ExecutionPrivileges{{Admin: true, Name: "", Privilege: AllPrivileges}} } // ShowFieldKeysStatement represents a command for listing field keys. @@ -1903,7 +1941,7 @@ func (s *ShowFieldKeysStatement) String() string { // RequiredPrivileges returns the privilege(s) required to execute a ShowFieldKeysStatement func (s *ShowFieldKeysStatement) RequiredPrivileges() ExecutionPrivileges { - return ExecutionPrivileges{{Name: "", Privilege: ReadPrivilege}} + return ExecutionPrivileges{{Admin: false, Name: "", Privilege: ReadPrivilege}} } // Fields represents a list of fields. diff --git a/influxql/parser.go b/influxql/parser.go index 90c8297ce63..2788eb53312 100644 --- a/influxql/parser.go +++ b/influxql/parser.go @@ -521,39 +521,58 @@ func (p *Parser) parseString() (string, error) { // parseRevokeStatement parses a string and returns a revoke statement. // This function assumes the REVOKE token has already been consumed. -func (p *Parser) parseRevokeStatement() (*RevokeStatement, error) { - stmt := &RevokeStatement{} - +func (p *Parser) parseRevokeStatement() (Statement, error) { // Parse the privilege to be revoked. priv, err := p.parsePrivilege() if err != nil { return nil, err } - stmt.Privilege = priv - // Parse ON clause. + // Check for ON or FROM clauses. tok, pos, lit := p.scanIgnoreWhitespace() if tok == ON { - // Parse the name of the thing we're revoking a privilege to use. - lit, err := p.parseIdent() + stmt, err := p.parseRevokeOnStatement() if err != nil { return nil, err } - stmt.On = lit + stmt.Privilege = priv + return stmt, nil + } else if tok == FROM { + // Admin privilege is only revoked on ALL PRIVILEGES. + if priv != AllPrivileges { + return nil, newParseError(tokstr(tok, lit), []string{"ON"}, pos) + } + return p.parseRevokeAdminStatement() + } - tok, pos, lit = p.scanIgnoreWhitespace() - } else if priv != AllPrivileges { - // ALL PRIVILEGES is the only privilege allowed cluster-wide. - // No ON clause means query is requesting cluster-wide. - return nil, newParseError(tokstr(tok, lit), []string{"ON"}, pos) + // Only ON or FROM clauses are allowed after privilege. + if priv == AllPrivileges { + return nil, newParseError(tokstr(tok, lit), []string{"ON", "FROM"}, pos) } + return nil, newParseError(tokstr(tok, lit), []string{"ON"}, pos) +} + +// parseRevokeOnStatement parses a string and returns a revoke statement. +// This function assumes the [PRIVILEGE] ON tokens have already been consumed. +func (p *Parser) parseRevokeOnStatement() (*RevokeStatement, error) { + stmt := &RevokeStatement{} + + // Parse the name of the database. + lit, err := p.parseIdent() + if err != nil { + return nil, err + } + stmt.On = lit + + // Parse FROM clause. + tok, pos, lit := p.scanIgnoreWhitespace() // Check for required FROM token. if tok != FROM { return nil, newParseError(tokstr(tok, lit), []string{"FROM"}, pos) } - // Parse the name of the user we're revoking the privilege from. + // Parse the name of the user. lit, err = p.parseIdent() if err != nil { return nil, err @@ -563,41 +582,76 @@ func (p *Parser) parseRevokeStatement() (*RevokeStatement, error) { return stmt, nil } +// parseRevokeAdminStatement parses a string and returns a revoke admin statement. +// This function assumes the ALL [PRVILEGES] FROM token has already been consumed. +func (p *Parser) parseRevokeAdminStatement() (*RevokeAdminStatement, error) { + // Admin privilege is always false when revoke admin clause is called. + stmt := &RevokeAdminStatement{} + + // Parse the name of the user. + lit, err := p.parseIdent() + if err != nil { + return nil, err + } + stmt.User = lit + + return stmt, nil +} + // parseGrantStatement parses a string and returns a grant statement. // This function assumes the GRANT token has already been consumed. -func (p *Parser) parseGrantStatement() (*GrantStatement, error) { - stmt := &GrantStatement{} - +func (p *Parser) parseGrantStatement() (Statement, error) { // Parse the privilege to be granted. priv, err := p.parsePrivilege() if err != nil { return nil, err } - stmt.Privilege = priv - // Parse ON clause. + // Check for ON or TO clauses. tok, pos, lit := p.scanIgnoreWhitespace() if tok == ON { - // Parse the name of the thing we're granting a privilege to use. - lit, err := p.parseIdent() + stmt, err := p.parseGrantOnStatement() if err != nil { return nil, err } - stmt.On = lit + stmt.Privilege = priv + return stmt, nil + } else if tok == TO { + // Admin privilege is only granted on ALL PRIVILEGES. + if priv != AllPrivileges { + return nil, newParseError(tokstr(tok, lit), []string{"ON"}, pos) + } + return p.parseGrantAdminStatement() + } - tok, pos, lit = p.scanIgnoreWhitespace() - } else if priv != AllPrivileges { - // ALL PRIVILEGES is the only privilege allowed cluster-wide. - // No ON clause means query is requesting cluster-wide. - return nil, newParseError(tokstr(tok, lit), []string{"ON"}, pos) + // Only ON or TO clauses are allowed after privilege. + if priv == AllPrivileges { + return nil, newParseError(tokstr(tok, lit), []string{"ON", "TO"}, pos) } + return nil, newParseError(tokstr(tok, lit), []string{"ON"}, pos) +} + +// parseGrantOnStatement parses a string and returns a grant statement. +// This function assumes the [PRIVILEGE] ON tokens have already been consumed. +func (p *Parser) parseGrantOnStatement() (*GrantStatement, error) { + stmt := &GrantStatement{} + + // Parse the name of the database. + lit, err := p.parseIdent() + if err != nil { + return nil, err + } + stmt.On = lit + + // Parse TO clause. + tok, pos, lit := p.scanIgnoreWhitespace() // Check for required TO token. if tok != TO { return nil, newParseError(tokstr(tok, lit), []string{"TO"}, pos) } - // Parse the name of the user we're granting the privilege to. + // Parse the name of the user. lit, err = p.parseIdent() if err != nil { return nil, err @@ -607,6 +661,22 @@ func (p *Parser) parseGrantStatement() (*GrantStatement, error) { return stmt, nil } +// parseGrantAdminStatement parses a string and returns a grant admin statement. +// This function assumes the ALL [PRVILEGES] TO tokens have already been consumed. +func (p *Parser) parseGrantAdminStatement() (*GrantAdminStatement, error) { + // Admin privilege is always true when grant admin clause is called. + stmt := &GrantAdminStatement{} + + // Parse the name of the user. + lit, err := p.parseIdent() + if err != nil { + return nil, err + } + stmt.User = lit + + return stmt, nil +} + // parsePrivilege parses a string and returns a Privilege func (p *Parser) parsePrivilege() (Privilege, error) { tok, pos, lit := p.scanIgnoreWhitespace() @@ -1259,12 +1329,12 @@ func (p *Parser) parseCreateUserStatement() (*CreateUserStatement, error) { return stmt, nil } - // We only allow granting of "ALL PRIVILEGES" during CREATE USER. - // All other privileges must be granted using a GRANT statement. + // "WITH ALL PRIVILEGES" grants the new user admin privilege. + // Only admin privilege can be set on user creation. if err := p.parseTokens([]Token{ALL, PRIVILEGES}); err != nil { return nil, err } - stmt.Privilege = NewPrivilege(AllPrivileges) + stmt.Admin = true return stmt, nil } diff --git a/influxql/parser_test.go b/influxql/parser_test.go index d4e8aacb1b1..6d79f1de625 100644 --- a/influxql/parser_test.go +++ b/influxql/parser_test.go @@ -942,9 +942,9 @@ func TestParser_ParseStatement(t *testing.T) { { s: `CREATE USER testuser WITH PASSWORD 'pwd1337' WITH ALL PRIVILEGES`, stmt: &influxql.CreateUserStatement{ - Name: "testuser", - Password: "pwd1337", - Privilege: influxql.NewPrivilege(influxql.AllPrivileges), + Name: "testuser", + Password: "pwd1337", + Admin: true, }, }, @@ -1030,12 +1030,19 @@ func TestParser_ParseStatement(t *testing.T) { }, }, - // GRANT cluster admin + // GRANT ALL admin privilege + { + s: `GRANT ALL TO jdoe`, + stmt: &influxql.GrantAdminStatement{ + User: "jdoe", + }, + }, + + // GRANT ALL PRVILEGES admin privilege { s: `GRANT ALL PRIVILEGES TO jdoe`, - stmt: &influxql.GrantStatement{ - Privilege: influxql.AllPrivileges, - User: "jdoe", + stmt: &influxql.GrantAdminStatement{ + User: "jdoe", }, }, @@ -1079,12 +1086,19 @@ func TestParser_ParseStatement(t *testing.T) { }, }, - // REVOKE cluster admin + // REVOKE ALL admin privilege { s: `REVOKE ALL FROM jdoe`, - stmt: &influxql.RevokeStatement{ - Privilege: influxql.AllPrivileges, - User: "jdoe", + stmt: &influxql.RevokeAdminStatement{ + User: "jdoe", + }, + }, + + // REVOKE ALL PRIVILEGES admin privilege + { + s: `REVOKE ALL PRIVILEGES FROM jdoe`, + stmt: &influxql.RevokeAdminStatement{ + User: "jdoe", }, }, @@ -1263,16 +1277,67 @@ func TestParser_ParseStatement(t *testing.T) { {s: `GRANT`, err: `found EOF, expected READ, WRITE, ALL [PRIVILEGES] at line 1, char 7`}, {s: `GRANT BOGUS`, err: `found BOGUS, expected READ, WRITE, ALL [PRIVILEGES] at line 1, char 7`}, {s: `GRANT READ`, err: `found EOF, expected ON at line 1, char 12`}, - {s: `GRANT READ TO jdoe`, err: `found TO, expected ON at line 1, char 12`}, + {s: `GRANT READ FROM`, err: `found FROM, expected ON at line 1, char 12`}, {s: `GRANT READ ON`, err: `found EOF, expected identifier at line 1, char 15`}, + {s: `GRANT READ ON TO`, err: `found TO, expected identifier at line 1, char 15`}, {s: `GRANT READ ON testdb`, err: `found EOF, expected TO at line 1, char 22`}, - {s: `GRANT READ ON testdb TO`, err: `found EOF, expected identifier at line 1, char 25`}, {s: `GRANT`, err: `found EOF, expected READ, WRITE, ALL [PRIVILEGES] at line 1, char 7`}, + {s: `GRANT READ ON testdb TO`, err: `found EOF, expected identifier at line 1, char 25`}, + {s: `GRANT READ TO`, err: `found TO, expected ON at line 1, char 12`}, + {s: `GRANT WRITE`, err: `found EOF, expected ON at line 1, char 13`}, + {s: `GRANT WRITE FROM`, err: `found FROM, expected ON at line 1, char 13`}, + {s: `GRANT WRITE ON`, err: `found EOF, expected identifier at line 1, char 16`}, + {s: `GRANT WRITE ON TO`, err: `found TO, expected identifier at line 1, char 16`}, + {s: `GRANT WRITE ON testdb`, err: `found EOF, expected TO at line 1, char 23`}, + {s: `GRANT WRITE ON testdb TO`, err: `found EOF, expected identifier at line 1, char 26`}, + {s: `GRANT WRITE TO`, err: `found TO, expected ON at line 1, char 13`}, + {s: `GRANT ALL`, err: `found EOF, expected ON, TO at line 1, char 11`}, + {s: `GRANT ALL PRIVILEGES`, err: `found EOF, expected ON, TO at line 1, char 22`}, + {s: `GRANT ALL FROM`, err: `found FROM, expected ON, TO at line 1, char 11`}, + {s: `GRANT ALL PRIVILEGES FROM`, err: `found FROM, expected ON, TO at line 1, char 22`}, + {s: `GRANT ALL ON`, err: `found EOF, expected identifier at line 1, char 14`}, + {s: `GRANT ALL PRIVILEGES ON`, err: `found EOF, expected identifier at line 1, char 25`}, + {s: `GRANT ALL ON TO`, err: `found TO, expected identifier at line 1, char 14`}, + {s: `GRANT ALL PRIVILEGES ON TO`, err: `found TO, expected identifier at line 1, char 25`}, + {s: `GRANT ALL ON testdb`, err: `found EOF, expected TO at line 1, char 21`}, + {s: `GRANT ALL PRIVILEGES ON testdb`, err: `found EOF, expected TO at line 1, char 32`}, + {s: `GRANT ALL ON testdb FROM`, err: `found FROM, expected TO at line 1, char 21`}, + {s: `GRANT ALL PRIVILEGES ON testdb FROM`, err: `found FROM, expected TO at line 1, char 32`}, + {s: `GRANT ALL ON testdb TO`, err: `found EOF, expected identifier at line 1, char 24`}, + {s: `GRANT ALL PRIVILEGES ON testdb TO`, err: `found EOF, expected identifier at line 1, char 35`}, + {s: `GRANT ALL TO`, err: `found EOF, expected identifier at line 1, char 14`}, + {s: `GRANT ALL PRIVILEGES TO`, err: `found EOF, expected identifier at line 1, char 25`}, + {s: `REVOKE`, err: `found EOF, expected READ, WRITE, ALL [PRIVILEGES] at line 1, char 8`}, {s: `REVOKE BOGUS`, err: `found BOGUS, expected READ, WRITE, ALL [PRIVILEGES] at line 1, char 8`}, {s: `REVOKE READ`, err: `found EOF, expected ON at line 1, char 13`}, - {s: `REVOKE READ TO jdoe`, err: `found TO, expected ON at line 1, char 13`}, + {s: `REVOKE READ TO`, err: `found TO, expected ON at line 1, char 13`}, {s: `REVOKE READ ON`, err: `found EOF, expected identifier at line 1, char 16`}, + {s: `REVOKE READ ON FROM`, err: `found FROM, expected identifier at line 1, char 16`}, {s: `REVOKE READ ON testdb`, err: `found EOF, expected FROM at line 1, char 23`}, {s: `REVOKE READ ON testdb FROM`, err: `found EOF, expected identifier at line 1, char 28`}, + {s: `REVOKE READ FROM`, err: `found FROM, expected ON at line 1, char 13`}, + {s: `REVOKE WRITE`, err: `found EOF, expected ON at line 1, char 14`}, + {s: `REVOKE WRITE TO`, err: `found TO, expected ON at line 1, char 14`}, + {s: `REVOKE WRITE ON`, err: `found EOF, expected identifier at line 1, char 17`}, + {s: `REVOKE WRITE ON FROM`, err: `found FROM, expected identifier at line 1, char 17`}, + {s: `REVOKE WRITE ON testdb`, err: `found EOF, expected FROM at line 1, char 24`}, + {s: `REVOKE WRITE ON testdb FROM`, err: `found EOF, expected identifier at line 1, char 29`}, + {s: `REVOKE WRITE FROM`, err: `found FROM, expected ON at line 1, char 14`}, + {s: `REVOKE ALL`, err: `found EOF, expected ON, FROM at line 1, char 12`}, + {s: `REVOKE ALL PRIVILEGES`, err: `found EOF, expected ON, FROM at line 1, char 23`}, + {s: `REVOKE ALL TO`, err: `found TO, expected ON, FROM at line 1, char 12`}, + {s: `REVOKE ALL PRIVILEGES TO`, err: `found TO, expected ON, FROM at line 1, char 23`}, + {s: `REVOKE ALL ON`, err: `found EOF, expected identifier at line 1, char 15`}, + {s: `REVOKE ALL PRIVILEGES ON`, err: `found EOF, expected identifier at line 1, char 26`}, + {s: `REVOKE ALL ON FROM`, err: `found FROM, expected identifier at line 1, char 15`}, + {s: `REVOKE ALL PRIVILEGES ON FROM`, err: `found FROM, expected identifier at line 1, char 26`}, + {s: `REVOKE ALL ON testdb`, err: `found EOF, expected FROM at line 1, char 22`}, + {s: `REVOKE ALL PRIVILEGES ON testdb`, err: `found EOF, expected FROM at line 1, char 33`}, + {s: `REVOKE ALL ON testdb TO`, err: `found TO, expected FROM at line 1, char 22`}, + {s: `REVOKE ALL PRIVILEGES ON testdb TO`, err: `found TO, expected FROM at line 1, char 33`}, + {s: `REVOKE ALL ON testdb FROM`, err: `found EOF, expected identifier at line 1, char 27`}, + {s: `REVOKE ALL PRIVILEGES ON testdb FROM`, err: `found EOF, expected identifier at line 1, char 38`}, + {s: `REVOKE ALL FROM`, err: `found EOF, expected identifier at line 1, char 17`}, + {s: `REVOKE ALL PRIVILEGES FROM`, err: `found EOF, expected identifier at line 1, char 28`}, {s: `CREATE RETENTION`, err: `found EOF, expected POLICY at line 1, char 18`}, {s: `CREATE RETENTION POLICY`, err: `found EOF, expected identifier at line 1, char 25`}, {s: `CREATE RETENTION POLICY policy1`, err: `found EOF, expected ON at line 1, char 33`}, diff --git a/meta/data.go b/meta/data.go index 4129b5a4e85..35a3f6e0654 100644 --- a/meta/data.go +++ b/meta/data.go @@ -480,7 +480,19 @@ func (data *Data) SetPrivilege(name, database string, p influxql.Privilege) erro return nil } -// UserPrivileges get privileges for a user. +// SetAdminPrivilege sets the admin privilege for a user. +func (data *Data) SetAdminPrivilege(name string, admin bool) error { + ui := data.User(name) + if ui == nil { + return ErrUserNotFound + } + + ui.Admin = admin + + return nil +} + +// UserPrivileges gets the privileges for a user. func (data *Data) UserPrivileges(name string) (map[string]influxql.Privilege, error) { ui := data.User(name) if ui == nil { @@ -490,6 +502,22 @@ func (data *Data) UserPrivileges(name string) (map[string]influxql.Privilege, er return ui.Privileges, nil } +// UserPrivilege gets the privilege for a user on a database. +func (data *Data) UserPrivilege(name, database string) (*influxql.Privilege, error) { + ui := data.User(name) + if ui == nil { + return nil, ErrUserNotFound + } + + for db, p := range ui.Privileges { + if db == database { + return &p, nil + } + } + + return influxql.NewPrivilege(influxql.NoPrivileges), nil +} + // Clone returns a copy of data with a new version. func (data *Data) Clone() *Data { other := *data @@ -958,8 +986,11 @@ type UserInfo struct { // 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 { + return true + } p, ok := ui.Privileges[database] - return (ok && p >= privilege) || (ui.Admin) + return ok && (p == privilege || p == influxql.AllPrivileges) } // clone returns a deep copy of si. diff --git a/meta/errors.go b/meta/errors.go index 82c5b6895f4..dfc69aa360f 100644 --- a/meta/errors.go +++ b/meta/errors.go @@ -114,18 +114,3 @@ func lookupError(err error) error { } return err } - -// AuthError represents an authorization error. -type AuthError struct { - text string -} - -// NewAuthError returns a new instance of AuthError. -func NewAuthError(text string) AuthError { - return AuthError{text: text} -} - -// Error returns the text of the error. -func (e AuthError) Error() string { - return e.text -} diff --git a/meta/internal/meta.pb.go b/meta/internal/meta.pb.go index 342a6422d0c..979c842b9ed 100644 --- a/meta/internal/meta.pb.go +++ b/meta/internal/meta.pb.go @@ -36,6 +36,7 @@ It has these top-level messages: UpdateUserCommand SetPrivilegeCommand SetDataCommand + SetAdminPrivilegeCommand Response */ package internal @@ -67,6 +68,7 @@ const ( Command_UpdateUserCommand Command_Type = 15 Command_SetPrivilegeCommand Command_Type = 16 Command_SetDataCommand Command_Type = 17 + Command_SetAdminPrivilegeCommand Command_Type = 18 ) var Command_Type_name = map[int32]string{ @@ -87,6 +89,7 @@ var Command_Type_name = map[int32]string{ 15: "UpdateUserCommand", 16: "SetPrivilegeCommand", 17: "SetDataCommand", + 18: "SetAdminPrivilegeCommand", } var Command_Type_value = map[string]int32{ "CreateNodeCommand": 1, @@ -106,6 +109,7 @@ var Command_Type_value = map[string]int32{ "UpdateUserCommand": 15, "SetPrivilegeCommand": 16, "SetDataCommand": 17, + "SetAdminPrivilegeCommand": 18, } func (x Command_Type) Enum() *Command_Type { @@ -1076,6 +1080,38 @@ var E_SetDataCommand_Command = &proto.ExtensionDesc{ Tag: "bytes,117,opt,name=command", } +type SetAdminPrivilegeCommand struct { + Username *string `protobuf:"bytes,1,req" json:"Username,omitempty"` + Admin *bool `protobuf:"varint,2,req" json:"Admin,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SetAdminPrivilegeCommand) Reset() { *m = SetAdminPrivilegeCommand{} } +func (m *SetAdminPrivilegeCommand) String() string { return proto.CompactTextString(m) } +func (*SetAdminPrivilegeCommand) ProtoMessage() {} + +func (m *SetAdminPrivilegeCommand) GetUsername() string { + if m != nil && m.Username != nil { + return *m.Username + } + return "" +} + +func (m *SetAdminPrivilegeCommand) GetAdmin() bool { + if m != nil && m.Admin != nil { + return *m.Admin + } + return false +} + +var E_SetAdminPrivilegeCommand_Command = &proto.ExtensionDesc{ + ExtendedType: (*Command)(nil), + ExtensionType: (*SetAdminPrivilegeCommand)(nil), + Field: 118, + Name: "internal.SetAdminPrivilegeCommand.command", + Tag: "bytes,118,opt,name=command", +} + type Response struct { OK *bool `protobuf:"varint,1,req" json:"OK,omitempty"` Error *string `protobuf:"bytes,2,opt" json:"Error,omitempty"` @@ -1127,4 +1163,5 @@ func init() { proto.RegisterExtension(E_UpdateUserCommand_Command) proto.RegisterExtension(E_SetPrivilegeCommand_Command) proto.RegisterExtension(E_SetDataCommand_Command) + proto.RegisterExtension(E_SetAdminPrivilegeCommand_Command) } diff --git a/meta/internal/meta.proto b/meta/internal/meta.proto index 934781dbe45..d5f5bf1fec6 100644 --- a/meta/internal/meta.proto +++ b/meta/internal/meta.proto @@ -242,6 +242,14 @@ message SetDataCommand { required Data Data = 1; } +message SetAdminPrivilegeCommand { + extend Command { + optional SetAdminPrivilegeCommand command = 118; + } + required string Username = 1; + required bool Admin = 2; +} + message Response { required bool OK = 1; optional string Error = 2; diff --git a/meta/statement_executor.go b/meta/statement_executor.go index b26b2703c7b..58f86393ca4 100644 --- a/meta/statement_executor.go +++ b/meta/statement_executor.go @@ -27,7 +27,9 @@ type StatementExecutor struct { UpdateUser(name, password string) error DropUser(name string) error SetPrivilege(username, database string, p influxql.Privilege) error + SetAdminPrivilege(username string, admin bool) error UserPrivileges(username string) (map[string]influxql.Privilege, error) + UserPrivilege(username, database string) (*influxql.Privilege, error) CreateContinuousQuery(database, name, query string) error DropContinuousQuery(database, name string) error @@ -57,8 +59,12 @@ func (e *StatementExecutor) ExecuteStatement(stmt influxql.Statement) *influxql. return e.executeShowUsersStatement(stmt) case *influxql.GrantStatement: return e.executeGrantStatement(stmt) + case *influxql.GrantAdminStatement: + return e.executeGrantAdminStatement(stmt) case *influxql.RevokeStatement: return e.executeRevokeStatement(stmt) + case *influxql.RevokeAdminStatement: + return e.executeRevokeAdminStatement(stmt) case *influxql.CreateRetentionPolicyStatement: return e.executeCreateRetentionPolicyStatement(stmt) case *influxql.AlterRetentionPolicyStatement: @@ -129,12 +135,7 @@ func (e *StatementExecutor) executeShowServersStatement(q *influxql.ShowServersS } func (e *StatementExecutor) executeCreateUserStatement(q *influxql.CreateUserStatement) *influxql.Result { - admin := false - if q.Privilege != nil { - admin = (*q.Privilege == influxql.AllPrivileges) - } - - _, err := e.Store.CreateUser(q.Name, q.Password, admin) + _, err := e.Store.CreateUser(q.Name, q.Password, q.Admin) return &influxql.Result{Err: err} } @@ -163,8 +164,28 @@ func (e *StatementExecutor) executeGrantStatement(stmt *influxql.GrantStatement) return &influxql.Result{Err: e.Store.SetPrivilege(stmt.User, stmt.On, stmt.Privilege)} } +func (e *StatementExecutor) executeGrantAdminStatement(stmt *influxql.GrantAdminStatement) *influxql.Result { + return &influxql.Result{Err: e.Store.SetAdminPrivilege(stmt.User, true)} +} + func (e *StatementExecutor) executeRevokeStatement(stmt *influxql.RevokeStatement) *influxql.Result { - return &influxql.Result{Err: e.Store.SetPrivilege(stmt.User, stmt.On, influxql.NoPrivileges)} + priv := influxql.NoPrivileges + + // Revoking all privileges means there's no need to look at existing user privileges. + if stmt.Privilege != influxql.AllPrivileges { + p, err := e.Store.UserPrivilege(stmt.User, stmt.On) + if err != nil { + return &influxql.Result{Err: err} + } + // Bit clear (AND NOT) the user's privilege with the revoked privilege. + priv = *p &^ stmt.Privilege + } + + return &influxql.Result{Err: e.Store.SetPrivilege(stmt.User, stmt.On, priv)} +} + +func (e *StatementExecutor) executeRevokeAdminStatement(stmt *influxql.RevokeAdminStatement) *influxql.Result { + return &influxql.Result{Err: e.Store.SetAdminPrivilege(stmt.User, false)} } func (e *StatementExecutor) executeCreateRetentionPolicyStatement(stmt *influxql.CreateRetentionPolicyStatement) *influxql.Result { diff --git a/meta/statement_executor_test.go b/meta/statement_executor_test.go index 91d71037c41..b382a09f685 100644 --- a/meta/statement_executor_test.go +++ b/meta/statement_executor_test.go @@ -312,6 +312,37 @@ func TestStatementExecutor_ExecuteStatement_Grant_Err(t *testing.T) { } } +// Ensure a GRANT statement for admin privilege can be executed. +func TestStatementExecutor_ExecuteStatement_GrantAdmin(t *testing.T) { + e := NewStatementExecutor() + e.Store.SetAdminPrivilegeFn = func(username string, admin bool) error { + if username != "susy" { + t.Fatalf("unexpected username: %s", username) + } else if admin != true { + t.Fatalf("unexpected admin privilege: %t", admin) + } + return nil + } + + if res := e.ExecuteStatement(influxql.MustParseStatement(`GRANT ALL TO susy`)); res.Err != nil { + t.Fatal(res.Err) + } else if res.Series != nil { + t.Fatalf("unexpected rows: %#v", res.Series) + } +} + +// Ensure a GRANT statement for admin privilege returns errors from the store. +func TestStatementExecutor_ExecuteStatement_GrantAdmin_Err(t *testing.T) { + e := NewStatementExecutor() + e.Store.SetAdminPrivilegeFn = func(username string, admin bool) error { + return errors.New("marker") + } + + if res := e.ExecuteStatement(influxql.MustParseStatement(`GRANT ALL PRIVILEGES TO susy`)); res.Err == nil || res.Err.Error() != "marker" { + t.Fatalf("unexpected error: %s", res.Err) + } +} + // Ensure a REVOKE statement can be executed. func TestStatementExecutor_ExecuteStatement_Revoke(t *testing.T) { e := NewStatementExecutor() @@ -345,6 +376,37 @@ func TestStatementExecutor_ExecuteStatement_Revoke_Err(t *testing.T) { } } +// Ensure a REVOKE statement for admin privilege can be executed. +func TestStatementExecutor_ExecuteStatement_RevokeAdmin(t *testing.T) { + e := NewStatementExecutor() + e.Store.SetAdminPrivilegeFn = func(username string, admin bool) error { + if username != "susy" { + t.Fatalf("unexpected username: %s", username) + } else if admin != false { + t.Fatalf("unexpected admin privilege: %t", admin) + } + return nil + } + + if res := e.ExecuteStatement(influxql.MustParseStatement(`REVOKE ALL PRIVILEGES FROM susy`)); res.Err != nil { + t.Fatal(res.Err) + } else if res.Series != nil { + t.Fatalf("unexpected rows: %#v", res.Series) + } +} + +// Ensure a REVOKE statement for admin privilege returns errors from the store. +func TestStatementExecutor_ExecuteStatement_RevokeAdmin_Err(t *testing.T) { + e := NewStatementExecutor() + e.Store.SetAdminPrivilegeFn = func(username string, admin bool) error { + return errors.New("marker") + } + + if res := e.ExecuteStatement(influxql.MustParseStatement(`REVOKE ALL PRIVILEGES FROM susy`)); res.Err == nil || res.Err.Error() != "marker" { + t.Fatalf("unexpected error: %s", res.Err) + } +} + // Ensure a CREATE RETENTION POLICY statement can be executed. func TestStatementExecutor_ExecuteStatement_CreateRetentionPolicy(t *testing.T) { e := NewStatementExecutor() @@ -730,7 +792,9 @@ type StatementExecutorStore struct { UpdateUserFn func(name, password string) error DropUserFn func(name string) error SetPrivilegeFn func(username, database string, p influxql.Privilege) error + SetAdminPrivilegeFn func(username string, admin bool) error UserPrivilegesFn func(username string) (map[string]influxql.Privilege, error) + UserPrivilegeFn func(username, database string) (*influxql.Privilege, error) ContinuousQueriesFn func() ([]meta.ContinuousQueryInfo, error) CreateContinuousQueryFn func(database, name, query string) error DropContinuousQueryFn func(database, name string) error @@ -796,10 +860,18 @@ func (s *StatementExecutorStore) SetPrivilege(username, database string, p influ return s.SetPrivilegeFn(username, database, p) } +func (s *StatementExecutorStore) SetAdminPrivilege(username string, admin bool) error { + return s.SetAdminPrivilegeFn(username, admin) +} + func (s *StatementExecutorStore) UserPrivileges(username string) (map[string]influxql.Privilege, error) { return s.UserPrivilegesFn(username) } +func (s *StatementExecutorStore) UserPrivilege(username, database string) (*influxql.Privilege, error) { + return s.UserPrivilegeFn(username, database) +} + func (s *StatementExecutorStore) ContinuousQueries() ([]meta.ContinuousQueryInfo, error) { return s.ContinuousQueriesFn() } diff --git a/meta/store.go b/meta/store.go index a9337a465a7..a73372b0455 100644 --- a/meta/store.go +++ b/meta/store.go @@ -1134,6 +1134,16 @@ func (s *Store) SetPrivilege(username, database string, p influxql.Privilege) er ) } +// SetAdminPrivilege sets the admin privilege for a user on a database. +func (s *Store) SetAdminPrivilege(username string, admin bool) error { + return s.exec(internal.Command_SetAdminPrivilegeCommand, internal.E_SetAdminPrivilegeCommand_Command, + &internal.SetAdminPrivilegeCommand{ + Username: proto.String(username), + Admin: proto.Bool(admin), + }, + ) +} + // UserPrivileges returns a list of all databases. func (s *Store) UserPrivileges(username string) (p map[string]influxql.Privilege, err error) { err = s.read(func(data *Data) error { @@ -1143,6 +1153,15 @@ func (s *Store) UserPrivileges(username string) (p map[string]influxql.Privilege return } +// UserPrivilege returns the privilege for a database. +func (s *Store) UserPrivilege(username, database string) (p *influxql.Privilege, err error) { + err = s.read(func(data *Data) error { + p, err = data.UserPrivilege(username, database) + return err + }) + return +} + // UserCount returns the number of users defined in the cluster. func (s *Store) UserCount() (count int, err error) { err = s.read(func(data *Data) error { @@ -1441,6 +1460,8 @@ func (fsm *storeFSM) Apply(l *raft.Log) interface{} { return fsm.applyUpdateUserCommand(&cmd) case internal.Command_SetPrivilegeCommand: return fsm.applySetPrivilegeCommand(&cmd) + case internal.Command_SetAdminPrivilegeCommand: + return fsm.applySetAdminPrivilegeCommand(&cmd) case internal.Command_SetDataCommand: return fsm.applySetDataCommand(&cmd) default: @@ -1701,6 +1722,19 @@ func (fsm *storeFSM) applySetPrivilegeCommand(cmd *internal.Command) interface{} return nil } +func (fsm *storeFSM) applySetAdminPrivilegeCommand(cmd *internal.Command) interface{} { + ext, _ := proto.GetExtension(cmd, internal.E_SetAdminPrivilegeCommand_Command) + v := ext.(*internal.SetAdminPrivilegeCommand) + + // Copy data and update. + other := fsm.data.Clone() + if err := other.SetAdminPrivilege(v.GetUsername(), v.GetAdmin()); err != nil { + return err + } + fsm.data = other + return nil +} + func (fsm *storeFSM) applySetDataCommand(cmd *internal.Command) interface{} { ext, _ := proto.GetExtension(cmd, internal.E_SetDataCommand_Command) v := ext.(*internal.SetDataCommand) diff --git a/services/httpd/handler.go b/services/httpd/handler.go index 82a91b7e6bb..f36f1530343 100644 --- a/services/httpd/handler.go +++ b/services/httpd/handler.go @@ -60,6 +60,7 @@ type Handler struct { } QueryExecutor interface { + Authorize(u *meta.UserInfo, q *influxql.Query, db string) error ExecuteQuery(q *influxql.Query, db string, chunkSize int) (<-chan *influxql.Result, error) } @@ -220,6 +221,15 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user *meta. return } + // Check authorization. + if h.requireAuthentication { + err = h.QueryExecutor.Authorize(user, query, db) + if err != nil { + httpError(w, "error authorizing query: "+err.Error(), pretty, http.StatusUnauthorized) + return + } + } + // Parse chunk size. Use default if not provided or unparsable. chunked := (q.Get("chunked") == "true") chunkSize := DefaultChunkSize @@ -233,34 +243,19 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user *meta. w.Header().Add("content-type", "application/json") results, err := h.QueryExecutor.ExecuteQuery(query, db, chunkSize) - if _, ok := err.(meta.AuthError); ok { - w.WriteHeader(http.StatusUnauthorized) - return - } else if err != nil { + if err != nil { w.WriteHeader(http.StatusInternalServerError) return } // if we're not chunking, this will be the in memory buffer for all results before sending to client resp := Response{Results: make([]*influxql.Result, 0)} - statusWritten := false + + // Status header is OK once this point is reached. + w.WriteHeader(http.StatusOK) // pull all results from the channel for r := range results { - // write the status header based on the first result returned in the channel - if !statusWritten { - status := http.StatusOK - - if r != nil && r.Err != nil { - if _, ok := r.Err.(meta.AuthError); ok { - status = http.StatusUnauthorized - } - } - - w.WriteHeader(status) - statusWritten = true - } - // Ignore nil results. if r == nil { continue diff --git a/services/httpd/handler_test.go b/services/httpd/handler_test.go index a0fe0bd8b34..907821900f9 100644 --- a/services/httpd/handler_test.go +++ b/services/httpd/handler_test.go @@ -221,18 +221,18 @@ func TestHandler_Query_ErrInvalidQuery(t *testing.T) { } // Ensure the handler returns a status 401 if the user is not authorized. -func TestHandler_Query_ErrUnauthorized(t *testing.T) { - h := NewHandler(false) - h.QueryExecutor.ExecuteQueryFn = func(q *influxql.Query, db string, chunkSize int) (<-chan *influxql.Result, error) { - return nil, meta.NewAuthError("marker") - } - - w := httptest.NewRecorder() - h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SHOW+SERIES+FROM+bar", nil)) - if w.Code != http.StatusUnauthorized { - t.Fatalf("unexpected status: %d", w.Code) - } -} +// func TestHandler_Query_ErrUnauthorized(t *testing.T) { +// h := NewHandler(false) +// h.QueryExecutor.AuthorizeFn = func(u *meta.UserInfo, q *influxql.Query, db string) error { +// return errors.New("marker") +// } + +// w := httptest.NewRecorder() +// h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?u=bar&db=foo&q=SHOW+SERIES+FROM+bar", nil)) +// if w.Code != http.StatusUnauthorized { +// t.Fatalf("unexpected status: %d", w.Code) +// } +// } // Ensure the handler returns a status 500 if an error is returned from the query executor. func TestHandler_Query_ErrExecuteQuery(t *testing.T) { @@ -264,22 +264,6 @@ func TestHandler_Query_ErrResult(t *testing.T) { } } -// Ensure the handler returns a status 401 if an auth error is returned from the result. -func TestHandler_Query_Result_ErrUnauthorized(t *testing.T) { - h := NewHandler(false) - h.QueryExecutor.ExecuteQueryFn = func(q *influxql.Query, db string, chunkSize int) (<-chan *influxql.Result, error) { - return NewResultChan(&influxql.Result{Err: meta.NewAuthError("marker")}), nil - } - - w := httptest.NewRecorder() - h.ServeHTTP(w, MustNewJSONRequest("GET", "/query?db=foo&q=SHOW+SERIES+from+bin", nil)) - if w.Code != http.StatusUnauthorized { - t.Fatalf("unexpected status: %d", w.Code) - } else if w.Body.String() != `{"results":[{"error":"marker"}]}` { - t.Fatalf("unexpected body: %s", w.Body.String()) - } -} - // Ensure the shard mapper handler responds correctly to common errors func TestHandler_Mapper_BadRequest(t *testing.T) { h := NewHandler(false) @@ -462,9 +446,14 @@ func (s *HandlerMetaStore) Users() ([]meta.UserInfo, error) { // HandlerQueryExecutor is a mock implementation of Handler.QueryExecutor. type HandlerQueryExecutor struct { + AuthorizeFn func(u *meta.UserInfo, q *influxql.Query, db string) error ExecuteQueryFn func(q *influxql.Query, db string, chunkSize int) (<-chan *influxql.Result, error) } +func (e *HandlerQueryExecutor) Authorize(u *meta.UserInfo, q *influxql.Query, db string) error { + return e.AuthorizeFn(u, q, db) +} + func (e *HandlerQueryExecutor) ExecuteQuery(q *influxql.Query, db string, chunkSize int) (<-chan *influxql.Result, error) { return e.ExecuteQueryFn(q, db, chunkSize) } diff --git a/tsdb/query_executor.go b/tsdb/query_executor.go index ee2895f1df0..4aca953b1ff 100644 --- a/tsdb/query_executor.go +++ b/tsdb/query_executor.go @@ -60,27 +60,24 @@ func NewQueryExecutor(store *Store) *QueryExecutor { // If no user is provided it will return an error unless the query's first statement is to create // a root user. func (q *QueryExecutor) Authorize(u *meta.UserInfo, query *influxql.Query, database string) error { - const authErrLogFmt = "unauthorized request | user: %q | query: %q | database %q\n" - // Special case if no users exist. if count, err := q.MetaStore.UserCount(); count == 0 && err == nil { - // Get the first statement in the query. - stmt := query.Statements[0] - // First statement must create a root user. - if cu, ok := stmt.(*influxql.CreateUserStatement); !ok || - cu.Privilege == nil || - *cu.Privilege != influxql.AllPrivileges { - return ErrAuthorize{text: "no users exist. create root user first or disable authentication"} + // Ensure there is at least one statement. + if len(query.Statements) > 0 { + // First statement in the query must create a user with admin privilege. + cu, ok := query.Statements[0].(*influxql.CreateUserStatement) + if ok && cu.Admin == true { + return nil + } } - return nil + return NewErrAuthorize(q, query, "", database, "create admin user first or disable authentication") } if u == nil { - q.Logger.Printf(authErrLogFmt, "", query.String(), database) - return ErrAuthorize{text: "no user provided"} + return NewErrAuthorize(q, query, "", database, "no user provided") } - // Cluster admins can do anything. + // Admin privilege allows the user to execute all statements. if u.Admin { return nil } @@ -90,29 +87,26 @@ func (q *QueryExecutor) Authorize(u *meta.UserInfo, query *influxql.Query, datab // Get the privileges required to execute the statement. privs := stmt.RequiredPrivileges() - // Make sure the user has each privilege required to execute - // the statement. + // Make sure the user has the privileges required to execute + // each statement. for _, p := range privs { + if p.Admin { + // Admin privilege already checked so statement requiring admin + // privilege cannot be run. + msg := fmt.Sprintf("statement '%s', requires admin privilege", stmt) + return NewErrAuthorize(q, query, u.Name, database, msg) + } + // Use the db name specified by the statement or the db // name passed by the caller if one wasn't specified by // the statement. - dbname := p.Name - if dbname == "" { - dbname = database + db := p.Name + if db == "" { + db = database } - - // Check if user has required privilege. - if !u.Authorize(p.Privilege, dbname) { - var msg string - if dbname == "" { - msg = "requires cluster admin" - } else { - msg = fmt.Sprintf("requires %s privilege on %s", p.Privilege.String(), dbname) - } - q.Logger.Printf(authErrLogFmt, u.Name, query.String(), database) - return ErrAuthorize{ - text: fmt.Sprintf("%s not authorized to execute '%s'. %s", u.Name, stmt.String(), msg), - } + if !u.Authorize(p.Privilege, db) { + msg := fmt.Sprintf("statement '%s', requires %s on %s", stmt, p.Privilege.String(), db) + return NewErrAuthorize(q, query, u.Name, database, msg) } } } @@ -1000,17 +994,29 @@ func (q *QueryExecutor) executeShowDiagnosticsStatement(stmt *influxql.ShowDiagn // ErrAuthorize represents an authorization error. type ErrAuthorize struct { - text string + q *QueryExecutor + query *influxql.Query + user string + database string + message string +} + +const authErrLogFmt string = "unauthorized request | user: %q | query: %q | database %q\n" + +// newAuthorizationError returns a new instance of AuthorizationError. +func NewErrAuthorize(qe *QueryExecutor, q *influxql.Query, u, db, m string) *ErrAuthorize { + return &ErrAuthorize{q: qe, query: q, user: u, database: db, message: m} } // Error returns the text of the error. func (e ErrAuthorize) Error() string { - return e.text + e.q.Logger.Printf(authErrLogFmt, e.user, e.query.String(), e.database) + if e.user == "" { + return fmt.Sprint(e.message) + } + return fmt.Sprintf("%s not authorized to execute %s", e.user, e.message) } -// authorize satisfies isAuthorizationError -func (ErrAuthorize) authorize() {} - var ( // ErrInvalidQuery is returned when executing an unknown query type. ErrInvalidQuery = errors.New("invalid query")