diff --git a/x/acp/auth_engine/test_suite.go b/x/acp/auth_engine/test_suite.go index 62ba149c..2633bf74 100644 --- a/x/acp/auth_engine/test_suite.go +++ b/x/acp/auth_engine/test_suite.go @@ -193,3 +193,6 @@ func runSuite(t *testing.T, suite *TestSuite) { }) } } + +// TODO Add Tests for Check, Filter Relationship and yada yada +// (should this be tested directly? seems redundant) diff --git a/x/acp/relationship/authorizer.go b/x/acp/relationship/authorizer.go new file mode 100644 index 00000000..26d5aef7 --- /dev/null +++ b/x/acp/relationship/authorizer.go @@ -0,0 +1,40 @@ +package relationship + +import ( + "context" + + "github.com/sourcenetwork/sourcehub/x/acp/auth_engine" + "github.com/sourcenetwork/sourcehub/x/acp/types" +) + +func NewRelationshipAuthorizer(engine auth_engine.AuthEngine) *RelationshipAuthorizer { + return &RelationshipAuthorizer{ + engine: engine, + } +} + +// RelationshipAuthorizer acts as an Authorization Request engine +// which validates whether a Relationship can be set or deleted by an Actor. +// +// The Permission evaluation is done through a Check call using the auxiliary permissions +// auto generated by the ACP module and attached to a permission. +// +// For instance, take the Relationship (obj:foo, reader, steve) being submitted by Actor Bob. +// Bob is allowed to Create that relationship if and only if: +// Bob has the permission _can_manage_reader for "obj:foo". +type RelationshipAuthorizer struct { + engine auth_engine.AuthEngine +} + +// IsAuthorized validates whether actor is a manager for the given relationship. +// +// A given Relationship is only valid if for the Relationship's Object and Relation +// the Actor has an associated permission to manage the Object, Relation pair. +func (a *RelationshipAuthorizer) IsAuthorized(ctx context.Context, policy *types.Policy, relationship *types.Relationship, actor *types.Actor) (bool, error) { + authRequest := &types.Operation{ + Object: relationship.Object, + Permission: policy.GetManagementPermissionName(relationship.Relation), + } + + return a.engine.Check(ctx, policy, authRequest, actor) +} diff --git a/x/acp/relationship/commands.go b/x/acp/relationship/commands.go new file mode 100644 index 00000000..c7f6d5b8 --- /dev/null +++ b/x/acp/relationship/commands.go @@ -0,0 +1,389 @@ +package relationship + +import ( + "context" + "fmt" + + prototypes "github.com/cosmos/gogoproto/types" + + "github.com/sourcenetwork/sourcehub/x/acp/auth_engine" + "github.com/sourcenetwork/sourcehub/x/acp/policy" + "github.com/sourcenetwork/sourcehub/x/acp/types" +) + +// RegisterObjectCommand creates an "owner" Relationship for the given object and subject, +// if the object does not have a previous owner. +// If the relationship exists but is archived by the same actor, unarchives it +// if relationship is active this command is a noop +type RegisterObjectCommand struct { + Registration *types.Registration + Policy *types.Policy + CreationTs *prototypes.Timestamp +} + +func (c *RegisterObjectCommand) Execute(ctx context.Context, engine auth_engine.AuthEngine) (types.RegistrationResult, error) { + var err error + var result types.RegistrationResult + + err = c.validate() + if err != nil { + return result, fmt.Errorf("RegisterObject: %w", err) + } + + record, err := c.getOwnerRelationship(ctx, engine) + if err != nil { + return result, fmt.Errorf("RegisterObject: %w", err) + } + + switch c.resolveObjectStatus(record) { + case statusUnregistered: + result, err = c.unregisteredStrategy(ctx, engine) + case statusArchived: + result, err = c.archivedObjectStrategy(ctx, engine, record) + case statusActive: + result, err = c.activeObjectStrategy(record) + } + + if err != nil { + return result, fmt.Errorf("RegisterObject: %w", err) + } + + return result, nil +} + +// validates the command input params +func (c *RegisterObjectCommand) validate() error { + if c.Policy == nil { + return types.ErrPolicyNil + } + + if c.Registration == nil { + return types.ErrRegistrationNil + } + + if c.Registration.Actor == nil { + return types.ErrActorNil + } + if c.Registration.Object == nil { + return types.ErrActorNil + } + + if c.CreationTs == nil { + return types.ErrTimestampNil + } + + return nil +} + +func (c *RegisterObjectCommand) getOwnerRelationship(ctx context.Context, engine auth_engine.AuthEngine) (*types.RelationshipRecord, error) { + selector := &types.RelationshipSelector{ + ObjectSelector: &types.ObjectSelector{ + Selector: &types.ObjectSelector_Object{ + Object: c.Registration.Object, + }, + }, + RelationSelector: &types.RelationSelector{ + Selector: &types.RelationSelector_Relation{ + Relation: policy.OwnerRelation, + }, + }, + SubjectSelector: &types.SubjectSelector{ + Selector: &types.SubjectSelector_Wildcard{ + Wildcard: &types.WildcardSelector{}, + }, + }, + } + + records, err := engine.FilterRelationships(ctx, c.Policy, selector) + if err != nil { + return nil, err + } + if len(records) == 0 { + return nil, nil + } else if len(records) > 1 { + // This is a bad and unexpected condition. + // If this happens in the system there might be a vulnerability or a protocol rule was updated + err := fmt.Errorf("object %v has more than one owner: %w", c.Registration.Object, types.ErrAcpProtocolViolation) + return nil, err + } + + return records[0], nil +} + +func (c *RegisterObjectCommand) resolveObjectStatus(record *types.RelationshipRecord) objectRegistrationStatus { + if record == nil { + return statusUnregistered + } + if record.Archived == true { + return statusArchived + } + return statusActive +} + +func (c *RegisterObjectCommand) unregisteredStrategy(ctx context.Context, engine auth_engine.AuthEngine) (types.RegistrationResult, error) { + err := c.createOwnerRelationship(ctx, engine) + if err != nil { + return types.RegistrationResult_NoOp, err + } + + return types.RegistrationResult_Registered, nil +} + +func (c *RegisterObjectCommand) createOwnerRelationship(ctx context.Context, engine auth_engine.AuthEngine) error { + record := types.RelationshipRecord{ + Relationship: &types.Relationship{ + Object: c.Registration.Object, + Relation: policy.OwnerRelation, + Subject: &types.Subject{ + Subject: &types.Subject_Actor{ + Actor: c.Registration.Actor, + }, + }, + }, + Creator: c.Registration.Actor.Id, + PolicyId: c.Policy.Id, + Archived: false, + CreationTime: c.CreationTs, + } + _, err := engine.SetRelationship(ctx, c.Policy, &record) + return err +} + +func (c *RegisterObjectCommand) activeObjectStrategy(record *types.RelationshipRecord) (types.RegistrationResult, error) { + if record.Creator != c.Registration.Actor.Id { + return types.RegistrationResult_NoOp, types.ErrNotAuthorized + } + + return types.RegistrationResult_NoOp, nil +} + +func (c *RegisterObjectCommand) archivedObjectStrategy(ctx context.Context, engine auth_engine.AuthEngine, record *types.RelationshipRecord) (types.RegistrationResult, error) { + if record.Creator != c.Registration.Actor.Id { + return types.RegistrationResult_NoOp, types.ErrNotAuthorized + } + + err := c.unarchiveRelationship(ctx, engine, record) + if err != nil { + return types.RegistrationResult_NoOp, err + } + + return types.RegistrationResult_Unarchived, nil +} + +func (c *RegisterObjectCommand) unarchiveRelationship(ctx context.Context, engine auth_engine.AuthEngine, record *types.RelationshipRecord) error { + record.Archived = false + _, err := engine.SetRelationship(ctx, c.Policy, record) + return err +} + +type SetRelationshipCommand struct { + Policy *types.Policy + CreationTs *prototypes.Timestamp + Creator string + Relationship *types.Relationship +} + +func (c *SetRelationshipCommand) Execute(ctx context.Context, engine auth_engine.AuthEngine, authorizer *RelationshipAuthorizer) (auth_engine.RecordFound, error) { + err := c.validate() + if err != nil { + return false, fmt.Errorf("SetRelationship: %w", err) + } + + creatorActor := types.Actor{ + Id: c.Creator, + } + authorized, err := authorizer.IsAuthorized(ctx, c.Policy, c.Relationship, &creatorActor) + if err != nil { + return false, fmt.Errorf("SetRelationship: %w", err) + } + if !authorized { + return false, fmt.Errorf("SetRelationship: %w", types.ErrNotAuthorized) + } + + record, err := engine.GetRelationship(ctx, c.Policy, c.Relationship) + if err != nil { + return false, fmt.Errorf("SetRelationship: %w", err) + } + if record != nil { + return true, nil + } + + record = &types.RelationshipRecord{ + PolicyId: c.Policy.Id, + Relationship: c.Relationship, + CreationTime: c.CreationTs, + Creator: c.Creator, + Archived: false, + } + _, err = engine.SetRelationship(ctx, c.Policy, record) + if err != nil { + return false, fmt.Errorf("SetRelationship: %w", err) + } + + return false, nil +} + +func (c *SetRelationshipCommand) validate() error { + if c.Relationship.Relation == policy.OwnerRelation { + return ErrSetOwnerRel + } + + return nil +} + +// DeleteRelationshipCommand encapsulates the process of removing a relationship from a Policy +type DeleteRelationshipCommand struct { + // Policy from which Relationship will be removed + Policy *types.Policy + + // Relationship to be removed + Relationship *types.Relationship + + // Id of actor that initiated the deletion + Actor string +} + +func (c *DeleteRelationshipCommand) Execute(ctx context.Context, engine auth_engine.AuthEngine, authorizer *RelationshipAuthorizer) (auth_engine.RecordFound, error) { + err := c.validate() + if err != nil { + return false, fmt.Errorf("DeleteRelationshipCommand: %w", err) + } + + isAuthorized, err := c.isActorAuthorized(ctx, authorizer) + if err != nil { + return false, fmt.Errorf("DeleteRelationshipCommand: %w", err) + } + + if !isAuthorized { + return false, fmt.Errorf("DeleteRelationshipCommand: %w", types.ErrNotAuthorized) + } + + found, err := engine.DeleteRelationship(ctx, c.Policy, c.Relationship) + if err != nil { + return false, fmt.Errorf("DeleteRelationshipCommand: %w", err) + } + + return found, nil +} + +func (c *DeleteRelationshipCommand) validate() error { + if c.Policy == nil { + return types.ErrPolicyNil + } + + if c.Relationship == nil { + return types.ErrRelationshipNil + } + + if c.Actor == "" { + return types.ErrActorNil + } + + if c.Relationship.Relation == policy.OwnerRelation { + return ErrDeleteOwnerRel + } + + return nil +} + +// verifies whether actor is authorized to remove the specified Relationship +func (c *DeleteRelationshipCommand) isActorAuthorized(ctx context.Context, authorizer *RelationshipAuthorizer) (bool, error) { + creatorActor := types.Actor{ + Id: c.Actor, + } + return authorizer.IsAuthorized(ctx, c.Policy, c.Relationship, &creatorActor) +} + +type UnregisterObjectCommand struct { + // Target Policy + Policy *types.Policy + + // Object to be unregistered + Object *types.Object + + // Actor which initiated request + Actor string +} + +func (c *UnregisterObjectCommand) Execute(ctx context.Context, engine auth_engine.AuthEngine, authorizer *RelationshipAuthorizer) (uint, error) { + err := c.validate() + if err != nil { + return 0, fmt.Errorf("UnregisterObject: %w", err) + } + + authRelationship := types.NewActorRelationship(c.Object.Resource, c.Object.Id, policy.OwnerRelation, c.Actor) + actor := types.Actor{Id: c.Actor} + authorized, err := authorizer.IsAuthorized(ctx, c.Policy, authRelationship, &actor) + if err != nil { + return 0, fmt.Errorf("UnregisterObject: %w", err) + } + if !authorized { + return 0, fmt.Errorf("UnregisterObject: %w", types.ErrNotAuthorized) + } + + ownerRecord, err := engine.GetRelationship(ctx, c.Policy, authRelationship) + if err != nil { + return 0, fmt.Errorf("UnregisterObject: %w", err) + } + if ownerRecord.Archived { + return 0, nil + } + + count, err := c.removeObjectRelationships(ctx, engine) + + err = c.archiveObject(ctx, engine, ownerRecord) + if err != nil { + return 0, fmt.Errorf("UnregisterObject: archiving object: %w", err) + } + + return count, nil +} + +func (c *UnregisterObjectCommand) archiveObject(ctx context.Context, engine auth_engine.AuthEngine, ownerRecord *types.RelationshipRecord) error { + ownerRecord.Archived = true + _, err := engine.SetRelationship(ctx, c.Policy, ownerRecord) + return err + +} + +func (c *UnregisterObjectCommand) removeObjectRelationships(ctx context.Context, engine auth_engine.AuthEngine) (uint, error) { + selector := &types.RelationshipSelector{ + ObjectSelector: &types.ObjectSelector{ + Selector: &types.ObjectSelector_Object{ + Object: c.Object, + }, + }, + RelationSelector: &types.RelationSelector{ + Selector: &types.RelationSelector_Wildcard{ + Wildcard: &types.WildcardSelector{}, + }, + }, + SubjectSelector: &types.SubjectSelector{ + Selector: &types.SubjectSelector_Wildcard{ + Wildcard: &types.WildcardSelector{}, + }, + }, + } + count, err := engine.DeleteRelationships(ctx, c.Policy, selector) + if err != nil { + return 0, fmt.Errorf("UnregisterObject: %w", err) + } + + return count, nil +} + +func (c *UnregisterObjectCommand) validate() error { + if c.Policy == nil { + return types.ErrPolicyNil + } + + if c.Object == nil { + return types.ErrObjectNil + } + + if c.Actor == "" { + return types.ErrActorNil + } + + return nil +} diff --git a/x/acp/relationship/commands_test.go b/x/acp/relationship/commands_test.go new file mode 100644 index 00000000..7a206f67 --- /dev/null +++ b/x/acp/relationship/commands_test.go @@ -0,0 +1,534 @@ +package relationship + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sourcenetwork/sourcehub/x/acp/auth_engine" + "github.com/sourcenetwork/sourcehub/x/acp/policy" + "github.com/sourcenetwork/sourcehub/x/acp/testutil" + "github.com/sourcenetwork/sourcehub/x/acp/types" +) + +var timestamp = testutil.MustDateTimeToProto("2023-07-26 14:08:30") + +var testPolicy = &types.Policy{ + Id: "1", + Resources: []*types.Resource{ + &types.Resource{ + Name: "test", + Relations: []*types.Relation{ + &types.Relation{ + Name: "owner", + VrTypes: []*types.Restriction{ + &types.Restriction{ + ResourceName: "actor", + }, + }, + }, + }, + Permissions: []*types.Permission{}, + }, + }, + ActorResource: &types.ActorResource{ + Name: "actor", + }, +} + +func setup(t *testing.T) (context.Context, auth_engine.AuthEngine) { + engine, _, ctx := testutil.GetTestAuthEngine(t) + + rec, err := types.NewPolicyRecord(testPolicy) + require.Nil(t, err) + + err = engine.SetPolicy(ctx, rec) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, testPolicy, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("test", "archived", "owner", "bob"), + Archived: true, + Creator: "bob", + }) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, testPolicy, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("test", "active", "owner", "bob"), + Archived: false, + Creator: "bob", + }) + require.Nil(t, err) + + return ctx, engine +} + +func TestRegisterObjectCommand_ValidObjectIsRegistered(t *testing.T) { + ctx, engine := setup(t) + + cmd := RegisterObjectCommand{ + Registration: &types.Registration{ + Object: &types.Object{ + Resource: "test", + Id: "unregistered", + }, + Actor: &types.Actor{ + Id: "bob", + }, + }, + Policy: testPolicy, + CreationTs: timestamp, + } + + result, err := cmd.Execute(ctx, engine) + + require.Equal(t, result, types.RegistrationResult_Registered) + require.Nil(t, err) +} + +func TestRegisterObjectCommand_CannotRegisterObjectThatHasBeenArchivedBySomeoneElse(t *testing.T) { + ctx, engine := setup(t) + + cmd := RegisterObjectCommand{ + Registration: &types.Registration{ + Object: &types.Object{ + Resource: "test", + Id: "archived", + }, + Actor: &types.Actor{ + Id: "alice", + }, + }, + Policy: testPolicy, + CreationTs: timestamp, + } + + result, err := cmd.Execute(ctx, engine) + + require.ErrorIs(t, err, types.ErrNotAuthorized) + require.Equal(t, result, types.RegistrationResult_NoOp) +} + +func TestRegisterObjectCommand_RegisteringActiveObjectOwnedBySomeoneElseErrors(t *testing.T) { + ctx, engine := setup(t) + + cmd := RegisterObjectCommand{ + Registration: &types.Registration{ + Object: &types.Object{ + Resource: "test", + Id: "active", + }, + Actor: &types.Actor{ + Id: "alice", + }, + }, + Policy: testPolicy, + CreationTs: timestamp, + } + + result, err := cmd.Execute(ctx, engine) + + require.ErrorIs(t, err, types.ErrNotAuthorized) + require.Equal(t, result, types.RegistrationResult_NoOp) +} + +func TestRegisterObjectCommand_RegisteringArchivedObjectByOwnerActivatesIt(t *testing.T) { + ctx, engine := setup(t) + + cmd := RegisterObjectCommand{ + Registration: &types.Registration{ + Object: &types.Object{ + Resource: "test", + Id: "archived", + }, + Actor: &types.Actor{ + Id: "bob", + }, + }, + Policy: testPolicy, + CreationTs: timestamp, + } + + result, err := cmd.Execute(ctx, engine) + + require.Equal(t, types.RegistrationResult_Unarchived.String(), result.String()) + require.Nil(t, err) +} + +// setu for SetRelationship tests +// sets alice as the owner of readme.txt and sets bob as an admin of readme.txt +func setupTestSetRelationship(t *testing.T) (context.Context, auth_engine.AuthEngine, *RelationshipAuthorizer, *types.Policy) { + polStr := ` + id: set-rel-pol + resources: + file: + permissions: + relations: + owner: + types: + - actor + reader: + types: + - actor + admin: + manages: + - reader + types: + - actor + + actor: + name: actor + ` + polIR, err := policy.Unmarshal(polStr, types.PolicyMarshalingType_SHORT_YAML) + require.Nil(t, err) + + engine, _, ctx := testutil.GetTestAuthEngine(t) + + createCmd := policy.CreatePolicyCommand{ + Policy: polIR, + Creator: "cosmos1gue5de6a8fdff0jut08vw5sg9pk6rr00cstakj", + CreationTime: timestamp, + } + pol, err := createCmd.Execute(ctx, &testutil.AccountKeeperStub{}, engine) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, pol, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("file", "readme.txt", "owner", "alice"), + Archived: false, + Creator: "alice", + }) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, pol, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("file", "readme.txt", "admin", "bob"), + Archived: false, + Creator: "bob", + }) + require.Nil(t, err) + + authorizer := NewRelationshipAuthorizer(engine) + return ctx, engine, authorizer, pol +} + +func TestSetRelationship_ValidRelationshipIsCreated(t *testing.T) { + ctx, engine, authorizer, policy := setupTestSetRelationship(t) + + command := SetRelationshipCommand{ + Policy: policy, + CreationTs: timestamp, + Creator: "bob", + Relationship: types.NewActorRelationship("file", "readme.txt", "reader", "charlie"), + } + result, err := command.Execute(ctx, engine, authorizer) + + require.Nil(t, err) + require.False(t, bool(result)) +} + +func TestSetRelationship_CannotCreateRelationshipWithOwnerRelation(t *testing.T) { + ctx, engine, authorizer, policy := setupTestSetRelationship(t) + + command := SetRelationshipCommand{ + Policy: policy, + CreationTs: timestamp, + Creator: "bob", + Relationship: types.NewActorRelationship("file", "any.txt", "owner", "bob"), + } + result, err := command.Execute(ctx, engine, authorizer) + + require.ErrorIs(t, err, ErrSetOwnerRel) + require.False(t, bool(result)) +} + +func TestSetRelationship_UnauthorizedActorDoesNotCreateRelationship(t *testing.T) { + ctx, engine, authorizer, policy := setupTestSetRelationship(t) + + command := SetRelationshipCommand{ + Policy: policy, + CreationTs: timestamp, + Creator: "eve", + Relationship: types.NewActorRelationship("file", "readme.txt", "reader", "charlie"), + } + result, err := command.Execute(ctx, engine, authorizer) + + require.False(t, bool(result)) + require.ErrorIs(t, err, types.ErrNotAuthorized) +} + +func TestSetRelationship_SettingAnExistingRelationshipIsNoop(t *testing.T) { + ctx, engine, authorizer, policy := setupTestSetRelationship(t) + + command := SetRelationshipCommand{ + Policy: policy, + CreationTs: timestamp, + Creator: "bob", + Relationship: types.NewActorRelationship("file", "readme.txt", "reader", "charlie"), + } + result, err := command.Execute(ctx, engine, authorizer) + require.Nil(t, err) + require.False(t, bool(result)) + + // repeating the same action is a noop + result, err = command.Execute(ctx, engine, authorizer) + require.Nil(t, err) + require.True(t, bool(result)) +} + +func TestSetRelationship_CannotCreateRelationshipForUndefinedObject(t *testing.T) { + ctx, engine, authorizer, policy := setupTestSetRelationship(t) + + command := SetRelationshipCommand{ + Policy: policy, + CreationTs: timestamp, + Creator: "bob", + Relationship: types.NewActorRelationship("file", "askdfjas", "reader", "charlie"), + } + result, err := command.Execute(ctx, engine, authorizer) + + require.False(t, bool(result)) + require.ErrorIs(t, err, types.ErrNotAuthorized) +} + +// setup for DeleteRelationship tests +// sets alice as the owner of readme.txt and sets bob as an admin of readme.txt +func setupTestDeleteRelationship(t *testing.T) (context.Context, auth_engine.AuthEngine, *RelationshipAuthorizer, *types.Policy) { + polStr := ` + id: set-rel-pol + resources: + file: + permissions: + relations: + owner: + types: + - actor + reader: + types: + - actor + admin: + manages: + - reader + types: + - actor + + actor: + name: actor + ` + polIR, err := policy.Unmarshal(polStr, types.PolicyMarshalingType_SHORT_YAML) + require.Nil(t, err) + + engine, _, ctx := testutil.GetTestAuthEngine(t) + + createCmd := policy.CreatePolicyCommand{ + Policy: polIR, + Creator: "cosmos1gue5de6a8fdff0jut08vw5sg9pk6rr00cstakj", + CreationTime: timestamp, + } + pol, err := createCmd.Execute(ctx, &testutil.AccountKeeperStub{}, engine) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, pol, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("file", "readme.txt", "owner", "alice"), + Archived: false, + Creator: "alice", + }) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, pol, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("file", "readme.txt", "admin", "bob"), + Archived: false, + Creator: "bob", + }) + require.Nil(t, err) + + authorizer := NewRelationshipAuthorizer(engine) + return ctx, engine, authorizer, pol +} + +func TestDeleteRelationship_AttemptingToDeleteAnOwnerRelationshipAsOwnerErrors(t *testing.T) { + ctx, engine, authorizer, pol := setupTestDeleteRelationship(t) + + cmd := DeleteRelationshipCommand{ + Policy: pol, + Relationship: types.NewActorRelationship("file", "readme.txt", "owner", "alice"), + Actor: "alice", + } + result, err := cmd.Execute(ctx, engine, authorizer) + + require.Equal(t, bool(result), false) + require.ErrorIs(t, err, ErrDeleteOwnerRel) +} + +func TestDeleteRelationship_AttemptToDeleteARelationshipForANonExistingObjectReturnsNotAuthorized(t *testing.T) { + ctx, engine, authorizer, pol := setupTestDeleteRelationship(t) + + cmd := DeleteRelationshipCommand{ + Policy: pol, + Relationship: types.NewActorRelationship("file", "none", "reader", "bob"), + Actor: "bob", + } + result, err := cmd.Execute(ctx, engine, authorizer) + + require.False(t, bool(result)) + require.ErrorIs(t, err, types.ErrNotAuthorized) +} + +func TestDeleteRelationship_AttemptingToDeleteARelationshipActorDoesNotManageReturnsUnauthorized(t *testing.T) { + ctx, engine, authorizer, pol := setupTestDeleteRelationship(t) + + cmd := DeleteRelationshipCommand{ + Policy: pol, + Relationship: types.NewActorRelationship("file", "readme.txt", "admin", "bob"), + Actor: "bob", + } + result, err := cmd.Execute(ctx, engine, authorizer) + + require.False(t, bool(result)) + require.ErrorIs(t, err, types.ErrNotAuthorized) +} + +func TestDeleteRelationship_AttemptingToDeleteARelationshipWithUnknownRelationErrors(t *testing.T) { + ctx, engine, authorizer, pol := setupTestDeleteRelationship(t) + + cmd := DeleteRelationshipCommand{ + Policy: pol, + Relationship: types.NewActorRelationship("file", "readme.txt", "unknown-relation", "bob"), + Actor: "bob", + } + result, err := cmd.Execute(ctx, engine, authorizer) + + require.False(t, bool(result)) + require.NotNil(t, err) // TODO refine error +} + +func TestDeleteRelationship_AuthorizedActorRemovesRelationship(t *testing.T) { + ctx, engine, authorizer, pol := setupTestDeleteRelationship(t) + + cmd := DeleteRelationshipCommand{ + Policy: pol, + Relationship: types.NewActorRelationship("file", "readme.txt", "admin", "bob"), + Actor: "alice", + } + result, err := cmd.Execute(ctx, engine, authorizer) + + require.True(t, bool(result)) + require.Nil(t, err) +} + +// setup for DeleteRelationship tests +// sets alice as the owner of readme.txt and sets bob as an admin of readme.txt +func setupUnregisterObjectTests(t *testing.T) (context.Context, auth_engine.AuthEngine, *RelationshipAuthorizer, *types.Policy) { + polStr := ` + id: unregister-pol + resources: + file: + permissions: + relations: + owner: + types: + - actor + reader: + types: + - actor + - file->reader + admin: + manages: + - reader + types: + - actor + + actor: + name: actor + ` + polIR, err := policy.Unmarshal(polStr, types.PolicyMarshalingType_SHORT_YAML) + require.Nil(t, err) + + engine, _, ctx := testutil.GetTestAuthEngine(t) + + createCmd := policy.CreatePolicyCommand{ + Policy: polIR, + Creator: "cosmos1gue5de6a8fdff0jut08vw5sg9pk6rr00cstakj", + CreationTime: timestamp, + } + pol, err := createCmd.Execute(ctx, &testutil.AccountKeeperStub{}, engine) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, pol, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("file", "readme.txt", "owner", "alice"), + Archived: false, + Creator: "alice", + }) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, pol, &types.RelationshipRecord{ + Relationship: types.NewActorRelationship("file", "readme.txt", "admin", "bob"), + Archived: false, + Creator: "bob", + }) + require.Nil(t, err) + + _, err = engine.SetRelationship(ctx, pol, &types.RelationshipRecord{ + Relationship: types.NewActorSetRelationship("file", "delagated", "reader", "file", "readme.txt", "reader"), + Archived: false, + Creator: "charlie", + }) + require.Nil(t, err) + + authorizer := NewRelationshipAuthorizer(engine) + return ctx, engine, authorizer, pol +} + +func TestUnregisterObject_RegisteredObjectCanBeUnregisteredByAuthor(t *testing.T) { + ctx, engine, authorizer, policy := setupUnregisterObjectTests(t) + + command := UnregisterObjectCommand{ + Policy: policy, + Object: &types.Object{"file", "readme.txt"}, + Actor: "alice", + } + count, err := command.Execute(ctx, engine, authorizer) + + require.Nil(t, err) + require.Equal(t, count, uint(2)) +} + +func TestUnregisterObject_ActorCannotUnregisterObjectTheyDoNotOwn(t *testing.T) { + ctx, engine, authorizer, policy := setupUnregisterObjectTests(t) + + command := UnregisterObjectCommand{ + Policy: policy, + Object: &types.Object{"file", "readme.txt"}, + Actor: "bob", + } + count, err := command.Execute(ctx, engine, authorizer) + + require.ErrorIs(t, err, types.ErrNotAuthorized) + require.Equal(t, count, uint(0)) +} + +func TestUnregisterObject_UnregisteringAnObjectThatDoesNotExistReturnsUnauthorized(t *testing.T) { + ctx, engine, authorizer, policy := setupUnregisterObjectTests(t) + + command := UnregisterObjectCommand{ + Policy: policy, + Object: &types.Object{"file", "nothing.txt"}, + Actor: "bob", + } + count, err := command.Execute(ctx, engine, authorizer) + + require.ErrorIs(t, err, types.ErrNotAuthorized) + require.Equal(t, count, uint(0)) +} + +func TestUnregisterObject_UnregisteringAnAlreadyArchivedObjectDoesNothing(t *testing.T) { + ctx, engine, authorizer, policy := setupUnregisterObjectTests(t) + + command := UnregisterObjectCommand{ + Policy: policy, + Object: &types.Object{"file", "nothing.txt"}, + Actor: "bob", + } + count, err := command.Execute(ctx, engine, authorizer) + + require.ErrorIs(t, err, types.ErrNotAuthorized) + require.Equal(t, count, uint(0)) +} diff --git a/x/acp/relationship/doc.go b/x/acp/relationship/doc.go new file mode 100644 index 00000000..c68dc7a3 --- /dev/null +++ b/x/acp/relationship/doc.go @@ -0,0 +1,12 @@ +// package relationship deals with relationship persistance for the ACP module. +// +// Relationships are required to meet certain criteria before they can be passed onto Zanzi for storage. +// Due to the discretionary and public nature of the ACP module, prior to relationship storage, +// it's necessary validating that the relationship actor is allowed to create realationship with +// the specified relation for the specified object. +// +// An exemple of that would be: bob tries to submit relationship (file:foo.txt, read, charlie). +// Before storing the relationship the ACP package validates that bob is allowed to create read relations +// for file:foo.txt. +// This validation is done using the manages rules in a policy. +package relationship diff --git a/x/acp/relationship/errors.go b/x/acp/relationship/errors.go new file mode 100644 index 00000000..e9c93d38 --- /dev/null +++ b/x/acp/relationship/errors.go @@ -0,0 +1,10 @@ +package relationship + +import ( + "github.com/sourcenetwork/sourcehub/x/acp/types" +) + +var ( + ErrDeleteOwnerRel = types.ErrAcpProtocolViolation.Wrapf("cannot delete an owner relationship") + ErrSetOwnerRel = types.ErrAcpProtocolViolation.Wrapf("cannot set an owner relationship") +) diff --git a/x/acp/relationship/types.go b/x/acp/relationship/types.go new file mode 100644 index 00000000..eff05e6c --- /dev/null +++ b/x/acp/relationship/types.go @@ -0,0 +1,9 @@ +package relationship + +type objectRegistrationStatus int + +const ( + statusUnregistered objectRegistrationStatus = iota + statusArchived + statusActive +)