From 044db7593b02696729b6bba8b911600fb0dd5bc8 Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Sat, 2 Sep 2023 18:23:21 -0700 Subject: [PATCH] feat(repository): apply retention policies server-side (#3249) * 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 * Update internal/server/api_manifest.go Co-authored-by: Guillaume * Update internal/server/api_manifest.go Co-authored-by: Guillaume * Update internal/server/grpc_session.go Co-authored-by: Guillaume --------- Co-authored-by: Guillaume --- cli/command_acl_add.go | 10 +- cli/command_acl_enable.go | 2 +- cli/command_snapshot_expire.go | 4 - internal/acl/acl_manager.go | 20 +- internal/acl/acl_manager_test.go | 4 +- internal/auth/authz_test.go | 2 +- internal/grpcapi/repository_server.pb.go | 567 +++++++++++++++-------- internal/grpcapi/repository_server.proto | 11 + internal/remoterepoapi/remoterepoapi.go | 13 +- internal/server/api_manifest.go | 47 ++ internal/server/grpc_session.go | 58 ++- internal/server/server.go | 1 + repo/api_server_repository.go | 13 + repo/grpc_repository_client.go | 27 ++ repo/manifest/manifest_manager.go | 22 + repo/repository.go | 6 + snapshot/policy/expire.go | 31 +- tests/end_to_end_test/acl_test.go | 78 +++- 18 files changed, 688 insertions(+), 228 deletions(-) diff --git a/cli/command_acl_add.go b/cli/command_acl_add.go index a7048ffb8..63471024b 100644 --- a/cli/command_acl_add.go +++ b/cli/command_acl_add.go @@ -11,9 +11,10 @@ ) 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) { @@ -21,6 +22,7 @@ func (c *commandACLAdd) setup(svc appServices, parent commandParent) { 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)) } @@ -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") } diff --git a/cli/command_acl_enable.go b/cli/command_acl_enable.go index d7905d1aa..169425528 100644 --- a/cli/command_acl_enable.go +++ b/cli/command_acl_enable.go @@ -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") } } diff --git a/cli/command_snapshot_expire.go b/cli/command_snapshot_expire.go index c9b1cb45c..b2d8d4cd2 100644 --- a/cli/command_snapshot_expire.go +++ b/cli/command_snapshot_expire.go @@ -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())) - } } } diff --git a/internal/acl/acl_manager.go b/internal/acl/acl_manager.go index 9bcd8c863..3026f09e9 100644 --- a/internal/acl/acl_manager.go +++ b/internal/acl/acl_manager.go @@ -6,6 +6,7 @@ "strings" "github.com/pkg/errors" + "golang.org/x/exp/maps" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/manifest" @@ -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) diff --git a/internal/acl/acl_manager_test.go b/internal/acl/acl_manager_test.go index 02b3146f3..c58385ed8 100644 --- a/internal/acl/acl_manager_test.go +++ b/internal/acl/acl_manager_test.go @@ -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) @@ -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) diff --git a/internal/auth/authz_test.go b/internal/auth/authz_test.go index 1360a1c6c..931c7ab75 100644 --- a/internal/auth/authz_test.go +++ b/internal/auth/authz_test.go @@ -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()) diff --git a/internal/grpcapi/repository_server.pb.go b/internal/grpcapi/repository_server.pb.go index b6969d7ef..d72e1ef6c 100644 --- a/internal/grpcapi/repository_server.pb.go +++ b/internal/grpcapi/repository_server.pb.go @@ -1357,6 +1357,108 @@ func (x *PrefetchContentsResponse) GetContentIds() []string { return nil } +type ApplyRetentionPolicyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SourcePath string `protobuf:"bytes,1,opt,name=source_path,json=sourcePath,proto3" json:"source_path,omitempty"` + ReallyDelete bool `protobuf:"varint,2,opt,name=really_delete,json=reallyDelete,proto3" json:"really_delete,omitempty"` +} + +func (x *ApplyRetentionPolicyRequest) Reset() { + *x = ApplyRetentionPolicyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_server_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ApplyRetentionPolicyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyRetentionPolicyRequest) ProtoMessage() {} + +func (x *ApplyRetentionPolicyRequest) ProtoReflect() protoreflect.Message { + mi := &file_repository_server_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyRetentionPolicyRequest.ProtoReflect.Descriptor instead. +func (*ApplyRetentionPolicyRequest) Descriptor() ([]byte, []int) { + return file_repository_server_proto_rawDescGZIP(), []int{24} +} + +func (x *ApplyRetentionPolicyRequest) GetSourcePath() string { + if x != nil { + return x.SourcePath + } + return "" +} + +func (x *ApplyRetentionPolicyRequest) GetReallyDelete() bool { + if x != nil { + return x.ReallyDelete + } + return false +} + +type ApplyRetentionPolicyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ManifestIds []string `protobuf:"bytes,1,rep,name=manifest_ids,json=manifestIds,proto3" json:"manifest_ids,omitempty"` +} + +func (x *ApplyRetentionPolicyResponse) Reset() { + *x = ApplyRetentionPolicyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_repository_server_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ApplyRetentionPolicyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyRetentionPolicyResponse) ProtoMessage() {} + +func (x *ApplyRetentionPolicyResponse) ProtoReflect() protoreflect.Message { + mi := &file_repository_server_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyRetentionPolicyResponse.ProtoReflect.Descriptor instead. +func (*ApplyRetentionPolicyResponse) Descriptor() ([]byte, []int) { + return file_repository_server_proto_rawDescGZIP(), []int{25} +} + +func (x *ApplyRetentionPolicyResponse) GetManifestIds() []string { + if x != nil { + return x.ManifestIds + } + return nil +} + type SessionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1379,13 +1481,14 @@ type SessionRequest struct { // *SessionRequest_FindManifests // *SessionRequest_DeleteManifest // *SessionRequest_PrefetchContents + // *SessionRequest_ApplyRetentionPolicy Request isSessionRequest_Request `protobuf_oneof:"request"` } func (x *SessionRequest) Reset() { *x = SessionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_repository_server_proto_msgTypes[24] + mi := &file_repository_server_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1398,7 +1501,7 @@ func (x *SessionRequest) String() string { func (*SessionRequest) ProtoMessage() {} func (x *SessionRequest) ProtoReflect() protoreflect.Message { - mi := &file_repository_server_proto_msgTypes[24] + mi := &file_repository_server_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1411,7 +1514,7 @@ func (x *SessionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRequest.ProtoReflect.Descriptor instead. func (*SessionRequest) Descriptor() ([]byte, []int) { - return file_repository_server_proto_rawDescGZIP(), []int{24} + return file_repository_server_proto_rawDescGZIP(), []int{26} } func (x *SessionRequest) GetRequestId() int64 { @@ -1505,6 +1608,13 @@ func (x *SessionRequest) GetPrefetchContents() *PrefetchContentsRequest { return nil } +func (x *SessionRequest) GetApplyRetentionPolicy() *ApplyRetentionPolicyRequest { + if x, ok := x.GetRequest().(*SessionRequest_ApplyRetentionPolicy); ok { + return x.ApplyRetentionPolicy + } + return nil +} + type isSessionRequest_Request interface { isSessionRequest_Request() } @@ -1549,6 +1659,10 @@ type SessionRequest_PrefetchContents struct { PrefetchContents *PrefetchContentsRequest `protobuf:"bytes,19,opt,name=prefetch_contents,json=prefetchContents,proto3,oneof"` } +type SessionRequest_ApplyRetentionPolicy struct { + ApplyRetentionPolicy *ApplyRetentionPolicyRequest `protobuf:"bytes,20,opt,name=apply_retention_policy,json=applyRetentionPolicy,proto3,oneof"` +} + func (*SessionRequest_InitializeSession) isSessionRequest_Request() {} func (*SessionRequest_GetContentInfo) isSessionRequest_Request() {} @@ -1569,13 +1683,15 @@ func (*SessionRequest_DeleteManifest) isSessionRequest_Request() {} func (*SessionRequest_PrefetchContents) isSessionRequest_Request() {} +func (*SessionRequest_ApplyRetentionPolicy) isSessionRequest_Request() {} + type SessionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields RequestId int64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` // corresponds to request ID - HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` + HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` // if set to true, the client should expect more responses with the same request_id. // Types that are assignable to Response: // *SessionResponse_Error // *SessionResponse_InitializeSession @@ -1588,13 +1704,14 @@ type SessionResponse struct { // *SessionResponse_FindManifests // *SessionResponse_DeleteManifest // *SessionResponse_PrefetchContents + // *SessionResponse_ApplyRetentionPolicy Response isSessionResponse_Response `protobuf_oneof:"response"` } func (x *SessionResponse) Reset() { *x = SessionResponse{} if protoimpl.UnsafeEnabled { - mi := &file_repository_server_proto_msgTypes[25] + mi := &file_repository_server_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1607,7 +1724,7 @@ func (x *SessionResponse) String() string { func (*SessionResponse) ProtoMessage() {} func (x *SessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_repository_server_proto_msgTypes[25] + mi := &file_repository_server_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1620,7 +1737,7 @@ func (x *SessionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionResponse.ProtoReflect.Descriptor instead. func (*SessionResponse) Descriptor() ([]byte, []int) { - return file_repository_server_proto_rawDescGZIP(), []int{25} + return file_repository_server_proto_rawDescGZIP(), []int{27} } func (x *SessionResponse) GetRequestId() int64 { @@ -1721,6 +1838,13 @@ func (x *SessionResponse) GetPrefetchContents() *PrefetchContentsResponse { return nil } +func (x *SessionResponse) GetApplyRetentionPolicy() *ApplyRetentionPolicyResponse { + if x, ok := x.GetResponse().(*SessionResponse_ApplyRetentionPolicy); ok { + return x.ApplyRetentionPolicy + } + return nil +} + type isSessionResponse_Response interface { isSessionResponse_Response() } @@ -1769,6 +1893,10 @@ type SessionResponse_PrefetchContents struct { PrefetchContents *PrefetchContentsResponse `protobuf:"bytes,19,opt,name=prefetch_contents,json=prefetchContents,proto3,oneof"` } +type SessionResponse_ApplyRetentionPolicy struct { + ApplyRetentionPolicy *ApplyRetentionPolicyResponse `protobuf:"bytes,20,opt,name=apply_retention_policy,json=applyRetentionPolicy,proto3,oneof"` +} + func (*SessionResponse_Error) isSessionResponse_Response() {} func (*SessionResponse_InitializeSession) isSessionResponse_Response() {} @@ -1791,6 +1919,8 @@ func (*SessionResponse_DeleteManifest) isSessionResponse_Response() {} func (*SessionResponse_PrefetchContents) isSessionResponse_Response() {} +func (*SessionResponse_ApplyRetentionPolicy) isSessionResponse_Response() {} + var File_repository_server_proto protoreflect.FileDescriptor var file_repository_server_proto_rawDesc = []byte{ @@ -1952,139 +2082,162 @@ func (*SessionResponse_PrefetchContents) isSessionResponse_Response() {} 0x0a, 0x18, 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x22, 0xe9, 0x07, 0x0a, 0x0e, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, - 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x57, 0x0a, - 0x0d, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x63, 0x65, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x5b, 0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, - 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x10, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, - 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, - 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x36, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, - 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, - 0x12, 0x4c, 0x0a, 0x0d, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, - 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, - 0x52, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x46, - 0x0a, 0x0b, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, - 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, + 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x22, 0x63, 0x0a, 0x1b, 0x41, + 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, + 0x65, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x61, 0x6c, 0x6c, 0x79, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x22, 0x41, 0x0a, 0x1c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x49, 0x64, 0x73, 0x22, 0xd0, 0x08, 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x57, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, - 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, - 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, - 0x0b, 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x4f, 0x0a, 0x0e, - 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, - 0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x52, 0x0a, - 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, - 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x12, 0x58, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6b, - 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, - 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x70, 0x72, 0x65, 0x66, 0x65, - 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x54, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xaf, 0x07, 0x0a, 0x0f, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, - 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, - 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x5c, + 0x52, 0x0c, 0x74, 0x72, 0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x5b, 0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x6b, 0x6f, 0x70, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x54, 0x0a, 0x10, - 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x48, 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0d, 0x77, - 0x72, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72, - 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x67, 0x65, - 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x10, 0x67, + 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x36, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4c, 0x0a, 0x0d, 0x77, 0x72, 0x69, 0x74, + 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x0b, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6b, 0x6f, + 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x49, + 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x0f, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, + 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x70, 0x75, 0x74, + 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, - 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, - 0x65, 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, - 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, - 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, - 0x4a, 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, - 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0b, - 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x50, 0x0a, 0x0e, 0x66, - 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, - 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0d, - 0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x53, 0x0a, - 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x48, 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, - 0x73, 0x74, 0x12, 0x59, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, - 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, - 0x2e, 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x10, 0x70, 0x72, 0x65, - 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x0a, 0x0a, - 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x0f, 0x4b, 0x6f, 0x70, - 0x69, 0x61, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x52, 0x0a, 0x07, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6b, 0x6f, 0x70, 0x69, - 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, - 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, - 0x6f, 0x70, 0x69, 0x61, 0x2f, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, + 0x66, 0x65, 0x73, 0x74, 0x12, 0x4f, 0x0a, 0x0e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, + 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, + 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, + 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x52, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, + 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x58, 0x0a, 0x11, 0x70, 0x72, 0x65, + 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x10, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x73, 0x12, 0x65, 0x0a, 0x16, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x14, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x48, 0x00, 0x52, 0x14, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x1a, 0x3f, 0x0a, 0x11, 0x54, 0x72, + 0x61, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x97, 0x08, 0x0a, 0x0f, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, + 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, + 0x4d, 0x6f, 0x72, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x5c, 0x0a, + 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x6b, 0x6f, 0x70, 0x69, + 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x69, + 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x54, 0x0a, 0x10, 0x67, + 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, + 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, + 0x6f, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0d, 0x77, 0x72, + 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72, 0x69, + 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x67, 0x65, 0x74, + 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, + 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, + 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, + 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4d, + 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, + 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x4a, + 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x10, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x70, + 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x50, 0x0a, 0x0e, 0x66, 0x69, + 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, + 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x66, + 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x53, 0x0a, 0x0f, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, + 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, + 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, + 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x12, 0x59, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6b, + 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x50, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x10, 0x70, 0x72, 0x65, 0x66, + 0x65, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x66, 0x0a, 0x16, + 0x61, 0x70, 0x70, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6b, + 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x14, + 0x61, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0x65, 0x0a, 0x0f, 0x4b, 0x6f, 0x70, 0x69, 0x61, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x12, 0x52, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, + 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x21, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x2f, 0x6b, 0x6f, 0x70, 0x69, + 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x61, + 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2100,50 +2253,52 @@ func file_repository_server_proto_rawDescGZIP() []byte { } var file_repository_server_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_repository_server_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_repository_server_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_repository_server_proto_goTypes = []interface{}{ - (ErrorResponse_Code)(0), // 0: kopia_repository.ErrorResponse.Code - (*ContentInfo)(nil), // 1: kopia_repository.ContentInfo - (*ManifestEntryMetadata)(nil), // 2: kopia_repository.ManifestEntryMetadata - (*ErrorResponse)(nil), // 3: kopia_repository.ErrorResponse - (*RepositoryParameters)(nil), // 4: kopia_repository.RepositoryParameters - (*InitializeSessionRequest)(nil), // 5: kopia_repository.InitializeSessionRequest - (*InitializeSessionResponse)(nil), // 6: kopia_repository.InitializeSessionResponse - (*GetContentInfoRequest)(nil), // 7: kopia_repository.GetContentInfoRequest - (*GetContentInfoResponse)(nil), // 8: kopia_repository.GetContentInfoResponse - (*GetContentRequest)(nil), // 9: kopia_repository.GetContentRequest - (*GetContentResponse)(nil), // 10: kopia_repository.GetContentResponse - (*FlushRequest)(nil), // 11: kopia_repository.FlushRequest - (*FlushResponse)(nil), // 12: kopia_repository.FlushResponse - (*WriteContentRequest)(nil), // 13: kopia_repository.WriteContentRequest - (*WriteContentResponse)(nil), // 14: kopia_repository.WriteContentResponse - (*GetManifestRequest)(nil), // 15: kopia_repository.GetManifestRequest - (*GetManifestResponse)(nil), // 16: kopia_repository.GetManifestResponse - (*PutManifestRequest)(nil), // 17: kopia_repository.PutManifestRequest - (*PutManifestResponse)(nil), // 18: kopia_repository.PutManifestResponse - (*DeleteManifestRequest)(nil), // 19: kopia_repository.DeleteManifestRequest - (*DeleteManifestResponse)(nil), // 20: kopia_repository.DeleteManifestResponse - (*FindManifestsRequest)(nil), // 21: kopia_repository.FindManifestsRequest - (*FindManifestsResponse)(nil), // 22: kopia_repository.FindManifestsResponse - (*PrefetchContentsRequest)(nil), // 23: kopia_repository.PrefetchContentsRequest - (*PrefetchContentsResponse)(nil), // 24: kopia_repository.PrefetchContentsResponse - (*SessionRequest)(nil), // 25: kopia_repository.SessionRequest - (*SessionResponse)(nil), // 26: kopia_repository.SessionResponse - nil, // 27: kopia_repository.ManifestEntryMetadata.LabelsEntry - nil, // 28: kopia_repository.PutManifestRequest.LabelsEntry - nil, // 29: kopia_repository.FindManifestsRequest.LabelsEntry - nil, // 30: kopia_repository.SessionRequest.TraceContextEntry + (ErrorResponse_Code)(0), // 0: kopia_repository.ErrorResponse.Code + (*ContentInfo)(nil), // 1: kopia_repository.ContentInfo + (*ManifestEntryMetadata)(nil), // 2: kopia_repository.ManifestEntryMetadata + (*ErrorResponse)(nil), // 3: kopia_repository.ErrorResponse + (*RepositoryParameters)(nil), // 4: kopia_repository.RepositoryParameters + (*InitializeSessionRequest)(nil), // 5: kopia_repository.InitializeSessionRequest + (*InitializeSessionResponse)(nil), // 6: kopia_repository.InitializeSessionResponse + (*GetContentInfoRequest)(nil), // 7: kopia_repository.GetContentInfoRequest + (*GetContentInfoResponse)(nil), // 8: kopia_repository.GetContentInfoResponse + (*GetContentRequest)(nil), // 9: kopia_repository.GetContentRequest + (*GetContentResponse)(nil), // 10: kopia_repository.GetContentResponse + (*FlushRequest)(nil), // 11: kopia_repository.FlushRequest + (*FlushResponse)(nil), // 12: kopia_repository.FlushResponse + (*WriteContentRequest)(nil), // 13: kopia_repository.WriteContentRequest + (*WriteContentResponse)(nil), // 14: kopia_repository.WriteContentResponse + (*GetManifestRequest)(nil), // 15: kopia_repository.GetManifestRequest + (*GetManifestResponse)(nil), // 16: kopia_repository.GetManifestResponse + (*PutManifestRequest)(nil), // 17: kopia_repository.PutManifestRequest + (*PutManifestResponse)(nil), // 18: kopia_repository.PutManifestResponse + (*DeleteManifestRequest)(nil), // 19: kopia_repository.DeleteManifestRequest + (*DeleteManifestResponse)(nil), // 20: kopia_repository.DeleteManifestResponse + (*FindManifestsRequest)(nil), // 21: kopia_repository.FindManifestsRequest + (*FindManifestsResponse)(nil), // 22: kopia_repository.FindManifestsResponse + (*PrefetchContentsRequest)(nil), // 23: kopia_repository.PrefetchContentsRequest + (*PrefetchContentsResponse)(nil), // 24: kopia_repository.PrefetchContentsResponse + (*ApplyRetentionPolicyRequest)(nil), // 25: kopia_repository.ApplyRetentionPolicyRequest + (*ApplyRetentionPolicyResponse)(nil), // 26: kopia_repository.ApplyRetentionPolicyResponse + (*SessionRequest)(nil), // 27: kopia_repository.SessionRequest + (*SessionResponse)(nil), // 28: kopia_repository.SessionResponse + nil, // 29: kopia_repository.ManifestEntryMetadata.LabelsEntry + nil, // 30: kopia_repository.PutManifestRequest.LabelsEntry + nil, // 31: kopia_repository.FindManifestsRequest.LabelsEntry + nil, // 32: kopia_repository.SessionRequest.TraceContextEntry } var file_repository_server_proto_depIdxs = []int32{ - 27, // 0: kopia_repository.ManifestEntryMetadata.labels:type_name -> kopia_repository.ManifestEntryMetadata.LabelsEntry + 29, // 0: kopia_repository.ManifestEntryMetadata.labels:type_name -> kopia_repository.ManifestEntryMetadata.LabelsEntry 0, // 1: kopia_repository.ErrorResponse.code:type_name -> kopia_repository.ErrorResponse.Code 4, // 2: kopia_repository.InitializeSessionResponse.parameters:type_name -> kopia_repository.RepositoryParameters 1, // 3: kopia_repository.GetContentInfoResponse.info:type_name -> kopia_repository.ContentInfo 2, // 4: kopia_repository.GetManifestResponse.metadata:type_name -> kopia_repository.ManifestEntryMetadata - 28, // 5: kopia_repository.PutManifestRequest.labels:type_name -> kopia_repository.PutManifestRequest.LabelsEntry - 29, // 6: kopia_repository.FindManifestsRequest.labels:type_name -> kopia_repository.FindManifestsRequest.LabelsEntry + 30, // 5: kopia_repository.PutManifestRequest.labels:type_name -> kopia_repository.PutManifestRequest.LabelsEntry + 31, // 6: kopia_repository.FindManifestsRequest.labels:type_name -> kopia_repository.FindManifestsRequest.LabelsEntry 2, // 7: kopia_repository.FindManifestsResponse.metadata:type_name -> kopia_repository.ManifestEntryMetadata - 30, // 8: kopia_repository.SessionRequest.trace_context:type_name -> kopia_repository.SessionRequest.TraceContextEntry + 32, // 8: kopia_repository.SessionRequest.trace_context:type_name -> kopia_repository.SessionRequest.TraceContextEntry 5, // 9: kopia_repository.SessionRequest.initialize_session:type_name -> kopia_repository.InitializeSessionRequest 7, // 10: kopia_repository.SessionRequest.get_content_info:type_name -> kopia_repository.GetContentInfoRequest 11, // 11: kopia_repository.SessionRequest.flush:type_name -> kopia_repository.FlushRequest @@ -2154,24 +2309,26 @@ func file_repository_server_proto_rawDescGZIP() []byte { 21, // 16: kopia_repository.SessionRequest.find_manifests:type_name -> kopia_repository.FindManifestsRequest 19, // 17: kopia_repository.SessionRequest.delete_manifest:type_name -> kopia_repository.DeleteManifestRequest 23, // 18: kopia_repository.SessionRequest.prefetch_contents:type_name -> kopia_repository.PrefetchContentsRequest - 3, // 19: kopia_repository.SessionResponse.error:type_name -> kopia_repository.ErrorResponse - 6, // 20: kopia_repository.SessionResponse.initialize_session:type_name -> kopia_repository.InitializeSessionResponse - 8, // 21: kopia_repository.SessionResponse.get_content_info:type_name -> kopia_repository.GetContentInfoResponse - 12, // 22: kopia_repository.SessionResponse.flush:type_name -> kopia_repository.FlushResponse - 14, // 23: kopia_repository.SessionResponse.write_content:type_name -> kopia_repository.WriteContentResponse - 10, // 24: kopia_repository.SessionResponse.get_content:type_name -> kopia_repository.GetContentResponse - 16, // 25: kopia_repository.SessionResponse.get_manifest:type_name -> kopia_repository.GetManifestResponse - 18, // 26: kopia_repository.SessionResponse.put_manifest:type_name -> kopia_repository.PutManifestResponse - 22, // 27: kopia_repository.SessionResponse.find_manifests:type_name -> kopia_repository.FindManifestsResponse - 20, // 28: kopia_repository.SessionResponse.delete_manifest:type_name -> kopia_repository.DeleteManifestResponse - 24, // 29: kopia_repository.SessionResponse.prefetch_contents:type_name -> kopia_repository.PrefetchContentsResponse - 25, // 30: kopia_repository.KopiaRepository.Session:input_type -> kopia_repository.SessionRequest - 26, // 31: kopia_repository.KopiaRepository.Session:output_type -> kopia_repository.SessionResponse - 31, // [31:32] is the sub-list for method output_type - 30, // [30:31] is the sub-list for method input_type - 30, // [30:30] is the sub-list for extension type_name - 30, // [30:30] is the sub-list for extension extendee - 0, // [0:30] is the sub-list for field type_name + 25, // 19: kopia_repository.SessionRequest.apply_retention_policy:type_name -> kopia_repository.ApplyRetentionPolicyRequest + 3, // 20: kopia_repository.SessionResponse.error:type_name -> kopia_repository.ErrorResponse + 6, // 21: kopia_repository.SessionResponse.initialize_session:type_name -> kopia_repository.InitializeSessionResponse + 8, // 22: kopia_repository.SessionResponse.get_content_info:type_name -> kopia_repository.GetContentInfoResponse + 12, // 23: kopia_repository.SessionResponse.flush:type_name -> kopia_repository.FlushResponse + 14, // 24: kopia_repository.SessionResponse.write_content:type_name -> kopia_repository.WriteContentResponse + 10, // 25: kopia_repository.SessionResponse.get_content:type_name -> kopia_repository.GetContentResponse + 16, // 26: kopia_repository.SessionResponse.get_manifest:type_name -> kopia_repository.GetManifestResponse + 18, // 27: kopia_repository.SessionResponse.put_manifest:type_name -> kopia_repository.PutManifestResponse + 22, // 28: kopia_repository.SessionResponse.find_manifests:type_name -> kopia_repository.FindManifestsResponse + 20, // 29: kopia_repository.SessionResponse.delete_manifest:type_name -> kopia_repository.DeleteManifestResponse + 24, // 30: kopia_repository.SessionResponse.prefetch_contents:type_name -> kopia_repository.PrefetchContentsResponse + 26, // 31: kopia_repository.SessionResponse.apply_retention_policy:type_name -> kopia_repository.ApplyRetentionPolicyResponse + 27, // 32: kopia_repository.KopiaRepository.Session:input_type -> kopia_repository.SessionRequest + 28, // 33: kopia_repository.KopiaRepository.Session:output_type -> kopia_repository.SessionResponse + 33, // [33:34] is the sub-list for method output_type + 32, // [32:33] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_repository_server_proto_init() } @@ -2469,7 +2626,7 @@ func file_repository_server_proto_init() { } } file_repository_server_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SessionRequest); i { + switch v := v.(*ApplyRetentionPolicyRequest); i { case 0: return &v.state case 1: @@ -2481,6 +2638,30 @@ func file_repository_server_proto_init() { } } file_repository_server_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ApplyRetentionPolicyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_server_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SessionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_repository_server_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SessionResponse); i { case 0: return &v.state @@ -2493,7 +2674,7 @@ func file_repository_server_proto_init() { } } } - file_repository_server_proto_msgTypes[24].OneofWrappers = []interface{}{ + file_repository_server_proto_msgTypes[26].OneofWrappers = []interface{}{ (*SessionRequest_InitializeSession)(nil), (*SessionRequest_GetContentInfo)(nil), (*SessionRequest_Flush)(nil), @@ -2504,8 +2685,9 @@ func file_repository_server_proto_init() { (*SessionRequest_FindManifests)(nil), (*SessionRequest_DeleteManifest)(nil), (*SessionRequest_PrefetchContents)(nil), + (*SessionRequest_ApplyRetentionPolicy)(nil), } - file_repository_server_proto_msgTypes[25].OneofWrappers = []interface{}{ + file_repository_server_proto_msgTypes[27].OneofWrappers = []interface{}{ (*SessionResponse_Error)(nil), (*SessionResponse_InitializeSession)(nil), (*SessionResponse_GetContentInfo)(nil), @@ -2517,6 +2699,7 @@ func file_repository_server_proto_init() { (*SessionResponse_FindManifests)(nil), (*SessionResponse_DeleteManifest)(nil), (*SessionResponse_PrefetchContents)(nil), + (*SessionResponse_ApplyRetentionPolicy)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -2524,7 +2707,7 @@ type x struct{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_repository_server_proto_rawDesc, NumEnums: 1, - NumMessages: 30, + NumMessages: 32, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/grpcapi/repository_server.proto b/internal/grpcapi/repository_server.proto index 7969a6ce3..c6edd354d 100644 --- a/internal/grpcapi/repository_server.proto +++ b/internal/grpcapi/repository_server.proto @@ -136,6 +136,15 @@ message PrefetchContentsResponse { repeated string content_ids = 1; } +message ApplyRetentionPolicyRequest { + string source_path = 1; + bool really_delete = 2; +} + +message ApplyRetentionPolicyResponse { + repeated string manifest_ids = 1; +} + message SessionRequest { int64 request_id = 1; map trace_context = 2; @@ -154,6 +163,7 @@ message SessionRequest { FindManifestsRequest find_manifests = 17; DeleteManifestRequest delete_manifest = 18; PrefetchContentsRequest prefetch_contents = 19; + ApplyRetentionPolicyRequest apply_retention_policy = 20; } } @@ -174,6 +184,7 @@ message SessionResponse { FindManifestsResponse find_manifests = 17; DeleteManifestResponse delete_manifest = 18; PrefetchContentsResponse prefetch_contents = 19; + ApplyRetentionPolicyResponse apply_retention_policy = 20; } } diff --git a/internal/remoterepoapi/remoterepoapi.go b/internal/remoterepoapi/remoterepoapi.go index 4deec668a..90534ac81 100644 --- a/internal/remoterepoapi/remoterepoapi.go +++ b/internal/remoterepoapi/remoterepoapi.go @@ -37,7 +37,18 @@ type PrefetchContentsRequest struct { Hint string `json:"hint"` } -// PrefetchContentsResponse represents a request from request to prefetch contents. +// PrefetchContentsResponse represents a response from request to prefetch contents. type PrefetchContentsResponse struct { ContentIDs []content.ID `json:"contents"` } + +// ApplyRetentionPolicyRequest represents a request to apply retention policy to a given source path. +type ApplyRetentionPolicyRequest struct { + SourcePath string `json:"sourcePath"` + ReallyDelete bool `json:"reallyDelete"` +} + +// ApplyRetentionPolicyResponse represents a response to a request to apply retention policy. +type ApplyRetentionPolicyResponse struct { + ManifestIDs []manifest.ID `json:"manifests"` +} diff --git a/internal/server/api_manifest.go b/internal/server/api_manifest.go index abf47e2af..48cf405e0 100644 --- a/internal/server/api_manifest.go +++ b/internal/server/api_manifest.go @@ -3,6 +3,7 @@ import ( "context" "encoding/json" + "strings" "github.com/pkg/errors" @@ -11,6 +12,8 @@ "github.com/kopia/kopia/internal/serverapi" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/manifest" + "github.com/kopia/kopia/snapshot" + "github.com/kopia/kopia/snapshot/policy" ) func handleManifestGet(ctx context.Context, rc requestContext) (interface{}, *apiError) { @@ -123,3 +126,47 @@ func handleManifestCreate(ctx context.Context, rc requestContext) (interface{}, return &manifest.EntryMetadata{ID: id}, nil } + +func handleApplyRetentionPolicy(ctx context.Context, rc requestContext) (interface{}, *apiError) { + rw, ok := rc.rep.(repo.RepositoryWriter) + if !ok { + return nil, repositoryNotWritableError() + } + + var req remoterepoapi.ApplyRetentionPolicyRequest + + if err := json.Unmarshal(rc.body, &req); err != nil { + return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request") + } + + usernameAtHostname, _, _ := rc.req.BasicAuth() + + parts := strings.Split(usernameAtHostname, "@") + if len(parts) != 2 { //nolint:gomnd + return nil, requestError(serverapi.ErrorMalformedRequest, "malformed username") + } + + // only allow users to apply retention policy if they have permission to add snapshots + // for a particular path. + if !hasManifestAccess(ctx, rc, map[string]string{ + manifest.TypeLabelKey: snapshot.ManifestType, + snapshot.UsernameLabel: parts[0], + snapshot.HostnameLabel: parts[1], + snapshot.PathLabel: req.SourcePath, + }, auth.AccessLevelAppend) { + return nil, accessDeniedError() + } + + ids, err := policy.ApplyRetentionPolicy(ctx, rw, snapshot.SourceInfo{ + UserName: parts[0], + Host: parts[1], + Path: req.SourcePath, + }, req.ReallyDelete) + if err != nil { + return nil, internalServerError(err) + } + + return &remoterepoapi.ApplyRetentionPolicyResponse{ + ManifestIDs: ids, + }, nil +} diff --git a/internal/server/grpc_session.go b/internal/server/grpc_session.go index b49ca0855..3dbe2cc4a 100644 --- a/internal/server/grpc_session.go +++ b/internal/server/grpc_session.go @@ -26,6 +26,8 @@ "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/repo/object" + "github.com/kopia/kopia/snapshot" + "github.com/kopia/kopia/snapshot/policy" ) type grpcServerState struct { @@ -82,12 +84,12 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error { return status.Errorf(codes.Unavailable, "not connected to a direct repository") } - username, err := s.authenticateGRPCSession(ctx, dr) + usernameAtHostname, err := s.authenticateGRPCSession(ctx, dr) if err != nil { return err } - authz := s.authorizer.Authorize(ctx, dr, username) + authz := s.authorizer.Authorize(ctx, dr, usernameAtHostname) if authz == nil { authz = auth.NoAccess() } @@ -97,8 +99,8 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error { return status.Errorf(codes.PermissionDenied, "peer not found in context") } - log(ctx).Infof("starting session for user %q from %v", username, p.Addr) - defer log(ctx).Infof("session ended for user %q from %v", username, p.Addr) + log(ctx).Infof("starting session for user %q from %v", usernameAtHostname, p.Addr) + defer log(ctx).Infof("session ended for user %q from %v", usernameAtHostname, p.Addr) opt, err := s.handleInitialSessionHandshake(srv, dr) if err != nil { @@ -131,7 +133,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error { go func() { defer s.grpcServerState.sem.Release(1) - handleSessionRequest(ctx, dw, authz, req, func(resp *grpcapi.SessionResponse) { + handleSessionRequest(ctx, dw, authz, usernameAtHostname, req, func(resp *grpcapi.SessionResponse) { if err := s.send(srv, req.RequestId, resp); err != nil { select { case lastErr <- err: @@ -148,7 +150,7 @@ func (s *Server) Session(srv grpcapi.KopiaRepository_SessionServer) error { var tracer = otel.Tracer("kopia/grpc") -func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) { +func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.SessionRequest, respond func(*grpcapi.SessionResponse)) { if req.TraceContext != nil { var tc propagation.TraceContext ctx = tc.Extract(ctx, propagation.MapCarrier(req.TraceContext)) @@ -182,6 +184,9 @@ func handleSessionRequest(ctx context.Context, dw repo.DirectRepositoryWriter, a case *grpcapi.SessionRequest_PrefetchContents: respond(handlePrefetchContentsRequest(ctx, dw, authz, inner.PrefetchContents)) + case *grpcapi.SessionRequest_ApplyRetentionPolicy: + respond(handleApplyRetentionPolicyRequest(ctx, dw, authz, usernameAtHostname, inner.ApplyRetentionPolicy)) + case *grpcapi.SessionRequest_InitializeSession: respond(errorResponse(errors.Errorf("InitializeSession must be the first request in a session"))) @@ -440,6 +445,47 @@ func handlePrefetchContentsRequest(ctx context.Context, rep repo.Repository, aut } } +func handleApplyRetentionPolicyRequest(ctx context.Context, rep repo.RepositoryWriter, authz auth.AuthorizationInfo, usernameAtHostname string, req *grpcapi.ApplyRetentionPolicyRequest) *grpcapi.SessionResponse { + ctx, span := tracer.Start(ctx, "GRPCSession.ApplyRetentionPolicy") + defer span.End() + + parts := strings.Split(usernameAtHostname, "@") + if len(parts) != 2 { //nolint:gomnd + return errorResponse(errors.Errorf("invalid username@hostname: %q", usernameAtHostname)) + } + + username := parts[0] + hostname := parts[1] + + // only allow users to apply retention policy if they have permission to add snapshots + // for a particular path. + if authz.ManifestAccessLevel(map[string]string{ + manifest.TypeLabelKey: snapshot.ManifestType, + snapshot.UsernameLabel: username, + snapshot.HostnameLabel: hostname, + snapshot.PathLabel: req.SourcePath, + }) < auth.AccessLevelAppend { + return accessDeniedResponse() + } + + manifestIDs, err := policy.ApplyRetentionPolicy(ctx, rep, snapshot.SourceInfo{ + Host: hostname, + UserName: username, + Path: req.SourcePath, + }, req.ReallyDelete) + if err != nil { + return errorResponse(err) + } + + return &grpcapi.SessionResponse{ + Response: &grpcapi.SessionResponse_ApplyRetentionPolicy{ + ApplyRetentionPolicy: &grpcapi.ApplyRetentionPolicyResponse{ + ManifestIds: manifest.IDsToStrings(manifestIDs), + }, + }, + } +} + func accessDeniedResponse() *grpcapi.SessionResponse { return &grpcapi.SessionResponse{ Response: &grpcapi.SessionResponse_Error{ diff --git a/internal/server/server.go b/internal/server/server.go index dd6ddc53b..a9917ea6f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -163,6 +163,7 @@ func (s *Server) SetupRepositoryAPIHandlers(m *mux.Router) { m.HandleFunc("/api/v1/manifests/{manifestID}", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleManifestDelete)).Methods(http.MethodDelete) m.HandleFunc("/api/v1/manifests", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleManifestCreate)).Methods(http.MethodPost) m.HandleFunc("/api/v1/manifests", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleManifestList)).Methods(http.MethodGet) + m.HandleFunc("/api/v1/policies/apply-retention", s.handleRepositoryAPI(handlerWillCheckAuthorization, handleApplyRetentionPolicy)).Methods(http.MethodPost) } // SetupControlAPIHandlers registers control API handlers. diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go index 154ddd9c9..f4e44a996 100644 --- a/repo/api_server_repository.go +++ b/repo/api_server_repository.go @@ -282,6 +282,19 @@ func (r *apiServerRepository) PrefetchContents(ctx context.Context, contentIDs [ return resp.ContentIDs } +func (r *apiServerRepository) ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error) { + var result remoterepoapi.ApplyRetentionPolicyResponse + + if err := r.cli.Post(ctx, "policies/apply-retention", remoterepoapi.ApplyRetentionPolicyRequest{ + SourcePath: sourcePath, + ReallyDelete: reallyDelete, + }, &result); err != nil { + return nil, errors.Wrap(err, "unable to apply retention policy") + } + + return result.ManifestIDs, nil +} + // OnSuccessfulFlush registers the provided callback to be invoked after flush succeeds. func (r *apiServerRepository) OnSuccessfulFlush(callback RepositoryWriterCallback) { r.afterFlush = append(r.afterFlush, callback) diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go index 809128dab..81275eab7 100644 --- a/repo/grpc_repository_client.go +++ b/repo/grpc_repository_client.go @@ -441,6 +441,33 @@ func (r *grpcInnerSession) PrefetchContents(ctx context.Context, contentIDs []co return nil } +func (r *grpcRepositoryClient) ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error) { + return inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) ([]manifest.ID, error) { + return sess.ApplyRetentionPolicy(ctx, sourcePath, reallyDelete) + }) +} + +func (r *grpcInnerSession) ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error) { + for resp := range r.sendRequest(ctx, &apipb.SessionRequest{ + Request: &apipb.SessionRequest_ApplyRetentionPolicy{ + ApplyRetentionPolicy: &apipb.ApplyRetentionPolicyRequest{ + SourcePath: sourcePath, + ReallyDelete: reallyDelete, + }, + }, + }) { + switch rr := resp.Response.(type) { + case *apipb.SessionResponse_ApplyRetentionPolicy: + return manifest.IDsFromStrings(rr.ApplyRetentionPolicy.ManifestIds), nil + + default: + return nil, unhandledSessionResponse(resp) + } + } + + return nil, errNoSessionResponse() +} + func (r *grpcRepositoryClient) Time() time.Time { return clock.Now() } diff --git a/repo/manifest/manifest_manager.go b/repo/manifest/manifest_manager.go index 9b02459d6..8bb2f39f4 100644 --- a/repo/manifest/manifest_manager.go +++ b/repo/manifest/manifest_manager.go @@ -253,6 +253,28 @@ func (m *Manager) Compact(ctx context.Context) error { return m.committed.compact(ctx) } +// IDsToStrings converts the IDs to strings. +func IDsToStrings(input []ID) []string { + var result []string + + for _, v := range input { + result = append(result, string(v)) + } + + return result +} + +// IDsFromStrings converts the IDs to strings. +func IDsFromStrings(input []string) []ID { + var result []ID + + for _, v := range input { + result = append(result, ID(v)) + } + + return result +} + func copyLabels(m map[string]string) map[string]string { r := map[string]string{} for k, v := range m { diff --git a/repo/repository.go b/repo/repository.go index 7883f35fe..1110625bf 100644 --- a/repo/repository.go +++ b/repo/repository.go @@ -54,6 +54,12 @@ type RepositoryWriter interface { Flush(ctx context.Context) error } +// RemoteRetentionPolicy is an interface implemented by repository clients that support remote retention policy. +// when implemented, the repository server will invoke ApplyRetentionPolicy() server-side. +type RemoteRetentionPolicy interface { + ApplyRetentionPolicy(ctx context.Context, sourcePath string, reallyDelete bool) ([]manifest.ID, error) +} + // DirectRepository provides additional low-level repository functionality. // //nolint:interfacebloat diff --git a/snapshot/policy/expire.go b/snapshot/policy/expire.go index 7bf086453..fbf05b8a3 100644 --- a/snapshot/policy/expire.go +++ b/snapshot/policy/expire.go @@ -7,11 +7,24 @@ "github.com/pkg/errors" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot" ) // ApplyRetentionPolicy applies retention policy to a given source by deleting expired snapshots. -func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceInfo snapshot.SourceInfo, reallyDelete bool) ([]*snapshot.Manifest, error) { +func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceInfo snapshot.SourceInfo, reallyDelete bool) ([]manifest.ID, error) { + // it is desired to not allow snapshots to be deleted by repository clients, + // while still maintain the ability to apply snapshot retention policies server-side. + if remote, ok := rep.(repo.RemoteRetentionPolicy); ok { + if sourceInfo.UserName == rep.ClientOptions().Username && sourceInfo.Host == rep.ClientOptions().Hostname { + // for repository clients, apply retention policy on the server. + log(ctx).Debug("applying retention policy on the server") + + //nolint:wrapcheck + return remote.ApplyRetentionPolicy(ctx, sourceInfo.Path, reallyDelete) + } + } + snapshots, err := snapshot.ListSnapshots(ctx, rep, sourceInfo) if err != nil { return nil, errors.Wrap(err, "error listing snapshots") @@ -23,9 +36,9 @@ func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, source } if reallyDelete { - for _, it := range toDelete { - if err := rep.DeleteManifest(ctx, it.ID); err != nil { - return toDelete, errors.Wrapf(err, "error deleting manifest %v", it.ID) + for _, manifestID := range toDelete { + if err := rep.DeleteManifest(ctx, manifestID); err != nil { + return toDelete, errors.Wrapf(err, "error deleting manifest %v", manifestID) } } } @@ -33,8 +46,8 @@ func ApplyRetentionPolicy(ctx context.Context, rep repo.RepositoryWriter, source return toDelete, nil } -func getExpiredSnapshots(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]*snapshot.Manifest, error) { - var toDelete []*snapshot.Manifest +func getExpiredSnapshots(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]manifest.ID, error) { + var toDelete []manifest.ID for _, snapshotGroup := range snapshot.GroupBySource(snapshots) { td, err := getExpiredSnapshotsForSource(ctx, rep, snapshotGroup) @@ -48,7 +61,7 @@ func getExpiredSnapshots(ctx context.Context, rep repo.Repository, snapshots []* return toDelete, nil } -func getExpiredSnapshotsForSource(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]*snapshot.Manifest, error) { +func getExpiredSnapshotsForSource(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]manifest.ID, error) { src := snapshots[0].Source pol, _, _, err := GetEffectivePolicy(ctx, rep, src) @@ -58,12 +71,12 @@ func getExpiredSnapshotsForSource(ctx context.Context, rep repo.Repository, snap pol.RetentionPolicy.ComputeRetentionReasons(snapshots) - var toDelete []*snapshot.Manifest + var toDelete []manifest.ID for _, s := range snapshots { if len(s.RetentionReasons) == 0 && len(s.Pins) == 0 { log(ctx).Debugf(" deleting %v", s.StartTime) - toDelete = append(toDelete, s) + toDelete = append(toDelete, s.ID) } else { log(ctx).Debugf(" keeping %v retention: [%v] pins: [%v]", s.StartTime, strings.Join(s.RetentionReasons, ","), strings.Join(s.Pins, ",")) } diff --git a/tests/end_to_end_test/acl_test.go b/tests/end_to_end_test/acl_test.go index 56f5de556..6faa2b4f5 100644 --- a/tests/end_to_end_test/acl_test.go +++ b/tests/end_to_end_test/acl_test.go @@ -1,17 +1,34 @@ package endtoend_test import ( + "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/kopia/kopia/internal/auth" "github.com/kopia/kopia/internal/testutil" "github.com/kopia/kopia/tests/clitestutil" "github.com/kopia/kopia/tests/testenv" ) -func TestACL(t *testing.T) { +func TestACL_GRPC(t *testing.T) { + verifyACL(t, false) +} + +func TestACL_HTTP(t *testing.T) { + verifyACL(t, true) +} + +//nolint:thelper +func verifyACL(t *testing.T, disableGRPC bool) { t.Parallel() + grpcArgument := "--grpc" + if disableGRPC { + grpcArgument = "--no-grpc" + } + serverRunner := testenv.NewInProcRunner(t) serverEnvironment := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, serverRunner) @@ -19,27 +36,38 @@ func TestACL(t *testing.T) { serverEnvironment.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", serverEnvironment.RepoDir, "--override-hostname=foo", "--override-username=foo", "--enable-actions") - if got, want := len(serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list")), 0; got != want { - t.Fatalf("unexpected ACLs found") - } + require.Len(t, serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list"), 0) // enable ACLs - that should insert all the rules. serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "enable") - if got, want := len(serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list")), len(auth.DefaultACLs); got != want { - t.Fatalf("unexpected ACLs found") - } + require.Len(t, serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "list"), len(auth.DefaultACLs)) + + // reduce default access to snapshots to APPEND - this will fail because exactly identical rule already exists and grants FULL access. + serverEnvironment.RunAndExpectFailure(t, "server", "acl", "add", "--user", "*@*", "--target", "type=snapshot,username=OWN_USER,hostname=OWN_HOST", "--access=APPEND") + + // reduce default access to snapshots to APPEND with --overwrite, this wil succeed. + serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "*@*", "--target", "type=snapshot,username=OWN_USER,hostname=OWN_HOST", "--access=APPEND", "--overwrite") // add read access to all snapshots and policies for user foo@bar serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "foo@bar", "--target", "type=snapshot", "--access=READ") serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "foo@bar", "--target", "type=policy", "--access=READ") + // add append access to all snapshots and read-only access to policies for user another@bar + serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "another@bar", "--target", "type=snapshot", "--access=APPEND") + serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "another@bar", "--target", "type=policy", "--access=READ") + // add full access to global policy for all users serverEnvironment.RunAndExpectSuccess(t, "server", "acl", "add", "--user", "*@*", "--target", "type=policy,policyType=global", "--access=FULL") serverEnvironment.RunAndExpectSuccess(t, "server", "users", "add", "foo@bar", "--user-password", "baz") + serverEnvironment.RunAndExpectSuccess(t, "server", "users", "add", "another@bar", "--user-password", "baz") serverEnvironment.RunAndExpectSuccess(t, "server", "users", "add", "alice@wonderland", "--user-password", "baz") + const keepLatestSnapshots = 3 + + serverEnvironment.RunAndExpectSuccess(t, "policy", "set", "another@bar", fmt.Sprintf("--keep-latest=%v", keepLatestSnapshots)) + var sp testutil.ServerParameters wait, kill := serverEnvironment.RunAndProcessStderr(t, sp.ProcessOutput, @@ -70,6 +98,24 @@ func TestACL(t *testing.T) { "--override-username", "foo", "--override-hostname", "bar", "--password", "baz", + grpcArgument, + ) + + anotherBarRunner := testenv.NewInProcRunner(t) + anotherBarClientEnvironment := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, anotherBarRunner) + + defer anotherBarClientEnvironment.RunAndExpectSuccess(t, "repo", "disconnect") + + delete(anotherBarClientEnvironment.Environment, "KOPIA_PASSWORD") + + // connect as foo@bar with password baz + anotherBarClientEnvironment.RunAndExpectSuccess(t, "repo", "connect", "server", + "--url", sp.BaseURL+"/", + "--server-cert-fingerprint", sp.SHA256Fingerprint, + "--override-username", "another", + "--override-hostname", "bar", + "--password", "baz", + grpcArgument, ) aliceInWonderlandRunner := testenv.NewInProcRunner(t) @@ -86,6 +132,7 @@ func TestACL(t *testing.T) { "--override-username", "alice", "--override-hostname", "wonderland", "--password", "baz", + grpcArgument, ) // both alice and foo@bar can see global policy @@ -117,6 +164,23 @@ func TestACL(t *testing.T) { t.Fatalf("foo@bar expected to see 1 source (own), got %v", snaps) } + // another@bar can create snapshots but not delete them + anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1) + anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1) + anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1) + anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1) + anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1) + anotherBarClientEnvironment.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1) + + // make sure only `keepLatestSnapshots` snapshots are kept, so retention policy + // is working. + snapshots := clitestutil.ListSnapshotsAndExpectSuccess(t, anotherBarClientEnvironment, sharedTestDataDir1)[0].Snapshots + require.Len(t, snapshots, keepLatestSnapshots) + + // APPEND policy despite being able to maintain retention rules, prevents snapshots from being deleted + // by the client. + anotherBarClientEnvironment.RunAndExpectFailure(t, "snapshot", "delete", snapshots[0].SnapshotID, "--delete") + // alice changes her own password and reconnects aliceInWonderlandClientEnvironment.RunAndExpectSuccess(t, "server", "users", "set", "alice@wonderland", "--user-password", "new-password")