Skip to content

Commit

Permalink
policy: implement Store
Browse files Browse the repository at this point in the history
Implement the policy store that adds policy management semantics on top
KVStore. Policies added to the store are automatically versioned. Policy
version is now recorded along the Policy ID in the AttestationResult.

Signed-off-by: Sergei Trofimov <sergei.trofimov@arm.com>
  • Loading branch information
setrofim committed Sep 30, 2022
1 parent 3440167 commit ef0f50c
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 95 deletions.
7 changes: 5 additions & 2 deletions policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ package policy
// Policy allows enforcing additional constraints on top of the regular attestation schemes.
type Policy struct {
// ID is used to reference the policy in the result.
ID string
ID string `json:"id"`

// Version gets bumped every time a new policy with existing ID is added to the store.
Version int32 `json:"version"`

// Rules of the policy to be interpreted and execute by the policy agent.
Rules string
Rules string `json:"rules"`
}
116 changes: 116 additions & 0 deletions policy/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2022 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package policy

import (
"encoding/json"
"errors"
"fmt"

"github.com/spf13/viper"
"github.com/veraison/services/kvstore"
)

var ErrNoPolicy = errors.New("no policy found")

// NewStore returns a new policy store. Config options are the same as those
// used for kvstore.New().
func NewStore(v *viper.Viper) (*Store, error) {
kvStore, err := kvstore.New(v)
if err != nil {
return nil, err
}

return &Store{KVStore: kvStore}, nil
}

type Store struct {
KVStore kvstore.IKVStore
}

// Setup the underyling kvstore. This is a one-time setup that only needs to be
// performed once for a deployment.
func (o *Store) Setup() error {
return o.KVStore.Setup()
}

// Add a policy with the specified ID and rules. If a policy with that ID
// already exists, an error is returned.
func (o *Store) Add(id, rules string) error {
if _, err := o.Get(id); err == nil {
return fmt.Errorf("policy with id %q already exists", id)
}

return o.Update(id, rules)
}

// Update sets the provided rules as the latest version of the policy with the
// specified ID. If a policy with that ID does not exist, it is created.
func (o *Store) Update(id, rules string) error {
var oldVersion int32

oldPolicy, err := o.GetLatest(id)

if err == nil {
oldVersion = oldPolicy.Version
} else if errors.Is(err, ErrNoPolicy) {
oldVersion = 0
} else {
return err
}

newPolicy := Policy{ID: id, Rules: rules, Version: oldVersion + 1}

newPolicyBytes, err := json.Marshal(newPolicy)
if err != nil {
return err
}

return o.KVStore.Add(id, string(newPolicyBytes))
}

// Get returns the slice of all Policies associated with he specfied ID. Each
// Policy represents a different version of the same logical policy.
func (o *Store) Get(id string) ([]Policy, error) {
vals, err := o.KVStore.Get(id)
if err != nil {
if errors.Is(err, kvstore.ErrKeyNotFound) {
return nil, fmt.Errorf("%w: %q", ErrNoPolicy, id)
}
return nil, err
}

var policies []Policy

for _, v := range vals {
var p Policy
if err = json.Unmarshal([]byte(v), &p); err != nil {
return nil, err
}

policies = append(policies, p)
}

return policies, nil
}

// GetLatest returns the latest version of the policy with the specified ID. If
// no such policy exists, a wrapped ErrNoPolicy is returned.
func (o *Store) GetLatest(id string) (Policy, error) {
policies, err := o.Get(id)
if err != nil {
return Policy{}, err
}

return policies[len(policies)-1], nil
}

// Del removes all policy versisions associated with the specfied id.
func (o *Store) Del(id string) error {
return o.KVStore.Del(id)
}

// Close the connection to the underlying kvstore.
func (o *Store) Close() error {
return o.KVStore.Close()
}
51 changes: 51 additions & 0 deletions policy/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2022 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package policy

import (
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_Store_CRUD(t *testing.T) {
v := viper.New()
v.Set("backend", "memory")

store, err := NewStore(v)
require.NoError(t, err)
defer store.Close()

err = store.Add("p1", "1. the chief's always right; 2. if the chief's wrong, see 1.")
require.NoError(t, err)

policy, err := store.GetLatest("p1")
require.NoError(t, err)

assert.Equal(t, "p1", policy.ID)
assert.Equal(t, int32(1), policy.Version)

err = store.Add("p1", "On second thought, chief's not always right.")
assert.ErrorContains(t, err, "already exists")

err = store.Update("p1", "On second thought, chief's not always right.")
require.NoError(t, err)

policy, err = store.GetLatest("p1")
require.NoError(t, err)
assert.Equal(t, int32(2), policy.Version)
assert.Equal(t, "On second thought, chief's not always right.", policy.Rules)

versions, err := store.Get("p1")
require.NoError(t, err)
assert.Len(t, versions, 2)
assert.Equal(t, int32(2), versions[1].Version)

err = store.Del("p1")
require.NoError(t, err)

_, err = store.GetLatest("p1")
assert.ErrorIs(t, err, ErrNoPolicy)
}
67 changes: 39 additions & 28 deletions proto/result.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions proto/result.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ message AttestationResult {
google.protobuf.Timestamp timestamp = 4 [json_name = "timestamp"];
EndorsedClaims endorsed_claims = 5 [json_name = "endorsed-claims"];
string AppraisalPolicyID = 6 [json_name = "appraisal-policy-id"];
int32 AppraisalPolicyVersion = 7 [json_name = "appraisal-policy-version"];

// Extension
google.protobuf.Struct processed_evidence = 7 [json_name = "veraison-processed-evidence"];
google.protobuf.Struct verifier_added_claims = 8 [json_name = "veraison-verifier-added-claims"];
google.protobuf.Struct processed_evidence = 8 [json_name = "veraison-processed-evidence"];
google.protobuf.Struct verifier_added_claims = 9 [json_name = "veraison-verifier-added-claims"];
}

// vim: set et sts=4 sw=4 ai:
3 changes: 2 additions & 1 deletion vts/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/spf13/viper"

"github.com/veraison/services/kvstore"
"github.com/veraison/services/policy"
"github.com/veraison/services/vts/pluginmanager"
"github.com/veraison/services/vts/policymanager"
"github.com/veraison/services/vts/trustedservices"
Expand Down Expand Up @@ -43,7 +44,7 @@ func main() {
log.Fatalf("endorsement store initialization failed: %v", err)
}

poStore, err := kvstore.New(v.Sub("po-store"))
poStore, err := policy.NewStore(v.Sub("po-store"))
if err != nil {
log.Fatalf("policy store initialization failed: %v", err)
}
Expand Down
31 changes: 7 additions & 24 deletions vts/policymanager/policymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,16 @@ import (
"fmt"

"github.com/spf13/viper"
"github.com/veraison/services/kvstore"
"github.com/veraison/services/policy"
"github.com/veraison/services/proto"
)

var ErrNoPolicy = errors.New("no policy found")

type PolicyManager struct {
Store kvstore.IKVStore
Store *policy.Store
Agent policy.IAgent
}

func New(v *viper.Viper, store kvstore.IKVStore) (*PolicyManager, error) {
func New(v *viper.Viper, store *policy.Store) (*PolicyManager, error) {
agent, err := policy.CreateAgent(v)
if err != nil {
return nil, err
Expand All @@ -38,16 +35,16 @@ func (o *PolicyManager) Evaluate(
) error {
evidence := ac.Evidence

policy, err := o.getPolicy(evidence)
pol, err := o.getPolicy(evidence)
if err != nil {
if errors.Is(err, ErrNoPolicy) {
if errors.Is(err, policy.ErrNoPolicy) {
return nil // No policy? No problem!
}

return err
}

updatedResult, err := o.Agent.Evaluate(ctx, policy, ac.Result, evidence, endorsements)
updatedResult, err := o.Agent.Evaluate(ctx, pol, ac.Result, evidence, endorsements)
if err != nil {
return err
}
Expand All @@ -58,30 +55,16 @@ func (o *PolicyManager) Evaluate(
}

func (o *PolicyManager) getPolicy(ev *proto.EvidenceContext) (*policy.Policy, error) {

policyID := fmt.Sprintf("%s://%s/%s",
o.Agent.GetBackendName(),
ev.TenantId,
ev.Format.String(),
)

vals, err := o.Store.Get(policyID)
p, err := o.Store.GetLatest(policyID)
if err != nil {
if errors.Is(err, kvstore.ErrKeyNotFound) {
return nil, fmt.Errorf("%w: %q", ErrNoPolicy, policyID)
}
return nil, err
}

// TODO(setrofim): for now, assuming that there should be exactly one
// matching policy. Once we have a more sophisticated policy management
// framework worked out, we might allow multiple policies here.
if len(vals) == 0 {
return nil, fmt.Errorf("%w: %q", ErrNoPolicy, policyID)
} else if len(vals) > 1 {
return nil, fmt.Errorf("found %d policy entries for id %q; must be at most 1",
len(vals), policyID)
}

return &policy.Policy{ID: policyID, Rules: vals[0]}, nil
return &p, nil
}
Loading

0 comments on commit ef0f50c

Please sign in to comment.