M cmd/sf/main.go => cmd/sf/main.go +8 -12
@@ 1,11 1,10 @@
package main
import (
- "errors"
+ "encoding/json"
"flag"
"fmt"
"os"
- "strings"
"syscall"
"egt.run/sf"
@@ 64,21 63,18 @@ func run() error {
}
defer fi.Close()
+ perms, err := sf.BuildPermissions(db, fi)
+ if err != nil {
+ return fmt.Errorf("build permissions: %w", err)
+ }
if f.dry {
- grants, err := sf.BuildGrants(db, fi)
+ byt, err := json.MarshalIndent(perms, "", "\t")
if err != nil {
- return fmt.Errorf("build grants: %w", err)
- }
- if len(grants) == 0 {
- return errors.New("no grants")
+ return fmt.Errorf("marshal indent: %w", err)
}
- fmt.Println(strings.Join(grants, ";\n") + ";")
+ fmt.Println(string(byt))
return nil
}
- perms, err := sf.BuildPermissions(db, fi)
- if err != nil {
- return fmt.Errorf("build permissions: %w", err)
- }
if err = db.Apply(perms); err != nil {
return fmt.Errorf("apply permissions: %w", err)
}
M parser.go => parser.go +1 -100
@@ 5,7 5,6 @@ import (
"errors"
"fmt"
"io"
- "sort"
"strings"
)
@@ 112,101 111,6 @@ func compile(
return userPerms, nil
}
-// dbsAllGranted searches through all sub-permissions to determine if any are
-// denied. It reports true iff all permissions are granted across every
-// sub-resource.
-func dbsAllGranted(dbs map[string]*DBPermission) bool {
- for _, db := range dbs {
- if db.Deny {
- return false
- }
- if !tablesAllGranted(db.Tables) {
- return false
- }
- }
- return true
-}
-
-// tablesAllGranted searches through all sub-permissions to determine if any
-// are denied. It reports true iff all permissions are granted across every
-// sub-resource.
-func tablesAllGranted(tables map[string]*TablePermission) bool {
- for _, s := range tables {
- if s.Deny {
- return false
- }
- for _, t := range s.Statements {
- if t.Deny {
- return false
- }
- for _, c := range t.Columns {
- if c {
- return false
- }
- }
- }
- }
- return true
-}
-
-// grants outputs the minimum set of GRANT and REVOKE statements that will
-// enforce the permissions.
-func (p *Permissions) grants(user string) []string {
- // Default to any host, but allow overriding it
- if !strings.Contains(user, "@") {
- user += "@'%'"
- }
-
- // Combine all grants into a cross-db grant where appropriate.
- if dbsAllGranted(p.Databases) {
- return []string{fmt.Sprintf("GRANT ALL PRIVILEGES ON *.* TO %s WITH GRANT OPTION", user)}
- }
-
- // If we're here, that mean we're denying at least one thing for this
- // user.
- var out []string
- for d, db := range p.Databases {
- // If every single privilege was granted across the database,
- // then combine that into a large GRANT.
- if tablesAllGranted(db.Tables) {
- out = append(out, fmt.Sprintf("GRANT ALL PRIVILEGES ON %s TO %s", d, user))
- continue
- }
-
- // We want to selectively allow things inside this database.
- // This combines all statements per table into single grants.
- for t, table := range db.Tables {
- var parts []string
- for s, statement := range table.Statements {
- var columns []string
- for c, deny := range statement.Columns {
- if !deny {
- columns = append(columns, string(c))
- }
- }
- if len(columns) == 0 {
- // Skip statements which have no
- // columns granted
- continue
- }
- if len(statement.Columns) == len(columns) && !statement.Deny {
- parts = append(parts, s)
- continue
- }
- sort.Strings(columns)
- tmp := strings.Join(columns, ", ")
- parts = append(parts, fmt.Sprintf("%s (%s)", s, tmp))
- }
- if len(parts) > 0 {
- sort.Strings(parts)
- tmp := strings.Join(parts, ", ")
- out = append(out, fmt.Sprintf("GRANT %s ON %s.%s TO %s", tmp, d, t, user))
- }
- }
- }
- return out
-}
-
// apply permissions for a given line.
func (p *Permissions) apply(vars map[string][]string, l *line) error {
deny := l.verb == "deny"
@@ 581,7 485,6 @@ func parseCollection(leader string, words []string) ([]string, []string, error)
}
return []string{words[i]}, remainder, nil
}
- return nil, nil, errors.New("unexpected end")
}
// parseVar into key and values.
@@ 613,9 516,7 @@ func substituteVars(ss []string, vars map[string][]string) []string {
out = append(out, s)
continue
}
- for _, v := range vars[strings.TrimPrefix(s, "$")] {
- out = append(out, v)
- }
+ out = append(out, vars[strings.TrimPrefix(s, "$")]...)
}
return out
}
M parser_test.go => parser_test.go +4 -145
@@ 5,7 5,6 @@ import (
"fmt"
"io/ioutil"
"path/filepath"
- "sort"
"strings"
"testing"
)
@@ 184,7 183,9 @@ func TestPermissionsApply(t *testing.T) {
}
allStatements := []string{"select", "insert"}
perms := permsForSchema(schema, allStatements)
- perms.apply(nil, tc.have)
+ if err := perms.apply(nil, tc.have); err != nil {
+ t.Fatal(err)
+ }
gotByt, err := json.Marshal(perms)
if err != nil {
t.Fatal(err)
@@ 298,114 299,6 @@ func TestPermsForLines(t *testing.T) {
}
}
-func TestGrants(t *testing.T) {
- tcs := []struct {
- have *Permissions
- want []string
- }{
- { // 0 - deny all
- have: &Permissions{
- Deny: true,
- Databases: map[string]*DBPermission{
- "db": &DBPermission{
- Deny: true,
- Tables: map[string]*TablePermission{
- "table": &TablePermission{
- Deny: true,
- Statements: map[string]*StatementPermission{
- "select": &StatementPermission{
- Deny: true,
- Columns: map[string]bool{
- "column": true,
- },
- },
- "insert": &StatementPermission{
- Deny: true,
- Columns: map[string]bool{
- "column": true,
- },
- },
- },
- },
- },
- },
- },
- },
- want: nil,
- },
- { // 1 - allow insert on db
- have: &Permissions{
- Databases: map[string]*DBPermission{
- "db": &DBPermission{
- Tables: map[string]*TablePermission{
- "table": &TablePermission{
- Statements: map[string]*StatementPermission{
- "select": &StatementPermission{
- Deny: true,
- Columns: map[string]bool{
- "column": true,
- },
- },
- "insert": &StatementPermission{
- Columns: map[string]bool{
- "column": false,
- },
- },
- },
- },
- },
- },
- },
- },
- want: []string{"GRANT insert ON db.table TO user@'%'"},
- },
- { // 2 - grant all
- have: &Permissions{
- Databases: map[string]*DBPermission{
- "db": &DBPermission{
- Tables: map[string]*TablePermission{
- "table": &TablePermission{
- Statements: map[string]*StatementPermission{
- "select": &StatementPermission{
- Columns: map[string]bool{
- "column": false,
- },
- },
- "insert": &StatementPermission{
- Columns: map[string]bool{
- "column": false,
- },
- },
- },
- },
- },
- },
- },
- },
- want: []string{"GRANT ALL PRIVILEGES ON *.* TO user@'%' WITH GRANT OPTION"},
- },
- }
- for i, tc := range tcs {
- tc := tc
- t.Run(fmt.Sprint(i), func(t *testing.T) {
- t.Parallel()
-
- got := tc.have.grants("user")
- gotByt, err := json.Marshal(got)
- if err != nil {
- t.Fatal(err)
- }
- wantByt, err := json.Marshal(tc.want)
- if err != nil {
- t.Fatal(err)
- }
- if string(gotByt) != string(wantByt) {
- t.Fatalf("got %s, want %s", gotByt, wantByt)
- }
- })
- }
-}
-
func TestParse(t *testing.T) {
t.Parallel()
@@ 442,43 335,9 @@ func TestParse(t *testing.T) {
"delete",
"alter",
}
- userPerms, err := compile(schema, allStatements, ast)
- if err != nil {
+ if _, err := compile(schema, allStatements, ast); err != nil {
t.Fatal(err)
}
- for user, perms := range userPerms {
- gotGrants := perms.grants(user)
- sort.Strings(gotGrants)
- got, _ := json.Marshal(gotGrants)
- var wantGrants []string
- switch user {
- case "bob", "jim":
- wantGrants = []string{
- "GRANT select ON dashboard.admins TO " + user + "@'%'",
- "GRANT select ON dashboard.users TO " + user + "@'%'",
- }
- case "alice":
- wantGrants = []string{
- "GRANT delete, insert, select, update ON dashboard.admins TO alice@'%'",
- "GRANT delete, insert, select, update ON dashboard.users TO alice@'%'",
- }
- case "sarah":
- wantGrants = []string{
- "GRANT ALL PRIVILEGES ON *.* TO sarah@'%' WITH GRANT OPTION",
- }
- case "root":
- wantGrants = []string{
- "GRANT ALL PRIVILEGES ON *.* TO root@'%' WITH GRANT OPTION",
- }
- default:
- continue
- }
- sort.Strings(wantGrants)
- want, _ := json.Marshal(wantGrants)
- if string(got) != string(want) {
- t.Fatalf("got %s, want %s", got, want)
- }
- }
}
func loadFixture(s string) string {
M sf.go => sf.go +0 -30
@@ 5,36 5,6 @@ import (
"io"
)
-// BuildGrants which can be applied in a database. Note that grant and revoke
-// statements take effect immediately, so you may lock yourself out by applying
-// this. Instead use this as a quick way to audit permissions generated by sf,
-// but use sf to apply those permissions directly to the database by editing
-// priv tables directly.
-func BuildGrants(db Store, r io.Reader) ([]string, error) {
- schema, err := db.GetSchema()
- if err != nil {
- return nil, fmt.Errorf("get schema: %w", err)
- }
- allStatements := db.Statements()
- ast, err := parse(r)
- if err != nil {
- return nil, fmt.Errorf("parse: %w", err)
- }
- userPerms, err := compile(schema, allStatements, ast)
- if err != nil {
- return nil, fmt.Errorf("compile: %w", err)
- }
- allGrants := make([]string, 0, len(schema.Users))
- for _, u := range schema.Users {
- g := fmt.Sprintf("REVOKE ALL PRIVILEGES, GRANT OPTION FROM %s", u)
- allGrants = append(allGrants, g)
- }
- for u, perm := range userPerms {
- allGrants = append(allGrants, perm.grants(u)...)
- }
- return allGrants, nil
-}
-
// BuildPermissions for each user.
func BuildPermissions(
db Store,