diff --git a/app/src/types/generated/firewall_pb.d.ts b/app/src/types/generated/firewall_pb.d.ts index 2d15559b0..f2739fb0a 100644 --- a/app/src/types/generated/firewall_pb.d.ts +++ b/app/src/types/generated/firewall_pb.d.ts @@ -15,6 +15,11 @@ export class PrivacyMapConversionRequest extends jspb.Message { getInput(): string; setInput(value: string): void; + getGroupId(): Uint8Array | string; + getGroupId_asU8(): Uint8Array; + getGroupId_asB64(): string; + setGroupId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): PrivacyMapConversionRequest.AsObject; static toObject(includeInstance: boolean, msg: PrivacyMapConversionRequest): PrivacyMapConversionRequest.AsObject; @@ -30,6 +35,7 @@ export namespace PrivacyMapConversionRequest { realToPseudo: boolean, sessionId: Uint8Array | string, input: string, + groupId: Uint8Array | string, } } @@ -89,6 +95,11 @@ export class ListActionsRequest extends jspb.Message { getEndTimestamp(): string; setEndTimestamp(value: string): void; + getGroupId(): Uint8Array | string; + getGroupId_asU8(): Uint8Array; + getGroupId_asB64(): string; + setGroupId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ListActionsRequest.AsObject; static toObject(includeInstance: boolean, msg: ListActionsRequest): ListActionsRequest.AsObject; @@ -112,6 +123,7 @@ export namespace ListActionsRequest { sessionId: Uint8Array | string, startTimestamp: string, endTimestamp: string, + groupId: Uint8Array | string, } } diff --git a/app/src/types/generated/firewall_pb.js b/app/src/types/generated/firewall_pb.js index 76fdae118..e8cdc0ed9 100644 --- a/app/src/types/generated/firewall_pb.js +++ b/app/src/types/generated/firewall_pb.js @@ -69,7 +69,8 @@ proto.litrpc.PrivacyMapConversionRequest.toObject = function(includeInstance, ms var f, obj = { realToPseudo: jspb.Message.getFieldWithDefault(msg, 1, false), sessionId: msg.getSessionId_asB64(), - input: jspb.Message.getFieldWithDefault(msg, 3, "") + input: jspb.Message.getFieldWithDefault(msg, 3, ""), + groupId: msg.getGroupId_asB64() }; if (includeInstance) { @@ -118,6 +119,10 @@ proto.litrpc.PrivacyMapConversionRequest.deserializeBinaryFromReader = function( var value = /** @type {string} */ (reader.readString()); msg.setInput(value); break; + case 4: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setGroupId(value); + break; default: reader.skipField(); break; @@ -168,6 +173,13 @@ proto.litrpc.PrivacyMapConversionRequest.serializeBinaryToWriter = function(mess f ); } + f = message.getGroupId_asU8(); + if (f.length > 0) { + writer.writeBytes( + 4, + f + ); + } }; @@ -242,6 +254,45 @@ proto.litrpc.PrivacyMapConversionRequest.prototype.setInput = function(value) { }; +/** + * optional bytes group_id = 4; + * @return {!(string|Uint8Array)} + */ +proto.litrpc.PrivacyMapConversionRequest.prototype.getGroupId = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * optional bytes group_id = 4; + * This is a type-conversion wrapper around `getGroupId()` + * @return {string} + */ +proto.litrpc.PrivacyMapConversionRequest.prototype.getGroupId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getGroupId())); +}; + + +/** + * optional bytes group_id = 4; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getGroupId()` + * @return {!Uint8Array} + */ +proto.litrpc.PrivacyMapConversionRequest.prototype.getGroupId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getGroupId())); +}; + + +/** @param {!(string|Uint8Array)} value */ +proto.litrpc.PrivacyMapConversionRequest.prototype.setGroupId = function(value) { + jspb.Message.setProto3BytesField(this, 4, value); +}; + + /** * Generated by JsPbCodeGenerator. @@ -441,7 +492,8 @@ proto.litrpc.ListActionsRequest.toObject = function(includeInstance, msg) { countTotal: jspb.Message.getFieldWithDefault(msg, 8, false), sessionId: msg.getSessionId_asB64(), startTimestamp: jspb.Message.getFieldWithDefault(msg, 10, "0"), - endTimestamp: jspb.Message.getFieldWithDefault(msg, 11, "0") + endTimestamp: jspb.Message.getFieldWithDefault(msg, 11, "0"), + groupId: msg.getGroupId_asB64() }; if (includeInstance) { @@ -522,6 +574,10 @@ proto.litrpc.ListActionsRequest.deserializeBinaryFromReader = function(msg, read var value = /** @type {string} */ (reader.readUint64String()); msg.setEndTimestamp(value); break; + case 12: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setGroupId(value); + break; default: reader.skipField(); break; @@ -628,6 +684,13 @@ proto.litrpc.ListActionsRequest.serializeBinaryToWriter = function(message, writ f ); } + f = message.getGroupId_asU8(); + if (f.length > 0) { + writer.writeBytes( + 12, + f + ); + } }; @@ -824,6 +887,45 @@ proto.litrpc.ListActionsRequest.prototype.setEndTimestamp = function(value) { }; +/** + * optional bytes group_id = 12; + * @return {!(string|Uint8Array)} + */ +proto.litrpc.ListActionsRequest.prototype.getGroupId = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 12, "")); +}; + + +/** + * optional bytes group_id = 12; + * This is a type-conversion wrapper around `getGroupId()` + * @return {string} + */ +proto.litrpc.ListActionsRequest.prototype.getGroupId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getGroupId())); +}; + + +/** + * optional bytes group_id = 12; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getGroupId()` + * @return {!Uint8Array} + */ +proto.litrpc.ListActionsRequest.prototype.getGroupId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getGroupId())); +}; + + +/** @param {!(string|Uint8Array)} value */ +proto.litrpc.ListActionsRequest.prototype.setGroupId = function(value) { + jspb.Message.setProto3BytesField(this, 12, value); +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/app/src/types/generated/lit-autopilot_pb.d.ts b/app/src/types/generated/lit-autopilot_pb.d.ts index f32731d68..ddcd56cd5 100644 --- a/app/src/types/generated/lit-autopilot_pb.d.ts +++ b/app/src/types/generated/lit-autopilot_pb.d.ts @@ -27,6 +27,11 @@ export class AddAutopilotSessionRequest extends jspb.Message { getNoPrivacyMapper(): boolean; setNoPrivacyMapper(value: boolean): void; + getLinkedGroupId(): Uint8Array | string; + getLinkedGroupId_asU8(): Uint8Array; + getLinkedGroupId_asB64(): string; + setLinkedGroupId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): AddAutopilotSessionRequest.AsObject; static toObject(includeInstance: boolean, msg: AddAutopilotSessionRequest): AddAutopilotSessionRequest.AsObject; @@ -46,6 +51,7 @@ export namespace AddAutopilotSessionRequest { featuresMap: Array<[string, FeatureConfig.AsObject]>, sessionRules?: lit_sessions_pb.RulesMap.AsObject, noPrivacyMapper: boolean, + linkedGroupId: Uint8Array | string, } } diff --git a/app/src/types/generated/lit-autopilot_pb.js b/app/src/types/generated/lit-autopilot_pb.js index 10fc96a0e..44c47f02c 100644 --- a/app/src/types/generated/lit-autopilot_pb.js +++ b/app/src/types/generated/lit-autopilot_pb.js @@ -80,7 +80,8 @@ proto.litrpc.AddAutopilotSessionRequest.toObject = function(includeInstance, msg devServer: jspb.Message.getFieldWithDefault(msg, 4, false), featuresMap: (f = msg.getFeaturesMap()) ? f.toObject(includeInstance, proto.litrpc.FeatureConfig.toObject) : [], sessionRules: (f = msg.getSessionRules()) && lit$sessions_pb.RulesMap.toObject(includeInstance, f), - noPrivacyMapper: jspb.Message.getFieldWithDefault(msg, 7, false) + noPrivacyMapper: jspb.Message.getFieldWithDefault(msg, 7, false), + linkedGroupId: msg.getLinkedGroupId_asB64() }; if (includeInstance) { @@ -148,6 +149,10 @@ proto.litrpc.AddAutopilotSessionRequest.deserializeBinaryFromReader = function(m var value = /** @type {boolean} */ (reader.readBool()); msg.setNoPrivacyMapper(value); break; + case 8: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setLinkedGroupId(value); + break; default: reader.skipField(); break; @@ -224,6 +229,13 @@ proto.litrpc.AddAutopilotSessionRequest.serializeBinaryToWriter = function(messa f ); } + f = message.getLinkedGroupId_asU8(); + if (f.length > 0) { + writer.writeBytes( + 8, + f + ); + } }; @@ -354,6 +366,45 @@ proto.litrpc.AddAutopilotSessionRequest.prototype.setNoPrivacyMapper = function( }; +/** + * optional bytes linked_group_id = 8; + * @return {!(string|Uint8Array)} + */ +proto.litrpc.AddAutopilotSessionRequest.prototype.getLinkedGroupId = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 8, "")); +}; + + +/** + * optional bytes linked_group_id = 8; + * This is a type-conversion wrapper around `getLinkedGroupId()` + * @return {string} + */ +proto.litrpc.AddAutopilotSessionRequest.prototype.getLinkedGroupId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getLinkedGroupId())); +}; + + +/** + * optional bytes linked_group_id = 8; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getLinkedGroupId()` + * @return {!Uint8Array} + */ +proto.litrpc.AddAutopilotSessionRequest.prototype.getLinkedGroupId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getLinkedGroupId())); +}; + + +/** @param {!(string|Uint8Array)} value */ +proto.litrpc.AddAutopilotSessionRequest.prototype.setLinkedGroupId = function(value) { + jspb.Message.setProto3BytesField(this, 8, value); +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/app/src/types/generated/lit-sessions_pb.d.ts b/app/src/types/generated/lit-sessions_pb.d.ts index c9ce8cc50..b3ff42c35 100644 --- a/app/src/types/generated/lit-sessions_pb.d.ts +++ b/app/src/types/generated/lit-sessions_pb.d.ts @@ -153,6 +153,11 @@ export class Session extends jspb.Message { getRevokedAt(): string; setRevokedAt(value: string): void; + getGroupId(): Uint8Array | string; + getGroupId_asU8(): Uint8Array; + getGroupId_asB64(): string; + setGroupId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): Session.AsObject; static toObject(includeInstance: boolean, msg: Session): Session.AsObject; @@ -181,6 +186,7 @@ export namespace Session { accountId: string, autopilotFeatureInfoMap: Array<[string, RulesMap.AsObject]>, revokedAt: string, + groupId: Uint8Array | string, } } diff --git a/app/src/types/generated/lit-sessions_pb.js b/app/src/types/generated/lit-sessions_pb.js index e09f51593..39d427029 100644 --- a/app/src/types/generated/lit-sessions_pb.js +++ b/app/src/types/generated/lit-sessions_pb.js @@ -758,7 +758,8 @@ proto.litrpc.Session.toObject = function(includeInstance, msg) { macaroonRecipe: (f = msg.getMacaroonRecipe()) && proto.litrpc.MacaroonRecipe.toObject(includeInstance, f), accountId: jspb.Message.getFieldWithDefault(msg, 13, ""), autopilotFeatureInfoMap: (f = msg.getAutopilotFeatureInfoMap()) ? f.toObject(includeInstance, proto.litrpc.RulesMap.toObject) : [], - revokedAt: jspb.Message.getFieldWithDefault(msg, 16, "0") + revokedAt: jspb.Message.getFieldWithDefault(msg, 16, "0"), + groupId: msg.getGroupId_asB64() }; if (includeInstance) { @@ -862,6 +863,10 @@ proto.litrpc.Session.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {string} */ (reader.readUint64String()); msg.setRevokedAt(value); break; + case 17: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setGroupId(value); + break; default: reader.skipField(); break; @@ -1001,6 +1006,13 @@ proto.litrpc.Session.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getGroupId_asU8(); + if (f.length > 0) { + writer.writeBytes( + 17, + f + ); + } }; @@ -1360,6 +1372,45 @@ proto.litrpc.Session.prototype.setRevokedAt = function(value) { }; +/** + * optional bytes group_id = 17; + * @return {!(string|Uint8Array)} + */ +proto.litrpc.Session.prototype.getGroupId = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 17, "")); +}; + + +/** + * optional bytes group_id = 17; + * This is a type-conversion wrapper around `getGroupId()` + * @return {string} + */ +proto.litrpc.Session.prototype.getGroupId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getGroupId())); +}; + + +/** + * optional bytes group_id = 17; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getGroupId()` + * @return {!Uint8Array} + */ +proto.litrpc.Session.prototype.getGroupId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getGroupId())); +}; + + +/** @param {!(string|Uint8Array)} value */ +proto.litrpc.Session.prototype.setGroupId = function(value) { + jspb.Message.setProto3BytesField(this, 17, value); +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/app/src/util/tests/sampleData.ts b/app/src/util/tests/sampleData.ts index e178a9d57..bc043b26f 100644 --- a/app/src/util/tests/sampleData.ts +++ b/app/src/util/tests/sampleData.ts @@ -906,6 +906,7 @@ export const litListSessions: LIT.ListSessionsResponse.AsObject = { sessionType: LIT.SessionType.TYPE_UI_PASSWORD, createdAt: '253300000000', accountId: '', + groupId: '', revokedAt: '453300000000', autopilotFeatureInfoMap: [ [ @@ -986,6 +987,7 @@ export const litListSessions: LIT.ListSessionsResponse.AsObject = { sessionType: LIT.SessionType.TYPE_UI_PASSWORD, createdAt: '253300000000', revokedAt: '453300000000', + groupId: '', accountId: '', autopilotFeatureInfoMap: [ [ diff --git a/autopilotserver/client.go b/autopilotserver/client.go index 599c0466a..7e2eabb61 100644 --- a/autopilotserver/client.go +++ b/autopilotserver/client.go @@ -376,11 +376,12 @@ func (c *Client) ListFeatures(ctx context.Context) (map[string]*Feature, // // Note: this is part of the Autopilot interface. func (c *Client) RegisterSession(ctx context.Context, pubKey *btcec.PublicKey, - mailboxAddr string, devServer bool, featureConf map[string][]byte) ( - *btcec.PublicKey, error) { + mailboxAddr string, devServer bool, featureConf map[string][]byte, + groupKey *btcec.PublicKey, linkSig []byte) (*btcec.PublicKey, error) { remotePub, err := c.registerSession( ctx, pubKey, mailboxAddr, devServer, featureConf, + groupKey, linkSig, ) if err != nil { log.Errorf("unsuccessful registration of session %x", @@ -425,8 +426,9 @@ func (c *Client) trackClient(pubKey *btcec.PublicKey) { // registerSession attempts to register a session with the given local static // public key with the autopilot server. func (c *Client) registerSession(ctx context.Context, pubKey *btcec.PublicKey, - mailboxAddr string, devServer bool, - featureConfig map[string][]byte) (*btcec.PublicKey, error) { + mailboxAddr string, devServer bool, featureConfig map[string][]byte, + groupLocalPub *btcec.PublicKey, linkSig []byte) (*btcec.PublicKey, + error) { client, cleanup, err := c.getClientConn() if err != nil { @@ -434,14 +436,21 @@ func (c *Client) registerSession(ctx context.Context, pubKey *btcec.PublicKey, } defer cleanup() + var groupKey []byte + if groupLocalPub != nil { + groupKey = groupLocalPub.SerializeCompressed() + } + resp, err := client.RegisterSession( ctx, &autopilotserverrpc.RegisterSessionRequest{ - ResponderPubKey: pubKey.SerializeCompressed(), - MailboxAddr: mailboxAddr, - DevServer: devServer, - FeatureConfigs: featureConfig, - LitVersion: marshalVersion(c.cfg.LitVersion), - LndVersion: marshalVersion(c.cfg.LndVersion), + ResponderPubKey: pubKey.SerializeCompressed(), + MailboxAddr: mailboxAddr, + DevServer: devServer, + FeatureConfigs: featureConfig, + LitVersion: marshalVersion(c.cfg.LitVersion), + LndVersion: marshalVersion(c.cfg.LndVersion), + GroupResponderKey: groupKey, + GroupResponderSig: linkSig, }, ) if err != nil { diff --git a/autopilotserver/client_test.go b/autopilotserver/client_test.go index 755b3a640..ad616dea2 100644 --- a/autopilotserver/client_test.go +++ b/autopilotserver/client_test.go @@ -45,7 +45,7 @@ func TestAutopilotClient(t *testing.T) { require.ErrorContains(t, err, "no such client") // Register the client. - _, err = client.RegisterSession(ctx, pubKey, "", false, nil) + _, err = client.RegisterSession(ctx, pubKey, "", false, nil, nil, nil) require.NoError(t, err) // Assert that the server sees the new client and has it in the Active diff --git a/autopilotserver/interface.go b/autopilotserver/interface.go index 3c0f4bcbe..feb763282 100644 --- a/autopilotserver/interface.go +++ b/autopilotserver/interface.go @@ -29,7 +29,8 @@ type Autopilot interface { // remains active. RegisterSession(ctx context.Context, pubKey *btcec.PublicKey, mailboxAddr string, devServer bool, - featureConf map[string][]byte) (*btcec.PublicKey, error) + featureConf map[string][]byte, linkedGroupKey *btcec.PublicKey, + linkSig []byte) (*btcec.PublicKey, error) // ActivateSession attempts to inform the autopilot server that the // given session is still active. After this is called, the autopilot diff --git a/autopilotserverrpc/autopilotserver.pb.go b/autopilotserverrpc/autopilotserver.pb.go index ae08d9d0b..ac75a2f84 100644 --- a/autopilotserverrpc/autopilotserver.pb.go +++ b/autopilotserverrpc/autopilotserver.pb.go @@ -644,6 +644,12 @@ type RegisterSessionRequest struct { LitVersion *Version `protobuf:"bytes,5,opt,name=lit_version,json=litVersion,proto3" json:"lit_version,omitempty"` // The version of the LND that the Lit client is using. LndVersion *Version `protobuf:"bytes,6,opt,name=lnd_version,json=lndVersion,proto3" json:"lnd_version,omitempty"` + // If set, this is the responder public key of a first session in the group + // that this new session should be linked to. + GroupResponderKey []byte `protobuf:"bytes,7,opt,name=group_responder_key,json=groupResponderKey,proto3" json:"group_responder_key,omitempty"` + // The signature by the first responder key of the serialised new responder + // key. + GroupResponderSig []byte `protobuf:"bytes,8,opt,name=group_responder_sig,json=groupResponderSig,proto3" json:"group_responder_sig,omitempty"` } func (x *RegisterSessionRequest) Reset() { @@ -720,6 +726,20 @@ func (x *RegisterSessionRequest) GetLndVersion() *Version { return nil } +func (x *RegisterSessionRequest) GetGroupResponderKey() []byte { + if x != nil { + return x.GroupResponderKey + } + return nil +} + +func (x *RegisterSessionRequest) GetGroupResponderSig() []byte { + if x != nil { + return x.GroupResponderSig + } + return nil +} + type RegisterSessionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -930,7 +950,7 @@ var file_autopilotserver_proto_rawDesc = []byte{ 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xae, 0x03, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, @@ -953,7 +973,13 @@ var file_autopilotserver_proto_rawDesc = []byte{ 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6c, 0x6e, 0x64, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x41, 0x0a, 0x13, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x11, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, + 0x72, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x13, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x11, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x65, + 0x72, 0x53, 0x69, 0x67, 0x1a, 0x41, 0x0a, 0x13, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 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, 0x0c, 0x52, 0x05, 0x76, 0x61, diff --git a/autopilotserverrpc/autopilotserver.proto b/autopilotserverrpc/autopilotserver.proto index 8bb8bce67..5a5ad7467 100644 --- a/autopilotserverrpc/autopilotserver.proto +++ b/autopilotserverrpc/autopilotserver.proto @@ -170,6 +170,18 @@ message RegisterSessionRequest { The version of the LND that the Lit client is using. */ Version lnd_version = 6; + + /* + If set, this is the responder public key of a first session in the group + that this new session should be linked to. + */ + bytes group_responder_key = 7; + + /* + The signature by the first responder key of the serialised new responder + key. + */ + bytes group_responder_sig = 8; } message RegisterSessionResponse { diff --git a/cmd/litcli/actions.go b/cmd/litcli/actions.go index d520c4522..27d6b4e71 100644 --- a/cmd/litcli/actions.go +++ b/cmd/litcli/actions.go @@ -35,9 +35,9 @@ var listActionsCommand = cli.Command{ }, cli.StringFlag{ Name: "session_id", - Usage: "The session ID to filter the actions by. If " + - "left empty, then all actions will be " + - "returned.", + Usage: "The hex encoded session ID to filter the " + + "actions by. If left empty, then all actions " + + "will be returned.", }, cli.Uint64Flag{ Name: "start_timestamp", @@ -84,6 +84,16 @@ var listActionsCommand = cli.Command{ "index_offset is the index of the action in " + "the db regardless of filter.", }, + cli.StringFlag{ + Name: "group_id", + Usage: "The hex encoded group ID to filter the " + + "actions by. The group ID is the same for " + + "all sessions that have been linked. If a " + + "session has no linked sessions then the " + + "group ID will be the same as the " + + "session ID. This flag will be ignored if " + + "the `session_id` flag is set.", + }, }, } @@ -109,6 +119,14 @@ func listActions(ctx *cli.Context) error { } } + var groupID []byte + if ctx.String("group_id") != "" { + groupID, err = hex.DecodeString(ctx.String("group_id")) + if err != nil { + return err + } + } + resp, err := client.ListActions( ctxb, &litrpc.ListActionsRequest{ SessionId: sessionID, @@ -122,6 +140,7 @@ func listActions(ctx *cli.Context) error { CountTotal: ctx.Bool("count_total"), StartTimestamp: ctx.Uint64("start_timestamp"), EndTimestamp: ctx.Uint64("end_timestamp"), + GroupId: groupID, }, ) if err != nil { diff --git a/cmd/litcli/autopilot.go b/cmd/litcli/autopilot.go index c4d643807..27595bfab 100644 --- a/cmd/litcli/autopilot.go +++ b/cmd/litcli/autopilot.go @@ -65,6 +65,11 @@ var addAutopilotSessionCmd = cli.Command{ "perform actions on. In the " + "form of: peerID1,peerID2,...", }, + cli.StringFlag{ + Name: "group_id", + Usage: "The hex encoded group ID of the session " + + "group to link this one to", + }, }, } @@ -224,6 +229,14 @@ func initAutopilotSession(ctx *cli.Context) error { } } + var groupID []byte + if ctx.IsSet("group_id") { + groupID, err = hex.DecodeString(ctx.String("group_id")) + if err != nil { + return err + } + } + resp, err := client.AddAutopilotSession( ctxb, &litrpc.AddAutopilotSessionRequest{ Label: ctx.String("label"), @@ -231,6 +244,7 @@ func initAutopilotSession(ctx *cli.Context) error { MailboxServerAddr: ctx.String("mailboxserveraddr"), DevServer: ctx.Bool("devserver"), Features: featureMap, + LinkedGroupId: groupID, }, ) if err != nil { diff --git a/cmd/litcli/privacy_map.go b/cmd/litcli/privacy_map.go index e080bd371..6bf0c9f7a 100644 --- a/cmd/litcli/privacy_map.go +++ b/cmd/litcli/privacy_map.go @@ -18,9 +18,9 @@ var privacyMapCommands = cli.Command{ Category: "Privacy", Flags: []cli.Flag{ cli.StringFlag{ - Name: "session_id", - Usage: "The id of the session in question", - Required: true, + Name: "session_id", + Usage: "Deprecated, use group_id instead.", + Hidden: true, }, cli.BoolFlag{ Name: "realtopseudo", @@ -30,6 +30,11 @@ var privacyMapCommands = cli.Command{ "as the pseudo value that should be " + "mapped to its real counterpart.", }, + cli.StringFlag{ + Name: "group_id", + Usage: "The ID of the session group who's privacy " + + "map DB should be queried.", + }, }, Subcommands: []cli.Command{ privacyMapConvertStrCommand, @@ -60,16 +65,26 @@ func privacyMapConvertStr(ctx *cli.Context) error { defer cleanup() client := litrpc.NewFirewallClient(clientConn) - id, err := hex.DecodeString(ctx.GlobalString("session_id")) - if err != nil { - return err + var groupID []byte + if ctx.GlobalIsSet("group_id") { + groupID, err = hex.DecodeString(ctx.GlobalString("group_id")) + if err != nil { + return err + } + } else if ctx.GlobalIsSet("session_id") { + groupID, err = hex.DecodeString(ctx.GlobalString("session_id")) + if err != nil { + return err + } + } else { + return fmt.Errorf("must set group_id") } resp, err := client.PrivacyMapConversion( ctxb, &litrpc.PrivacyMapConversionRequest{ - SessionId: id, RealToPseudo: ctx.GlobalBool("realtopseudo"), Input: ctx.String("input"), + GroupId: groupID, }, ) if err != nil { @@ -104,18 +119,28 @@ func privacyMapConvertUint64(ctx *cli.Context) error { defer cleanup() client := litrpc.NewFirewallClient(clientConn) - id, err := hex.DecodeString(ctx.GlobalString("session_id")) - if err != nil { - return err + var groupID []byte + if ctx.GlobalIsSet("group_id") { + groupID, err = hex.DecodeString(ctx.GlobalString("group_id")) + if err != nil { + return err + } + } else if ctx.GlobalIsSet("session_id") { + groupID, err = hex.DecodeString(ctx.GlobalString("session_id")) + if err != nil { + return err + } + } else { + return fmt.Errorf("must set group_id") } input := firewalldb.Uint64ToStr(ctx.Uint64("input")) resp, err := client.PrivacyMapConversion( ctxb, &litrpc.PrivacyMapConversionRequest{ - SessionId: id, RealToPseudo: ctx.GlobalBool("realtopseudo"), Input: input, + GroupId: groupID, }, ) if err != nil { diff --git a/firewall/privacy_mapper.go b/firewall/privacy_mapper.go index 8bf55b174..598e9746b 100644 --- a/firewall/privacy_mapper.go +++ b/firewall/privacy_mapper.go @@ -46,16 +46,22 @@ var _ mid.RequestInterceptor = (*PrivacyMapper)(nil) // PrivacyMapper is a RequestInterceptor that maps any pseudo names in certain // requests to their real values and vice versa for responses. type PrivacyMapper struct { - newDB firewalldb.NewPrivacyMapDB - randIntn func(int) (int, error) + newDB firewalldb.NewPrivacyMapDB + randIntn func(int) (int, error) + sessionIDIndexDB session.IDToGroupIndex } // NewPrivacyMapper returns a new instance of PrivacyMapper. The randIntn // function is used to draw randomness for request field obfuscation. func NewPrivacyMapper(newDB firewalldb.NewPrivacyMapDB, - randIntn func(int) (int, error)) *PrivacyMapper { + randIntn func(int) (int, error), + sessionIDIndexDB session.IDToGroupIndex) *PrivacyMapper { - return &PrivacyMapper{newDB: newDB, randIntn: randIntn} + return &PrivacyMapper{ + newDB: newDB, + randIntn: randIntn, + sessionIDIndexDB: sessionIDIndexDB, + } } // Name returns the name of the interceptor. @@ -91,6 +97,12 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, return nil, fmt.Errorf("could not extract ID from macaroon") } + // Get group ID for session ID. + groupID, err := p.sessionIDIndexDB.GetGroupID(sessionID) + if err != nil { + return nil, err + } + log.Tracef("PrivacyMapper: Intercepting %v", ri) switch r := req.InterceptType.(type) { @@ -108,7 +120,7 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, } replacement, err := p.checkAndReplaceIncomingRequest( - ctx, r.Request.MethodFullUri, msg, sessionID, + ctx, r.Request.MethodFullUri, msg, groupID, ) if err != nil { return mid.RPCErr(req, err) @@ -142,7 +154,7 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, } replacement, err := p.replaceOutgoingResponse( - ctx, r.Response.MethodFullUri, msg, sessionID, + ctx, r.Response.MethodFullUri, msg, groupID, ) if err != nil { return mid.RPCErr(req, err) @@ -167,10 +179,10 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, // checkAndReplaceIncomingRequest inspects an incoming request and optionally // modifies some of the request parameters. func (p *PrivacyMapper) checkAndReplaceIncomingRequest(ctx context.Context, - uri string, req proto.Message, sessionID session.ID) (proto.Message, + uri string, req proto.Message, groupID session.ID) (proto.Message, error) { - db := p.newDB(sessionID) + db := p.newDB(groupID) // If we don't have a handler for the URI, we don't allow the request // to go through. @@ -193,9 +205,9 @@ func (p *PrivacyMapper) checkAndReplaceIncomingRequest(ctx context.Context, // replaceOutgoingResponse inspects the responses before sending them out to the // client and replaces them if needed. func (p *PrivacyMapper) replaceOutgoingResponse(ctx context.Context, uri string, - resp proto.Message, sessionID session.ID) (proto.Message, error) { + resp proto.Message, groupID session.ID) (proto.Message, error) { - db := p.newDB(sessionID) + db := p.newDB(groupID) // If we don't have a handler for the URI, we don't allow the response // to go to avoid accidental leaks. diff --git a/firewall/privacy_mapper_test.go b/firewall/privacy_mapper_test.go index 3778363b6..23cc1494b 100644 --- a/firewall/privacy_mapper_test.go +++ b/firewall/privacy_mapper_test.go @@ -2,6 +2,7 @@ package firewall import ( "context" + "fmt" "testing" "time" @@ -292,9 +293,12 @@ func TestPrivacyMapper(t *testing.T) { db := newMockDB(t, mapPreloadRealToPseudo, sessionID) + err = db.AddSessionAndGroupIDPair(sessionID, sessionID) + require.NoError(t, err) + // randIntn is used for deterministic testing. randIntn := func(n int) (int, error) { return 100, nil } - p := NewPrivacyMapper(db.NewSessionDB, randIntn) + p := NewPrivacyMapper(db.NewSessionDB, randIntn, db) for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -355,7 +359,7 @@ func TestPrivacyMapper(t *testing.T) { rawMsg, err := proto.Marshal(msg) require.NoError(t, err) - p = NewPrivacyMapper(db.NewSessionDB, CryptoRandIntn) + p = NewPrivacyMapper(db.NewSessionDB, CryptoRandIntn, db) require.NoError(t, err) // We test the independent outgoing amount (incoming amount @@ -440,12 +444,21 @@ func TestPrivacyMapper(t *testing.T) { }) } -type mockDB map[string]*mockPrivacyMapDB +type mockDB struct { + privDB map[string]*mockPrivacyMapDB + + sessionIDIndex map[session.ID]session.ID + groupIDIndex map[session.ID][]session.ID +} func newMockDB(t *testing.T, preloadRealToPseudo map[string]string, sessID session.ID) mockDB { - db := make(mockDB) + db := mockDB{ + privDB: make(map[string]*mockPrivacyMapDB), + sessionIDIndex: make(map[session.ID]session.ID), + groupIDIndex: make(map[session.ID][]session.ID), + } sessDB := db.NewSessionDB(sessID) _ = sessDB.Update(func(tx firewalldb.PrivacyMapTx) error { @@ -459,17 +472,41 @@ func newMockDB(t *testing.T, preloadRealToPseudo map[string]string, } func (m mockDB) NewSessionDB(sessionID session.ID) firewalldb.PrivacyMapDB { - db, ok := m[string(sessionID[:])] + db, ok := m.privDB[string(sessionID[:])] if ok { return db } newDB := newMockPrivacyMapDB() - m[string(sessionID[:])] = newDB + m.privDB[string(sessionID[:])] = newDB return newDB } +func (m mockDB) AddSessionAndGroupIDPair(sessionID, groupID session.ID) error { + m.sessionIDIndex[sessionID] = groupID + m.groupIDIndex[groupID] = append(m.groupIDIndex[groupID], sessionID) + return nil +} + +func (m mockDB) GetGroupID(sessionID session.ID) (session.ID, error) { + groupID, ok := m.sessionIDIndex[sessionID] + if !ok { + return session.ID{}, fmt.Errorf("group ID not found") + } + + return groupID, nil +} + +func (m mockDB) GetSessionIDs(groupID session.ID) ([]session.ID, error) { + sessionIDs, ok := m.groupIDIndex[groupID] + if !ok { + return nil, fmt.Errorf("group ID not found") + } + + return sessionIDs, nil +} + func newMockPrivacyMapDB() *mockPrivacyMapDB { return &mockPrivacyMapDB{ r2p: make(map[string]string), diff --git a/firewall/rule_enforcer.go b/firewall/rule_enforcer.go index 8b2c03060..db304b7a6 100644 --- a/firewall/rule_enforcer.go +++ b/firewall/rule_enforcer.go @@ -30,6 +30,7 @@ var _ mid.RequestInterceptor = (*RuleEnforcer)(nil) type RuleEnforcer struct { ruleDB firewalldb.RulesDB actionsDB firewalldb.ActionReadDBGetter + sessionIDIndexDB session.IDToGroupIndex markActionErrored func(reqID uint64, reason string) error newPrivMap firewalldb.NewPrivacyMapDB @@ -50,8 +51,9 @@ type featurePerms func(ctx context.Context) (map[string]map[string]bool, error) // NewRuleEnforcer constructs a new RuleEnforcer instance. func NewRuleEnforcer(ruleDB firewalldb.RulesDB, - actionsDB firewalldb.ActionReadDBGetter, getFeaturePerms featurePerms, - permsMgr *perms.Manager, nodeID [33]byte, + actionsDB firewalldb.ActionReadDBGetter, + sessionIDIndex session.IDToGroupIndex, + getFeaturePerms featurePerms, permsMgr *perms.Manager, nodeID [33]byte, routerClient lndclient.RouterClient, lndClient lndclient.LightningClient, ruleMgrs rules.ManagerSet, markActionErrored func(reqID uint64, reason string) error, @@ -68,6 +70,7 @@ func NewRuleEnforcer(ruleDB firewalldb.RulesDB, ruleMgrs: ruleMgrs, markActionErrored: markActionErrored, newPrivMap: privMap, + sessionIDIndexDB: sessionIDIndex, } } @@ -221,7 +224,12 @@ func (r *RuleEnforcer) handleRequest(ctx context.Context, return nil, fmt.Errorf("could not extract ID from macaroon") } - rules, err := r.collectEnforcers(ri, sessionID) + groupID, err := r.sessionIDIndexDB.GetGroupID(sessionID) + if err != nil { + return nil, err + } + + rules, err := r.collectEnforcers(ri, groupID) if err != nil { return nil, fmt.Errorf("error parsing rules: %v", err) } @@ -261,7 +269,12 @@ func (r *RuleEnforcer) handleResponse(ctx context.Context, return nil, fmt.Errorf("could not extract ID from macaroon") } - enforcers, err := r.collectEnforcers(ri, sessionID) + groupID, err := r.sessionIDIndexDB.GetGroupID(sessionID) + if err != nil { + return nil, err + } + + enforcers, err := r.collectEnforcers(ri, groupID) if err != nil { return nil, fmt.Errorf("error parsing rules: %v", err) } @@ -295,7 +308,12 @@ func (r *RuleEnforcer) handleErrorResponse(ctx context.Context, return nil, fmt.Errorf("could not extract ID from macaroon") } - enforcers, err := r.collectEnforcers(ri, sessionID) + groupID, err := r.sessionIDIndexDB.GetGroupID(sessionID) + if err != nil { + return nil, err + } + + enforcers, err := r.collectEnforcers(ri, groupID) if err != nil { return nil, fmt.Errorf("error parsing rules: %v", err) } @@ -320,7 +338,7 @@ func (r *RuleEnforcer) handleErrorResponse(ctx context.Context, // collectRule initialises and returns all the Rules that need to be enforced // for the given request. -func (r *RuleEnforcer) collectEnforcers(ri *RequestInfo, sessionID session.ID) ( +func (r *RuleEnforcer) collectEnforcers(ri *RequestInfo, groupID session.ID) ( []rules.Enforcer, error) { ruleEnforcers := make( @@ -331,7 +349,7 @@ func (r *RuleEnforcer) collectEnforcers(ri *RequestInfo, sessionID session.ID) ( for rule, value := range ri.Rules.FeatureRules[ri.MetaInfo.Feature] { r, err := r.initRule( ri.RequestID, rule, []byte(value), ri.MetaInfo.Feature, - sessionID, false, ri.WithPrivacy, + groupID, false, ri.WithPrivacy, ) if err != nil { return nil, err @@ -345,7 +363,7 @@ func (r *RuleEnforcer) collectEnforcers(ri *RequestInfo, sessionID session.ID) ( // initRule initialises a rule.Rule with any required config values. func (r *RuleEnforcer) initRule(reqID uint64, name string, value []byte, - featureName string, sessionID session.ID, sessionRule, + featureName string, groupID session.ID, sessionRule, privacy bool) (rules.Enforcer, error) { ruleValues, err := r.ruleMgrs.InitRuleValues(name, value) @@ -354,7 +372,7 @@ func (r *RuleEnforcer) initRule(reqID uint64, name string, value []byte, } if privacy { - privMap := r.newPrivMap(sessionID) + privMap := r.newPrivMap(groupID) ruleValues, err = ruleValues.PseudoToReal(privMap) if err != nil { return nil, fmt.Errorf("could not prepare rule "+ @@ -362,13 +380,13 @@ func (r *RuleEnforcer) initRule(reqID uint64, name string, value []byte, } } - allActionsDB := r.actionsDB.GetActionsReadDB(sessionID, featureName) - actionsDB := allActionsDB.FeatureActionsDB() - rulesDB := r.ruleDB.GetKVStores(name, sessionID, featureName) + allActionsDB := r.actionsDB.GetActionsReadDB(groupID, featureName) + actionsDB := allActionsDB.GroupFeatureActionsDB() + rulesDB := r.ruleDB.GetKVStores(name, groupID, featureName) if sessionRule { - actionsDB = allActionsDB.SessionActionsDB() - rulesDB = r.ruleDB.GetKVStores(name, sessionID, "") + actionsDB = allActionsDB.GroupActionsDB() + rulesDB = r.ruleDB.GetKVStores(name, groupID, "") } cfg := &rules.ConfigImpl{ diff --git a/firewalldb/actions.go b/firewalldb/actions.go index 0e491af86..8ddd1e3b5 100644 --- a/firewalldb/actions.go +++ b/firewalldb/actions.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/binary" + "errors" "fmt" "io" "time" @@ -386,6 +387,79 @@ func (db *DB) ListSessionActions(sessionID session.ID, return actions, lastIndex, totalCount, nil } +// ListGroupActions returns a list of the given session group's Actions that +// pass the filterFn requirements. +// +// TODO: update to allow for pagination. +func (db *DB) ListGroupActions(groupID session.ID, + filterFn ListActionsFilterFn) ([]*Action, error) { + + if filterFn == nil { + filterFn = func(a *Action, reversed bool) (bool, bool) { + return true, true + } + } + + sessionIDs, err := db.sessionIDIndex.GetSessionIDs(groupID) + if err != nil { + return nil, err + } + + var ( + actions []*Action + errDone = errors.New("done iterating") + ) + err = db.View(func(tx *bbolt.Tx) error { + mainActionsBucket, err := getBucket(tx, actionsBucketKey) + if err != nil { + return err + } + + actionsBucket := mainActionsBucket.Bucket(actionsKey) + if actionsBucket == nil { + return ErrNoSuchKeyFound + } + + // Iterate over each session ID in this group. + for _, sessionID := range sessionIDs { + sessionsBucket := actionsBucket.Bucket(sessionID[:]) + if sessionsBucket == nil { + return nil + } + + err = sessionsBucket.ForEach(func(_, v []byte) error { + action, err := DeserializeAction( + bytes.NewReader(v), sessionID, + ) + if err != nil { + return err + } + + include, cont := filterFn(action, false) + if include { + actions = append(actions, action) + } + + if !cont { + return errDone + } + + return nil + }) + if err != nil { + return err + } + } + + return nil + }) + if err != nil && !errors.Is(err, errDone) { + return nil, err + } + + return actions, nil +} + // SerializeAction binary serializes the given action to the writer using the // tlv format. func SerializeAction(w io.Writer, action *Action) error { @@ -501,26 +575,26 @@ type ActionsDB interface { ListActions(ctx context.Context) ([]*RuleAction, error) } -// ActionsReadDB is an abstraction gives a caller access to either a session -// specific or feature specific rules.ActionDB +// ActionsReadDB is an abstraction gives a caller access to either a group +// specific or group and feature specific rules.ActionDB. type ActionsReadDB interface { - SessionActionsDB() ActionsDB - FeatureActionsDB() ActionsDB + GroupActionsDB() ActionsDB + GroupFeatureActionsDB() ActionsDB } // ActionReadDBGetter represents a function that can be used to construct // an ActionsReadDB. type ActionReadDBGetter interface { - GetActionsReadDB(sessionID session.ID, featureName string) ActionsReadDB + GetActionsReadDB(groupID session.ID, featureName string) ActionsReadDB } // GetActionsReadDB is a method on DB that constructs an ActionsReadDB. -func (db *DB) GetActionsReadDB(sessionID session.ID, +func (db *DB) GetActionsReadDB(groupID session.ID, featureName string) ActionsReadDB { return &allActionsReadDB{ db: db, - sessionID: sessionID, + groupID: groupID, featureName: featureName, } } @@ -528,40 +602,40 @@ func (db *DB) GetActionsReadDB(sessionID session.ID, // allActionsReadDb is an implementation of the ActionsReadDB. type allActionsReadDB struct { db *DB - sessionID session.ID + groupID session.ID featureName string } var _ ActionsReadDB = (*allActionsReadDB)(nil) -// SessionActionsDB returns a rules.ActionsDB that will give the caller access -// to all of a sessions Actions. -func (a *allActionsReadDB) SessionActionsDB() ActionsDB { - return &sessionActionsReadDB{a} +// GroupActionsDB returns a rules.ActionsDB that will give the caller access +// to all of a groups Actions. +func (a *allActionsReadDB) GroupActionsDB() ActionsDB { + return &groupActionsReadDB{a} } -// FeatureActionsDB returns an rules.ActionsDB that will give the caller access -// to only a specific features Actions in a specific session. -func (a *allActionsReadDB) FeatureActionsDB() ActionsDB { - return &featureActionsReadDB{a} +// GroupFeatureActionsDB returns a rules.ActionsDB that will give the caller +// access to only a specific features Actions in a specific group. +func (a *allActionsReadDB) GroupFeatureActionsDB() ActionsDB { + return &groupFeatureActionsReadDB{a} } -// sessionActionReadDB is an implementation of the rules.ActionsDB that will -// provide read access to all the Actions of a particular session. -type sessionActionsReadDB struct { +// groupActionsReadDB is an implementation of the rules.ActionsDB that will +// provide read access to all the Actions of a particular group. +type groupActionsReadDB struct { *allActionsReadDB } -var _ ActionsDB = (*sessionActionsReadDB)(nil) +var _ ActionsDB = (*groupActionsReadDB)(nil) -// ListActions will return all the Actions for a particular session. -func (s *sessionActionsReadDB) ListActions(_ context.Context) ([]*RuleAction, +// ListActions will return all the Actions for a particular group. +func (s *groupActionsReadDB) ListActions(_ context.Context) ([]*RuleAction, error) { - sessionActions, _, _, err := s.db.ListSessionActions( - s.sessionID, func(a *Action, _ bool) (bool, bool) { + sessionActions, err := s.db.ListGroupActions( + s.groupID, func(a *Action, _ bool) (bool, bool) { return a.State == ActionStateDone, true - }, nil, + }, ) if err != nil { return nil, err @@ -575,25 +649,25 @@ func (s *sessionActionsReadDB) ListActions(_ context.Context) ([]*RuleAction, return actions, nil } -// featureActionReadDB is an implementation of the rules.ActionsDB that will -// provide read access to all the Actions of a feature within a particular -// session. -type featureActionsReadDB struct { +// groupFeatureActionsReadDB is an implementation of the rules.ActionsDB that +// will provide read access to all the Actions of a feature within a particular +// group. +type groupFeatureActionsReadDB struct { *allActionsReadDB } -var _ ActionsDB = (*featureActionsReadDB)(nil) +var _ ActionsDB = (*groupFeatureActionsReadDB)(nil) -// ListActions will return all the Actions for a particular session that were +// ListActions will return all the Actions for a particular group that were // executed by a particular feature. -func (a *featureActionsReadDB) ListActions(_ context.Context) ([]*RuleAction, - error) { +func (a *groupFeatureActionsReadDB) ListActions(_ context.Context) ( + []*RuleAction, error) { - featureActions, _, _, err := a.db.ListSessionActions( - a.sessionID, func(action *Action, _ bool) (bool, bool) { + featureActions, err := a.db.ListGroupActions( + a.groupID, func(action *Action, _ bool) (bool, bool) { return action.State == ActionStateDone && action.FeatureName == a.featureName, true - }, nil, + }, ) if err != nil { return nil, err diff --git a/firewalldb/actions_test.go b/firewalldb/actions_test.go index e0502903d..5a9c4f41f 100644 --- a/firewalldb/actions_test.go +++ b/firewalldb/actions_test.go @@ -5,21 +5,15 @@ import ( "testing" "time" + "github.com/lightninglabs/lightning-terminal/session" "github.com/stretchr/testify/require" ) -// TestActionStorage tests that the ActionsDB CRUD logic. -func TestActionStorage(t *testing.T) { - tmpDir := t.TempDir() - - db, err := NewDB(tmpDir, "test.db") - require.NoError(t, err) - t.Cleanup(func() { - _ = db.Close() - }) +var ( + sessionID1 = intToSessionID(1) + sessionID2 = intToSessionID(2) - sessionID1 := [4]byte{1, 1, 1, 1} - action1 := &Action{ + action1 = &Action{ SessionID: sessionID1, ActorName: "Autopilot", FeatureName: "auto-fees", @@ -32,8 +26,7 @@ func TestActionStorage(t *testing.T) { State: ActionStateDone, } - sessionID2 := [4]byte{2, 2, 2, 2} - action2 := &Action{ + action2 = &Action{ SessionID: sessionID2, ActorName: "Autopilot", FeatureName: "rebalancer", @@ -44,6 +37,17 @@ func TestActionStorage(t *testing.T) { AttemptedAt: time.Unix(12300, 0), State: ActionStateInit, } +) + +// TestActionStorage tests that the ActionsDB CRUD logic. +func TestActionStorage(t *testing.T) { + tmpDir := t.TempDir() + + db, err := NewDB(tmpDir, "test.db", nil) + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) actionsStateFilterFn := func(state ActionState) ListActionsFilterFn { return func(a *Action, _ bool) (bool, bool) { @@ -147,7 +151,7 @@ func TestActionStorage(t *testing.T) { func TestListActions(t *testing.T) { tmpDir := t.TempDir() - db, err := NewDB(tmpDir, "test.db") + db, err := NewDB(tmpDir, "test.db", nil) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -335,3 +339,91 @@ func TestListActions(t *testing.T) { {sessionID2, "6"}, }) } + +// TestListGroupActions tests that the ListGroupActions correctly returns all +// actions in a particular session group. +func TestListGroupActions(t *testing.T) { + group1 := intToSessionID(0) + + // Link session 1 and session 2 to group 1. + index := newMockSessionIDIndex() + index.addPair(sessionID1, group1) + index.addPair(sessionID2, group1) + + db, err := NewDB(t.TempDir(), "test.db", index) + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + // There should not be any actions in group 1 yet. + al, err := db.ListGroupActions(group1, nil) + require.NoError(t, err) + require.Empty(t, al) + + // Add an action under session 1. + _, err = db.AddAction(sessionID1, action1) + require.NoError(t, err) + + // There should now be one action in the group. + al, err = db.ListGroupActions(group1, nil) + require.NoError(t, err) + require.Len(t, al, 1) + require.Equal(t, sessionID1, al[0].SessionID) + + // Add an action under session 2. + _, err = db.AddAction(sessionID2, action2) + require.NoError(t, err) + + // There should now be actions in the group. + al, err = db.ListGroupActions(group1, nil) + require.NoError(t, err) + require.Len(t, al, 2) + require.Equal(t, sessionID1, al[0].SessionID) + require.Equal(t, sessionID2, al[1].SessionID) +} + +type mockSessionIDIndex struct { + sessionToGroupID map[session.ID]session.ID + groupToSessionIDs map[session.ID][]session.ID +} + +var _ session.IDToGroupIndex = (*mockSessionIDIndex)(nil) + +func newMockSessionIDIndex() *mockSessionIDIndex { + return &mockSessionIDIndex{ + sessionToGroupID: make(map[session.ID]session.ID), + groupToSessionIDs: make(map[session.ID][]session.ID), + } +} + +func (m *mockSessionIDIndex) addPair(sessionID, groupID session.ID) { + m.sessionToGroupID[sessionID] = groupID + + m.groupToSessionIDs[groupID] = append( + m.groupToSessionIDs[groupID], sessionID, + ) +} + +func (m *mockSessionIDIndex) GetGroupID(sessionID session.ID) (session.ID, + error) { + + id, ok := m.sessionToGroupID[sessionID] + if !ok { + return session.ID{}, fmt.Errorf("no group ID found for " + + "session ID") + } + + return id, nil +} + +func (m *mockSessionIDIndex) GetSessionIDs(groupID session.ID) ([]session.ID, + error) { + + ids, ok := m.groupToSessionIDs[groupID] + if !ok { + return nil, fmt.Errorf("no session IDs found for group ID") + } + + return ids, nil +} diff --git a/firewalldb/db.go b/firewalldb/db.go index 7c862d5a4..a79fcb478 100644 --- a/firewalldb/db.go +++ b/firewalldb/db.go @@ -8,6 +8,7 @@ import ( "path/filepath" "time" + "github.com/lightninglabs/lightning-terminal/session" "go.etcd.io/bbolt" ) @@ -40,10 +41,14 @@ var ( // DB is a bolt-backed persistent store. type DB struct { *bbolt.DB + + sessionIDIndex session.IDToGroupIndex } // NewDB creates a new bolt database that can be found at the given directory. -func NewDB(dir, fileName string) (*DB, error) { +func NewDB(dir, fileName string, sessionIDIndex session.IDToGroupIndex) (*DB, + error) { + firstInit := false path := filepath.Join(dir, fileName) @@ -66,7 +71,10 @@ func NewDB(dir, fileName string) (*DB, error) { return nil, err } - return &DB{DB: db}, nil + return &DB{ + DB: db, + sessionIDIndex: sessionIDIndex, + }, nil } // fileExists reports whether the named file or directory exists. diff --git a/firewalldb/kvstores.go b/firewalldb/kvstores.go index caf44e9b7..76e4b9d88 100644 --- a/firewalldb/kvstores.go +++ b/firewalldb/kvstores.go @@ -13,11 +13,11 @@ the `perm` and `temp` buckets are identical in structure. The only difference is that the `temp` bucket is cleared on restart of the db. rules -> perm -> rule-name -> global -> {k:v} - -> sessions -> sessionID -> session-kv-store -> {k:v} + -> sessions -> group ID -> session-kv-store -> {k:v} -> feature-kv-stores -> feature-name -> {k:v} -> temp -> rule-name -> global -> {k:v} - -> sessions -> sessionID -> session-kv-store -> {k:v} + -> sessions -> group ID -> session-kv-store -> {k:v} -> feature-kv-stores -> feature-name -> {k:v} */ @@ -44,7 +44,7 @@ var ( sessKVStoreBucketKey = []byte("session-kv-store") // featureKVStoreBucketKey is the kye under which a kv store specific - // the session id and feature name is stored. + // the group id and feature name is stored. featureKVStoreBucketKey = []byte("feature-kv-store") ) @@ -72,12 +72,12 @@ type KVStores interface { type KVStoreTx interface { // Global returns a persisted global, rule-name indexed, kv store. A // rule with a given name will have access to this store independent of - // session ID or feature. + // group ID or feature. Global() KVStore // Local returns a persisted local kv store for the rule. Depending on // how the implementation is initialised, this will either be under the - // session ID namespace or the session ID _and_ feature name namespace. + // group ID namespace or the group ID _and_ feature name namespace. Local() KVStore // GlobalTemp is similar to the Global store except that its contents @@ -105,17 +105,17 @@ type KVStore interface { // RulesDB can be used to initialise a new rules.KVStores. type RulesDB interface { - GetKVStores(rule string, sessionID session.ID, feature string) KVStores + GetKVStores(rule string, groupID session.ID, feature string) KVStores } // GetKVStores constructs a new rules.KVStores backed by a bbolt db. -func (db *DB) GetKVStores(rule string, sessionID session.ID, +func (db *DB) GetKVStores(rule string, groupID session.ID, feature string) KVStores { return &kvStores{ DB: db, ruleName: rule, - sessionID: sessionID, + groupID: groupID, featureName: feature, } } @@ -124,7 +124,7 @@ func (db *DB) GetKVStores(rule string, sessionID session.ID, type kvStores struct { *DB ruleName string - sessionID session.ID + groupID session.ID featureName string } @@ -237,10 +237,10 @@ func (tx *kvStoreTx) Global() KVStore { // // NOTE: this is part of the KVStoreTx interface. func (tx *kvStoreTx) Local() KVStore { - fn := getSessionRuleBucket(true, tx.ruleName, tx.sessionID) + fn := getSessionRuleBucket(true, tx.ruleName, tx.groupID) if tx.featureName != "" { fn = getSessionFeatureRuleBucket( - true, tx.ruleName, tx.sessionID, tx.featureName, + true, tx.ruleName, tx.groupID, tx.featureName, ) } @@ -268,10 +268,10 @@ func (tx *kvStoreTx) GlobalTemp() KVStore { // // NOTE: this is part of the KVStoreTx interface. func (tx *kvStoreTx) LocalTemp() KVStore { - fn := getSessionRuleBucket(true, tx.ruleName, tx.sessionID) + fn := getSessionRuleBucket(true, tx.ruleName, tx.groupID) if tx.featureName != "" { fn = getSessionFeatureRuleBucket( - false, tx.ruleName, tx.sessionID, tx.featureName, + false, tx.ruleName, tx.groupID, tx.featureName, ) } @@ -390,11 +390,11 @@ func getGlobalRuleBucket(perm bool, ruleName string) getBucketFunc { } // getSessionRuleBucket returns a function that can be used to fetch the -// bucket under which a kv store for a specific rule-name and session ID is +// bucket under which a kv store for a specific rule-name and group ID is // stored. The `perm` param determines if the temporary or permanent store is // used. func getSessionRuleBucket(perm bool, ruleName string, - sessionID session.ID) getBucketFunc { + groupID session.ID) getBucketFunc { return func(tx *bbolt.Tx, create bool) (*bbolt.Bucket, error) { ruleBucket, err := getRuleBucket(perm, ruleName)(tx, create) @@ -414,27 +414,28 @@ func getSessionRuleBucket(perm bool, ruleName string, return nil, err } - return sessBucket.CreateBucketIfNotExists(sessionID[:]) + return sessBucket.CreateBucketIfNotExists(groupID[:]) } sessBucket := ruleBucket.Bucket(sessKVStoreBucketKey) if sessBucket == nil { return nil, nil } - return sessBucket.Bucket(sessionID[:]), nil + return sessBucket.Bucket(groupID[:]), nil } } // getSessionFeatureRuleBucket returns a function that can be used to fetch the -// bucket under which a kv store for a specific rule-name, session ID and +// bucket under which a kv store for a specific rule-name, group ID and // feature name is stored. The `perm` param determines if the temporary or // permanent store is used. func getSessionFeatureRuleBucket(perm bool, ruleName string, - sessionID session.ID, featureName string) getBucketFunc { + groupID session.ID, featureName string) getBucketFunc { return func(tx *bbolt.Tx, create bool) (*bbolt.Bucket, error) { sessBucket, err := getSessionRuleBucket( - perm, ruleName, sessionID)(tx, create) + perm, ruleName, groupID, + )(tx, create) if err != nil { return nil, err } diff --git a/firewalldb/kvstores_test.go b/firewalldb/kvstores_test.go index dc5076439..607dc233a 100644 --- a/firewalldb/kvstores_test.go +++ b/firewalldb/kvstores_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/lightninglabs/lightning-terminal/session" "github.com/stretchr/testify/require" ) @@ -17,7 +18,7 @@ func TestKVStoreTxs(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - db, err := NewDB(tmpDir, "test.db") + db, err := NewDB(tmpDir, "test.db", nil) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -64,7 +65,7 @@ func TestTempAndPermStores(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - db, err := NewDB(tmpDir, "test.db") + db, err := NewDB(tmpDir, "test.db", nil) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -112,7 +113,7 @@ func TestTempAndPermStores(t *testing.T) { require.NoError(t, db.Close()) // Restart it. - db, err = NewDB(tmpDir, "test.db") + db, err = NewDB(tmpDir, "test.db", nil) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -146,24 +147,24 @@ func TestKVStoreNameSpaces(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - db, err := NewDB(tmpDir, "test.db") + db, err := NewDB(tmpDir, "test.db", nil) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() }) var ( - sessionID1 = [4]byte{1, 1, 1, 1} - sessionID2 = [4]byte{2, 2, 2, 2} + groupID1 = intToSessionID(1) + groupID2 = intToSessionID(2) ) - // Two DBs for same session but different features. - rulesDB1 := db.GetKVStores("test-rule", sessionID1, "auto-fees") - rulesDB2 := db.GetKVStores("test-rule", sessionID1, "re-balance") + // Two DBs for same group but different features. + rulesDB1 := db.GetKVStores("test-rule", groupID1, "auto-fees") + rulesDB2 := db.GetKVStores("test-rule", groupID1, "re-balance") - // The third DB is for the same rule but a different session. It is + // The third DB is for the same rule but a different group. It is // for the same feature as db 2. - rulesDB3 := db.GetKVStores("test-rule", sessionID2, "re-balance") + rulesDB3 := db.GetKVStores("test-rule", groupID2, "re-balance") // Test that the three ruleDBs share the same global space. err = rulesDB1.Update(func(tx KVStoreTx) error { @@ -270,12 +271,12 @@ func TestKVStoreNameSpaces(t *testing.T) { require.NoError(t, err) require.True(t, bytes.Equal(v, []byte("3"))) - // Test that the session space is shared by the first two dbs but not + // Test that the group space is shared by the first two dbs but not // the third. To do this, we re-init the DB's but leave the feature - // names out. This way, we will access the session storage. - rulesDB1 = db.GetKVStores("test-rule", sessionID1, "") - rulesDB2 = db.GetKVStores("test-rule", sessionID1, "") - rulesDB3 = db.GetKVStores("test-rule", sessionID2, "") + // names out. This way, we will access the group storage. + rulesDB1 = db.GetKVStores("test-rule", groupID1, "") + rulesDB2 = db.GetKVStores("test-rule", groupID1, "") + rulesDB3 = db.GetKVStores("test-rule", groupID2, "") err = rulesDB1.Update(func(tx KVStoreTx) error { return tx.Local().Set(ctx, "test", []byte("thing 1")) @@ -325,3 +326,10 @@ func TestKVStoreNameSpaces(t *testing.T) { require.NoError(t, err) require.True(t, bytes.Equal(v, []byte("thing 3"))) } + +func intToSessionID(i uint32) session.ID { + var id session.ID + byteOrder.PutUint32(id[:], i) + + return id +} diff --git a/firewalldb/privacy_mapper.go b/firewalldb/privacy_mapper.go index 7d96060fc..12a7541c6 100644 --- a/firewalldb/privacy_mapper.go +++ b/firewalldb/privacy_mapper.go @@ -16,8 +16,8 @@ import ( /* The PrivacyMapper data is stored in the following structure in the db: - privacy -> session id -> real-to-pseudo -> {k:v} - -> pseudo-to-real -> {k:v} + privacy -> group id -> real-to-pseudo -> {k:v} + -> pseudo-to-real -> {k:v} */ const ( @@ -33,16 +33,16 @@ var ( pseudoStrAlphabetLen = len(pseudoStrAlphabet) ) -// NewPrivacyMapDB is a function type that takes a session ID and uses it to +// NewPrivacyMapDB is a function type that takes a group ID and uses it to // construct a new PrivacyMapDB. -type NewPrivacyMapDB func(sessionID session.ID) PrivacyMapDB +type NewPrivacyMapDB func(groupID session.ID) PrivacyMapDB // PrivacyDB constructs a PrivacyMapDB that will be indexed under the given -// sessionID key. -func (db *DB) PrivacyDB(sessionID session.ID) PrivacyMapDB { +// group ID key. +func (db *DB) PrivacyDB(groupID session.ID) PrivacyMapDB { return &privacyMapDB{ - DB: db, - sessionID: sessionID, + DB: db, + groupID: groupID, } } @@ -83,7 +83,7 @@ type PrivacyMapTx interface { // privacyMapDB is an implementation of PrivacyMapDB. type privacyMapDB struct { *DB - sessionID session.ID + groupID session.ID } // beginTx starts db transaction. The transaction will be a read or read-write @@ -175,7 +175,7 @@ func (p *privacyMapTx) NewPair(real, pseudo string) error { return err } - sessBucket, err := privacyBucket.CreateBucketIfNotExists(p.sessionID[:]) + sessBucket, err := privacyBucket.CreateBucketIfNotExists(p.groupID[:]) if err != nil { return err } @@ -210,7 +210,7 @@ func (p *privacyMapTx) PseudoToReal(pseudo string) (string, error) { return "", err } - sessBucket := privacyBucket.Bucket(p.sessionID[:]) + sessBucket := privacyBucket.Bucket(p.groupID[:]) if sessBucket == nil { return "", ErrNoSuchKeyFound } @@ -236,7 +236,7 @@ func (p *privacyMapTx) RealToPseudo(real string) (string, error) { return "", err } - sessBucket := privacyBucket.Bucket(p.sessionID[:]) + sessBucket := privacyBucket.Bucket(p.groupID[:]) if sessBucket == nil { return "", ErrNoSuchKeyFound } diff --git a/firewalldb/privacy_mapper_test.go b/firewalldb/privacy_mapper_test.go index 827d42c42..0a39a40b1 100644 --- a/firewalldb/privacy_mapper_test.go +++ b/firewalldb/privacy_mapper_test.go @@ -10,7 +10,7 @@ import ( // TestPrivacyMapStorage tests the privacy mapper CRUD logic. func TestPrivacyMapStorage(t *testing.T) { tmpDir := t.TempDir() - db, err := NewDB(tmpDir, "test.db") + db, err := NewDB(tmpDir, "test.db", nil) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -68,7 +68,7 @@ func TestPrivacyMapStorage(t *testing.T) { // `Update` function, then all the changes prior should be rolled back. func TestPrivacyMapTxs(t *testing.T) { tmpDir := t.TempDir() - db, err := NewDB(tmpDir, "test.db") + db, err := NewDB(tmpDir, "test.db", nil) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() diff --git a/litrpc/firewall.pb.go b/litrpc/firewall.pb.go index 5a1e312ea..79e42dfb2 100644 --- a/litrpc/firewall.pb.go +++ b/litrpc/firewall.pb.go @@ -88,10 +88,15 @@ type PrivacyMapConversionRequest struct { // the response will the the pseudo value it if exists. Otherwise, the input // string will be assumed to be the pseudo value. RealToPseudo bool `protobuf:"varint,1,opt,name=real_to_pseudo,json=realToPseudo,proto3" json:"real_to_pseudo,omitempty"` + // Deprecated, use group_id. // The session ID under which to search for the real-pseudo pair. + // + // Deprecated: Marked as deprecated in firewall.proto. SessionId []byte `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // The input to be converted into the real or pseudo value. Input string `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` + // The group ID under which to search for the real-pseudo pair. + GroupId []byte `protobuf:"bytes,4,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` } func (x *PrivacyMapConversionRequest) Reset() { @@ -133,6 +138,7 @@ func (x *PrivacyMapConversionRequest) GetRealToPseudo() bool { return false } +// Deprecated: Marked as deprecated in firewall.proto. func (x *PrivacyMapConversionRequest) GetSessionId() []byte { if x != nil { return x.SessionId @@ -147,6 +153,13 @@ func (x *PrivacyMapConversionRequest) GetInput() string { return "" } +func (x *PrivacyMapConversionRequest) GetGroupId() []byte { + if x != nil { + return x.GroupId + } + return nil +} + type PrivacyMapConversionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -234,6 +247,8 @@ type ListActionsRequest struct { // If specified, then only actions created before the given timestamp will be // considered. EndTimestamp uint64 `protobuf:"varint,11,opt,name=end_timestamp,json=endTimestamp,proto3" json:"end_timestamp,omitempty"` + // If specified, then only actions under the given group will be queried. + GroupId []byte `protobuf:"bytes,12,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` } func (x *ListActionsRequest) Reset() { @@ -345,6 +360,13 @@ func (x *ListActionsRequest) GetEndTimestamp() uint64 { return 0 } +func (x *ListActionsRequest) GetGroupId() []byte { + if x != nil { + return x.GroupId + } + return nil +} + type ListActionsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -557,18 +579,20 @@ var File_firewall_proto protoreflect.FileDescriptor var file_firewall_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x22, 0x78, 0x0a, 0x1b, 0x50, 0x72, 0x69, 0x76, - 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65, 0x61, 0x6c, 0x5f, - 0x74, 0x6f, 0x5f, 0x70, 0x73, 0x65, 0x75, 0x64, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0c, 0x72, 0x65, 0x61, 0x6c, 0x54, 0x6f, 0x50, 0x73, 0x65, 0x75, 0x64, 0x6f, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, - 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, - 0x75, 0x74, 0x22, 0x36, 0x0a, 0x1c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, + 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x22, 0x97, 0x01, 0x0a, 0x1b, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65, 0x61, 0x6c, + 0x5f, 0x74, 0x6f, 0x5f, 0x70, 0x73, 0x65, 0x75, 0x64, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0c, 0x72, 0x65, 0x61, 0x6c, 0x54, 0x6f, 0x50, 0x73, 0x65, 0x75, 0x64, 0x6f, 0x12, 0x21, + 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x42, 0x02, 0x18, 0x01, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x49, 0x64, 0x22, 0x36, 0x0a, 0x1c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x9f, 0x03, 0x0a, 0x12, 0x4c, + 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0xba, 0x03, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, @@ -594,61 +618,63 @@ var file_firewall_proto_rawDesc = []byte{ 0x02, 0x30, 0x01, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x27, 0x0a, 0x0d, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0c, - 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x8c, 0x01, 0x0a, - 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, - 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x6f, 0x66, 0x66, - 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, - 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x84, 0x03, 0x0a, 0x06, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x6f, - 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x65, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, - 0x67, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x74, - 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x64, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x75, 0x72, 0x65, 0x64, 0x4a, 0x73, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x70, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x72, 0x70, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x72, - 0x70, 0x63, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x70, 0x63, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x4a, - 0x73, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, - 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x49, 0x64, 0x2a, 0x54, 0x0a, 0x0b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, - 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, - 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x44, 0x4f, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x32, 0xb5, 0x01, 0x0a, 0x08, 0x46, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x12, 0x46, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, - 0x14, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, + 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x19, 0x0a, 0x08, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x8c, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x28, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x4f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x84, 0x03, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, + 0x72, 0x65, 0x64, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x64, 0x4a, + 0x73, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x70, 0x63, 0x5f, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x70, 0x63, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x72, 0x70, 0x63, 0x5f, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x73, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x72, 0x70, 0x63, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x20, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x13, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x2a, 0x54, 0x0a, + 0x0b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x11, 0x0a, 0x0d, + 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, + 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, + 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x4f, 0x4e, 0x45, + 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, + 0x52, 0x10, 0x03, 0x32, 0xb5, 0x01, 0x0a, 0x08, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, + 0x12, 0x46, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x14, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x23, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, + 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6c, 0x69, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, 0x70, 0x43, 0x6f, - 0x6e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, - 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, - 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, + 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, + 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/litrpc/firewall.proto b/litrpc/firewall.proto index 6631755a1..1a4dff43a 100644 --- a/litrpc/firewall.proto +++ b/litrpc/firewall.proto @@ -36,14 +36,20 @@ message PrivacyMapConversionRequest { bool real_to_pseudo = 1; /* + Deprecated, use group_id. The session ID under which to search for the real-pseudo pair. */ - bytes session_id = 2; + bytes session_id = 2 [deprecated = true]; /* The input to be converted into the real or pseudo value. */ string input = 3; + + /* + The group ID under which to search for the real-pseudo pair. + */ + bytes group_id = 4; } message PrivacyMapConversionResponse { @@ -120,6 +126,11 @@ message ListActionsRequest { considered. */ uint64 end_timestamp = 11 [jstype = JS_STRING]; + + /* + If specified, then only actions under the given group will be queried. + */ + bytes group_id = 12; } message ListActionsResponse { diff --git a/litrpc/firewall.swagger.json b/litrpc/firewall.swagger.json index e7bab1aaf..074a2593f 100644 --- a/litrpc/firewall.swagger.json +++ b/litrpc/firewall.swagger.json @@ -197,6 +197,11 @@ "type": "string", "format": "uint64", "description": "If specified, then only actions created before the given timestamp will be\nconsidered." + }, + "group_id": { + "type": "string", + "format": "byte", + "description": "If specified, then only actions under the given group will be queried." } } }, @@ -232,11 +237,16 @@ "session_id": { "type": "string", "format": "byte", - "description": "The session ID under which to search for the real-pseudo pair." + "description": "Deprecated, use group_id.\nThe session ID under which to search for the real-pseudo pair." }, "input": { "type": "string", "description": "The input to be converted into the real or pseudo value." + }, + "group_id": { + "type": "string", + "format": "byte", + "description": "The group ID under which to search for the real-pseudo pair." } } }, diff --git a/litrpc/lit-autopilot.pb.go b/litrpc/lit-autopilot.pb.go index 6b630e9dc..1417a5bf6 100644 --- a/litrpc/lit-autopilot.pb.go +++ b/litrpc/lit-autopilot.pb.go @@ -41,6 +41,8 @@ type AddAutopilotSessionRequest struct { SessionRules *RulesMap `protobuf:"bytes,6,opt,name=session_rules,json=sessionRules,proto3" json:"session_rules,omitempty"` // Set to true of the session should not make use of the privacy mapper. NoPrivacyMapper bool `protobuf:"varint,7,opt,name=no_privacy_mapper,json=noPrivacyMapper,proto3" json:"no_privacy_mapper,omitempty"` + // Set to the ID of the group to link this session to, if any. + LinkedGroupId []byte `protobuf:"bytes,8,opt,name=linked_group_id,json=linkedGroupId,proto3" json:"linked_group_id,omitempty"` } func (x *AddAutopilotSessionRequest) Reset() { @@ -124,6 +126,13 @@ func (x *AddAutopilotSessionRequest) GetNoPrivacyMapper() bool { return false } +func (x *AddAutopilotSessionRequest) GetLinkedGroupId() []byte { + if x != nil { + return x.LinkedGroupId + } + return nil +} + type FeatureConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -717,7 +726,7 @@ var file_lit_autopilot_proto_rawDesc = []byte{ 0x0a, 0x13, 0x6c, 0x69, 0x74, 0x2d, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x1a, 0x12, 0x6c, 0x69, 0x74, 0x2d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0xc4, 0x03, 0x0a, 0x1a, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x22, 0xec, 0x03, 0x0a, 0x1a, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x3c, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, @@ -740,116 +749,118 @@ var file_lit_autopilot_proto_rawDesc = []byte{ 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6e, 0x6f, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6e, 0x6f, 0x50, 0x72, 0x69, 0x76, 0x61, 0x63, 0x79, 0x4d, 0x61, - 0x70, 0x70, 0x65, 0x72, 0x1a, 0x52, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4f, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, 0x05, 0x72, 0x75, 0x6c, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x1e, 0x0a, 0x1c, 0x4c, 0x69, 0x73, - 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4c, 0x0a, 0x1d, 0x4c, 0x69, 0x73, - 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x73, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, - 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x1b, 0x41, 0x64, 0x64, 0x41, 0x75, - 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, - 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x22, 0x1e, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, - 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0xbe, 0x01, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, - 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x65, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x1a, 0x4c, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x25, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x49, 0x0a, 0x1d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, - 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x6c, - 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, - 0x1e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0xaa, 0x02, 0x0a, 0x07, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x30, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x72, 0x75, - 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x0f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x4c, - 0x69, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, - 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x72, - 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x1a, 0x4c, - 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb1, 0x01, 0x0a, - 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6b, - 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6b, 0x6e, 0x6f, 0x77, - 0x6e, 0x12, 0x2d, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, - 0x12, 0x2e, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x2e, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0x61, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x3a, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x69, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x32, 0xa0, 0x03, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, - 0x74, 0x12, 0x64, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, - 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x24, 0x2e, 0x6c, 0x69, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, - 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, - 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x41, 0x75, - 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, - 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, - 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, + 0x70, 0x70, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x6c, 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x5f, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x6c, + 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x1a, 0x52, 0x0a, 0x0d, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x2b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0x4f, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x26, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, + 0x61, 0x70, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x22, 0x1e, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x4c, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, + 0x48, 0x0a, 0x1b, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, + 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x1e, 0x0a, 0x1c, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbe, 0x01, 0x0a, 0x1d, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x08, 0x66, + 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, + 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, + 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x1a, 0x4c, 0x0a, 0x0d, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x25, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x49, 0x0a, 0x1d, 0x52, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6c, + 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, + 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xaa, 0x02, 0x0a, 0x07, 0x46, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x05, 0x72, 0x75, 0x6c, + 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0f, 0x70, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x72, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x55, + 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x1a, 0x4c, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb1, 0x01, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x05, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x12, 0x2d, 0x0a, 0x08, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, + 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, + 0x6d, 0x69, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2e, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, + 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x61, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x3a, 0x0a, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x63, + 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0xa0, 0x03, 0x0a, 0x09, + 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x12, 0x64, 0x0a, 0x15, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x12, 0x24, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, + 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x5e, 0x0a, 0x13, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x64, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x16, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, + 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x24, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, - 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, - 0x16, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, - 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, - 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, - 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, - 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, - 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, + 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, + 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, + 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/litrpc/lit-autopilot.proto b/litrpc/lit-autopilot.proto index 174940d4b..169955a20 100644 --- a/litrpc/lit-autopilot.proto +++ b/litrpc/lit-autopilot.proto @@ -73,6 +73,11 @@ message AddAutopilotSessionRequest { Set to true of the session should not make use of the privacy mapper. */ bool no_privacy_mapper = 7; + + /* + Set to the ID of the group to link this session to, if any. + */ + bytes linked_group_id = 8; } message FeatureConfig { diff --git a/litrpc/lit-autopilot.swagger.json b/litrpc/lit-autopilot.swagger.json index c0c9b3d28..8f65810d0 100644 --- a/litrpc/lit-autopilot.swagger.json +++ b/litrpc/lit-autopilot.swagger.json @@ -162,6 +162,11 @@ "no_privacy_mapper": { "type": "boolean", "description": "Set to true of the session should not make use of the privacy mapper." + }, + "linked_group_id": { + "type": "string", + "format": "byte", + "description": "Set to the ID of the group to link this session to, if any." } } }, @@ -578,6 +583,11 @@ "type": "string", "format": "uint64", "description": "The unix timestamp indicating the time at which the session was revoked.\nNote that this field has not been around since the beginning and so it\ncould be the case that a session has been revoked but that this field\nwill not have been set for that session. Therefore, it is suggested that\nreaders should not assume that if this field is zero that the session is\nnot revoked. Readers should instead first check the session_state field." + }, + "group_id": { + "type": "string", + "format": "byte", + "description": "The ID of the group of Session's that this Session is linked to. If this\nsession is not linked to any older Session, then this value will be the\nsame as the ID." } } }, diff --git a/litrpc/lit-sessions.pb.go b/litrpc/lit-sessions.pb.go index bd8e26680..7b882f04f 100644 --- a/litrpc/lit-sessions.pb.go +++ b/litrpc/lit-sessions.pb.go @@ -397,6 +397,10 @@ type Session struct { // readers should not assume that if this field is zero that the session is // not revoked. Readers should instead first check the session_state field. RevokedAt uint64 `protobuf:"varint,16,opt,name=revoked_at,json=revokedAt,proto3" json:"revoked_at,omitempty"` + // The ID of the group of Session's that this Session is linked to. If this + // session is not linked to any older Session, then this value will be the + // same as the ID. + GroupId []byte `protobuf:"bytes,17,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` } func (x *Session) Reset() { @@ -543,6 +547,13 @@ func (x *Session) GetRevokedAt() uint64 { return 0 } +func (x *Session) GetGroupId() []byte { + if x != nil { + return x.GroupId + } + return nil +} + type MacaroonRecipe struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1556,7 +1567,7 @@ var file_lit_sessions_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, - 0xc6, 0x06, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0xe1, 0x06, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x39, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, @@ -1602,164 +1613,166 @@ var file_lit_sessions_proto_rawDesc = []byte{ 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0a, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, - 0x30, 0x01, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x59, 0x0a, - 0x19, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x69, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x68, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x61, - 0x72, 0x6f, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x69, 0x70, 0x65, 0x12, 0x3c, 0x0a, 0x0b, 0x70, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, - 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x61, 0x76, 0x65, - 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x61, 0x76, 0x65, 0x61, - 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, 0x14, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x40, - 0x0a, 0x14, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8a, 0x01, 0x0a, 0x08, 0x52, 0x75, - 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x31, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x1a, 0x4b, 0x0a, 0x0a, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x92, 0x04, 0x0a, 0x09, 0x52, 0x75, 0x6c, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x32, 0x0a, 0x0a, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x48, 0x00, 0x52, 0x09, 0x72, - 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x4b, 0x0a, 0x12, 0x63, 0x68, 0x61, 0x6e, - 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, - 0x73, 0x48, 0x00, 0x52, 0x10, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, - 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x3b, 0x0a, 0x0d, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, - 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, - 0x69, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, - 0x69, 0x74, 0x12, 0x42, 0x0a, 0x10, 0x6f, 0x66, 0x66, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, - 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, - 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, - 0x64, 0x67, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x6f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, - 0x69, 0x6e, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, - 0x6e, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x36, 0x0a, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x5f, - 0x74, 0x6f, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, 0x65, 0x6c, - 0x66, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, 0x65, 0x6c, 0x66, 0x12, - 0x44, 0x0a, 0x10, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, - 0x69, 0x63, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x69, 0x74, 0x72, - 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, - 0x63, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, - 0x74, 0x72, 0x69, 0x63, 0x74, 0x12, 0x3b, 0x0a, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x72, 0x65, - 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, - 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, - 0x63, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, - 0x63, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x67, 0x0a, 0x09, 0x52, - 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2b, 0x0a, 0x0a, 0x72, 0x65, 0x61, 0x64, - 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, - 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x52, 0x09, 0x72, 0x65, 0x61, 0x64, - 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2d, 0x0a, 0x0b, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, 0x69, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4c, - 0x69, 0x6d, 0x69, 0x74, 0x22, 0x43, 0x0a, 0x04, 0x52, 0x61, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, - 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, - 0x6e, 0x75, 0x6d, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x08, 0x6e, 0x75, 0x6d, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x0c, 0x48, 0x69, 0x73, - 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x21, 0x0a, 0x0a, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, - 0x01, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x08, - 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, - 0x30, 0x01, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xc5, 0x02, 0x0a, - 0x13, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x6f, - 0x75, 0x6e, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x61, 0x73, 0x65, - 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, - 0x0b, 0x6d, 0x69, 0x6e, 0x42, 0x61, 0x73, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, - 0x6d, 0x61, 0x78, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x42, 0x61, 0x73, 0x65, - 0x4d, 0x73, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x6d, 0x69, 0x6e, 0x5f, 0x72, 0x61, 0x74, 0x65, - 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x52, - 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x20, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x61, - 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x61, - 0x78, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x6e, 0x5f, - 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x43, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x24, - 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x43, 0x6c, 0x74, 0x76, 0x44, - 0x65, 0x6c, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x68, 0x74, 0x6c, 0x63, - 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, - 0x0b, 0x6d, 0x69, 0x6e, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, - 0x6d, 0x61, 0x78, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x48, 0x74, 0x6c, 0x63, - 0x4d, 0x73, 0x61, 0x74, 0x22, 0x5e, 0x0a, 0x0e, 0x4f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x24, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x6d, - 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, - 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x41, 0x6d, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, - 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x65, 0x65, 0x73, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x46, 0x65, 0x65, 0x73, - 0x4d, 0x73, 0x61, 0x74, 0x22, 0x6f, 0x0a, 0x0d, 0x4f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, - 0x75, 0x64, 0x67, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x11, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, - 0x65, 0x5f, 0x61, 0x6d, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x42, 0x02, 0x30, 0x01, 0x52, 0x0f, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x41, 0x6d, - 0x74, 0x53, 0x61, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x61, 0x74, - 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x76, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, - 0x56, 0x42, 0x79, 0x74, 0x65, 0x22, 0x0c, 0x0a, 0x0a, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, - 0x65, 0x6c, 0x66, 0x22, 0x36, 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, - 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x12, 0x23, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, - 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x73, 0x22, 0x29, 0x0a, 0x0c, 0x50, - 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x70, - 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x70, - 0x65, 0x65, 0x72, 0x49, 0x64, 0x73, 0x2a, 0xa1, 0x01, 0x0a, 0x0b, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x16, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, - 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x4f, 0x4e, 0x4c, 0x59, - 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, - 0x4f, 0x4f, 0x4e, 0x5f, 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x43, 0x55, 0x53, - 0x54, 0x4f, 0x4d, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x49, - 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x41, 0x55, 0x54, 0x4f, 0x50, 0x49, 0x4c, 0x4f, 0x54, 0x10, 0x04, 0x12, - 0x19, 0x0a, 0x15, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, - 0x5f, 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x10, 0x05, 0x2a, 0x59, 0x0a, 0x0c, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, - 0x41, 0x54, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, - 0x0c, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x5f, 0x55, 0x53, 0x45, 0x10, 0x01, 0x12, - 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, - 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x45, 0x58, 0x50, 0x49, - 0x52, 0x45, 0x44, 0x10, 0x03, 0x32, 0xe8, 0x01, 0x0a, 0x08, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x19, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x69, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, + 0x30, 0x01, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x19, 0x0a, + 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x1a, 0x59, 0x0a, 0x19, 0x41, 0x75, 0x74, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x68, 0x0a, 0x0e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x52, + 0x65, 0x63, 0x69, 0x70, 0x65, 0x12, 0x3c, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x61, 0x76, 0x65, 0x61, 0x74, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x61, 0x76, 0x65, 0x61, 0x74, 0x73, 0x22, 0x15, 0x0a, + 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x08, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x40, 0x0a, 0x14, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, - 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, - 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, - 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x17, 0x0a, 0x15, 0x52, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8a, 0x01, 0x0a, 0x08, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, 0x61, + 0x70, 0x12, 0x31, 0x0a, 0x05, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x4d, + 0x61, 0x70, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x72, + 0x75, 0x6c, 0x65, 0x73, 0x1a, 0x4b, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x75, 0x6c, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0x92, 0x04, 0x0a, 0x09, 0x52, 0x75, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x32, 0x0a, 0x0a, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x61, 0x74, + 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x48, 0x00, 0x52, 0x09, 0x72, 0x61, 0x74, 0x65, 0x4c, 0x69, + 0x6d, 0x69, 0x74, 0x12, 0x4b, 0x0a, 0x12, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x5f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x48, 0x00, 0x52, 0x10, + 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, + 0x12, 0x3b, 0x0a, 0x0d, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x48, 0x00, 0x52, + 0x0c, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x42, 0x0a, + 0x10, 0x6f, 0x66, 0x66, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x62, 0x75, 0x64, 0x67, 0x65, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x4f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x48, + 0x00, 0x52, 0x0e, 0x6f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, 0x64, 0x67, 0x65, + 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x62, 0x75, + 0x64, 0x67, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, 0x64, 0x67, 0x65, + 0x74, 0x48, 0x00, 0x52, 0x0d, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, 0x64, 0x67, + 0x65, 0x74, 0x12, 0x36, 0x0a, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x6f, 0x5f, 0x73, 0x65, + 0x6c, 0x66, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, 0x65, 0x6c, 0x66, 0x48, 0x00, 0x52, 0x0a, + 0x73, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x44, 0x0a, 0x10, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x48, 0x00, 0x52, + 0x0f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, + 0x12, 0x3b, 0x0a, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x48, 0x00, 0x52, + 0x0c, 0x70, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x42, 0x07, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x67, 0x0a, 0x09, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, + 0x6d, 0x69, 0x74, 0x12, 0x2b, 0x0a, 0x0a, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x52, 0x61, 0x74, 0x65, 0x52, 0x09, 0x72, 0x65, 0x61, 0x64, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x12, 0x2d, 0x0a, 0x0b, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x61, 0x74, 0x65, 0x52, 0x0a, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x22, + 0x43, 0x0a, 0x04, 0x52, 0x61, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x74, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x75, 0x6d, 0x5f, 0x68, + 0x6f, 0x75, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6e, 0x75, 0x6d, 0x48, + 0x6f, 0x75, 0x72, 0x73, 0x22, 0x51, 0x0a, 0x0c, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, + 0x69, 0x6d, 0x69, 0x74, 0x12, 0x21, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x09, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x08, 0x64, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xc5, 0x02, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, + 0x26, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x69, 0x6e, 0x42, + 0x61, 0x73, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x62, + 0x61, 0x73, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, + 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x42, 0x61, 0x73, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, + 0x20, 0x0a, 0x0c, 0x6d, 0x69, 0x6e, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, + 0x6d, 0x12, 0x20, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, + 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x52, 0x61, 0x74, 0x65, + 0x50, 0x70, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x6e, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, + 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x69, 0x6e, + 0x43, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, + 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x43, 0x6c, 0x74, 0x76, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, + 0x26, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x69, 0x6e, 0x48, + 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x68, + 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, + 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x61, 0x74, 0x22, + 0x5e, 0x0a, 0x0e, 0x4f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, 0x64, 0x67, 0x65, + 0x74, 0x12, 0x24, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x6d, 0x74, 0x5f, 0x6d, 0x73, 0x61, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0a, 0x6d, 0x61, 0x78, + 0x41, 0x6d, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x66, + 0x65, 0x65, 0x73, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, + 0x30, 0x01, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x46, 0x65, 0x65, 0x73, 0x4d, 0x73, 0x61, 0x74, 0x22, + 0x6f, 0x0a, 0x0d, 0x4f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, + 0x12, 0x2e, 0x0a, 0x11, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x5f, 0x61, 0x6d, 0x74, + 0x5f, 0x73, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, + 0x0f, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x41, 0x6d, 0x74, 0x53, 0x61, 0x74, 0x73, + 0x12, 0x2e, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, + 0x76, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, + 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x42, 0x79, 0x74, 0x65, + 0x22, 0x0c, 0x0a, 0x0a, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x53, 0x65, 0x6c, 0x66, 0x22, 0x36, + 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, + 0x74, 0x12, 0x23, 0x0a, 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x73, 0x22, 0x29, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, + 0x73, 0x2a, 0xa1, 0x01, 0x0a, 0x0b, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x1a, 0x0a, 0x16, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, + 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x00, 0x12, 0x17, 0x0a, + 0x13, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x41, + 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, + 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x02, + 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x49, 0x5f, 0x50, 0x41, 0x53, 0x53, + 0x57, 0x4f, 0x52, 0x44, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, + 0x55, 0x54, 0x4f, 0x50, 0x49, 0x4c, 0x4f, 0x54, 0x10, 0x04, 0x12, 0x19, 0x0a, 0x15, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x41, 0x52, 0x4f, 0x4f, 0x4e, 0x5f, 0x41, 0x43, 0x43, 0x4f, + 0x55, 0x4e, 0x54, 0x10, 0x05, 0x2a, 0x59, 0x0a, 0x0c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, + 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x49, 0x4e, 0x5f, 0x55, 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, + 0x41, 0x54, 0x45, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, + 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x03, + 0x32, 0xe8, 0x01, 0x0a, 0x08, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x43, 0x0a, + 0x0a, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x2e, 0x6c, 0x69, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x41, 0x64, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, + 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, + 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, + 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, + 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/litrpc/lit-sessions.proto b/litrpc/lit-sessions.proto index a3f47db83..dfbd8a8c9 100644 --- a/litrpc/lit-sessions.proto +++ b/litrpc/lit-sessions.proto @@ -199,6 +199,13 @@ message Session { not revoked. Readers should instead first check the session_state field. */ uint64 revoked_at = 16 [jstype = JS_STRING]; + + /* + The ID of the group of Session's that this Session is linked to. If this + session is not linked to any older Session, then this value will be the + same as the ID. + */ + bytes group_id = 17; } message MacaroonRecipe { diff --git a/litrpc/lit-sessions.swagger.json b/litrpc/lit-sessions.swagger.json index b29b9b6b2..106944dcd 100644 --- a/litrpc/lit-sessions.swagger.json +++ b/litrpc/lit-sessions.swagger.json @@ -461,6 +461,11 @@ "type": "string", "format": "uint64", "description": "The unix timestamp indicating the time at which the session was revoked.\nNote that this field has not been around since the beginning and so it\ncould be the case that a session has been revoked but that this field\nwill not have been set for that session. Therefore, it is suggested that\nreaders should not assume that if this field is zero that the session is\nnot revoked. Readers should instead first check the session_state field." + }, + "group_id": { + "type": "string", + "format": "byte", + "description": "The ID of the group of Session's that this Session is linked to. If this\nsession is not linked to any older Session, then this value will be the\nsame as the ID." } } }, diff --git a/proto/firewall.proto b/proto/firewall.proto index af2df19d2..9f429b78e 100644 --- a/proto/firewall.proto +++ b/proto/firewall.proto @@ -36,14 +36,20 @@ message PrivacyMapConversionRequest { bool real_to_pseudo = 1; /* + Deprecated, use group_id. The session ID under which to search for the real-pseudo pair. */ - bytes session_id = 2; + bytes session_id = 2 [deprecated = true]; /* The input to be converted into the real or pseudo value. */ string input = 3; + + /* + The group ID under which to search for the real-pseudo pair. + */ + bytes group_id = 4; } message PrivacyMapConversionResponse { @@ -120,6 +126,11 @@ message ListActionsRequest { considered. */ uint64 end_timestamp = 11 [jstype = JS_STRING]; + + /* + If specified, then only actions under the given group will be queried. + */ + bytes group_id = 12; } message ListActionsResponse { diff --git a/proto/lit-autopilot.proto b/proto/lit-autopilot.proto index 174940d4b..169955a20 100644 --- a/proto/lit-autopilot.proto +++ b/proto/lit-autopilot.proto @@ -73,6 +73,11 @@ message AddAutopilotSessionRequest { Set to true of the session should not make use of the privacy mapper. */ bool no_privacy_mapper = 7; + + /* + Set to the ID of the group to link this session to, if any. + */ + bytes linked_group_id = 8; } message FeatureConfig { diff --git a/proto/lit-sessions.proto b/proto/lit-sessions.proto index a3f47db83..dfbd8a8c9 100644 --- a/proto/lit-sessions.proto +++ b/proto/lit-sessions.proto @@ -199,6 +199,13 @@ message Session { not revoked. Readers should instead first check the session_state field. */ uint64 revoked_at = 16 [jstype = JS_STRING]; + + /* + The ID of the group of Session's that this Session is linked to. If this + session is not linked to any older Session, then this value will be the + same as the ID. + */ + bytes group_id = 17; } message MacaroonRecipe { diff --git a/session/db.go b/session/db.go index 52a141145..0aa3417af 100644 --- a/session/db.go +++ b/session/db.go @@ -37,6 +37,9 @@ type DB struct { *bbolt.DB } +// A compile-time check to ensure that DB implements the Store interface. +var _ Store = (*DB)(nil) + // NewDB creates a new bolt database that can be found at the given directory. func NewDB(dir, fileName string) (*DB, error) { firstInit := false @@ -102,7 +105,18 @@ func initDB(filepath string, firstInit bool) (*bbolt.DB, error) { } } - _, err = tx.CreateBucketIfNotExists(sessionBucketKey) + sessionBkt, err := tx.CreateBucketIfNotExists(sessionBucketKey) + if err != nil { + return err + } + + _, err = sessionBkt.CreateBucketIfNotExists(idIndexKey) + if err != nil { + return err + } + + _, err = sessionBkt.CreateBucketIfNotExists(groupIDIndexKey) + return err }) if err != nil { diff --git a/session/interface.go b/session/interface.go index a87d64f80..1d080eb8e 100644 --- a/session/interface.go +++ b/session/interface.go @@ -63,6 +63,11 @@ type Session struct { RemotePublicKey *btcec.PublicKey FeatureConfig *FeaturesConfig WithPrivacyMapper bool + + // GroupID is the Session ID of the very first Session in the linked + // group of sessions. If this is the very first session in the group + // then this will be the same as ID. + GroupID ID } // MacaroonBaker is a function type for baking a super macaroon. @@ -70,27 +75,29 @@ type MacaroonBaker func(ctx context.Context, rootKeyID uint64, recipe *MacaroonRecipe) (string, error) // NewSession creates a new session with the given user-defined parameters. -func NewSession(label string, typ Type, expiry time.Time, serverAddr string, - devServer bool, perms []bakery.Op, caveats []macaroon.Caveat, - featureConfig FeaturesConfig, privacy bool) (*Session, error) { +func NewSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type, + expiry time.Time, serverAddr string, devServer bool, perms []bakery.Op, + caveats []macaroon.Caveat, featureConfig FeaturesConfig, + privacy bool, linkedGroupID *ID) (*Session, error) { _, pairingSecret, err := mailbox.NewPassphraseEntropy() if err != nil { return nil, fmt.Errorf("error deriving pairing secret: %v", err) } - privateKey, err := btcec.NewPrivateKey() - if err != nil { - return nil, fmt.Errorf("error deriving private key: %v", err) - } - pubKey := privateKey.PubKey() + macRootKey := NewSuperMacaroonRootKeyID(id) - var macRootKeyBase [4]byte - copy(macRootKeyBase[:], pubKey.SerializeCompressed()) - macRootKey := NewSuperMacaroonRootKeyID(macRootKeyBase) + // The group ID will by default be the same as the Session ID + // unless this session links to a previous session. + groupID := id + if linkedGroupID != nil { + // If this session is linked to a previous session, then the + // group ID is the same as the linked session's group ID. + groupID = *linkedGroupID + } sess := &Session{ - ID: macRootKeyBase, + ID: id, Label: label, State: StateCreated, Type: typ, @@ -100,10 +107,11 @@ func NewSession(label string, typ Type, expiry time.Time, serverAddr string, DevServer: devServer, MacaroonRootKey: macRootKey, PairingSecret: pairingSecret, - LocalPrivateKey: privateKey, - LocalPublicKey: pubKey, + LocalPrivateKey: localPrivKey, + LocalPublicKey: localPrivKey.PubKey(), RemotePublicKey: nil, WithPrivacyMapper: privacy, + GroupID: groupID, } if perms != nil || caveats != nil { @@ -120,18 +128,54 @@ func NewSession(label string, typ Type, expiry time.Time, serverAddr string, return sess, nil } +// IDToGroupIndex defines an interface for the session ID to group ID index. +type IDToGroupIndex interface { + // GetGroupID will return the group ID for the given session ID. + GetGroupID(sessionID ID) (ID, error) + + // GetSessionIDs will return the set of session IDs that are in the + // group with the given ID. + GetSessionIDs(groupID ID) ([]ID, error) +} + // Store is the interface a persistent storage must implement for storing and // retrieving Terminal Connect sessions. type Store interface { - // StoreSession stores a session in the store. If a session with the - // same local public key already exists, the existing record is updated/ - // overwritten instead. - StoreSession(*Session) error + // CreateSession adds a new session to the store. If a session with the + // same local public key already exists an error is returned. This + // can only be called with a Session with an ID that the Store has + // reserved. + CreateSession(*Session) error + + // GetSession fetches the session with the given key. + GetSession(key *btcec.PublicKey) (*Session, error) // ListSessions returns all sessions currently known to the store. - ListSessions() ([]*Session, error) + ListSessions(filterFn func(s *Session) bool) ([]*Session, error) // RevokeSession updates the state of the session with the given local // public key to be revoked. RevokeSession(*btcec.PublicKey) error + + // UpdateSessionRemotePubKey can be used to add the given remote pub key + // to the session with the given local pub key. + UpdateSessionRemotePubKey(localPubKey, + remotePubKey *btcec.PublicKey) error + + // GetUnusedIDAndKeyPair can be used to generate a new, unused, local + // private key and session ID pair. Care must be taken to ensure that no + // other thread calls this before the returned ID and key pair from this + // method are either used or discarded. + GetUnusedIDAndKeyPair() (ID, *btcec.PrivateKey, error) + + // GetSessionByID fetches the session with the given ID. + GetSessionByID(id ID) (*Session, error) + + // CheckSessionGroupPredicate iterates over all the sessions in a group + // and checks if each one passes the given predicate function. True is + // returned if each session passes. + CheckSessionGroupPredicate(groupID ID, + fn func(s *Session) bool) (bool, error) + + IDToGroupIndex } diff --git a/session/macaroon.go b/session/macaroon.go index 48a1e07b3..72dad8816 100644 --- a/session/macaroon.go +++ b/session/macaroon.go @@ -8,6 +8,7 @@ import ( "fmt" "strconv" + "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/lnrpc" "google.golang.org/protobuf/proto" "gopkg.in/macaroon-bakery.v2/bakery" @@ -127,3 +128,28 @@ func RootKeyIDFromMacaroon(mac *macaroon.Macaroon) (uint64, error) { // number. return strconv.ParseUint(string(decodedID.StorageId), 10, 64) } + +// NewSessionPrivKeyAndID randomly derives a new private key and session ID +// pair. +func NewSessionPrivKeyAndID() (*btcec.PrivateKey, ID, error) { + var id ID + + privateKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, id, fmt.Errorf("error deriving private key: %v", + err) + } + + pubKey := privateKey.PubKey() + + // NOTE: we use 4 bytes [1:5] of the serialised public key to create the + // macaroon root key base along with the Session ID. This will provide + // 4 bytes of entropy. Previously, bytes [0:4] where used but this + // resulted in lower entropy due to the first byte always being either + // 0x02 or 0x03. + copy(id[:], pubKey.SerializeCompressed()[1:5]) + + log.Debugf("Generated new Session ID: %x", id) + + return privateKey, id, nil +} diff --git a/session/metadata.go b/session/metadata.go index fe5c52a8d..407fad3b9 100644 --- a/session/metadata.go +++ b/session/metadata.go @@ -3,7 +3,10 @@ package session import ( "errors" "fmt" + "time" + "github.com/lightninglabs/lightning-terminal/session/migration1" + "github.com/lightninglabs/lightning-terminal/session/migration2" "go.etcd.io/bbolt" ) @@ -29,7 +32,14 @@ var ( // version of the database doesn't match the latest version this list // will be used for retrieving all migration function that are need to // apply to the current db. - dbVersions []migration + dbVersions = []migration{ + func(tx *bbolt.Tx) error { + return migration1.MigrateSessionIDToKeyIndex( + tx, time.Now, + ) + }, + migration2.MigrateSessionIDToGroupIndex, + } latestDBVersion = uint32(len(dbVersions)) ) diff --git a/session/migration1/id_to_key_index.go b/session/migration1/id_to_key_index.go new file mode 100644 index 000000000..d6d6fb7a2 --- /dev/null +++ b/session/migration1/id_to_key_index.go @@ -0,0 +1,155 @@ +package migration1 + +import ( + "bytes" + "errors" + "fmt" + "sort" + "time" + + "go.etcd.io/bbolt" +) + +var ( + // sessionBucketKey is the top level bucket where we can find all + // information about sessions. These sessions are indexed by their + // public key. + // + // The session bucket has the following structure: + // session -> -> + // -> id-index -> -> key -> + sessionBucketKey = []byte("session") + + // idIndexKey is the key used to define the id-index sub-bucket within + // the main session bucket. This bucket will be used to store the + // mapping from session ID to various other fields. + idIndexKey = []byte("id-index") + + // sessionKeyKey is the key used within the id-index bucket to store the + // session key (serialised local public key) associated with the given + // session ID. + sessionKeyKey = []byte("key") + + // ErrDBInitErr is returned when a bucket that we expect to have been + // set up during DB initialisation is not found. + ErrDBInitErr = errors.New("db did not initialise properly") +) + +type sessionInfo struct { + key []byte + state State + createdAt time.Time +} + +// MigrateSessionIDToKeyIndex back-fills the session ID to key index so that it +// has an entry for all sessions that the session store is currently aware of. +func MigrateSessionIDToKeyIndex(tx *bbolt.Tx, timeNow func() time.Time) error { + sessionBucket := tx.Bucket(sessionBucketKey) + if sessionBucket == nil { + return fmt.Errorf("session bucket not found") + } + + idIndexBkt := sessionBucket.Bucket(idIndexKey) + if idIndexBkt == nil { + return ErrDBInitErr + } + + // Collect all the index entries. + idToSessionPairs := make(map[ID][]*sessionInfo) + err := sessionBucket.ForEach(func(key, sessionBytes []byte) error { + // The session bucket contains both keys and sub-buckets. So + // here we ensure that we skip any sub-buckets. + if len(sessionBytes) == 0 { + return nil + } + + session, err := DeserializeSession( + bytes.NewReader(sessionBytes), + ) + if err != nil { + return err + } + + var id ID + copy(id[:], key[0:4]) + + idToSessionPairs[id] = append( + idToSessionPairs[id], &sessionInfo{ + key: key, + createdAt: session.CreatedAt, + state: session.State, + }, + ) + + return nil + }) + if err != nil { + return err + } + + addIndexEntry := func(id ID, key []byte) error { + idBkt, err := idIndexBkt.CreateBucket(id[:]) + if err != nil { + return err + } + + return idBkt.Put(sessionKeyKey[:], key) + } + + for id, sessions := range idToSessionPairs { + if len(sessions) == 1 { + err = addIndexEntry(id, sessions[0].key) + if err != nil { + return err + } + + continue + } + + // Sort the sessions from oldest to newest. + sort.Slice(sessions, func(i, j int) bool { + return sessions[i].createdAt.Before( + sessions[j].createdAt, + ) + }) + + // For each session other than the newest one, we ensure that + // the session is revoked. We do this in case there was a + // collision in the ID used for the session since now we want to + // populate the ID-to-key index which should be a one-to-one + // mapping. So there is a small chance that the DB contains a + // session with no entry in this ID-to-key index but at least + // this will not be an active session. + for _, session := range sessions[:len(sessions)-1] { + serialisedSession := sessionBucket.Get(session.key) + + sess, err := DeserializeSession( + bytes.NewReader(serialisedSession), + ) + if err != nil { + return err + } + + sess.State = StateRevoked + sess.RevokedAt = timeNow() + + var buf bytes.Buffer + if err := SerializeSession(&buf, sess); err != nil { + return err + } + + err = sessionBucket.Put(session.key, buf.Bytes()) + if err != nil { + return err + } + } + + // Add an entry for the last session in the set. + err = addIndexEntry(id, sessions[len(sessions)-1].key) + if err != nil { + return err + } + } + + return nil +} diff --git a/session/migration1/id_to_key_index_test.go b/session/migration1/id_to_key_index_test.go new file mode 100644 index 000000000..940342bb0 --- /dev/null +++ b/session/migration1/id_to_key_index_test.go @@ -0,0 +1,118 @@ +package migration1 + +import ( + "bytes" + "testing" + "time" + + "github.com/lightninglabs/lightning-terminal/session/migtest" + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" +) + +// TestMigrateSessionIDIndex tests that the MigrateSessionIDIndex migration +// correctly back-fills the session-ID to session-key index. +func TestMigrateSessionIDIndex(t *testing.T) { + t.Parallel() + + revokeTime := time.Unix(1000, 20) + + // Make a few sessions. + sess1ID, _, sess1Key, sess1Bytes := newSession(t) + sess2ID, _, sess2Key, sess2Bytes := newSession(t) + sess3ID, _, sess3Key, sess3Bytes := newSession(t) + _, sess4, sess4Key, _ := newSession(t) + + // Assert that the State of session 4 is StateCreated. + require.Equal(t, StateCreated, sess4.State) + + // Overwrite the CreatedAt time of session 4 so that it is definitely + // considered older than session 3. + sess4.CreatedAt = time.Now().Add(-time.Hour) + + // Now re-serialise session 4. + var sessBuff bytes.Buffer + require.NoError(t, SerializeSession(&sessBuff, sess4)) + sess4Bytes := sessBuff.Bytes() + + // Overwrite the first 4 bytes of the key of session 4 so that it is + // the same as session 3. This will mean that they have the same ID. + copy(sess4Key[0:4], sess3Key[0:4]) + + // Put together a sample session DB based on the above. + sessionDBBefore := map[string]interface{}{ + string(sess1Key): string(sess1Bytes), + string(sess2Key): string(sess2Bytes), + string(sess3Key): string(sess3Bytes), + string(sess4Key): string(sess4Bytes), + string(idIndexKey): map[string]interface{}{}, + } + + before := func(tx *bbolt.Tx) error { + return migtest.RestoreDB(tx, sessionBucketKey, sessionDBBefore) + } + + // Put together what we expect the resulting index will look like. + expectedIndex := map[string]interface{}{ + string(sess1ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess1Key), + }, + string(sess2ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess2Key), + }, + string(sess3ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess3Key), + }, + } + + // Since the DB contained two sessions with colliding IDs (session 3 + // and 4), we expect the oldest session of the two to be revoked during + // the migration. So let's construct what we expect session 4 to look + // like after the migration. + sess4.RevokedAt = revokeTime + sess4.State = StateRevoked + + sessBuff.Reset() + require.NoError(t, SerializeSession(&sessBuff, sess4)) + sess4BytesAfter := sessBuff.Bytes() + + sessionDBAfter := map[string]interface{}{ + string(sess1Key): string(sess1Bytes), + string(sess2Key): string(sess2Bytes), + string(sess3Key): string(sess3Bytes), + string(sess4Key): string(sess4BytesAfter), + string(idIndexKey): expectedIndex, + } + + // After the migration, we should have a new index bucket. + after := func(tx *bbolt.Tx) error { + return migtest.VerifyDB(tx, sessionBucketKey, sessionDBAfter) + } + + migrateFn := func(tx *bbolt.Tx) error { + return MigrateSessionIDToKeyIndex(tx, func() time.Time { + return revokeTime + }) + } + + migtest.ApplyMigration(t, before, after, migrateFn, false) +} + +// newSession is a helper function that can be used to generate a random new +// Session. It returns the session ID, key and the serialised session. +func newSession(t *testing.T) (ID, *Session, []byte, []byte) { + session, err := NewSession( + "test-session", TypeMacaroonAdmin, + time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC), + "foo.bar.baz:1234", true, nil, nil, nil, false, + ) + require.NoError(t, err) + + var sessBuff bytes.Buffer + err = SerializeSession(&sessBuff, session) + require.NoError(t, err) + + key := session.LocalPublicKey.SerializeCompressed() + + return session.ID, session, key, sessBuff.Bytes() +} diff --git a/session/migration1/interface.go b/session/migration1/interface.go new file mode 100644 index 000000000..0ad434b5d --- /dev/null +++ b/session/migration1/interface.go @@ -0,0 +1,121 @@ +package migration1 + +import ( + "fmt" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-node-connect/mailbox" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +// Type represents the type of session. +type Type uint8 + +const ( + TypeMacaroonReadonly Type = 0 + TypeMacaroonAdmin Type = 1 + TypeMacaroonCustom Type = 2 + TypeUIPassword Type = 3 + TypeAutopilot Type = 4 + TypeMacaroonAccount Type = 5 +) + +// State represents the state of a session. +type State uint8 + +const ( + StateCreated State = 0 + StateInUse State = 1 + StateRevoked State = 2 + StateExpired State = 3 +) + +// MacaroonRecipe defines the permissions and caveats that should be used +// to bake a macaroon. +type MacaroonRecipe struct { + Permissions []bakery.Op + Caveats []macaroon.Caveat +} + +// FeaturesConfig is a map from feature name to a raw byte array which stores +// any config feature config options. +type FeaturesConfig map[string][]byte + +// Session is a struct representing a long-term Terminal Connect session. +type Session struct { + ID ID + Label string + State State + Type Type + Expiry time.Time + CreatedAt time.Time + RevokedAt time.Time + ServerAddr string + DevServer bool + MacaroonRootKey uint64 + MacaroonRecipe *MacaroonRecipe + PairingSecret [mailbox.NumPassphraseEntropyBytes]byte + LocalPrivateKey *btcec.PrivateKey + LocalPublicKey *btcec.PublicKey + RemotePublicKey *btcec.PublicKey + FeatureConfig *FeaturesConfig + WithPrivacyMapper bool +} + +// NewSession creates a new session with the given user-defined parameters. +func NewSession(label string, typ Type, expiry time.Time, serverAddr string, + devServer bool, perms []bakery.Op, caveats []macaroon.Caveat, + featureConfig FeaturesConfig, privacy bool) (*Session, error) { + + _, pairingSecret, err := mailbox.NewPassphraseEntropy() + if err != nil { + return nil, fmt.Errorf("error deriving pairing secret: %v", err) + } + + privateKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, fmt.Errorf("error deriving private key: %v", err) + } + pubKey := privateKey.PubKey() + + // NOTE: after this migration, no new sessions use the first 4 bytes of + // the serialised pub key for the macaroon root key. After this + // migration, bytes 1-4 are used (instead of 0-3) this is so that we + // get the full 4 byte entropy instead of wasting one byte on the 0x02 + // and 0x03 prefix of the public key. + var macRootKeyBase [4]byte + copy(macRootKeyBase[:], pubKey.SerializeCompressed()) + macRootKey := NewSuperMacaroonRootKeyID(macRootKeyBase) + + sess := &Session{ + ID: macRootKeyBase, + Label: label, + State: StateCreated, + Type: typ, + Expiry: expiry, + CreatedAt: time.Now(), + ServerAddr: serverAddr, + DevServer: devServer, + MacaroonRootKey: macRootKey, + PairingSecret: pairingSecret, + LocalPrivateKey: privateKey, + LocalPublicKey: pubKey, + RemotePublicKey: nil, + WithPrivacyMapper: privacy, + } + + if perms != nil || caveats != nil { + sess.MacaroonRecipe = &MacaroonRecipe{ + Permissions: perms, + Caveats: caveats, + } + } + + if len(featureConfig) != 0 { + sess.FeatureConfig = &featureConfig + } + + return sess, nil +} diff --git a/session/migration1/macaroon.go b/session/migration1/macaroon.go new file mode 100644 index 000000000..9464abe42 --- /dev/null +++ b/session/migration1/macaroon.go @@ -0,0 +1,34 @@ +package migration1 + +import ( + "encoding/binary" +) + +var ( + // SuperMacaroonRootKeyPrefix is the prefix we set on a super macaroon's + // root key to clearly mark it as such. + SuperMacaroonRootKeyPrefix = [4]byte{0xFF, 0xEE, 0xDD, 0xCC} +) + +// ID represents the id of a session. +type ID [4]byte + +// NewSuperMacaroonRootKeyID returns a new macaroon root key ID that has the +// prefix to mark it as a super macaroon root key. +func NewSuperMacaroonRootKeyID(id [4]byte) uint64 { + rootKeyBytes := make([]byte, 8) + copy(rootKeyBytes[:], SuperMacaroonRootKeyPrefix[:]) + copy(rootKeyBytes[4:], id[:]) + return binary.BigEndian.Uint64(rootKeyBytes) +} + +// IDFromMacRootKeyID converts a macaroon root key ID to a session ID. +func IDFromMacRootKeyID(rootKeyID uint64) ID { + rootKeyBytes := make([]byte, 8) + binary.BigEndian.PutUint64(rootKeyBytes[:], rootKeyID) + + var id ID + copy(id[:], rootKeyBytes[4:]) + + return id +} diff --git a/session/migration1/tlv.go b/session/migration1/tlv.go new file mode 100644 index 000000000..aa3370b71 --- /dev/null +++ b/session/migration1/tlv.go @@ -0,0 +1,674 @@ +package migration1 + +import ( + "bytes" + "fmt" + "io" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/tlv" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +const ( + typeLabel tlv.Type = 1 + typeState tlv.Type = 2 + typeType tlv.Type = 3 + typeExpiry tlv.Type = 4 + typeServerAddr tlv.Type = 5 + typeDevServer tlv.Type = 6 + typeMacaroonRootKey tlv.Type = 7 + typePairingSecret tlv.Type = 9 + typeLocalPrivateKey tlv.Type = 10 + typeRemotePublicKey tlv.Type = 11 + typeMacaroonRecipe tlv.Type = 12 + typeCreatedAt tlv.Type = 13 + typeFeaturesConfig tlv.Type = 14 + typeWithPrivacy tlv.Type = 15 + typeRevokedAt tlv.Type = 16 + + // typeMacaroon is no longer used, but we leave it defined for backwards + // compatibility. + typeMacaroon tlv.Type = 8 // nolint + + typeMacPerms tlv.Type = 1 + typeMacCaveats tlv.Type = 2 + + typeCaveatID tlv.Type = 1 + typeCaveatVerificationID tlv.Type = 2 + typeCaveatLocation tlv.Type = 3 + + typePermEntity tlv.Type = 1 + typePermAction tlv.Type = 2 + + typeFeatureName tlv.Type = 1 + typeFeatureConfig tlv.Type = 2 +) + +// SerializeSession binary serializes the given session to the writer using the +// tlv format. +func SerializeSession(w io.Writer, session *Session) error { + if session == nil { + return fmt.Errorf("session cannot be nil") + } + + var ( + label = []byte(session.Label) + state = uint8(session.State) + typ = uint8(session.Type) + expiry = uint64(session.Expiry.Unix()) + serverAddr = []byte(session.ServerAddr) + devServer = uint8(0) + pairingSecret = session.PairingSecret[:] + privateKey = session.LocalPrivateKey.Serialize() + createdAt = uint64(session.CreatedAt.Unix()) + revokedAt uint64 + withPrivacy = uint8(0) + ) + + if !session.RevokedAt.IsZero() { + revokedAt = uint64(session.RevokedAt.Unix()) + } + + if session.WithPrivacyMapper { + withPrivacy = 1 + } + + if session.DevServer { + devServer = 1 + } + + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(typeLabel, &label), + tlv.MakePrimitiveRecord(typeState, &state), + tlv.MakePrimitiveRecord(typeType, &typ), + tlv.MakePrimitiveRecord(typeExpiry, &expiry), + tlv.MakePrimitiveRecord(typeServerAddr, &serverAddr), + tlv.MakePrimitiveRecord(typeDevServer, &devServer), + tlv.MakePrimitiveRecord( + typeMacaroonRootKey, &session.MacaroonRootKey, + ), + } + + tlvRecords = append( + tlvRecords, + tlv.MakePrimitiveRecord(typePairingSecret, &pairingSecret), + tlv.MakePrimitiveRecord(typeLocalPrivateKey, &privateKey), + ) + + if session.RemotePublicKey != nil { + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + typeRemotePublicKey, &session.RemotePublicKey, + )) + } + + if session.MacaroonRecipe != nil { + tlvRecords = append(tlvRecords, tlv.MakeDynamicRecord( + typeMacaroonRecipe, session.MacaroonRecipe, + func() uint64 { + return recordSize( + macaroonRecipeEncoder, + session.MacaroonRecipe, + ) + }, + macaroonRecipeEncoder, macaroonRecipeDecoder, + )) + } + + tlvRecords = append( + tlvRecords, tlv.MakePrimitiveRecord(typeCreatedAt, &createdAt), + ) + + if session.FeatureConfig != nil && len(*session.FeatureConfig) != 0 { + tlvRecords = append(tlvRecords, tlv.MakeDynamicRecord( + typeFeaturesConfig, session.FeatureConfig, + func() uint64 { + return recordSize( + featureConfigEncoder, + session.FeatureConfig, + ) + }, + featureConfigEncoder, featureConfigDecoder, + )) + } + + tlvRecords = append( + tlvRecords, + tlv.MakePrimitiveRecord(typeWithPrivacy, &withPrivacy), + tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt), + ) + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// DeserializeSession deserializes a session from the given reader, expecting +// the data to be encoded in the tlv format. +func DeserializeSession(r io.Reader) (*Session, error) { + var ( + session = &Session{} + label, serverAddr []byte + pairingSecret, privateKey []byte + state, typ, devServer, privacy uint8 + expiry, createdAt, revokedAt uint64 + macRecipe MacaroonRecipe + featureConfig FeaturesConfig + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(typeLabel, &label), + tlv.MakePrimitiveRecord(typeState, &state), + tlv.MakePrimitiveRecord(typeType, &typ), + tlv.MakePrimitiveRecord(typeExpiry, &expiry), + tlv.MakePrimitiveRecord(typeServerAddr, &serverAddr), + tlv.MakePrimitiveRecord(typeDevServer, &devServer), + tlv.MakePrimitiveRecord( + typeMacaroonRootKey, &session.MacaroonRootKey, + ), + tlv.MakePrimitiveRecord(typePairingSecret, &pairingSecret), + tlv.MakePrimitiveRecord(typeLocalPrivateKey, &privateKey), + tlv.MakePrimitiveRecord( + typeRemotePublicKey, &session.RemotePublicKey, + ), + tlv.MakeDynamicRecord( + typeMacaroonRecipe, &macRecipe, nil, + macaroonRecipeEncoder, macaroonRecipeDecoder, + ), + tlv.MakePrimitiveRecord(typeCreatedAt, &createdAt), + tlv.MakeDynamicRecord( + typeFeaturesConfig, &featureConfig, nil, + featureConfigEncoder, featureConfigDecoder, + ), + tlv.MakePrimitiveRecord(typeWithPrivacy, &privacy), + tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt), + ) + if err != nil { + return nil, err + } + + parsedTypes, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return nil, err + } + + session.ID = IDFromMacRootKeyID(session.MacaroonRootKey) + session.Label = string(label) + session.State = State(state) + session.Type = Type(typ) + session.Expiry = time.Unix(int64(expiry), 0) + session.CreatedAt = time.Unix(int64(createdAt), 0) + session.ServerAddr = string(serverAddr) + session.DevServer = devServer == 1 + session.WithPrivacyMapper = privacy == 1 + + if revokedAt != 0 { + session.RevokedAt = time.Unix(int64(revokedAt), 0) + } + + if t, ok := parsedTypes[typeMacaroonRecipe]; ok && t == nil { + session.MacaroonRecipe = &macRecipe + } + + if t, ok := parsedTypes[typePairingSecret]; ok && t == nil { + copy(session.PairingSecret[:], pairingSecret) + } + + if t, ok := parsedTypes[typeLocalPrivateKey]; ok && t == nil { + session.LocalPrivateKey, session.LocalPublicKey = btcec.PrivKeyFromBytes( + privateKey, + ) + } + + if t, ok := parsedTypes[typeFeaturesConfig]; ok && t == nil { + session.FeatureConfig = &featureConfig + } + + return session, nil +} + +func featureConfigEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*FeaturesConfig); ok { + for n, config := range *v { + name := []byte(n) + config := config + + var permsTLVBytes bytes.Buffer + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(typeFeatureName, &name), + tlv.MakePrimitiveRecord( + typeFeatureConfig, &config, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Encode(&permsTLVBytes) + if err != nil { + return err + } + + // We encode the record with a varint length followed by + // the _raw_ TLV bytes. + tlvLen := uint64(len(permsTLVBytes.Bytes())) + if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil { + return err + } + + _, err = w.Write(permsTLVBytes.Bytes()) + if err != nil { + return err + } + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "FeaturesConfig") +} + +func featureConfigDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + if v, ok := val.(*FeaturesConfig); ok { + featureConfig := make(map[string][]byte) + + // Using this information, we'll create a new limited + // reader that'll return an EOF once the end has been + // reached so the stream stops consuming bytes. + innerTlvReader := io.LimitedReader{ + R: r, + N: int64(l), + } + + for { + // Read out the varint that encodes the size of this + // inner TLV record. + blobSize, err := tlv.ReadVarInt(&innerTlvReader, buf) + if err == io.EOF { + break + } else if err != nil { + return err + } + + innerInnerTlvReader := io.LimitedReader{ + R: &innerTlvReader, + N: int64(blobSize), + } + + var ( + name []byte + config []byte + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + typeFeatureName, &name, + ), + tlv.MakePrimitiveRecord( + typeFeatureConfig, &config, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Decode(&innerInnerTlvReader) + if err != nil { + return err + } + + featureConfig[string(name)] = config + } + + *v = featureConfig + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "FeaturesConfig") +} + +// macaroonRecipeEncoder is a custom TLV encoder for a MacaroonRecipe record. +func macaroonRecipeEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*MacaroonRecipe); ok { + var recipeTLVBytes bytes.Buffer + tlvStream, err := tlv.NewStream( + tlv.MakeDynamicRecord( + typeMacPerms, &v.Permissions, func() uint64 { + return recordSize( + permsEncoder, &v.Permissions, + ) + }, permsEncoder, permsDecoder, + ), + tlv.MakeDynamicRecord( + typeMacCaveats, &v.Caveats, func() uint64 { + return recordSize( + caveatsEncoder, &v.Caveats, + ) + }, caveatsEncoder, caveatsDecoder, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Encode(&recipeTLVBytes) + if err != nil { + return err + } + + _, err = w.Write(recipeTLVBytes.Bytes()) + if err != nil { + return err + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "MacaroonRecipe") +} + +// macaroonRecipeDecoder is a custom TLV decoder for a MacaroonRecipe record. +func macaroonRecipeDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + if v, ok := val.(*MacaroonRecipe); ok { + // Using this information, we'll create a new limited + // reader that'll return an EOF once the end has been + // reached so the stream stops consuming bytes. + innerTlvReader := io.LimitedReader{ + R: r, + N: int64(l), + } + + var ( + perms []bakery.Op + caveats []macaroon.Caveat + ) + tlvStream, err := tlv.NewStream( + tlv.MakeDynamicRecord( + typeMacPerms, &perms, nil, permsEncoder, + permsDecoder, + ), + tlv.MakeDynamicRecord( + typeMacCaveats, &caveats, nil, caveatsEncoder, + caveatsDecoder, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Decode(&innerTlvReader) + if err != nil { + return err + } + + *v = MacaroonRecipe{ + Permissions: perms, + Caveats: caveats, + } + + return nil + } + + return tlv.NewTypeForDecodingErr(val, "MacaroonRecipe", l, l) +} + +// permsEncoder is a custom TLV encoder for macaroon Permission records. +func permsEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*[]bakery.Op); ok { + for _, c := range *v { + entity := []byte(c.Entity) + action := []byte(c.Action) + + var permsTLVBytes bytes.Buffer + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + typePermEntity, &entity, + ), + tlv.MakePrimitiveRecord( + typePermAction, &action, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Encode(&permsTLVBytes) + if err != nil { + return err + } + + // We encode the record with a varint length followed by + // the _raw_ TLV bytes. + tlvLen := uint64(len(permsTLVBytes.Bytes())) + if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil { + return err + } + + _, err = w.Write(permsTLVBytes.Bytes()) + if err != nil { + return err + } + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "MacaroonPermission") +} + +// permsDecoder is a custom TLV decoder for macaroon Permission records. +func permsDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*[]bakery.Op); ok { + var perms []bakery.Op + + // Using this information, we'll create a new limited + // reader that'll return an EOF once the end has been + // reached so the stream stops consuming bytes. + innerTlvReader := io.LimitedReader{ + R: r, + N: int64(l), + } + + for { + // Read out the varint that encodes the size of this + // inner TLV record. + blobSize, err := tlv.ReadVarInt(&innerTlvReader, buf) + if err == io.EOF { + break + } else if err != nil { + return err + } + + innerInnerTlvReader := io.LimitedReader{ + R: &innerTlvReader, + N: int64(blobSize), + } + + var ( + entity []byte + action []byte + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + typePermEntity, &entity, + ), + tlv.MakePrimitiveRecord( + typePermAction, &action, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Decode(&innerInnerTlvReader) + if err != nil { + return err + } + + perms = append(perms, bakery.Op{ + Entity: string(entity), + Action: string(action), + }) + } + + *v = perms + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "MacaroonPermission") +} + +// caveatsEncoder is a custom TLV decoder for macaroon Caveat records. +func caveatsEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*[]macaroon.Caveat); ok { + for _, c := range *v { + tlvRecords := []tlv.Record{ + tlv.MakePrimitiveRecord(typeCaveatID, &c.Id), + } + + if c.VerificationId != nil { + tlvRecords = append(tlvRecords, + tlv.MakePrimitiveRecord( + typeCaveatVerificationID, + &c.VerificationId, + ), + ) + } + + location := []byte(c.Location) + if location != nil { + tlvRecords = append(tlvRecords, + tlv.MakePrimitiveRecord( + typeCaveatLocation, &location, + ), + ) + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + var caveatTLVBytes bytes.Buffer + err = tlvStream.Encode(&caveatTLVBytes) + if err != nil { + return err + } + + // We encode the record with a varint length followed by + // the _raw_ TLV bytes. + tlvLen := uint64(len(caveatTLVBytes.Bytes())) + if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil { + return err + } + + _, err = w.Write(caveatTLVBytes.Bytes()) + if err != nil { + return err + } + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "MacaroonCaveat") +} + +// caveatsDecoder is a custom TLV decoder for the macaroon Caveat record. +func caveatsDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + if v, ok := val.(*[]macaroon.Caveat); ok { + var caveats []macaroon.Caveat + + // Using this information, we'll create a new limited + // reader that'll return an EOF once the end has been + // reached so the stream stops consuming bytes. + innerTlvReader := io.LimitedReader{ + R: r, + N: int64(l), + } + + for { + // Read out the varint that encodes the size of this + // inner TLV record. + blobSize, err := tlv.ReadVarInt(r, buf) + if err == io.EOF { + break + } else if err != nil { + return err + } + + innerInnerTlvReader := io.LimitedReader{ + R: &innerTlvReader, + N: int64(blobSize), + } + + var ( + id []byte + verificationID []byte + location []byte + ) + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + typeCaveatID, &id, + ), + tlv.MakePrimitiveRecord( + typeCaveatVerificationID, + &verificationID, + ), + tlv.MakePrimitiveRecord( + typeCaveatLocation, &location, + ), + ) + if err != nil { + return err + } + + err = tlvStream.Decode(&innerInnerTlvReader) + if err != nil { + return err + } + + caveats = append(caveats, macaroon.Caveat{ + Id: id, + VerificationId: verificationID, + Location: string(location), + }) + } + + *v = caveats + return nil + } + + return tlv.NewTypeForDecodingErr(val, "MacaroonCaveat", l, l) +} + +// recordSize returns the amount of bytes this TLV record will occupy when +// encoded. +func recordSize(encoder tlv.Encoder, v interface{}) uint64 { + var ( + b bytes.Buffer + buf [8]byte + ) + + // We know that encoding works since the tests pass in the build this + // file is checked into, so we'll simplify things and simply encode it + // ourselves then report the total amount of bytes used. + if err := encoder(&b, v, &buf); err != nil { + // This should never error out, but we log it just in case it + // does. + // log.Errorf("encoding the amp invoice state failed: %v", err) + } + + return uint64(len(b.Bytes())) +} diff --git a/session/migration2/id_to_group_index.go b/session/migration2/id_to_group_index.go new file mode 100644 index 000000000..382a352fd --- /dev/null +++ b/session/migration2/id_to_group_index.go @@ -0,0 +1,119 @@ +package migration2 + +import ( + "encoding/binary" + "errors" + "fmt" + + "go.etcd.io/bbolt" +) + +var ( + // sessionBucketKey is the top level bucket where we can find all + // information about sessions. These sessions are indexed by their + // public key. + // + // The session bucket has the following structure: + // session -> -> + // -> id-index -> -> key -> + // -> group -> + // -> group-id-index -> -> session-id -> sequence -> + sessionBucketKey = []byte("session") + + // idIndexKey is the key used to define the id-index sub-bucket within + // the main session bucket. This bucket will be used to store the + // mapping from session ID to various other fields. + idIndexKey = []byte("id-index") + + // sessionKeyKey is the key used within the id-index bucket to store the + // session key (serialised local public key) associated with the given + // session ID. + sessionKeyKey = []byte("key") + + // groupIDKey is the key used within the id-index bucket to store the + // group ID associated with the given session ID. + groupIDKey = []byte("group") + + // groupIDIndexKey is the key used to define the group-id-index + // sub-bucket within the main session bucket. This bucket will be used + // to store the mapping from group ID to various other fields. + groupIDIndexKey = []byte("group-id-index") + + // sessionIDKey is a key used in the group-id-index under a sub-bucket + // defined by a specific group ID. It will be used to store the session + // IDs associated with the given group ID. + sessionIDKey = []byte("session-id") + + // ErrDBInitErr is returned when a bucket that we expect to have been + // set up during DB initialisation is not found. + ErrDBInitErr = errors.New("db did not initialise properly") + + // byteOrder is the default byte order we'll use for serialization + // within the database. + byteOrder = binary.BigEndian +) + +// MigrateSessionIDToGroupIndex back-fills the session ID to group index so that +// it has an entry for all sessions that the session store is currently aware of. +func MigrateSessionIDToGroupIndex(tx *bbolt.Tx) error { + sessionBucket := tx.Bucket(sessionBucketKey) + if sessionBucket == nil { + return fmt.Errorf("session bucket not found") + } + + idIndexBkt := sessionBucket.Bucket(idIndexKey) + if idIndexBkt == nil { + return ErrDBInitErr + } + + groupIndexBkt := sessionBucket.Bucket(groupIDIndexKey) + if groupIndexBkt == nil { + return ErrDBInitErr + } + + // Collect all the index entries. + return idIndexBkt.ForEach(func(sessionID, _ []byte) error { + // This migration is done before the logic in LiT is added that + // would allow groupIDs to differ from session IDs. And so all + // this migration needs to do is add the current 1:1 mapping + // from group ID to session ID and vice versa where group ID is + // equal to the session ID. + groupID := sessionID + + // First we add the session ID to group ID mapping. + sessionIDBkt := idIndexBkt.Bucket(sessionID) + if sessionIDBkt == nil { + return fmt.Errorf("unexpected non-bucket entry in " + + "the id-index bucket") + } + + err := sessionIDBkt.Put(groupIDKey, groupID) + if err != nil { + return err + } + + // Now we will add the group ID to session ID mapping. + groupIDBkt, err := groupIndexBkt.CreateBucketIfNotExists( + groupID, + ) + if err != nil { + return err + } + + groupSessionIDBkt, err := groupIDBkt.CreateBucketIfNotExists( + sessionIDKey, + ) + if err != nil { + return err + } + + nextSeq, err := groupSessionIDBkt.NextSequence() + if err != nil { + return err + } + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], nextSeq) + + return groupSessionIDBkt.Put(seqNoBytes[:], groupID[:]) + }) +} diff --git a/session/migration2/id_to_group_index_test.go b/session/migration2/id_to_group_index_test.go new file mode 100644 index 000000000..c20352164 --- /dev/null +++ b/session/migration2/id_to_group_index_test.go @@ -0,0 +1,122 @@ +package migration2 + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-terminal/session/migtest" + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" +) + +// ID represents the id of a session. +type ID [4]byte + +// TestMigrateSessionIDToGroupIDIndex tests that the +// MigrateSessionIDToGroupIDIndex migration correctly back-fills the session ID +// to group ID index along with the group ID to session ID index. +func TestMigrateSessionIDToGroupIDIndex(t *testing.T) { + t.Parallel() + + // Make a few session IDs. + sess1ID, sess1Key := newSessionID(t) + sess2ID, sess2Key := newSessionID(t) + sess3ID, sess3Key := newSessionID(t) + + // Put together a sample session ID index DB based on the above. + idIndexBefore := map[string]interface{}{ + string(sess1ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess1Key), + }, + string(sess2ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess2Key), + }, + string(sess3ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess3Key), + }, + } + + // sessionDBBefore is what our session DB will look like before the + // migration. + sessionDBBefore := map[string]interface{}{ + string(idIndexKey): idIndexBefore, + string(groupIDIndexKey): map[string]interface{}{}, + } + + before := func(tx *bbolt.Tx) error { + return migtest.RestoreDB(tx, sessionBucketKey, sessionDBBefore) + } + + // Put together what we expect the resulting id-index bucket to look + // like after the migration. + idIndexAfter := map[string]interface{}{ + string(sess1ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess1Key), + string(groupIDKey): string(sess1ID[:]), + }, + string(sess2ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess2Key), + string(groupIDKey): string(sess2ID[:]), + }, + string(sess3ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess3Key), + string(groupIDKey): string(sess3ID[:]), + }, + } + + // Put together what we expect the resulting group-ID-index bucket to + // look like after the migration. + groupIDIndexAfter := map[string]interface{}{ + string(sess1ID[:]): map[string]interface{}{ + string(sessionIDKey): map[string]interface{}{ + sequenceString(1): string(sess1ID[:]), + }, + }, + string(sess2ID[:]): map[string]interface{}{ + string(sessionIDKey): map[string]interface{}{ + sequenceString(1): string(sess2ID[:]), + }, + }, + string(sess3ID[:]): map[string]interface{}{ + string(sessionIDKey): map[string]interface{}{ + sequenceString(1): string(sess3ID[:]), + }, + }, + } + + // sessionDBAfter is what our session DB will look like after the + // migration. + sessionDBAfter := map[string]interface{}{ + string(idIndexKey): idIndexAfter, + string(groupIDIndexKey): groupIDIndexAfter, + } + + after := func(tx *bbolt.Tx) error { + return migtest.VerifyDB(tx, sessionBucketKey, sessionDBAfter) + } + + migtest.ApplyMigration( + t, before, after, MigrateSessionIDToGroupIndex, false, + ) +} + +// newSessionID is a helper function that can be used to generate a new session +// ID and key. +func newSessionID(t *testing.T) (ID, []byte) { + privateKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + key := privateKey.PubKey().SerializeCompressed() + + var id ID + copy(id[:], key) + + return id, key +} + +func sequenceString(id uint64) string { + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], id) + + return string(seqNoBytes[:]) +} diff --git a/session/migtest/migtest.go b/session/migtest/migtest.go new file mode 100644 index 000000000..34f929b7f --- /dev/null +++ b/session/migtest/migtest.go @@ -0,0 +1,79 @@ +package migtest + +import ( + "fmt" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" +) + +const ( + // dbFilePermission is the default permission the rules' database file + // is created with. + dbFilePermission = 0600 +) + +// MakeDB creates a new instance of the firewall DB for testing purposes. +func MakeDB(t *testing.T) *bbolt.DB { + dir := t.TempDir() + path := filepath.Join(dir, "test.db") + + db, err := bbolt.Open(path, dbFilePermission, &bbolt.Options{ + Timeout: time.Second * 5, + }) + require.NoError(t, err) + + return db +} + +// ApplyMigration is a helper test function that encapsulates the general steps +// which are needed to properly check the result of applying migration function. +func ApplyMigration(t *testing.T, beforeMigration, afterMigration, + migrationFunc func(tx *bbolt.Tx) error, shouldFail bool) { + + t.Helper() + + db := MakeDB(t) + + // beforeMigration is usually used for populating the database with + // test data. + require.NoError(t, db.Update(beforeMigration)) + + defer func() { + t.Helper() + + var err error + if r := recover(); r != nil { + err = newError(r) + } + + if shouldFail { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + // afterMigration usually used for checking the database state + // and throwing the error if something went wrong. + err = db.Update(afterMigration) + require.NoError(t, err) + }() + + // Apply migration. + require.NoError(t, db.Update(migrationFunc)) +} + +func newError(e interface{}) error { + var err error + switch e := e.(type) { + case error: + err = e + default: + err = fmt.Errorf("%v", e) + } + + return err +} diff --git a/session/migtest/raw_db.go b/session/migtest/raw_db.go new file mode 100644 index 000000000..7227e41bb --- /dev/null +++ b/session/migtest/raw_db.go @@ -0,0 +1,175 @@ +package migtest + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "strings" + + "go.etcd.io/bbolt" +) + +// DumpDB dumps go code describing the contents of the database to stdout. This +// function is only intended for use during development. +// +// Example output: +// +// map[string]interface{}{ +// hex("1234"): map[string]interface{}{ +// "human-readable": hex("102030"), +// hex("1111"): hex("5783492373"), +// }, +// } +func DumpDB(tx *bbolt.Tx, rootKey []byte) error { + bucket := tx.Bucket(rootKey) + if bucket == nil { + return fmt.Errorf("bucket %v not found", string(rootKey)) + } + + return dumpBucket(bucket) +} + +func dumpBucket(bucket *bbolt.Bucket) error { + fmt.Printf("map[string]interface{} {\n") + err := bucket.ForEach(func(k, v []byte) error { + key := toString(k) + fmt.Printf("%v: ", key) + + subBucket := bucket.Bucket(k) + if subBucket != nil { + err := dumpBucket(subBucket) + if err != nil { + return err + } + } else { + fmt.Print(toHex(v)) + } + fmt.Printf(",\n") + + return nil + }) + if err != nil { + return err + } + fmt.Printf("}") + + return nil +} + +// RestoreDB primes the database with the given data set. +func RestoreDB(tx *bbolt.Tx, rootKey []byte, data map[string]interface{}) error { + bucket, err := tx.CreateBucket(rootKey) + if err != nil { + return err + } + + return restoreDB(bucket, data) +} + +func restoreDB(bucket *bbolt.Bucket, data map[string]interface{}) error { + for k, v := range data { + key := []byte(k) + + switch value := v.(type) { + // Key contains value. + case string: + err := bucket.Put(key, []byte(value)) + if err != nil { + return err + } + + // Key contains a sub-bucket. + case map[string]interface{}: + subBucket, err := bucket.CreateBucket(key) + if err != nil { + return err + } + + if err := restoreDB(subBucket, value); err != nil { + return err + } + + default: + return errors.New("invalid type") + } + } + + return nil +} + +// VerifyDB verifies the database against the given data set. +func VerifyDB(tx *bbolt.Tx, rootKey []byte, data map[string]interface{}) error { + bucket := tx.Bucket(rootKey) + if bucket == nil { + return fmt.Errorf("bucket %v not found", string(rootKey)) + } + + return verifyDB(bucket, data) +} + +func verifyDB(bucket *bbolt.Bucket, data map[string]interface{}) error { + for k, v := range data { + key := []byte(k) + + switch value := v.(type) { + // Key contains value. + case string: + expectedValue := []byte(value) + dbValue := bucket.Get(key) + + if !bytes.Equal(dbValue, expectedValue) { + return errors.New("value mismatch") + } + + // Key contains a sub-bucket. + case map[string]interface{}: + subBucket := bucket.Bucket(key) + if subBucket == nil { + return fmt.Errorf("bucket %v not found", k) + } + + err := verifyDB(subBucket, value) + if err != nil { + return err + } + + default: + return errors.New("invalid type") + } + } + + keyCount := 0 + err := bucket.ForEach(func(k, v []byte) error { + keyCount++ + return nil + }) + if err != nil { + return err + } + if keyCount != len(data) { + return errors.New("unexpected keys in database") + } + + return nil +} + +func toHex(v []byte) string { + if len(v) == 0 { + return "nil" + } + + return "hex(\"" + hex.EncodeToString(v) + "\")" +} + +func toString(v []byte) string { + readableChars := "abcdefghijklmnopqrstuvwxyz0123456789-" + + for _, c := range v { + if !strings.Contains(readableChars, string(c)) { + return toHex(v) + } + } + + return "\"" + string(v) + "\"" +} diff --git a/session/migtest/raw_db_test.go b/session/migtest/raw_db_test.go new file mode 100644 index 000000000..7f4a4083e --- /dev/null +++ b/session/migtest/raw_db_test.go @@ -0,0 +1,80 @@ +package migtest + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" +) + +// TestRestoreAndVerifyDB tests that the RestoreDB and VerifyDB helper methods +// works as expected. +func TestRestoreAndVerifyDB(t *testing.T) { + // Define the top leve key name and the DB structure. + topLevelKey := []byte("top-level") + + dbStructure := map[string]interface{}{ + "key1": "value1", + "bucket1": map[string]interface{}{ + "key2": "value2", + "bucket2": map[string]interface{}{ + "key3": "value3", + }, + "bucket3": map[string]interface{}{ + "key4": "value4", + }, + }, + } + + // Create the DB and attempt to restore the DB structure. + db := MakeDB(t) + + err := db.Update(func(tx *bbolt.Tx) error { + return RestoreDB(tx, topLevelKey, dbStructure) + }) + require.NoError(t, err) + + // Check that the VerifyDB method passes. + err = db.View(func(tx *bbolt.Tx) error { + return VerifyDB(tx, topLevelKey, dbStructure) + }) + require.NoError(t, err) + + // Now manually do some checks based on what we know about the db + // structure. + err = db.View(func(tx *bbolt.Tx) error { + // Query for a bucket we did not create and ensure that nil is + // returned. + bucket := tx.Bucket([]byte("top-level-2")) + require.Nil(t, bucket) + + // Query for the actual top level bucket. + bucket = tx.Bucket(topLevelKey) + require.NotNil(t, bucket) + + // Test the first bucket level. + require.Equal(t, []byte("value1"), bucket.Get([]byte("key1"))) + require.Nil(t, bucket.Get([]byte("bucket1"))) + require.Nil(t, bucket.Get([]byte("key2"))) + + // Test the second bucket level. + bucket1 := bucket.Bucket([]byte("bucket1")) + require.NotNil(t, bucket1) + + require.Equal(t, []byte("value2"), bucket1.Get([]byte("key2"))) + require.Nil(t, bucket1.Get([]byte("key1"))) + require.Nil(t, bucket1.Get([]byte("bucket1"))) + require.Nil(t, bucket1.Get([]byte("bucket2"))) + + bucket2 := bucket1.Bucket([]byte("bucket2")) + require.NotNil(t, bucket2) + require.Equal(t, []byte("value3"), bucket2.Get([]byte("key3"))) + + bucket3 := bucket1.Bucket([]byte("bucket3")) + require.NotNil(t, bucket3) + require.Equal(t, []byte("value4"), bucket3.Get([]byte("key4"))) + + return nil + }) + require.NoError(t, err) +} diff --git a/session/server.go b/session/server.go index daa7243ee..ae7a50121 100644 --- a/session/server.go +++ b/session/server.go @@ -33,7 +33,7 @@ func newMailboxSession() *mailboxSession { func (m *mailboxSession) start(session *Session, serverCreator GRPCServerCreator, authData []byte, - onUpdate func(sess *Session) error, + onUpdate func(local, remote *btcec.PublicKey) error, onNewStatus func(s mailbox.ServerStatus)) error { tlsConfig := &tls.Config{} @@ -46,8 +46,7 @@ func (m *mailboxSession) start(session *Session, keys := mailbox.NewConnData( ecdh, session.RemotePublicKey, session.PairingSecret[:], authData, func(key *btcec.PublicKey) error { - session.RemotePublicKey = key - return onUpdate(session) + return onUpdate(session.LocalPublicKey, key) }, nil, ) @@ -105,7 +104,7 @@ func NewServer(serverCreator GRPCServerCreator) *Server { } func (s *Server) StartSession(session *Session, authData []byte, - onUpdate func(sess *Session) error, + onUpdate func(local, remote *btcec.PublicKey) error, onNewStatus func(s mailbox.ServerStatus)) (chan struct{}, error) { s.activeSessionsMtx.Lock() diff --git a/session/store.go b/session/store.go index 82abe2e27..9de473927 100644 --- a/session/store.go +++ b/session/store.go @@ -3,6 +3,7 @@ package session import ( "bytes" "errors" + "fmt" "time" "github.com/btcsuite/btcd/btcec/v2" @@ -13,11 +14,45 @@ var ( // sessionBucketKey is the top level bucket where we can find all // information about sessions. These sessions are indexed by their // public key. + // + // The session bucket has the following structure: + // session -> -> + // -> id-index -> -> key -> + // -> group -> + // -> group-id-index -> -> session-id -> sequence -> sessionBucketKey = []byte("session") + // idIndexKey is the key used to define the id-index sub-bucket within + // the main session bucket. This bucket will be used to store the + // mapping from session ID to various other fields. + idIndexKey = []byte("id-index") + + // sessionKeyKey is the key used within the id-index bucket to store the + // session key (serialised local public key) associated with the given + // session ID. + sessionKeyKey = []byte("key") + + // groupIDKey is the key used within the id-index bucket to store the + // group ID associated with the given session ID. + groupIDKey = []byte("group") + + // groupIDIndexKey is the key used to define the group-id-index + // sub-bucket within the main session bucket. This bucket will be used + // to store the mapping from group ID to various other fields. + groupIDIndexKey = []byte("group-id-index") + + // sessionIDKey is a key used in the group-id-index under a sub-bucket + // defined by a specific group ID. It will be used to store the session + // IDs associated with the given group ID. + sessionIDKey = []byte("session-id") + // ErrSessionNotFound is an error returned when we attempt to retrieve // information about a session but it is not found. ErrSessionNotFound = errors.New("session not found") + + // ErrDBInitErr is returned when a bucket that we expect to have been + // set up during DB initialisation is not found. + ErrDBInitErr = errors.New("db did not initialise properly") ) // getSessionKey returns the key for a session. @@ -25,10 +60,11 @@ func getSessionKey(session *Session) []byte { return session.LocalPublicKey.SerializeCompressed() } -// StoreSession stores a session in the store. If a session with the -// same local public key already exists, the existing record is updated/ -// overwritten instead. -func (db *DB) StoreSession(session *Session) error { +// CreateSession adds a new session to the store. If a session with the same +// local public key already exists an error is returned. +// +// NOTE: this is part of the Store interface. +func (db *DB) CreateSession(session *Session) error { var buf bytes.Buffer if err := SerializeSession(&buf, session); err != nil { return err @@ -41,11 +77,125 @@ func (db *DB) StoreSession(session *Session) error { return err } + if len(sessionBucket.Get(sessionKey)) != 0 { + return fmt.Errorf("session with local public "+ + "key(%x) already exists", + session.LocalPublicKey.SerializeCompressed()) + } + + // If this is a linked session (meaning the group ID is + // different from the ID) the make sure that the Group ID of + // this session is an ID known by the store. We also need to + // check that all older sessions in this group have been + // revoked. + if session.ID != session.GroupID { + _, err = getKeyForID(sessionBucket, session.GroupID) + if err != nil { + return fmt.Errorf("unknown linked session "+ + "%x: %w", session.GroupID, err) + } + + // Fetch all the session IDs for this group. This will + // through an error if this group does not exist. + sessionIDs, err := getSessionIDs( + sessionBucket, session.GroupID, + ) + if err != nil { + return err + } + + for _, id := range sessionIDs { + keyBytes, err := getKeyForID( + sessionBucket, id, + ) + if err != nil { + return err + } + + v := sessionBucket.Get(keyBytes) + if len(v) == 0 { + return ErrSessionNotFound + } + + sess, err := DeserializeSession( + bytes.NewReader(v), + ) + if err != nil { + return err + } + + // Ensure that the session is no longer active. + if sess.State == StateCreated || + sess.State == StateInUse { + + return fmt.Errorf("session (id=%x) "+ + "in group %x is still active", + sess.ID, sess.GroupID) + } + } + } + + // Add the mapping from session ID to session key to the ID + // index. + err = addIDToKeyPair(sessionBucket, session.ID, sessionKey) + if err != nil { + return err + } + + // Add the mapping from session ID to group ID and vice versa. + err = addIDToGroupIDPair( + sessionBucket, session.ID, session.GroupID, + ) + if err != nil { + return err + } + return sessionBucket.Put(sessionKey, buf.Bytes()) }) } +// UpdateSessionRemotePubKey can be used to add the given remote pub key +// to the session with the given local pub key. +// +// NOTE: this is part of the Store interface. +func (db *DB) UpdateSessionRemotePubKey(localPubKey, + remotePubKey *btcec.PublicKey) error { + + key := localPubKey.SerializeCompressed() + + return db.Update(func(tx *bbolt.Tx) error { + sessionBucket, err := getBucket(tx, sessionBucketKey) + if err != nil { + return err + } + + serialisedSession := sessionBucket.Get(key) + + if len(serialisedSession) == 0 { + return ErrSessionNotFound + } + + session, err := DeserializeSession( + bytes.NewReader(serialisedSession), + ) + if err != nil { + return err + } + + session.RemotePublicKey = remotePubKey + + var buf bytes.Buffer + if err := SerializeSession(&buf, session); err != nil { + return err + } + + return sessionBucket.Put(key, buf.Bytes()) + }) +} + // GetSession fetches the session with the given key. +// +// NOTE: this is part of the Store interface. func (db *DB) GetSession(key *btcec.PublicKey) (*Session, error) { var session *Session err := db.View(func(tx *bbolt.Tx) error { @@ -74,6 +224,8 @@ func (db *DB) GetSession(key *btcec.PublicKey) (*Session, error) { } // ListSessions returns all sessions currently known to the store. +// +// NOTE: this is part of the Store interface. func (db *DB) ListSessions(filterFn func(s *Session) bool) ([]*Session, error) { var sessions []*Session err := db.View(func(tx *bbolt.Tx) error { @@ -112,9 +264,11 @@ func (db *DB) ListSessions(filterFn func(s *Session) bool) ([]*Session, error) { // RevokeSession updates the state of the session with the given local // public key to be revoked. +// +// NOTE: this is part of the Store interface. func (db *DB) RevokeSession(key *btcec.PublicKey) error { var session *Session - err := db.View(func(tx *bbolt.Tx) error { + return db.Update(func(tx *bbolt.Tx) error { sessionBucket, err := getBucket(tx, sessionBucketKey) if err != nil { return err @@ -126,14 +280,352 @@ func (db *DB) RevokeSession(key *btcec.PublicKey) error { } session, err = DeserializeSession(bytes.NewReader(sessionBytes)) + if err != nil { + return err + } + + session.State = StateRevoked + session.RevokedAt = time.Now() + + var buf bytes.Buffer + if err := SerializeSession(&buf, session); err != nil { + return err + } + + return sessionBucket.Put(key.SerializeCompressed(), buf.Bytes()) + }) +} + +// GetSessionByID fetches the session with the given ID. +// +// NOTE: this is part of the Store interface. +func (db *DB) GetSessionByID(id ID) (*Session, error) { + var session *Session + err := db.View(func(tx *bbolt.Tx) error { + sessionBucket, err := getBucket(tx, sessionBucketKey) + if err != nil { + return err + } + + keyBytes, err := getKeyForID(sessionBucket, id) + if err != nil { + return err + } + + v := sessionBucket.Get(keyBytes) + if len(v) == 0 { + return ErrSessionNotFound + } + + session, err = DeserializeSession(bytes.NewReader(v)) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return session, nil +} + +// GetUnusedIDAndKeyPair can be used to generate a new, unused, local private +// key and session ID pair. Care must be taken to ensure that no other thread +// calls this before the returned ID and key pair from this method are either +// used or discarded. +// +// NOTE: this is part of the Store interface. +func (db *DB) GetUnusedIDAndKeyPair() (ID, *btcec.PrivateKey, error) { + var ( + id ID + privKey *btcec.PrivateKey + ) + err := db.Update(func(tx *bbolt.Tx) error { + sessionBucket, err := getBucket(tx, sessionBucketKey) + if err != nil { + return err + } + + idIndexBkt := sessionBucket.Bucket(idIndexKey) + if idIndexBkt == nil { + return ErrDBInitErr + } + + // Spin until we find a key with an ID that does not collide + // with any of our existing IDs. + for { + // Generate a new private key and ID pair. + privKey, id, err = NewSessionPrivKeyAndID() + if err != nil { + return err + } + + // Check that no such ID exits in our id-to-key index. + idBkt := idIndexBkt.Bucket(id[:]) + if idBkt != nil { + continue + } + + break + } + + return nil + }) + if err != nil { + return id, nil, err + } + + return id, privKey, nil +} + +// GetGroupID will return the group ID for the given session ID. +// +// NOTE: this is part of the IDToGroupIndex interface. +func (db *DB) GetGroupID(sessionID ID) (ID, error) { + var groupID ID + err := db.View(func(tx *bbolt.Tx) error { + sessionBkt, err := getBucket(tx, sessionBucketKey) + if err != nil { + return err + } + + idIndex := sessionBkt.Bucket(idIndexKey) + if idIndex == nil { + return ErrDBInitErr + } + + sessionIDBkt := idIndex.Bucket(sessionID[:]) + if sessionIDBkt == nil { + return fmt.Errorf("no index entry for session ID: %x", + sessionID) + } + + groupIDBytes := sessionIDBkt.Get(groupIDKey) + if len(groupIDBytes) == 0 { + return fmt.Errorf("group ID not found for session "+ + "ID %x", sessionID) + } + + copy(groupID[:], groupIDBytes) + + return nil + }) + if err != nil { + return groupID, err + } + + return groupID, nil +} + +// GetSessionIDs will return the set of session IDs that are in the +// group with the given ID. +// +// NOTE: this is part of the IDToGroupIndex interface. +func (db *DB) GetSessionIDs(groupID ID) ([]ID, error) { + var ( + sessionIDs []ID + err error + ) + err = db.View(func(tx *bbolt.Tx) error { + sessionBkt, err := getBucket(tx, sessionBucketKey) + if err != nil { + return err + } + + sessionIDs, err = getSessionIDs(sessionBkt, groupID) + return err }) + if err != nil { + return nil, err + } + + return sessionIDs, nil +} + +// CheckSessionGroupPredicate iterates over all the sessions in a group and +// checks if each one passes the given predicate function. True is returned if +// each session passes. +// +// NOTE: this is part of the Store interface. +func (db *DB) CheckSessionGroupPredicate(groupID ID, + fn func(s *Session) bool) (bool, error) { + + var ( + pass bool + errFailedPred = errors.New("session failed predicate") + ) + err := db.View(func(tx *bbolt.Tx) error { + sessionBkt, err := getBucket(tx, sessionBucketKey) + if err != nil { + return err + } + + sessionIDs, err := getSessionIDs(sessionBkt, groupID) + if err != nil { + return err + } + + // Iterate over all the sessions. + for _, id := range sessionIDs { + key, err := getKeyForID(sessionBkt, id) + if err != nil { + return err + } + + v := sessionBkt.Get(key) + if len(v) == 0 { + return ErrSessionNotFound + } + + session, err := DeserializeSession(bytes.NewReader(v)) + if err != nil { + return err + } + + if !fn(session) { + return errFailedPred + } + } + + pass = true + + return nil + }) + if errors.Is(err, errFailedPred) { + return pass, nil + } + if err != nil { + return pass, err + } + + return pass, nil +} + +// getSessionIDs returns all the session IDs associated with the given group ID. +func getSessionIDs(sessionBkt *bbolt.Bucket, groupID ID) ([]ID, error) { + var sessionIDs []ID + + groupIndexBkt := sessionBkt.Bucket(groupIDIndexKey) + if groupIndexBkt == nil { + return nil, ErrDBInitErr + } + + groupIDBkt := groupIndexBkt.Bucket(groupID[:]) + if groupIDBkt == nil { + return nil, fmt.Errorf("no sessions for group ID %v", + groupID) + } + + sessionIDsBkt := groupIDBkt.Bucket(sessionIDKey) + if sessionIDsBkt == nil { + return nil, fmt.Errorf("no sessions for group ID %v", + groupID) + } + + err := sessionIDsBkt.ForEach(func(_, + sessionIDBytes []byte) error { + + var sessionID ID + copy(sessionID[:], sessionIDBytes) + sessionIDs = append(sessionIDs, sessionID) + + return nil + }) + if err != nil { + return nil, err + } + + return sessionIDs, nil +} + +// addIdToKeyPair inserts the mapping from session ID to session key into the +// id-index bucket. An error is returned if an entry for this ID already exists. +func addIDToKeyPair(sessionBkt *bbolt.Bucket, id ID, sessionKey []byte) error { + idIndexBkt := sessionBkt.Bucket(idIndexKey) + if idIndexBkt == nil { + return ErrDBInitErr + } + + idBkt, err := idIndexBkt.CreateBucketIfNotExists(id[:]) + if err != nil { + return err + } + + if len(idBkt.Get(sessionKeyKey)) != 0 { + return fmt.Errorf("a session with the given ID already exists") + } + + return idBkt.Put(sessionKeyKey[:], sessionKey) +} + +// getKeyForID fetches the session key associated with the given session ID. +func getKeyForID(sessionBkt *bbolt.Bucket, id ID) ([]byte, error) { + idIndexBkt := sessionBkt.Bucket(idIndexKey) + if idIndexBkt == nil { + return nil, ErrDBInitErr + } + + idBkt := idIndexBkt.Bucket(id[:]) + if idBkt == nil { + return nil, fmt.Errorf("no entry found in the ID index for "+ + "ID: %x", id) + } + + sessionKeyBytes := idBkt.Get(sessionKeyKey) + if len(sessionKeyKey) == 0 { + return nil, fmt.Errorf("no session key found in the ID "+ + "index for ID: %x", id) + } + + return sessionKeyBytes, nil +} + +// addIDToGroupIDPair inserts the mapping from session ID to group ID into the +// id-index bucket and also inserts the mapping from group ID to session ID into +// the group-id-index bucket. +func addIDToGroupIDPair(sessionBkt *bbolt.Bucket, id, groupID ID) error { + // First we will add the mapping from session ID to group ID. + idIndexBkt := sessionBkt.Bucket(idIndexKey) + if idIndexBkt == nil { + return ErrDBInitErr + } + + idBkt, err := idIndexBkt.CreateBucketIfNotExists(id[:]) + if err != nil { + return err + } + + err = idBkt.Put(groupIDKey, groupID[:]) + if err != nil { + return err + } + + // Now we add the mapping from group ID to session. + groupIdIndexBkt := sessionBkt.Bucket(groupIDIndexKey) + if groupIdIndexBkt == nil { + return ErrDBInitErr + } + + groupBkt, err := groupIdIndexBkt.CreateBucketIfNotExists(groupID[:]) + if err != nil { + return err + } + + sessionIDsBkt, err := groupBkt.CreateBucketIfNotExists(sessionIDKey) + if err != nil { + return err + } + + nextSeq, err := sessionIDsBkt.NextSequence() if err != nil { return err } - session.State = StateRevoked - session.RevokedAt = time.Now() + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], nextSeq) - return db.StoreSession(session) + return sessionIDsBkt.Put(seqNoBytes[:], id[:]) } diff --git a/session/store_test.go b/session/store_test.go new file mode 100644 index 000000000..a75a93d41 --- /dev/null +++ b/session/store_test.go @@ -0,0 +1,296 @@ +package session + +import ( + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +// TestBasicSessionStore tests the basic getters and setters of the session +// store. +func TestBasicSessionStore(t *testing.T) { + // Set up a new DB. + db, err := NewDB(t.TempDir(), "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + // Create a few sessions. + s1 := newSession(t, db, "session 1", nil) + s2 := newSession(t, db, "session 2", nil) + s3 := newSession(t, db, "session 3", nil) + s4 := newSession(t, db, "session 4", nil) + + // Persist session 1. This should now succeed. + require.NoError(t, db.CreateSession(s1)) + + // Trying to persist session 1 again should fail due to a session with + // the given pub key already existing. + require.ErrorContains(t, db.CreateSession(s1), "already exists") + + // Change the local pub key of session 4 such that it has the same + // ID as session 1. + s4.ID = s1.ID + s4.GroupID = s1.GroupID + + // Now try to insert session 4. This should fail due to an entry for + // the ID already existing. + require.ErrorContains(t, db.CreateSession(s4), "a session with the "+ + "given ID already exists") + + // Persist a few more sessions. + require.NoError(t, db.CreateSession(s2)) + require.NoError(t, db.CreateSession(s3)) + + // Ensure that we can retrieve each session by both its local pub key + // and by its ID. + for _, s := range []*Session{s1, s2, s3} { + session, err := db.GetSession(s.LocalPublicKey) + require.NoError(t, err) + require.Equal(t, s.Label, session.Label) + + session, err = db.GetSessionByID(s.ID) + require.NoError(t, err) + require.Equal(t, s.Label, session.Label) + } + + // Fetch session 1 and assert that it currently has no remote pub key. + session1, err := db.GetSession(s1.LocalPublicKey) + require.NoError(t, err) + require.Nil(t, session1.RemotePublicKey) + + // Use the update method to add a remote key. + remotePriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + remotePub := remotePriv.PubKey() + + err = db.UpdateSessionRemotePubKey(session1.LocalPublicKey, remotePub) + require.NoError(t, err) + + // Assert that the session now does have the remote pub key. + session1, err = db.GetSession(s1.LocalPublicKey) + require.NoError(t, err) + require.True(t, remotePub.IsEqual(session1.RemotePublicKey)) + + // Check that the session's state is currently StateCreated. + require.Equal(t, session1.State, StateCreated) + + // Now revoke the session and assert that the state is revoked. + require.NoError(t, db.RevokeSession(s1.LocalPublicKey)) + session1, err = db.GetSession(s1.LocalPublicKey) + require.NoError(t, err) + require.Equal(t, session1.State, StateRevoked) +} + +// TestLinkingSessions tests that session linking works as expected. +func TestLinkingSessions(t *testing.T) { + // Set up a new DB. + db, err := NewDB(t.TempDir(), "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + // Create a new session with no previous link. + s1 := newSession(t, db, "session 1", nil) + + // Create another session and link it to the first. + s2 := newSession(t, db, "session 2", &s1.GroupID) + + // Try to persist the second session and assert that it fails due to the + // linked session not existing in the DB yet. + require.ErrorContains(t, db.CreateSession(s2), "unknown linked session") + + // Now persist the first session and retry persisting the second one + // and assert that this now works. + require.NoError(t, db.CreateSession(s1)) + + // Persisting the second session immediately should fail due to the + // first session still being active. + require.ErrorContains(t, db.CreateSession(s2), "is still active") + + // Revoke the first session. + require.NoError(t, db.RevokeSession(s1.LocalPublicKey)) + + // Persisting the second linked session should now work. + require.NoError(t, db.CreateSession(s2)) +} + +// TestIDToGroupIDIndex tests that the session-ID-to-group-ID and +// group-ID-to-session-ID indexes work as expected by asserting the behaviour +// of the GetGroupID and GetSessionIDs methods. +func TestLinkedSessions(t *testing.T) { + // Set up a new DB. + db, err := NewDB(t.TempDir(), "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + // Create a few sessions. The first one is a new session and the two + // after are all linked to the prior one. All these sessions belong to + // the same group. The group ID is equivalent to the session ID of the + // first session. + s1 := newSession(t, db, "session 1", nil) + s2 := newSession(t, db, "session 2", &s1.GroupID) + s3 := newSession(t, db, "session 3", &s2.GroupID) + + // Persist the sessions. + require.NoError(t, db.CreateSession(s1)) + + require.NoError(t, db.RevokeSession(s1.LocalPublicKey)) + require.NoError(t, db.CreateSession(s2)) + + require.NoError(t, db.RevokeSession(s2.LocalPublicKey)) + require.NoError(t, db.CreateSession(s3)) + + // Assert that the session ID to group ID index works as expected. + for _, s := range []*Session{s1, s2, s3} { + groupID, err := db.GetGroupID(s.ID) + require.NoError(t, err) + require.Equal(t, s1.ID, groupID) + require.Equal(t, s.GroupID, groupID) + } + + // Assert that the group ID to session ID index works as expected. + sIDs, err := db.GetSessionIDs(s1.GroupID) + require.NoError(t, err) + require.EqualValues(t, []ID{s1.ID, s2.ID, s3.ID}, sIDs) + + // To ensure that different groups don't interfere with each other, + // let's add another set of linked sessions not linked to the first. + s4 := newSession(t, db, "session 4", nil) + s5 := newSession(t, db, "session 5", &s4.GroupID) + + require.NotEqual(t, s4.GroupID, s1.GroupID) + + // Persist the sessions. + require.NoError(t, db.CreateSession(s4)) + require.NoError(t, db.RevokeSession(s4.LocalPublicKey)) + + require.NoError(t, db.CreateSession(s5)) + + // Assert that the session ID to group ID index works as expected. + for _, s := range []*Session{s4, s5} { + groupID, err := db.GetGroupID(s.ID) + require.NoError(t, err) + require.Equal(t, s4.ID, groupID) + require.Equal(t, s.GroupID, groupID) + } + + // Assert that the group ID to session ID index works as expected. + sIDs, err = db.GetSessionIDs(s5.GroupID) + require.NoError(t, err) + require.EqualValues(t, []ID{s4.ID, s5.ID}, sIDs) +} + +// TestCheckSessionGroupPredicate asserts that the CheckSessionGroupPredicate +// method correctly checks if each session in a group passes a predicate. +func TestCheckSessionGroupPredicate(t *testing.T) { + // Set up a new DB. + db, err := NewDB(t.TempDir(), "test.db") + require.NoError(t, err) + t.Cleanup(func() { + _ = db.Close() + }) + + // We will use the Label of the Session to test that the predicate + // function is checked correctly. + + // Add a new session to the DB. + s1 := newSession(t, db, "label 1", nil) + require.NoError(t, db.CreateSession(s1)) + + // Check that the group passes against an appropriate predicate. + ok, err := db.CheckSessionGroupPredicate( + s1.GroupID, func(s *Session) bool { + return strings.Contains(s.Label, "label 1") + }, + ) + require.NoError(t, err) + require.True(t, ok) + + // Check that the group fails against an appropriate predicate. + ok, err = db.CheckSessionGroupPredicate( + s1.GroupID, func(s *Session) bool { + return strings.Contains(s.Label, "label 2") + }, + ) + require.NoError(t, err) + require.False(t, ok) + + // Revoke the first session. + require.NoError(t, db.RevokeSession(s1.LocalPublicKey)) + + // Add a new session to the same group as the first one. + s2 := newSession(t, db, "label 2", &s1.GroupID) + require.NoError(t, db.CreateSession(s2)) + + // Check that the group passes against an appropriate predicate. + ok, err = db.CheckSessionGroupPredicate( + s1.GroupID, func(s *Session) bool { + return strings.Contains(s.Label, "label") + }, + ) + require.NoError(t, err) + require.True(t, ok) + + // Check that the group fails against an appropriate predicate. + ok, err = db.CheckSessionGroupPredicate( + s1.GroupID, func(s *Session) bool { + return strings.Contains(s.Label, "label 1") + }, + ) + require.NoError(t, err) + require.False(t, ok) + + // Add a new session that is not linked to the first one. + s3 := newSession(t, db, "completely different", nil) + require.NoError(t, db.CreateSession(s3)) + + // Ensure that the first group is unaffected. + ok, err = db.CheckSessionGroupPredicate( + s1.GroupID, func(s *Session) bool { + return strings.Contains(s.Label, "label") + }, + ) + require.NoError(t, err) + require.True(t, ok) + + // And that the new session is evaluated separately. + ok, err = db.CheckSessionGroupPredicate( + s3.GroupID, func(s *Session) bool { + return strings.Contains(s.Label, "label") + }, + ) + require.NoError(t, err) + require.False(t, ok) + + ok, err = db.CheckSessionGroupPredicate( + s3.GroupID, func(s *Session) bool { + return strings.Contains(s.Label, "different") + }, + ) + require.NoError(t, err) + require.True(t, ok) +} + +func newSession(t *testing.T, db Store, label string, + linkedGroupID *ID) *Session { + + id, priv, err := db.GetUnusedIDAndKeyPair() + require.NoError(t, err) + + session, err := NewSession( + id, priv, label, TypeMacaroonAdmin, + time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC), + "foo.bar.baz:1234", true, nil, nil, nil, true, linkedGroupID, + ) + require.NoError(t, err) + + return session +} diff --git a/session/tlv.go b/session/tlv.go index f2dbb88c8..3e0feb02d 100644 --- a/session/tlv.go +++ b/session/tlv.go @@ -28,6 +28,7 @@ const ( typeFeaturesConfig tlv.Type = 14 typeWithPrivacy tlv.Type = 15 typeRevokedAt tlv.Type = 16 + typeGroupID tlv.Type = 17 // typeMacaroon is no longer used, but we leave it defined for backwards // compatibility. @@ -54,6 +55,28 @@ func SerializeSession(w io.Writer, session *Session) error { return fmt.Errorf("session cannot be nil") } + tlvRecords, err := constructSessionTLVRecords(session, true) + if err != nil { + return err + } + + tlvStream, err := tlv.NewStream(tlvRecords...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + +// constructSessionTlvRecords takes a Session and gathers all the TLV records +// that need to be serialised for the session. The withGroupID boolean can be +// used to exclude the tlv record for the GroupID of the session. This should +// only ever be set to true for testing purposes to ensure that older sessions +// which did do not have the GroupID serialised still correctly deserialize and +// set the correct GroupID in the Session struct. +func constructSessionTLVRecords(session *Session, withGroupID bool) ( + []tlv.Record, error) { + var ( label = []byte(session.Label) state = uint8(session.State) @@ -140,12 +163,14 @@ func SerializeSession(w io.Writer, session *Session) error { tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt), ) - tlvStream, err := tlv.NewStream(tlvRecords...) - if err != nil { - return err + if withGroupID { + groupID := session.GroupID[:] + tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord( + typeGroupID, &groupID, + )) } - return tlvStream.Encode(w) + return tlvRecords, nil } // DeserializeSession deserializes a session from the given reader, expecting @@ -159,6 +184,7 @@ func DeserializeSession(r io.Reader) (*Session, error) { expiry, createdAt, revokedAt uint64 macRecipe MacaroonRecipe featureConfig FeaturesConfig + groupID []byte ) tlvStream, err := tlv.NewStream( tlv.MakePrimitiveRecord(typeLabel, &label), @@ -186,6 +212,7 @@ func DeserializeSession(r io.Reader) (*Session, error) { ), tlv.MakePrimitiveRecord(typeWithPrivacy, &privacy), tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt), + tlv.MakePrimitiveRecord(typeGroupID, &groupID), ) if err != nil { return nil, err @@ -228,6 +255,15 @@ func DeserializeSession(r io.Reader) (*Session, error) { session.FeatureConfig = &featureConfig } + // The GroupID field might not be set for older sessions. So we attempt + // to read it from the DB, otherwise we set the group ID to the session + // ID. + if t, ok := parsedTypes[typeGroupID]; ok && t == nil { + copy(session.GroupID[:], groupID) + } else { + session.GroupID = session.ID + } + return session, nil } diff --git a/session/tlv_test.go b/session/tlv_test.go index f1d211dd8..631663758 100644 --- a/session/tlv_test.go +++ b/session/tlv_test.go @@ -45,11 +45,15 @@ var ( Id: []byte("caveat id3 here"), }, } + + groupID = ID{0, 1, 3, 4} ) // TestSerializeDeserializeSession makes sure that a session can be serialized // and deserialized from and to the tlv binary format successfully. func TestSerializeDeserializeSession(t *testing.T) { + t.Parallel() + tests := []struct { name string sessType Type @@ -57,37 +61,80 @@ func TestSerializeDeserializeSession(t *testing.T) { perms []bakery.Op caveats []macaroon.Caveat featureConfig map[string][]byte + linkedGroupID *ID }{ { - name: "session 1", + name: "revoked-at field", sessType: TypeMacaroonCustom, revokedAt: time.Date( 2023, 1, 10, 10, 10, 0, 0, time.UTC, ), }, { - name: "session 2", + name: "permissions and caveats", sessType: TypeMacaroonCustom, perms: perms, caveats: caveats, }, { - name: "session 3", + name: "feature configuration bytes", + sessType: TypeMacaroonCustom, + featureConfig: map[string][]byte{ + "AutoFees": {1, 2, 3, 4}, + "AutoSomething": {4, 3, 4, 5, 6, 6}, + }, + }, + { + name: "linked session with no group ID", sessType: TypeMacaroonCustom, featureConfig: map[string][]byte{ "AutoFees": {1, 2, 3, 4}, "AutoSomething": {4, 3, 4, 5, 6, 6}, }, + linkedGroupID: &groupID, + }, + { + name: "linked session with group ID", + sessType: TypeMacaroonCustom, + featureConfig: map[string][]byte{ + "AutoFees": {1, 2, 3, 4}, + "AutoSomething": {4, 3, 4, 5, 6, 6}, + }, + linkedGroupID: &groupID, + }, + { + name: "session with no optional fields", + sessType: TypeMacaroonCustom, + }, + { + name: "session with all optional fields", + sessType: TypeMacaroonCustom, + revokedAt: time.Date( + 2023, 1, 10, 10, 10, 0, 0, time.UTC, + ), + perms: perms, + caveats: caveats, + featureConfig: map[string][]byte{ + "AutoFees": {1, 2, 3, 4}, + "AutoSomething": {4, 3, 4, 5, 6, 6}, + }, }, } for _, test := range tests { + test := test t.Run(test.name, func(t *testing.T) { + t.Parallel() + + priv, id, err := NewSessionPrivKeyAndID() + require.NoError(t, err) + session, err := NewSession( - test.name, test.sessType, + id, priv, test.name, test.sessType, time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC), "foo.bar.baz:1234", true, test.perms, test.caveats, test.featureConfig, true, + test.linkedGroupID, ) require.NoError(t, err) @@ -126,9 +173,83 @@ func TestSerializeDeserializeSession(t *testing.T) { } } +// TestGroupIDForOlderSessions tests that older sessions that were added before +// the GroupID was introduced still deserialize correctly by using the session's +// ID as the GroupID. +func TestGroupIDForOlderSessions(t *testing.T) { + t.Parallel() + + priv, id, err := NewSessionPrivKeyAndID() + require.NoError(t, err) + + session, err := NewSession( + id, priv, "test-session", TypeMacaroonAdmin, + time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC), + "foo.bar.baz:1234", true, nil, nil, nil, false, nil, + ) + require.NoError(t, err) + + // Gather all the tlv records for the session _except for the group ID_. + records, err := constructSessionTLVRecords(session, false) + require.NoError(t, err) + + stream, err := tlv.NewStream(records...) + require.NoError(t, err) + + // Serialise the TLV stream. + var buf bytes.Buffer + require.NoError(t, stream.Encode(&buf)) + + // Now deserialize the session and ensure that the group ID _does_ get + // set correctly the session's ID. + sess, err := DeserializeSession(&buf) + require.NoError(t, err) + require.Equal(t, session.ID, sess.GroupID) +} + +// TestGroupID tests that a Session's GroupID member gets correctly set +// depending on if the Session is linked to a previous one. +func TestGroupID(t *testing.T) { + t.Parallel() + + priv, id, err := NewSessionPrivKeyAndID() + require.NoError(t, err) + + // Create session 1 which is not linked to any previous session. + session1, err := NewSession( + id, priv, "test-session", TypeMacaroonAdmin, + time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC), + "foo.bar.baz:1234", true, nil, nil, nil, false, nil, + ) + require.NoError(t, err) + + // The group ID of this session should be the same as the session ID. + require.Equal(t, session1.ID, session1.GroupID) + + // Create session 2 and link it to session 1. + priv, id, err = NewSessionPrivKeyAndID() + require.NoError(t, err) + session2, err := NewSession( + id, priv, "test-session", TypeMacaroonAdmin, + time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC), + "foo.bar.baz:1234", true, nil, nil, nil, false, + &session1.GroupID, + ) + require.NoError(t, err) + + // The group ID of this session should _not_ the same as its session ID. + require.NotEqual(t, session2.ID, session2.GroupID) + + // Instead, the group ID should match the session ID of session 1. + require.Equal(t, session1.ID, session2.GroupID) + +} + // TestSerializeDeserializeCaveats makes sure that a list of caveats can be // serialized and deserialized from and to the tlv binary format successfully. func TestSerializeDeserializeCaveats(t *testing.T) { + t.Parallel() + // We'll now make a sample invoice stream, and use that to encode the // amp state we created above. tlvStream, err := tlv.NewStream( @@ -170,6 +291,8 @@ func TestSerializeDeserializeCaveats(t *testing.T) { // TestSerializeDeserializePerms makes sure that a list of perms can be // serialized and deserialized from and to the tlv binary format successfully. func TestSerializeDeserializePerms(t *testing.T) { + t.Parallel() + // We'll now make a sample invoice stream, and use that to encode the // amp state we created above. tlvStream, err := tlv.NewStream( @@ -209,6 +332,8 @@ func TestSerializeDeserializePerms(t *testing.T) { // recipes can be serialized and deserialized from and to the tlv binary format // successfully. func TestSerializeDeserializeMacaroonRecipe(t *testing.T) { + t.Parallel() + recipe := MacaroonRecipe{ Permissions: perms, Caveats: caveats, diff --git a/session_rpcserver.go b/session_rpcserver.go index 29e0ef54f..d5ff84d23 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -10,6 +10,8 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightninglabs/lightning-node-connect/mailbox" "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/autopilotserver" @@ -41,9 +43,13 @@ type sessionRpcServer struct { litrpc.UnimplementedAutopilotServer cfg *sessionRpcServerConfig - db *session.DB sessionServer *session.Server + // sessRegMu is a mutex that should be held between acquiring an unused + // session ID and key pair from the session store and persisting that + // new session. + sessRegMu sync.Mutex + quit chan struct{} wg sync.WaitGroup stopOnce sync.Once @@ -52,8 +58,8 @@ type sessionRpcServer struct { // sessionRpcServerConfig holds the values used to configure the // sessionRpcServer. type sessionRpcServerConfig struct { + db *session.DB basicAuth string - dbDir string grpcOptions []grpc.ServerOption registerGrpcServers func(server *grpc.Server) superMacBaker session.MacaroonBaker @@ -69,12 +75,6 @@ type sessionRpcServerConfig struct { func newSessionRPCServer(cfg *sessionRpcServerConfig) (*sessionRpcServer, error) { - // Create an instance of the local Terminal Connect session store DB. - db, err := session.NewDB(cfg.dbDir, session.DBFilename) - if err != nil { - return nil, fmt.Errorf("error creating session DB: %v", err) - } - // Create the gRPC server that handles adding/removing sessions and the // actual mailbox server that spins up the Terminal Connect server // interface. @@ -91,7 +91,6 @@ func newSessionRPCServer(cfg *sessionRpcServerConfig) (*sessionRpcServer, return &sessionRpcServer{ cfg: cfg, - db: db, sessionServer: server, quit: make(chan struct{}), }, nil @@ -101,7 +100,7 @@ func newSessionRPCServer(cfg *sessionRpcServerConfig) (*sessionRpcServer, // requests. This includes resuming all non-revoked sessions. func (s *sessionRpcServer) start() error { // Start up all previously created sessions. - sessions, err := s.db.ListSessions(nil) + sessions, err := s.cfg.db.ListSessions(nil) if err != nil { return fmt.Errorf("error listing sessions: %v", err) } @@ -152,7 +151,7 @@ func (s *sessionRpcServer) start() error { err) if perm { - err := s.db.RevokeSession( + err := s.cfg.db.RevokeSession( sess.LocalPublicKey, ) if err != nil { @@ -177,7 +176,7 @@ func (s *sessionRpcServer) start() error { func (s *sessionRpcServer) stop() error { var returnErr error s.stopOnce.Do(func() { - if err := s.db.Close(); err != nil { + if err := s.cfg.db.Close(); err != nil { log.Errorf("Error closing session DB: %v", err) returnErr = err } @@ -315,15 +314,23 @@ func (s *sessionRpcServer) AddSession(_ context.Context, } } + s.sessRegMu.Lock() + defer s.sessRegMu.Unlock() + + id, localPrivKey, err := s.cfg.db.GetUnusedIDAndKeyPair() + if err != nil { + return nil, err + } + sess, err := session.NewSession( - req.Label, typ, expiry, req.MailboxServerAddr, req.DevServer, - uniquePermissions, caveats, nil, false, + id, localPrivKey, req.Label, typ, expiry, req.MailboxServerAddr, + req.DevServer, uniquePermissions, caveats, nil, false, nil, ) if err != nil { return nil, fmt.Errorf("error creating new session: %v", err) } - if err := s.db.StoreSession(sess); err != nil { + if err := s.cfg.db.CreateSession(sess); err != nil { return nil, fmt.Errorf("error storing session: %v", err) } @@ -362,7 +369,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { log.Debugf("Not resuming session %x with expiry %s", pubKeyBytes, sess.Expiry) - if err := s.db.RevokeSession(pubKey); err != nil { + if err := s.cfg.db.RevokeSession(pubKey); err != nil { return fmt.Errorf("error revoking session: %v", err) } @@ -442,7 +449,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { log.Debugf("Deadline for session %x has already "+ "passed. Revoking session", pubKeyBytes) - return s.db.RevokeSession(pubKey) + return s.cfg.db.RevokeSession(pubKey) } // Start the deadline timer. @@ -477,7 +484,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { authData := []byte(fmt.Sprintf("%s: %s", HeaderMacaroon, mac)) sessionClosedSub, err := s.sessionServer.StartSession( - sess, authData, s.db.StoreSession, onNewStatus, + sess, authData, s.cfg.db.UpdateSessionRemotePubKey, onNewStatus, ) if err != nil { return err @@ -522,7 +529,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { log.Debugf("Error stopping session: %v", err) } - err = s.db.RevokeSession(pubKey) + err = s.cfg.db.RevokeSession(pubKey) if err != nil { log.Debugf("error revoking session: %v", err) } @@ -535,7 +542,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { func (s *sessionRpcServer) ListSessions(_ context.Context, _ *litrpc.ListSessionsRequest) (*litrpc.ListSessionsResponse, error) { - sessions, err := s.db.ListSessions(nil) + sessions, err := s.cfg.db.ListSessions(nil) if err != nil { return nil, fmt.Errorf("error fetching sessions: %v", err) } @@ -564,7 +571,7 @@ func (s *sessionRpcServer) RevokeSession(ctx context.Context, return nil, fmt.Errorf("error parsing public key: %v", err) } - if err := s.db.RevokeSession(pubKey); err != nil { + if err := s.cfg.db.RevokeSession(pubKey); err != nil { return nil, fmt.Errorf("error revoking session: %v", err) } @@ -587,13 +594,29 @@ func (s *sessionRpcServer) PrivacyMapConversion(_ context.Context, req *litrpc.PrivacyMapConversionRequest) ( *litrpc.PrivacyMapConversionResponse, error) { - sessionID, err := session.IDFromBytes(req.SessionId) - if err != nil { - return nil, err + var ( + groupID session.ID + err error + ) + if len(req.GroupId) != 0 { + groupID, err = session.IDFromBytes(req.GroupId) + if err != nil { + return nil, err + } + } else { + sessionID, err := session.IDFromBytes(req.SessionId) + if err != nil { + return nil, err + } + + groupID, err = s.cfg.db.GetGroupID(sessionID) + if err != nil { + return nil, err + } } var res string - privMap := s.cfg.privMap(sessionID) + privMap := s.cfg.privMap(groupID) err = privMap.View(func(tx firewalldb.PrivacyMapTx) error { var err error if req.RealToPseudo { @@ -720,6 +743,16 @@ func (s *sessionRpcServer) ListActions(_ context.Context, if err != nil { return nil, err } + } else if req.GroupId != nil { + groupID, err := session.IDFromBytes(req.GroupId) + if err != nil { + return nil, err + } + + actions, err = db.ListGroupActions(groupID, filterFn) + if err != nil { + return nil, err + } } else { actions, lastIndex, totalCount, err = db.ListActions( filterFn, query, @@ -979,16 +1012,89 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context, caveats = append(caveats, firewall.MetaPrivacyCaveat) } + // If a previous session ID has been set to link this new one to, we + // first check if we have the referenced session, and we make sure it + // has been revoked. + var ( + linkedGroupID *session.ID + linkedGroupSession *session.Session + ) + if len(req.LinkedGroupId) != 0 { + var groupID session.ID + copy(groupID[:], req.LinkedGroupId) + + // Check that the group actually does exist. + groupSess, err := s.cfg.db.GetSessionByID(groupID) + if err != nil { + return nil, err + } + + // Ensure that the linked session is in fact the first session + // in its group. + if groupSess.ID != groupSess.GroupID { + return nil, fmt.Errorf("can not link to session "+ + "%x since it is not the first in the session "+ + "group %x", groupSess.ID, groupSess.GroupID) + } + + // Now we need to check that all the sessions in the group are + // no longer active. + ok, err := s.cfg.db.CheckSessionGroupPredicate( + groupID, func(s *session.Session) bool { + return s.State == session.StateRevoked || + s.State == session.StateExpired + }, + ) + if err != nil { + return nil, err + } + + if !ok { + return nil, fmt.Errorf("a linked session in group "+ + "%x is still active", groupID) + } + + linkedGroupID = &groupID + linkedGroupSession = groupSess + } + + s.sessRegMu.Lock() + defer s.sessRegMu.Unlock() + + id, localPrivKey, err := s.cfg.db.GetUnusedIDAndKeyPair() + if err != nil { + return nil, err + } + sess, err := session.NewSession( - req.Label, session.TypeAutopilot, expiry, req.MailboxServerAddr, - req.DevServer, perms, caveats, featureConfig, privacy, + id, localPrivKey, req.Label, session.TypeAutopilot, expiry, + req.MailboxServerAddr, req.DevServer, perms, caveats, + featureConfig, privacy, linkedGroupID, ) if err != nil { return nil, fmt.Errorf("error creating new session: %v", err) } + // If this session is being linked to a previous one, then we need to + // use the previous session's local private key to sign the new + // session's public key in order to prove to the Autopilot server that + // the two session's belong to the same owner. + var ( + linkSig []byte + prevSessionPub *btcec.PublicKey + ) + if linkedGroupID != nil { + privKey := linkedGroupSession.LocalPrivateKey + pubKey := sess.LocalPublicKey.SerializeCompressed() + + msg := chainhash.HashB(pubKey) + linkSig = ecdsa.Sign(privKey, msg).Serialize() + + prevSessionPub = linkedGroupSession.LocalPublicKey + } + // Register all the privacy map pairs for this session ID. - privDB := s.cfg.privMap(sess.ID) + privDB := s.cfg.privMap(sess.GroupID) err = privDB.Update(func(tx firewalldb.PrivacyMapTx) error { for r, p := range privacyMapPairs { err := tx.NewPair(r, p) @@ -1005,7 +1111,7 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context, // Attempt to register the session with the Autopilot server. remoteKey, err := s.cfg.autopilot.RegisterSession( ctx, sess.LocalPublicKey, sess.ServerAddr, sess.DevServer, - featureConfig, + featureConfig, prevSessionPub, linkSig, ) if err != nil { return nil, fmt.Errorf("error registering session with "+ @@ -1015,7 +1121,7 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context, // We only persist this session if we successfully retrieved the // autopilot's static key. sess.RemotePublicKey = remoteKey - if err := s.db.StoreSession(sess); err != nil { + if err := s.cfg.db.CreateSession(sess); err != nil { return nil, fmt.Errorf("error storing session: %v", err) } @@ -1039,7 +1145,7 @@ func (s *sessionRpcServer) ListAutopilotSessions(_ context.Context, _ *litrpc.ListAutopilotSessionsRequest) ( *litrpc.ListAutopilotSessionsResponse, error) { - sessions, err := s.db.ListSessions(func(s *session.Session) bool { + sessions, err := s.cfg.db.ListSessions(func(s *session.Session) bool { return s.Type == session.TypeAutopilot }) if err != nil { @@ -1070,7 +1176,7 @@ func (s *sessionRpcServer) RevokeAutopilotSession(ctx context.Context, return nil, fmt.Errorf("error parsing public key: %v", err) } - sess, err := s.db.GetSession(pubKey) + sess, err := s.cfg.db.GetSession(pubKey) if err != nil { return nil, err } @@ -1206,7 +1312,10 @@ func (s *sessionRpcServer) marshalRPCSession(sess *session.Session) ( revokedAt = uint64(sess.RevokedAt.Unix()) } - featureInfo := make(map[string]*litrpc.RulesMap) + var ( + featureInfo = make(map[string]*litrpc.RulesMap) + initRuleValues = s.cfg.ruleMgrs.InitRuleValues + ) if sess.MacaroonRecipe != nil { for _, cav := range sess.MacaroonRecipe.Caveats { info, err := firewall.ParseRuleCaveat(string(cav.Id)) @@ -1219,13 +1328,17 @@ func (s *sessionRpcServer) marshalRPCSession(sess *session.Session) ( for feature, rules := range info.FeatureRules { ruleMap := make(map[string]*litrpc.RuleValue) for name, rule := range rules { - val, err := s.cfg.ruleMgrs.InitRuleValues(name, []byte(rule)) + val, err := initRuleValues( + name, []byte(rule), + ) if err != nil { return nil, err } if sess.WithPrivacyMapper { - db := s.cfg.privMap(sess.ID) + db := s.cfg.privMap( + sess.GroupID, + ) val, err = val.PseudoToReal(db) if err != nil { return nil, err @@ -1235,7 +1348,9 @@ func (s *sessionRpcServer) marshalRPCSession(sess *session.Session) ( ruleMap[name] = val.ToProto() } - featureInfo[feature] = &litrpc.RulesMap{Rules: ruleMap} + featureInfo[feature] = &litrpc.RulesMap{ + Rules: ruleMap, + } } } } @@ -1256,6 +1371,7 @@ func (s *sessionRpcServer) marshalRPCSession(sess *session.Session) ( RevokedAt: revokedAt, MacaroonRecipe: macRecipe, AutopilotFeatureInfo: featureInfo, + GroupId: sess.GroupID[:], }, nil } diff --git a/terminal.go b/terminal.go index cea739f4c..ff4232028 100644 --- a/terminal.go +++ b/terminal.go @@ -184,6 +184,7 @@ type LightningTerminal struct { accountRpcServer *accounts.RPCServer firewallDB *firewalldb.DB + sessionDB *session.DB restHandler http.Handler restCancel func() @@ -317,12 +318,20 @@ func (g *LightningTerminal) start() error { g.ruleMgrs = rules.NewRuleManagerSet() + // Create an instance of the local Terminal Connect session store DB. networkDir := filepath.Join(g.cfg.LitDir, g.cfg.Network) - g.firewallDB, err = firewalldb.NewDB(networkDir, firewalldb.DBFilename) + g.sessionDB, err = session.NewDB(networkDir, session.DBFilename) if err != nil { return fmt.Errorf("error creating session DB: %v", err) } + g.firewallDB, err = firewalldb.NewDB( + networkDir, firewalldb.DBFilename, g.sessionDB, + ) + if err != nil { + return fmt.Errorf("error creating firewall DB: %v", err) + } + if !g.cfg.Autopilot.Disable { if g.cfg.Autopilot.Address == "" && len(g.cfg.Autopilot.DialOpts) == 0 { @@ -353,8 +362,8 @@ func (g *LightningTerminal) start() error { } g.sessionRpcServer, err = newSessionRPCServer(&sessionRpcServerConfig{ + db: g.sessionDB, basicAuth: g.rpcProxy.basicAuth, - dbDir: filepath.Join(g.cfg.LitDir, g.cfg.Network), grpcOptions: []grpc.ServerOption{ grpc.CustomCodec(grpcProxy.Codec()), // nolint: staticcheck, grpc.ChainStreamInterceptor( @@ -805,6 +814,7 @@ func (g *LightningTerminal) startInternalSubServers( privacyMapper := firewall.NewPrivacyMapper( g.firewallDB.PrivacyDB, firewall.CryptoRandIntn, + g.sessionDB, ) mw := []mid.RequestInterceptor{ @@ -815,7 +825,7 @@ func (g *LightningTerminal) startInternalSubServers( if !g.cfg.Autopilot.Disable { ruleEnforcer := firewall.NewRuleEnforcer( - g.firewallDB, g.firewallDB, + g.firewallDB, g.firewallDB, g.sessionDB, g.autopilotClient.ListFeaturePerms, g.permsMgr, g.lndClient.NodePubkey, g.lndClient.Router,