From 9e10f3db795177b63e37266a2ffbb0c8fa26b6ca Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Fri, 26 Jan 2024 19:47:03 -0500 Subject: [PATCH] PR(WIP): Store Policy Key and Register ACP Object --- acp/acp.go | 10 ++--- acp/acp_local.go | 13 +++--- client/collection.go | 20 +++++++++ core/key.go | 57 ++++++++++++++++++++++- db/collection.go | 87 ++++++++++++++++++++++++++++-------- db/collection_policy.go | 59 ++++++++++++++++++++++++ db/description/collection.go | 15 +++++++ db/errors.go | 2 + db/schema.go | 1 - tests/integration/acp.go | 2 +- 10 files changed, 234 insertions(+), 32 deletions(-) create mode 100644 db/collection_policy.go diff --git a/acp/acp.go b/acp/acp.go index 784a2bb13c..e819406ba5 100644 --- a/acp/acp.go +++ b/acp/acp.go @@ -42,7 +42,7 @@ type ACPModule interface { // AddPolicy attempts to add/load/create the given policy, if isYAML is false, assumes JSON format, // upon success a policyID is returned, otherwise returns error. - AddPolicy(ctx context.Context, policy string, creator string, isYAML bool) (string, error) + AddPolicy(ctx context.Context, creator string, policy string, isYAML bool) (string, error) // ValidatePolicyAndResourceExist returns an error if the policyID does not exist on the // acp module, or if the resource name is not a valid resource on the target policy, @@ -55,16 +55,16 @@ type ACPModule interface { // Note: // - This should be used upon document creation only. // - Some documents might be created without an identity signature so they would have public access. + // - creator here is the actorID, which will be the signature identity if it exists. // - resource here is the resource object name (likely collection name). // - docID here is the object identifier. - // - creator here is the actorID, which will be the signature identity if it exists. - RegisterDocCreation(ctx context.Context, policyID, creator, docID, resource string) error + RegisterDocCreation(ctx context.Context, creator, policyID, resource, docID string) error // CheckDocAccess returns true if request has access to the document, otherwise returns false or an error. // // Note: + // - permission here is the type of permission we are checking for ("read" or "write"). // - resource here is the resource object name (likely collection name). // - docID here is the object identifier. - // - permission here is the type of permission we are checking for ("read" or "write"). - CheckDocAccess(ctx context.Context, policyID, actorID, docID, resource, permission string) (bool, error) + CheckDocAccess(ctx context.Context, actorID, permission, policyID, resource, docID string) (bool, error) } diff --git a/acp/acp_local.go b/acp/acp_local.go index a70ee7a10a..ea1d655f20 100644 --- a/acp/acp_local.go +++ b/acp/acp_local.go @@ -79,8 +79,8 @@ func (l *ACPLocalEmbedded) Close() error { func (l *ACPLocalEmbedded) AddPolicy( ctx context.Context, - policy string, creator string, + policy string, isYAML bool, ) (string, error) { var createPolicy types.MsgCreatePolicy @@ -157,8 +157,8 @@ func (l *ACPLocalEmbedded) ValidatePolicyAndResourceExist( func (l *ACPLocalEmbedded) RegisterDocCreation( ctx context.Context, - policyID string, creator string, + policyID string, resource string, docID string, ) error { @@ -170,9 +170,10 @@ func (l *ACPLocalEmbedded) RegisterDocCreation( } registerDocResponse, err := l.localModule.GetMsgService().RegisterObject( - ctx, + l.localModule.GetCtx(), ®isterDoc, ) + if err != nil { log.FatalE( ctx, @@ -226,11 +227,11 @@ func (l *ACPLocalEmbedded) RegisterDocCreation( func (l *ACPLocalEmbedded) CheckDocAccess( ctx context.Context, - policyID string, actorID string, + permission string, + policyID string, resource string, docID string, - permission string, ) (bool, error) { checkDoc := types.QueryVerifyAccessRequestRequest{ PolicyId: policyID, @@ -248,7 +249,7 @@ func (l *ACPLocalEmbedded) CheckDocAccess( } checkDocResponse, err := l.localModule.GetQueryService().VerifyAccessRequest( - ctx, + l.localModule.GetCtx(), &checkDoc, ) if err != nil { diff --git a/client/collection.go b/client/collection.go index 9ce1e135d6..4c65521bc7 100644 --- a/client/collection.go +++ b/client/collection.go @@ -165,6 +165,12 @@ type Collection interface { // GetAllDocIDs returns all the document IDs that exist in the collection. GetAllDocIDs(ctx context.Context) (<-chan DocIDResult, error) + // AddPolicy adds a policy on the collection. + // `PolicyDescription` contains the description of the policy that is added. + // `PolicyDescription.ID` is a policyID that must be a valid policy on the acp module. + // `PolicyDescription.ResourceName` is a valid resource on the target (policyID) policy. + // AddPolicy(context.Context, PolicyDescription) (PolicyDescription, error) + // CreateIndex creates a new index on the collection. // `IndexDescription` contains the description of the index to be created. // `IndexDescription.Name` must start with a letter or an underscore and can @@ -179,6 +185,20 @@ type Collection interface { GetIndexes(ctx context.Context) ([]IndexDescription, error) } +// IsPermissioned returns true if the collection has a policy, otherwise returns false. +// +// This tells us if access control is enabled for this collection or not. +func IsPermissioned(c Collection) (string, string, bool) { + policy := c.Definition().Description.Policy + if policy.HasValue() && + policy.Value().ID != "" && + policy.Value().ResourceName != "" { + return policy.Value().ID, policy.Value().ResourceName, true + } + + return "", "", false +} + // DocIDResult wraps the result of an attempt at a DocID retrieval operation. type DocIDResult struct { // If a DocID was successfully retrieved, this will be that DocID. diff --git a/core/key.go b/core/key.go index cb67cc45d6..1ee320e182 100644 --- a/core/key.go +++ b/core/key.go @@ -47,6 +47,7 @@ const ( COLLECTION_NAME = "/collection/name" COLLECTION_SCHEMA_VERSION = "/collection/version" COLLECTION_INDEX = "/collection/index" + COLLECTION_POLICY = "/collection/policy" SCHEMA_MIGRATION = "/schema/migration" SCHEMA_VERSION = "/schema/version/v" SCHEMA_VERSION_HISTORY = "/schema/version/h" @@ -131,7 +132,7 @@ type CollectionSchemaVersionKey struct { var _ Key = (*CollectionSchemaVersionKey)(nil) -// CollectionIndexKey to a stored description of an index +// CollectionIndexKey points to a stored description of an index type CollectionIndexKey struct { // CollectionID is the id of the collection that the index is on CollectionID immutable.Option[uint32] @@ -141,6 +142,14 @@ type CollectionIndexKey struct { var _ Key = (*CollectionIndexKey)(nil) +// CollectionPolicyKey points to the stored policy description of the collection. +type CollectionPolicyKey struct { + // CollectionID is the id of the collection that the policy is on + CollectionID uint32 +} + +var _ Key = (*CollectionPolicyKey)(nil) + // SchemaVersionKey points to the json serialized schema at the specified version. // // It's corresponding value is immutable. @@ -291,6 +300,52 @@ func NewCollectionSchemaVersionKeyFromString(key string) (CollectionSchemaVersio }, nil } +// NewCollectionPolicyKey creates a new CollectionPolicyKey from a collectionID. +func NewCollectionPolicyKey( + colID uint32, +) CollectionPolicyKey { + return CollectionPolicyKey{ + CollectionID: colID, + } +} + +// NewCollectionPolicyKeyFromString creates a new CollectionIndexKey from a string. +// It expects the input string in the following format: +// +// /collection/policy/[CollectionID] +// +// Where [CollectionID] must not be omitted. +func NewCollectionPolicyKeyFromString(key string) (CollectionPolicyKey, error) { + keyElements := strings.Split(key, "/") + if len(keyElements) != 4 || keyElements[1] != "collection" || keyElements[2] != "policy" { + return CollectionPolicyKey{}, ErrInvalidKey + } + + colID, err := strconv.Atoi(keyElements[3]) + if err != nil { + return CollectionPolicyKey{}, err + } + + return CollectionPolicyKey{ + CollectionID: uint32(colID), + }, nil +} + +// ToString returns the string representation of the key +// It is in the following format: +// /collection/policy/[CollectionID] +func (k CollectionPolicyKey) ToString() string { + return fmt.Sprintf("%s/%s", COLLECTION_POLICY, fmt.Sprint(k.CollectionID)) +} + +func (k CollectionPolicyKey) Bytes() []byte { + return []byte(k.ToString()) +} + +func (k CollectionPolicyKey) ToDS() ds.Key { + return ds.NewKey(k.ToString()) +} + // NewCollectionIndexKey creates a new CollectionIndexKey from a collection name and index name. func NewCollectionIndexKey(colID immutable.Option[uint32], indexName string) CollectionIndexKey { return CollectionIndexKey{CollectionID: colID, IndexName: indexName} diff --git a/db/collection.go b/db/collection.go index d6dd36ffb8..3245dab7c9 100644 --- a/db/collection.go +++ b/db/collection.go @@ -128,6 +128,7 @@ func (db *db) createCollection( } col := db.newCollection(desc, schema) + for _, index := range desc.Indexes { if _, err := col.createIndex(ctx, txn, index); err != nil { return nil, err @@ -591,6 +592,11 @@ func (db *db) getCollectionsByVersionID( if err != nil { return nil, err } + + err = collections[i].loadPolicy(ctx, txn) + if err != nil { + return nil, err + } } return collections, nil @@ -608,11 +614,17 @@ func (db *db) getCollectionByID(ctx context.Context, txn datastore.Txn, id uint3 } collection := db.newCollection(col, schema) + err = collection.loadIndexes(ctx, txn) if err != nil { return nil, err } + err = collection.loadPolicy(ctx, txn) + if err != nil { + return nil, err + } + return collection, nil } @@ -633,11 +645,17 @@ func (db *db) getCollectionByName(ctx context.Context, txn datastore.Txn, name s } collection := db.newCollection(col, schema) + err = collection.loadIndexes(ctx, txn) if err != nil { return nil, err } + err = collection.loadPolicy(ctx, txn) + if err != nil { + return nil, err + } + return collection, nil } @@ -670,6 +688,11 @@ func (db *db) getCollectionsBySchemaRoot( if err != nil { return nil, err } + + err = collection.loadPolicy(ctx, txn) + if err != nil { + return nil, err + } } return collections, nil @@ -696,6 +719,11 @@ func (db *db) getAllCollections(ctx context.Context, txn datastore.Txn) ([]clien if err != nil { return nil, err } + + err = collection.loadPolicy(ctx, txn) + if err != nil { + return nil, err + } } return collections, nil @@ -722,6 +750,11 @@ func (db *db) getAllActiveDefinitions(ctx context.Context, txn datastore.Txn) ([ return nil, err } + err = collection.loadPolicy(ctx, txn) + if err != nil { + return nil, err + } + definitions[i] = collection.Definition() } @@ -865,24 +898,8 @@ func (c *collection) Create(ctx context.Context, doc *client.Document) error { if err != nil { return err } - err = c.commitImplicitTxn(ctx, txn) - if err != nil { - return err - } - - //fmt.Println("333333333333333333333333333333333333") - //spew.Dump(c.) - //fmt.Println("333333333333333333333333333333333333") - //c.db.ACPModule().RegisterDocCreation( - // ctx, - // policyID "123", - // collection string, - // creator string, - // docID string, - //) - - return nil + return c.commitImplicitTxn(ctx, txn) } // CreateMany creates a collection of documents at once. @@ -952,7 +969,41 @@ func (c *collection) create(ctx context.Context, txn datastore.Txn, doc *client. return err } - return c.indexNewDoc(ctx, txn, doc) + err = c.indexNewDoc(ctx, txn, doc) + if err != nil { + return err + } + + // If this collection has access control enabled, and the acp module is not missing, + // then register this document with acp module and give the creator access to it. + // Note: if the request was not permissioned then we want to keep this document as + // public (can be accessed by all), even if this collection has a policy and resource. + // TODO-ACP: Add another condition to check if is a permissioned request (after signatures) + // Below should all be under signature identity block because: + // Total 8 cases, and 3 outputs + // if (permReq, permCol, acpMod) => Create with req signature + // if (permReq, permCol, !acpMod) => Error, no acp available + // if (permReq, !permCol, acpMod) => Normal no acp + // if (permReq, !permCol, !acpMod) => Normal no acp, so ignore missing acp, no error + // if (!permReq, permCol, acpMod) => Public doc, no register with acp, no signature + // if (!permReq, !permCol, acpMod) => Public doc, no register with acp, no signature + // if (!permReq, permCol, !acpMod) => Public doc, no register with acp, no signature, we ignore missing acp + // if (!permReq, !permCol, !acpMod) => Public doc, no register with acp, no signature, we ignore missing acp + if policyID, resourceName, hasPolicy := client.IsPermissioned(c); hasPolicy { + if !c.db.ACPModule().HasValue() { + return ErrCanNotCreateDocOnPermColNoACP + } + + return c.db.ACPModule().Value().RegisterDocCreation( + ctx, + "cosmos1zzg43wdrhmmk89z3pmejwete2kkd4a3vn7w969", // TODO-ACP: Replace with signature identity + policyID, + resourceName, + doc.ID().String(), + ) + } + + return nil } // Update an existing document with the new values. diff --git a/db/collection_policy.go b/db/collection_policy.go new file mode 100644 index 0000000000..98b43cfe5c --- /dev/null +++ b/db/collection_policy.go @@ -0,0 +1,59 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package db + +import ( + "context" + "encoding/json" + + "github.com/sourcenetwork/immutable" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/core" + "github.com/sourcenetwork/defradb/datastore" + "github.com/sourcenetwork/defradb/errors" + + ds "github.com/ipfs/go-datastore" +) + +func (db *db) fetchCollectionPolicyDescription( + ctx context.Context, + txn datastore.Txn, + colID uint32, +) (immutable.Option[client.PolicyDescription], error) { + collectionPolicyKey := core.NewCollectionPolicyKey(colID) + policyBuf, err := txn.Systemstore().Get(ctx, collectionPolicyKey.ToDS()) + + if err != nil && errors.Is(err, ds.ErrNotFound) { + return immutable.None[client.PolicyDescription](), nil + } + + if err != nil { + return immutable.None[client.PolicyDescription](), err + } + + var policy client.PolicyDescription + err = json.Unmarshal(policyBuf, &policy) + if err != nil { + return immutable.None[client.PolicyDescription](), err + } + + return immutable.Some[client.PolicyDescription](policy), nil +} + +func (c *collection) loadPolicy(ctx context.Context, txn datastore.Txn) error { + policyDescription, err := c.db.fetchCollectionPolicyDescription(ctx, txn, c.ID()) + if err != nil { + return err + } + c.def.Description.Policy = policyDescription + return nil +} diff --git a/db/description/collection.go b/db/description/collection.go index 3daeaf31de..59eb3fcc36 100644 --- a/db/description/collection.go +++ b/db/description/collection.go @@ -52,6 +52,21 @@ func SaveCollection( } } + desc.Policy.HasValue() + if desc.Policy.HasValue() { + policy := desc.Policy.Value() + policyBuf, err := json.Marshal(policy) + if err != nil { + return client.CollectionDescription{}, err + } + + policyKey := core.NewCollectionPolicyKey(desc.ID) + err = txn.Systemstore().Put(ctx, policyKey.ToDS(), policyBuf) + if err != nil { + return client.CollectionDescription{}, err + } + } + // The need for this key is temporary, we should replace it with the global collection ID // https://github.com/sourcenetwork/defradb/issues/1085 schemaVersionKey := core.NewCollectionSchemaVersionKey(desc.SchemaVersionID, desc.ID) diff --git a/db/errors.go b/db/errors.go index c27d052408..cd630a1808 100644 --- a/db/errors.go +++ b/db/errors.go @@ -88,6 +88,7 @@ const ( errCanNotIndexNonUniqueFields string = "can not index a doc's field(s) that violates unique index" errInvalidViewQuery string = "the query provided is not valid as a View" errCanNotHavePolicyWithoutACPModule string = "can not specify policy on collection, without an acp module" + errCanNotCreateDocOnPermColNoACP string = "can not create doc on permissioned collection without an acp module" ) var ( @@ -110,6 +111,7 @@ var ( ErrExpectedJSONArray = errors.New(errExpectedJSONArray) ErrInvalidViewQuery = errors.New(errInvalidViewQuery) ErrCanNotIndexNonUniqueFields = errors.New(errCanNotIndexNonUniqueFields) + ErrCanNotCreateDocOnPermColNoACP = errors.New(errCanNotCreateDocOnPermColNoACP) ) // NewErrFailedToGetHeads returns a new error indicating that the heads of a document diff --git a/db/schema.go b/db/schema.go index 07700d1351..b232cbc224 100644 --- a/db/schema.go +++ b/db/schema.go @@ -48,7 +48,6 @@ func (db *db) addSchema( if err != nil { return nil, err } - returnDescriptions := make([]client.CollectionDescription, len(newDefinitions)) for i, definition := range newDefinitions { col, err := db.createCollection(ctx, txn, definition) diff --git a/tests/integration/acp.go b/tests/integration/acp.go index 358c4164e8..6e9276cc2d 100644 --- a/tests/integration/acp.go +++ b/tests/integration/acp.go @@ -63,8 +63,8 @@ func addPolicyACP( policyID, err := node.ACPModule().Value().AddPolicy( s.ctx, - action.Policy, action.Creator, + action.Policy, action.IsYAML, )