blob: 4e23a2141a0cc18f5f43900fdcc7f743914c50c4 [file] [log] [blame]
package state
import (
"fmt"
"veyron/services/store/memstore/acl"
"veyron/services/store/memstore/pathregex"
"veyron/services/store/memstore/refs"
"veyron2/security"
"veyron2/storage"
)
// Path matching is used to test whether a value has a pathname that matches a
// regular expression. The snapshot is a labeled directed graph that can be
// viewed as a finite automaton, and the pathregex is a finite automaton, so the
// matching problem asks whether the intersection of the regular languages
// defined by these automata is nonempty.
//
// We implement it in two phases.
//
// The first phase is an optimization, where we compute the intersection
// automaton from the reversed automata. This is typically more efficient
// because the subgraph formed from the incoming edges is usually much smaller
// than the entire graph. Note that the snapshot is deterministic when viewed
// as a forward automaton, since every field of a value has a unique field name,
// but the reversed automaton is nondeterministic. However, that doesn't have
// any bearing on the algorithm here.
//
// We keep a queue of work to do, and a visited set. The queue contains a set
// of cells that are scheduled to be visited, along with the StateSet of the
// pathregex when visiting. The visited table contains the set of cells that
// have already been visited, along with the StateSet when they were last
// visited. We build a reducedGraph that contains only the edges in the
// intersection.
//
// On each step, the main search loop pulls an element from the queue. If the
// cell has already been visited with a StateSet that is just as large as the
// one in the queue, quit. Otherwise, compute the transitions for all the
// inRefs and add the new entries to the queue. The search teriminates when the
// root is reached in a final state, or else the queue becomes empty and there
// is no match.
//
// Security properties are inherited, meaning that the properties are propagated
// down from the root along paths. That means access control can't be performed
// during the first phase.
//
// During the second phase, we recompute the intersection from the forward
// automata, this time taking security into account.
// PathRegex is the result of compiling the path regular expression to forward
// and reverse NFAs.
type PathRegex struct {
forwardSS forwardStateSet
reverseSS reverseStateSet
}
// forwardStateSet and reverseStateSet wrap the pathregex.StateSet to avoid
// confusion about which automata they belong to.
type forwardStateSet struct {
pathregex.StateSet
}
type reverseStateSet struct {
pathregex.StateSet
}
// reducer is used to compute the reducedGraph.
type reducer struct {
snapshot *snapshot
}
// reducedGraph is the forward reference graph, pruned to include only the
// references in the intersection automaton. Does not prune edges inaccessible
// due to security restrictions.
type reducedGraph map[storage.ID]*reducedCell
type reducedCell struct {
tags storage.TagList
refs refs.Set
}
type reduceVisitedSet map[storage.ID]reverseStateSet
type reduceQueue map[storage.ID]reverseStateSet
// matcher is used to perform a forward match on the reducedGraph.
type matcher struct {
graph reducedGraph
targetID storage.ID
}
// matchVisitedSet contains the checker/state-set encountered when visiting a
// node. Since an entry in the store might have multiple name, it might be
// visited multiple times with different checkers. The matchVisitedSet contains
// a list of checkers and state sets visited with each checker.
type matchVisitedSet map[storage.ID]*matchVisitedEntry
type matchVisitedEntry struct {
checkers []*matchVisitedCheckerSet
}
type matchVisitedCheckerSet struct {
checker *acl.Checker
ss forwardStateSet
}
// matchQueue contains the worklist for the matcher.
type matchQueue struct {
queue []*matchQueueEntry
}
type matchQueueEntry struct {
id storage.ID
checker *acl.Checker
ss forwardStateSet
}
// CompilePathRegex compiles the regular expression to a PathRegex.
func CompilePathRegex(regex string) (*PathRegex, error) {
forwardSS, err := pathregex.Compile(regex)
if err != nil {
return nil, err
}
reverseSS, err := pathregex.CompileReverse(regex)
if err != nil {
return nil, err
}
p := &PathRegex{
forwardSS: forwardStateSet{StateSet: forwardSS},
reverseSS: reverseStateSet{StateSet: reverseSS},
}
return p, nil
}
// PathMatch returns true iff there is a name for the store value that matches
// the pathRegex.
func (sn *snapshot) PathMatch(pid security.PublicID, id storage.ID, regex *PathRegex) bool {
r := &reducer{snapshot: sn}
g := r.reduce(id, regex.reverseSS)
m := &matcher{graph: g, targetID: id}
checker := sn.newPermChecker(pid)
return m.match(checker, sn.rootID, regex.forwardSS)
}
// reduce computes the reducedGraph (the intersection) without regard to
// security.
func (r *reducer) reduce(id storage.ID, rss reverseStateSet) reducedGraph {
visited := reduceVisitedSet{}
graph := reducedGraph{}
graph.add(id)
queue := reduceQueue{}
queue.add(id, rss)
for len(queue) != 0 {
for id, ssUpdate := range queue {
// Take an element from the queue.
delete(queue, id)
// Check whether it has already been visited.
ss, changed := visited.add(id, ssUpdate)
if !changed {
// We already visited this node.
continue
}
// Enqueue new entries for each of the inRefs.
c := r.find(id)
c.inRefs.Iter(func(it interface{}) bool {
ref := it.(*refs.Ref)
if ssNew := ss.step(ref.Path); !ssNew.IsReject() {
graph.addRef(ref, id)
queue.add(ref.ID, ssNew)
}
return true
})
}
}
graph.setTags(r.snapshot)
return graph
}
// find returns the cell for the id. Panics if the value doesn't exist.
func (r *reducer) find(id storage.ID) *Cell {
c := r.snapshot.Find(id)
if c == nil {
panic(fmt.Sprintf("dangling reference: %s", id))
}
return c
}
// add updates the visited state to include ssUpdate. Returns true iff the
// state set changed.
func (visited reduceVisitedSet) add(id storage.ID, ssUpdate reverseStateSet) (reverseStateSet, bool) {
ss, ok := visited[id]
if !ok {
visited[id] = ssUpdate
return ssUpdate, true
}
ssNew := reverseStateSet{StateSet: ss.Union(ssUpdate.StateSet)}
if ssNew.Equals(ss.StateSet) {
// Nothing changed.
return ss, false
}
visited[id] = ssNew
return ssNew, true
}
// add adds a new entry to the queue.
func (queue reduceQueue) add(id storage.ID, ss reverseStateSet) {
if ssCurrent, ok := queue[id]; ok {
ss = reverseStateSet{StateSet: ss.Union(ssCurrent.StateSet)}
}
queue[id] = ss
}
// add adds a reference if it doesn't already exist.
func (g reducedGraph) add(id storage.ID) *reducedCell {
v, ok := g[id]
if !ok {
// Ignore the tags, they will be filled in later in setTags.
v = &reducedCell{refs: refs.Empty}
g[id] = v
}
return v
}
// addRef adds a forward edge (r.ID -> id) to the reduced graph.
func (g reducedGraph) addRef(ref *refs.Ref, id storage.ID) {
v := g.add(ref.ID)
v.refs = v.refs.Put(&refs.Ref{ID: id, Path: ref.Path, Label: ref.Label})
}
// setTags sets the tags values in the graph.
func (g reducedGraph) setTags(sn Snapshot) {
for id, v := range g {
c := sn.Find(id)
if c == nil {
panic(fmt.Sprintf("dangling reference: %s", id))
}
v.tags = c.Tags
}
}
// match performs a secure forward match.
func (m *matcher) match(checker *acl.Checker, rootID storage.ID, rss forwardStateSet) bool {
if !m.contains(rootID) {
return false
}
visited := matchVisitedSet{}
queue := matchQueue{}
queue.add(rootID, checker, rss)
for !queue.isEmpty() {
// Take an entry from the queue.
qentry := queue.pop()
// Check whether it has already been visited.
ss, changed := visited.add(qentry)
if !changed {
// We already visited this node.
continue
}
// If we reached the target, we're done.
if qentry.id == m.targetID && ss.IsFinal() {
return true
}
// Enqueue new entries for each of the refs.
src := m.find(qentry.id)
src.refs.Iter(func(it interface{}) bool {
ref := it.(*refs.Ref)
if qentry.checker.IsAllowed(ref.Label) {
if ssNew := ss.step(ref.Path); !ssNew.IsReject() {
dst := m.find(ref.ID)
checker := qentry.checker.Copy()
checker.Update(dst.tags)
if checker.IsAllowed(security.ReadLabel) {
queue.add(ref.ID, checker, ssNew)
}
}
}
return true
})
}
return false
}
// contains returns true iff the reduced graph contains the ID.
func (m *matcher) contains(id storage.ID) bool {
_, ok := m.graph[id]
return ok
}
// find returns the entry in the matchQueue. Panics if the entry doesn't exist.
func (m *matcher) find(id storage.ID) *reducedCell {
v, ok := m.graph[id]
if !ok {
panic(fmt.Sprintf("dangling reference: %s", id))
}
return v
}
// add updates the visited state to include ssUpdate. Returns true iff the
// state set changed.
func (visited matchVisitedSet) add(qentry *matchQueueEntry) (forwardStateSet, bool) {
e, ok := visited[qentry.id]
if !ok {
c := &matchVisitedCheckerSet{checker: qentry.checker, ss: qentry.ss}
e = &matchVisitedEntry{checkers: []*matchVisitedCheckerSet{c}}
visited[qentry.id] = e
return c.ss, true
}
// Add the checker if it is different from the others.
c := e.addCheckerSet(qentry.checker)
// Update the state set.
ssNew := c.ss.Union(qentry.ss.StateSet)
if !ssNew.Equals(c.ss.StateSet) {
c.ss = forwardStateSet{StateSet: ssNew}
return c.ss, true
}
return c.ss, false
}
// addChecker add the checker if it is different from the others. Returns true iff
// the set of checkers changed.
func (e *matchVisitedEntry) addCheckerSet(checker *acl.Checker) *matchVisitedCheckerSet {
for _, c := range e.checkers {
if checker.IsEqual(c.checker) {
return c
}
}
c := &matchVisitedCheckerSet{checker: checker}
e.checkers = append(e.checkers, c)
return c
}
// add adds a new entry to the queue.
func (queue *matchQueue) add(id storage.ID, checker *acl.Checker, ss forwardStateSet) {
queue.queue = append(queue.queue, &matchQueueEntry{
id: id,
checker: checker,
ss: ss,
})
}
// isEmpty returns true iff the queue is empty.
func (queue *matchQueue) isEmpty() bool {
return len(queue.queue) == 0
}
// pop removes an entry from the queue.
func (queue *matchQueue) pop() *matchQueueEntry {
i := len(queue.queue)
qentry := queue.queue[i-1]
queue.queue = queue.queue[:i-1]
return qentry
}
// step advances the automaton for all components of the path.
func (ss reverseStateSet) step(p *refs.Path) reverseStateSet {
for p != nil {
var s string
p, s = p.Split()
ss = reverseStateSet{StateSet: ss.Step(s)}
}
return ss
}
// step advances the automaton for all components of the path.
func (ss forwardStateSet) step(p *refs.Path) forwardStateSet {
if p == nil {
return ss
}
p, s := p.Split()
return forwardStateSet{StateSet: ss.step(p).Step(s)}
}