diff --git a/context.go b/context.go index 0589a73..37b820e 100644 --- a/context.go +++ b/context.go @@ -188,6 +188,14 @@ func NewContext(db * badger.DB, log Logger) * Context { if err != nil { panic(err) } + err = ctx.RegisterNodeType(NewNodeDef((*PerNodePolicy)(nil), LoadPerNodePolicy, GQLTypeGraphNode())) + if err != nil { + panic(err) + } + err = ctx.RegisterNodeType(NewNodeDef((*AllNodePolicy)(nil), LoadAllNodePolicy, GQLTypeGraphNode())) + if err != nil { + panic(err) + } ctx.AddGQLType(GQLTypeSignal()) diff --git a/gql_test.go b/gql_test.go index 95a0078..cfbdb30 100644 --- a/gql_test.go +++ b/gql_test.go @@ -70,15 +70,22 @@ func TestGQLDBLoad(t * testing.T) { u1_r := NewUser("Test User", time.Now(), &u1_key.PublicKey, u1_shared) u1 := &u1_r + p1_r := NewAllNodePolicy(RandID(), []string{"*"}) + p1 := &p1_r + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) fatalErr(t, err) gql_r := NewGQLThread(RandID(), "GQL Thread", "init", ":0", ecdh.P256(), key) gql := &gql_r info := NewParentThreadInfo(true, "start", "restore") - err = UpdateStates(ctx, []Node{gql, t1, l1, u1}, func(nodes NodeMap) error { + err = UpdateStates(ctx, []Node{gql, t1, l1, u1, p1}, func(nodes NodeMap) error { + err := u1.AddPolicy(p1) + if err != nil { + return err + } gql.Users[KeyID(&u1_key.PublicKey)] = u1 - err := LinkLockables(ctx, gql, []Lockable{l1}, nodes) + err = LinkLockables(ctx, gql, []Lockable{l1}, nodes) if err != nil { return err } diff --git a/lockable.go b/lockable.go index 642aa6e..b66e09d 100644 --- a/lockable.go +++ b/lockable.go @@ -60,6 +60,7 @@ func (state * SimpleLockable) Type() NodeType { } type SimpleLockableJSON struct { + GraphNodeJSON Name string `json:"name"` Owner *NodeID `json:"owner"` Dependencies []NodeID `json:"dependencies"` @@ -98,7 +99,11 @@ func NewSimpleLockableJSON(lockable *SimpleLockable) SimpleLockableJSON { locks_held[lockable_id.String()] = &str } } + + node_json := NewGraphNodeJSON(&lockable.GraphNode) + return SimpleLockableJSON{ + GraphNodeJSON: node_json, Name: lockable.name, Owner: owner_id, Dependencies: dependency_ids, @@ -560,7 +565,7 @@ func RestoreSimpleLockable(ctx * Context, lockable Lockable, j SimpleLockableJSO } o_l, ok := o.(Lockable) if ok == false { - return fmt.Errorf("%s is not a Lockable", o.ID()) + return fmt.Errorf("%s is not a Lockable", *j.Owner) } lockable.SetOwner(o_l) } @@ -615,5 +620,5 @@ func RestoreSimpleLockable(ctx * Context, lockable Lockable, j SimpleLockableJSO lockable.RecordLock(l_l, h_l) } - return nil + return RestoreGraphNode(ctx, lockable, j.GraphNodeJSON, nodes) } diff --git a/node.go b/node.go index 67656e1..36cb837 100644 --- a/node.go +++ b/node.go @@ -6,6 +6,7 @@ import ( badger "github.com/dgraph-io/badger/v3" "fmt" "encoding/binary" + "encoding/json" "crypto/sha512" "crypto/ecdsa" "crypto/elliptic" @@ -56,13 +57,22 @@ func RandID() NodeID { // A Node represents data that can be read by multiple goroutines and written to by one, with a unique ID attached, and a method to process updates(including propagating them to connected nodes) // RegisterChannel and UnregisterChannel are used to connect arbitrary listeners to the node type Node interface { + // State Locking interface sync.Locker RLock() RUnlock() + // Serialize the Node for the database Serialize() ([]byte, error) + + // Nodes have an ID, type, and ACL policies ID() NodeID Type() NodeType + + Allowed(action string, principal NodeID) bool + AddPolicy(Policy) error + RemovePolicy(Policy) error + // Send a GraphSignal to the node, requires that the node is locked for read so that it can propagate Signal(ctx *Context, signal GraphSignal, nodes NodeMap) error // Register a channel to receive updates sent to the node @@ -78,11 +88,81 @@ type GraphNode struct { id NodeID listeners map[NodeID]chan GraphSignal + policies map[NodeID]Policy +} + +type GraphNodeJSON struct { + Policies []NodeID `json:"policies"` } -// GraphNode doesn't serialize any additional information by default func (node * GraphNode) Serialize() ([]byte, error) { - return nil, nil + node_json := NewGraphNodeJSON(node) + return json.MarshalIndent(&node_json, "", " ") +} + +func (node *GraphNode) Allowed(action string, principal NodeID) bool { + for _, policy := range(node.policies) { + if policy.Allows(action, principal) == true { + return true + } + } + return false +} + +func (node *GraphNode) AddPolicy(policy Policy) error { + if policy == nil { + return fmt.Errorf("Cannot add nil as a policy") + } + + _, exists := node.policies[policy.ID()] + if exists == true { + return fmt.Errorf("%s is already a policy for %s", policy.ID().String(), node.ID().String()) + } + + node.policies[policy.ID()] = policy + return nil +} + +func (node *GraphNode) RemovePolicy(policy Policy) error { + if policy == nil { + return fmt.Errorf("Cannot add nil as a policy") + } + + _, exists := node.policies[policy.ID()] + if exists == false { + return fmt.Errorf("%s is not a policy for %s", policy.ID().String(), node.ID().String()) + } + + delete(node.policies, policy.ID()) + return nil +} + +func NewGraphNodeJSON(node *GraphNode) GraphNodeJSON { + policies := make([]NodeID, len(node.policies)) + i := 0 + for _, policy := range(node.policies) { + policies[i] = policy.ID() + i += 1 + } + return GraphNodeJSON{ + Policies: policies, + } +} + +func RestoreGraphNode(ctx *Context, node Node, j GraphNodeJSON, nodes NodeMap) error { + for _, policy_id := range(j.Policies) { + policy_ptr, err := LoadNodeRecurse(ctx, policy_id, nodes) + if err != nil { + return err + } + + policy, ok := policy_ptr.(Policy) + if ok == false { + return fmt.Errorf("%s is not a Policy", policy_id) + } + node.AddPolicy(policy) + } + return nil } func LoadGraphNode(ctx * Context, id NodeID, data []byte, nodes NodeMap)(Node, error) { @@ -153,6 +233,7 @@ func NewGraphNode(id NodeID) GraphNode { return GraphNode{ id: id, listeners: map[NodeID]chan GraphSignal{}, + policies: map[NodeID]Policy{}, } } diff --git a/policy.go b/policy.go new file mode 100644 index 0000000..a7d0a04 --- /dev/null +++ b/policy.go @@ -0,0 +1,152 @@ +package graphvent + +import ( + "encoding/json" +) + +// A policy represents a set of rules attached to a Node that allow it to preform actions +type Policy interface { + Node + // Returns true if the policy allows the action on the given principal + Allows(action string, principal NodeID) bool +} + +type PerNodePolicy struct { + GraphNode + AllowedActions map[NodeID][]string +} + +type PerNodePolicyJSON struct { + GraphNodeJSON + AllowedActions map[string][]string `json:"allowed_actions"` +} + +func (policy *PerNodePolicy) Type() NodeType { + return NodeType("per_node_policy") +} + +func (policy *PerNodePolicy) Serialize() ([]byte, error) { + allowed_actions := map[string][]string{} + for principal, actions := range(policy.AllowedActions) { + allowed_actions[principal.String()] = actions + } + + return json.MarshalIndent(&PerNodePolicyJSON{ + GraphNodeJSON: NewGraphNodeJSON(&policy.GraphNode), + AllowedActions: allowed_actions, + }, "", " ") +} + +func NewPerNodePolicy(id NodeID, allowed_actions map[NodeID][]string) PerNodePolicy { + if allowed_actions == nil { + allowed_actions = map[NodeID][]string{} + } + + return PerNodePolicy{ + GraphNode: NewGraphNode(id), + AllowedActions: allowed_actions, + } +} + +func LoadPerNodePolicy(ctx *Context, id NodeID, data []byte, nodes NodeMap) (Node, error) { + var j PerNodePolicyJSON + err := json.Unmarshal(data, &j) + if err != nil { + return nil, err + } + + allowed_actions := map[NodeID][]string{} + for principal_str, actions := range(j.AllowedActions) { + principal_id, err := ParseID(principal_str) + if err != nil { + return nil, err + } + + allowed_actions[principal_id] = actions + } + + policy := NewPerNodePolicy(id, allowed_actions) + nodes[id] = &policy + + err = RestoreGraphNode(ctx, &policy.GraphNode, j.GraphNodeJSON, nodes) + if err != nil { + return nil, err + } + + return &policy, nil +} + +func (policy *PerNodePolicy) Allows(action string, principal NodeID) bool { + actions, exists := policy.AllowedActions[principal] + if exists == false { + return false + } + + for _, a := range(actions) { + if a == action { + return true + } + } + + return false +} + +type AllNodePolicy struct { + GraphNode + AllowedActions []string +} + +type AllNodePolicyJSON struct { + GraphNodeJSON + AllowedActions []string `json:"allowed_actions"` +} + +func (policy *AllNodePolicy) Type() NodeType { + return NodeType("all_node_policy") +} + +func (policy *AllNodePolicy) Serialize() ([]byte, error) { + return json.MarshalIndent(&AllNodePolicyJSON{ + GraphNodeJSON: NewGraphNodeJSON(&policy.GraphNode), + AllowedActions: policy.AllowedActions, + }, "", " ") +} + +func NewAllNodePolicy(id NodeID, allowed_actions []string) AllNodePolicy { + if allowed_actions == nil { + allowed_actions = []string{} + } + + return AllNodePolicy{ + GraphNode: NewGraphNode(id), + AllowedActions: allowed_actions, + } +} + +func LoadAllNodePolicy(ctx *Context, id NodeID, data []byte, nodes NodeMap) (Node, error) { + var j AllNodePolicyJSON + err := json.Unmarshal(data, &j) + if err != nil { + return nil, err + } + + policy := NewAllNodePolicy(id, j.AllowedActions) + nodes[id] = &policy + + err = RestoreGraphNode(ctx, &policy.GraphNode, j.GraphNodeJSON, nodes) + if err != nil { + return nil, err + } + + return &policy, nil +} + +func (policy *AllNodePolicy) Allows(action string, principal NodeID) bool { + for _, a := range(policy.AllowedActions) { + if a == action { + return true + } + } + return false +} +