Skip to content

Commit

Permalink
feat(repository): apply retention policies server-side (kopia#3249)
Browse files Browse the repository at this point in the history
* feat(repository): apply retention policies server-side

This allows append-only snapshots where the client can never delete
arbitrary manifests and policies are maintained on the server.

The client only needs permissions to create snapshots in a given, which
automatically gives them permission to invoke the server-side method
for their own snapshots only.

* Update cli/command_acl_add.go

Co-authored-by: Guillaume <[email protected]>

* Update internal/server/api_manifest.go

Co-authored-by: Guillaume <[email protected]>

* Update internal/server/api_manifest.go

Co-authored-by: Guillaume <[email protected]>

* Update internal/server/grpc_session.go

Co-authored-by: Guillaume <[email protected]>

---------

Co-authored-by: Guillaume <[email protected]>
  • Loading branch information
jkowalski and Gui13 authored Sep 3, 2023
1 parent 80423cf commit 044db75
Show file tree
Hide file tree
Showing 18 changed files with 688 additions and 228 deletions.
10 changes: 6 additions & 4 deletions cli/command_acl_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import (
)

type commandACLAdd struct {
user string
target string
level string
user string
target string
level string
overwrite bool
}

func (c *commandACLAdd) setup(svc appServices, parent commandParent) {
cmd := parent.Command("add", "Add ACL entry")
cmd.Flag("user", "User the ACL targets").Required().StringVar(&c.user)
cmd.Flag("target", "Manifests targeted by the rule (type:T,key1:value1,...,keyN:valueN)").Required().StringVar(&c.target)
cmd.Flag("access", "Access the user gets to subject").Required().EnumVar(&c.level, acl.SupportedAccessLevels()...)
cmd.Flag("overwrite", "Overwrite existing rule with the same user and target").BoolVar(&c.overwrite)
cmd.Action(svc.repositoryWriterAction(c.run))
}

Expand All @@ -47,5 +49,5 @@ func (c *commandACLAdd) run(ctx context.Context, rep repo.RepositoryWriter) erro
Access: al,
}

return errors.Wrap(acl.AddACL(ctx, rep, e), "error adding ACL entry")
return errors.Wrap(acl.AddACL(ctx, rep, e, c.overwrite), "error adding ACL entry")
}
2 changes: 1 addition & 1 deletion cli/command_acl_enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (c *commandACLEnable) run(ctx context.Context, rep repo.RepositoryWriter) e
}

for _, e := range auth.DefaultACLs {
if err := acl.AddACL(ctx, rep, e); err != nil {
if err := acl.AddACL(ctx, rep, e, false); err != nil {
return errors.Wrap(err, "unable to add default ACL")
}
}
Expand Down
4 changes: 0 additions & 4 deletions cli/command_snapshot_expire.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ func (c *commandSnapshotExpire) run(ctx context.Context, rep repo.RepositoryWrit
log(ctx).Infof("Deleted %v snapshots of %v...", len(deleted), src)
} else {
log(ctx).Infof("%v snapshot(s) of %v would be deleted. Pass --delete to do it.", len(deleted), src)

for _, it := range deleted {
log(ctx).Infof(" %v", formatTimestamp(it.StartTime.ToTime()))
}
}
}

Expand Down
20 changes: 19 additions & 1 deletion internal/acl/acl_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/pkg/errors"
"golang.org/x/exp/maps"

"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/manifest"
Expand Down Expand Up @@ -109,11 +110,28 @@ func LoadEntries(ctx context.Context, rep repo.Repository, old []*Entry) ([]*Ent
}

// AddACL validates and adds the specified ACL entry to the repository.
func AddACL(ctx context.Context, w repo.RepositoryWriter, e *Entry) error {
func AddACL(ctx context.Context, w repo.RepositoryWriter, e *Entry, overwrite bool) error {
if err := e.Validate(); err != nil {
return errors.Wrap(err, "error validating ACL")
}

entries, err := LoadEntries(ctx, w, nil)
if err != nil {
return errors.Wrap(err, "unable to load ACL entries")
}

for _, oldE := range entries {
if e.User == oldE.User && maps.Equal(e.Target, oldE.Target) {
if !overwrite && e.Access < oldE.Access {
return errors.Errorf("ACL entry for a given user and target already exists %v: %v", oldE.User, oldE.Target)
}

if err = w.DeleteManifest(ctx, oldE.ManifestID); err != nil {
return errors.Wrap(err, "error deleting old")
}
}
}

manifestID, err := w.PutManifest(ctx, map[string]string{
manifest.TypeLabelKey: aclManifestType,
}, e)
Expand Down
4 changes: 2 additions & 2 deletions internal/acl/acl_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func TestLoadEntries(t *testing.T) {
Access: acl.AccessLevelFull,
}

require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e1))
require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e1, false))

entries, err = acl.LoadEntries(ctx, env.RepositoryWriter, entries)
require.NoError(t, err)
Expand All @@ -192,7 +192,7 @@ func TestLoadEntries(t *testing.T) {
Access: acl.AccessLevelFull,
}

require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e2))
require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e2, false))

entries, err = acl.LoadEntries(ctx, env.RepositoryWriter, entries)
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestDefaultAuthorizer_DefaultACLs(t *testing.T) {
ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)

for _, e := range auth.DefaultACLs {
require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e))
require.NoError(t, acl.AddACL(ctx, env.RepositoryWriter, e, false))
}

verifyLegacyAuthorizer(ctx, t, env.Repository, auth.DefaultAuthorizer())
Expand Down
Loading

0 comments on commit 044db75

Please sign in to comment.