~egtann/sum

ce3ca90f2f89491cf342a1b14bd003ebac1fa278 — Evan Tann 4 months ago 69af265
add any keyword, validate line exists in schema
3 files changed, 74 insertions(+), 9 deletions(-)

M README.md
M parser.go
M testdata/complex
M README.md => README.md +5 -7
@@ 57,14 57,14 @@ deny all
# allow root to do anything
allow root

# grant access to specific statements across all tables
allow $write db dashboard statement { select insert update delete }
# grant access to specific statements across any table
allow $write db dashboard table any statement { select insert update delete }

# allow all select statements, except for admins.password and everything in
# admin recovery tables
allow $read db dashboard statement select
deny  $read db dashboard statement select table admins column password_hash
deny  $read db dashboard statement select table admin_recovery_codes
allow $read db dashboard table any statement select
deny  $read db dashboard table admins statement select column password_hash
deny  $read db dashboard table admin_recovery_codes statement select
```

This will be outputted to stdout using `-d` for a dry run:


@@ 111,7 111,5 @@ every database migration.

## TODO

* validate that every defined user, db, table, statement, column at least
  exists in the db, since otherwise the failure is silent
* quoted values, e.g. multi-word statement "alter routine"
* combine users into the `permissions` struct to simplify code?

M parser.go => parser.go +68 -1
@@ 205,6 205,24 @@ func (p *permissions) apply(vars map[string][]string, l *line) error {
	l.tables = substituteVars(l.tables, vars)
	l.columns = substituteVars(l.columns, vars)

	// Record all seen elements of the line. Any that haven't been seen by
	// the time we're done applying it to our permissions are because those
	// items don't exist in the schema. We want to fail loudly with a
	// descriptive error, not silently ignoring it. If a database doesn't
	// actually exist and we silently allow it, we revoke a user's
	// permissions and not re-grant them.
	seen := struct {
		databases  map[string]struct{}
		statements map[string]struct{}
		tables     map[string]struct{}
		columns    map[string]struct{}
	}{
		databases:  map[string]struct{}{},
		statements: map[string]struct{}{},
		tables:     map[string]struct{}{},
		columns:    map[string]struct{}{},
	}

	// Iterate through all maps and set the permission toggles accordingly.
	// Track the number of toggles to toggle the parent-level Deny flag if
	// all children are set.


@@ 233,25 251,74 @@ func (p *permissions) apply(vars map[string][]string, l *line) error {
						continue
					}
					p.Databases[d].Tables[t].Statements[s].Columns[c] = deny
					seen.databases[d] = struct{}{}
					seen.tables[t] = struct{}{}
					seen.statements[s] = struct{}{}
					seen.columns[c] = struct{}{}
					k++
				}
				if k == len(statement.Columns) {
					p.Databases[d].Tables[t].Statements[s].Deny = deny
					seen.databases[d] = struct{}{}
					seen.tables[t] = struct{}{}
					seen.statements[s] = struct{}{}
					j++
				}
			}
			if j == len(table.Statements) {
				p.Databases[d].Tables[t].Deny = deny
				seen.databases[d] = struct{}{}
				seen.tables[t] = struct{}{}
				i++
			}
		}
		if i == len(db.Tables) {
			p.Databases[d].Deny = deny
			seen.databases[d] = struct{}{}
			a++
		}
	}
	if a == len(p.Databases) {
		p.Deny = deny

		// Return early, since we must have seen everything defined in
		// the line, since it applied to everything.
		return nil
	}

	// Confirm that we saw every item in the line in our schema. Work from
	// most-specific to least-specific to produce better errors.
	for _, c := range l.columns {
		if c == "any" {
			break
		}
		if _, ok := seen.columns[c]; !ok {
			return fmt.Errorf("unknown column: %s", c)
		}
	}
	for _, s := range l.statements {
		if s == "any" {
			break
		}
		if _, ok := seen.statements[s]; !ok {
			return fmt.Errorf("unknown statement: %s", s)
		}
	}
	for _, t := range l.tables {
		if t == "any" {
			break
		}
		if _, ok := seen.tables[t]; !ok {
			return fmt.Errorf("unknown table: %s", t)
		}
	}
	for _, d := range l.databases {
		if d == "any" {
			break
		}
		if _, ok := seen.databases[d]; !ok {
			return fmt.Errorf("unknown database: %s", d)
		}
	}
	return nil
}


@@ 496,7 563,7 @@ func substituteVars(ss []string, vars map[string][]string) []string {
func in(ss []string, s string) bool {
	for _, x := range ss {
		switch x {
		case "all", s:
		case "any", s:
			return true
		}
	}

M testdata/complex => testdata/complex +1 -1
@@ 8,5 8,5 @@ deny all
allow root

allow $read  db dashboard table { admins users } statement select
allow $write db dashboard table all statement $crud
allow $write db dashboard table any statement $crud
allow $admin db { dashboard nogrant }