~egtann/sum

b7a0e8daaa648dec6d63eb586e5658f55bd7666d — Evan Tann 4 months ago 7215725
remove grant code, fix lint issues
4 files changed, 13 insertions(+), 287 deletions(-)

M cmd/sf/main.go
M parser.go
M parser_test.go
M sf.go
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,