From 3b95666f89cd007333228674b2eb32e07efa119a Mon Sep 17 00:00:00 2001 From: "maksim.nabokikh" Date: Mon, 27 Apr 2026 22:09:06 +0200 Subject: [PATCH] feat: gRPC API for sessions Signed-off-by: maksim.nabokikh --- api/v2/api.pb.go | 2560 ++++++++++++++++++++++++++++++++++++--- api/v2/api.proto | 254 ++++ api/v2/api_grpc.pb.go | 558 ++++++++- pkg/featureflags/set.go | 3 + server/api.go | 513 +++++++- server/api_test.go | 777 ++++++++++++ server/logout.go | 45 +- 7 files changed, 4487 insertions(+), 223 deletions(-) diff --git a/api/v2/api.pb.go b/api/v2/api.pb.go index d7f7f16c2e..02966b870d 100644 --- a/api/v2/api.pb.go +++ b/api/v2/api.pb.go @@ -2333,6 +2333,1890 @@ func (x *VerifyPasswordResp) GetNotFound() bool { return false } +// ClientAuthState represents authentication state for a specific client within a session. +// The user_id and connector_id are on the parent AuthSession message. +type ClientAuthState struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + Active bool `protobuf:"varint,2,opt,name=active,proto3" json:"active,omitempty"` + ExpiresAt int64 `protobuf:"varint,3,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + LastActivity int64 `protobuf:"varint,4,opt,name=last_activity,json=lastActivity,proto3" json:"last_activity,omitempty"` + LastTokenIssuedAt int64 `protobuf:"varint,5,opt,name=last_token_issued_at,json=lastTokenIssuedAt,proto3" json:"last_token_issued_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientAuthState) Reset() { + *x = ClientAuthState{} + mi := &file_api_v2_api_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientAuthState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientAuthState) ProtoMessage() {} + +func (x *ClientAuthState) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientAuthState.ProtoReflect.Descriptor instead. +func (*ClientAuthState) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{42} +} + +func (x *ClientAuthState) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *ClientAuthState) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +func (x *ClientAuthState) GetExpiresAt() int64 { + if x != nil { + return x.ExpiresAt + } + return 0 +} + +func (x *ClientAuthState) GetLastActivity() int64 { + if x != nil { + return x.LastActivity + } + return 0 +} + +func (x *ClientAuthState) GetLastTokenIssuedAt() int64 { + if x != nil { + return x.LastTokenIssuedAt + } + return 0 +} + +// AuthSession represents a user's authentication session. +type AuthSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + ClientStates []*ClientAuthState `protobuf:"bytes,3,rep,name=client_states,json=clientStates,proto3" json:"client_states,omitempty"` + CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastActivity int64 `protobuf:"varint,5,opt,name=last_activity,json=lastActivity,proto3" json:"last_activity,omitempty"` + IpAddress string `protobuf:"bytes,6,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + UserAgent string `protobuf:"bytes,7,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"` + AbsoluteExpiry int64 `protobuf:"varint,8,opt,name=absolute_expiry,json=absoluteExpiry,proto3" json:"absolute_expiry,omitempty"` + IdleExpiry int64 `protobuf:"varint,9,opt,name=idle_expiry,json=idleExpiry,proto3" json:"idle_expiry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthSession) Reset() { + *x = AuthSession{} + mi := &file_api_v2_api_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthSession) ProtoMessage() {} + +func (x *AuthSession) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthSession.ProtoReflect.Descriptor instead. +func (*AuthSession) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{43} +} + +func (x *AuthSession) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *AuthSession) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +func (x *AuthSession) GetClientStates() []*ClientAuthState { + if x != nil { + return x.ClientStates + } + return nil +} + +func (x *AuthSession) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *AuthSession) GetLastActivity() int64 { + if x != nil { + return x.LastActivity + } + return 0 +} + +func (x *AuthSession) GetIpAddress() string { + if x != nil { + return x.IpAddress + } + return "" +} + +func (x *AuthSession) GetUserAgent() string { + if x != nil { + return x.UserAgent + } + return "" +} + +func (x *AuthSession) GetAbsoluteExpiry() int64 { + if x != nil { + return x.AbsoluteExpiry + } + return 0 +} + +func (x *AuthSession) GetIdleExpiry() int64 { + if x != nil { + return x.IdleExpiry + } + return 0 +} + +// GetAuthSessionReq is a request to retrieve an auth session. +type GetAuthSessionReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAuthSessionReq) Reset() { + *x = GetAuthSessionReq{} + mi := &file_api_v2_api_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAuthSessionReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAuthSessionReq) ProtoMessage() {} + +func (x *GetAuthSessionReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAuthSessionReq.ProtoReflect.Descriptor instead. +func (*GetAuthSessionReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{44} +} + +func (x *GetAuthSessionReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *GetAuthSessionReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +// GetAuthSessionResp returns the auth session details. +type GetAuthSessionResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Session *AuthSession `protobuf:"bytes,1,opt,name=session,proto3" json:"session,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAuthSessionResp) Reset() { + *x = GetAuthSessionResp{} + mi := &file_api_v2_api_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAuthSessionResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAuthSessionResp) ProtoMessage() {} + +func (x *GetAuthSessionResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAuthSessionResp.ProtoReflect.Descriptor instead. +func (*GetAuthSessionResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{45} +} + +func (x *GetAuthSessionResp) GetSession() *AuthSession { + if x != nil { + return x.Session + } + return nil +} + +// ListAuthSessionsReq is a request to list auth sessions. +type ListAuthSessionsReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional filter: if set, only sessions for this user are returned. + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAuthSessionsReq) Reset() { + *x = ListAuthSessionsReq{} + mi := &file_api_v2_api_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAuthSessionsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAuthSessionsReq) ProtoMessage() {} + +func (x *ListAuthSessionsReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAuthSessionsReq.ProtoReflect.Descriptor instead. +func (*ListAuthSessionsReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{46} +} + +func (x *ListAuthSessionsReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +// ListAuthSessionsResp returns a list of auth sessions. +type ListAuthSessionsResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sessions []*AuthSession `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAuthSessionsResp) Reset() { + *x = ListAuthSessionsResp{} + mi := &file_api_v2_api_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAuthSessionsResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAuthSessionsResp) ProtoMessage() {} + +func (x *ListAuthSessionsResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAuthSessionsResp.ProtoReflect.Descriptor instead. +func (*ListAuthSessionsResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{47} +} + +func (x *ListAuthSessionsResp) GetSessions() []*AuthSession { + if x != nil { + return x.Sessions + } + return nil +} + +// DeleteAuthSessionReq is a request to delete an auth session. +// Deleting a session also revokes all associated refresh tokens (consistent with logout behavior). +type DeleteAuthSessionReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAuthSessionReq) Reset() { + *x = DeleteAuthSessionReq{} + mi := &file_api_v2_api_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAuthSessionReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAuthSessionReq) ProtoMessage() {} + +func (x *DeleteAuthSessionReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAuthSessionReq.ProtoReflect.Descriptor instead. +func (*DeleteAuthSessionReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{48} +} + +func (x *DeleteAuthSessionReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *DeleteAuthSessionReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +// DeleteAuthSessionResp returns the result of deleting an auth session. +type DeleteAuthSessionResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAuthSessionResp) Reset() { + *x = DeleteAuthSessionResp{} + mi := &file_api_v2_api_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAuthSessionResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAuthSessionResp) ProtoMessage() {} + +func (x *DeleteAuthSessionResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAuthSessionResp.ProtoReflect.Descriptor instead. +func (*DeleteAuthSessionResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{49} +} + +func (x *DeleteAuthSessionResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + +// TerminateSessionsByConnectorReq is a request to terminate all sessions for a connector. +// Use when connector configuration changes or is removed. Also revokes associated refresh tokens. +type TerminateSessionsByConnectorReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConnectorId string `protobuf:"bytes,1,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateSessionsByConnectorReq) Reset() { + *x = TerminateSessionsByConnectorReq{} + mi := &file_api_v2_api_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateSessionsByConnectorReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateSessionsByConnectorReq) ProtoMessage() {} + +func (x *TerminateSessionsByConnectorReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TerminateSessionsByConnectorReq.ProtoReflect.Descriptor instead. +func (*TerminateSessionsByConnectorReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{50} +} + +func (x *TerminateSessionsByConnectorReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +// TerminateSessionsByConnectorResp returns the count of terminated sessions. +type TerminateSessionsByConnectorResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionsTerminated int64 `protobuf:"varint,1,opt,name=sessions_terminated,json=sessionsTerminated,proto3" json:"sessions_terminated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateSessionsByConnectorResp) Reset() { + *x = TerminateSessionsByConnectorResp{} + mi := &file_api_v2_api_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateSessionsByConnectorResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateSessionsByConnectorResp) ProtoMessage() {} + +func (x *TerminateSessionsByConnectorResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TerminateSessionsByConnectorResp.ProtoReflect.Descriptor instead. +func (*TerminateSessionsByConnectorResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{51} +} + +func (x *TerminateSessionsByConnectorResp) GetSessionsTerminated() int64 { + if x != nil { + return x.SessionsTerminated + } + return 0 +} + +// TerminateSessionsByUserReq is a request to terminate all sessions for a user. +// Use for account compromise scenarios. Also revokes associated refresh tokens. +type TerminateSessionsByUserReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateSessionsByUserReq) Reset() { + *x = TerminateSessionsByUserReq{} + mi := &file_api_v2_api_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateSessionsByUserReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateSessionsByUserReq) ProtoMessage() {} + +func (x *TerminateSessionsByUserReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TerminateSessionsByUserReq.ProtoReflect.Descriptor instead. +func (*TerminateSessionsByUserReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{52} +} + +func (x *TerminateSessionsByUserReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +// TerminateSessionsByUserResp returns the count of terminated sessions. +type TerminateSessionsByUserResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionsTerminated int64 `protobuf:"varint,1,opt,name=sessions_terminated,json=sessionsTerminated,proto3" json:"sessions_terminated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateSessionsByUserResp) Reset() { + *x = TerminateSessionsByUserResp{} + mi := &file_api_v2_api_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateSessionsByUserResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateSessionsByUserResp) ProtoMessage() {} + +func (x *TerminateSessionsByUserResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TerminateSessionsByUserResp.ProtoReflect.Descriptor instead. +func (*TerminateSessionsByUserResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{53} +} + +func (x *TerminateSessionsByUserResp) GetSessionsTerminated() int64 { + if x != nil { + return x.SessionsTerminated + } + return 0 +} + +// ConsentEntry represents approved scopes for a single client. +type ConsentEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + Scopes []string `protobuf:"bytes,2,rep,name=scopes,proto3" json:"scopes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConsentEntry) Reset() { + *x = ConsentEntry{} + mi := &file_api_v2_api_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConsentEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConsentEntry) ProtoMessage() {} + +func (x *ConsentEntry) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConsentEntry.ProtoReflect.Descriptor instead. +func (*ConsentEntry) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{54} +} + +func (x *ConsentEntry) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *ConsentEntry) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +// MFASecret represents metadata of an enrolled MFA authenticator. +// The actual secret value is never exposed through the admin API. +type MFASecret struct { + state protoimpl.MessageState `protogen:"open.v1"` + AuthenticatorId string `protobuf:"bytes,1,opt,name=authenticator_id,json=authenticatorId,proto3" json:"authenticator_id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Confirmed bool `protobuf:"varint,3,opt,name=confirmed,proto3" json:"confirmed,omitempty"` + CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MFASecret) Reset() { + *x = MFASecret{} + mi := &file_api_v2_api_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MFASecret) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MFASecret) ProtoMessage() {} + +func (x *MFASecret) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MFASecret.ProtoReflect.Descriptor instead. +func (*MFASecret) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{55} +} + +func (x *MFASecret) GetAuthenticatorId() string { + if x != nil { + return x.AuthenticatorId + } + return "" +} + +func (x *MFASecret) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *MFASecret) GetConfirmed() bool { + if x != nil { + return x.Confirmed + } + return false +} + +func (x *MFASecret) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +// WebAuthnCredential represents metadata of a registered WebAuthn credential. +// The public key is never exposed through the admin API. +type WebAuthnCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + CredentialId []byte `protobuf:"bytes,1,opt,name=credential_id,json=credentialId,proto3" json:"credential_id,omitempty"` + AttestationType string `protobuf:"bytes,2,opt,name=attestation_type,json=attestationType,proto3" json:"attestation_type,omitempty"` + Aaguid []byte `protobuf:"bytes,3,opt,name=aaguid,proto3" json:"aaguid,omitempty"` + SignCount uint32 `protobuf:"varint,4,opt,name=sign_count,json=signCount,proto3" json:"sign_count,omitempty"` + CloneWarning bool `protobuf:"varint,5,opt,name=clone_warning,json=cloneWarning,proto3" json:"clone_warning,omitempty"` + Transport []string `protobuf:"bytes,6,rep,name=transport,proto3" json:"transport,omitempty"` + BackupEligible bool `protobuf:"varint,7,opt,name=backup_eligible,json=backupEligible,proto3" json:"backup_eligible,omitempty"` + BackupState bool `protobuf:"varint,8,opt,name=backup_state,json=backupState,proto3" json:"backup_state,omitempty"` + DisplayName string `protobuf:"bytes,9,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + CreatedAt int64 `protobuf:"varint,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebAuthnCredential) Reset() { + *x = WebAuthnCredential{} + mi := &file_api_v2_api_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebAuthnCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebAuthnCredential) ProtoMessage() {} + +func (x *WebAuthnCredential) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebAuthnCredential.ProtoReflect.Descriptor instead. +func (*WebAuthnCredential) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{56} +} + +func (x *WebAuthnCredential) GetCredentialId() []byte { + if x != nil { + return x.CredentialId + } + return nil +} + +func (x *WebAuthnCredential) GetAttestationType() string { + if x != nil { + return x.AttestationType + } + return "" +} + +func (x *WebAuthnCredential) GetAaguid() []byte { + if x != nil { + return x.Aaguid + } + return nil +} + +func (x *WebAuthnCredential) GetSignCount() uint32 { + if x != nil { + return x.SignCount + } + return 0 +} + +func (x *WebAuthnCredential) GetCloneWarning() bool { + if x != nil { + return x.CloneWarning + } + return false +} + +func (x *WebAuthnCredential) GetTransport() []string { + if x != nil { + return x.Transport + } + return nil +} + +func (x *WebAuthnCredential) GetBackupEligible() bool { + if x != nil { + return x.BackupEligible + } + return false +} + +func (x *WebAuthnCredential) GetBackupState() bool { + if x != nil { + return x.BackupState + } + return false +} + +func (x *WebAuthnCredential) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *WebAuthnCredential) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +// MFADeviceInfo groups MFA secret and WebAuthn credentials for one authenticator. +type MFADeviceInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + AuthenticatorId string `protobuf:"bytes,1,opt,name=authenticator_id,json=authenticatorId,proto3" json:"authenticator_id,omitempty"` + MfaSecret *MFASecret `protobuf:"bytes,2,opt,name=mfa_secret,json=mfaSecret,proto3" json:"mfa_secret,omitempty"` + WebauthnCredentials []*WebAuthnCredential `protobuf:"bytes,3,rep,name=webauthn_credentials,json=webauthnCredentials,proto3" json:"webauthn_credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MFADeviceInfo) Reset() { + *x = MFADeviceInfo{} + mi := &file_api_v2_api_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MFADeviceInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MFADeviceInfo) ProtoMessage() {} + +func (x *MFADeviceInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MFADeviceInfo.ProtoReflect.Descriptor instead. +func (*MFADeviceInfo) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{57} +} + +func (x *MFADeviceInfo) GetAuthenticatorId() string { + if x != nil { + return x.AuthenticatorId + } + return "" +} + +func (x *MFADeviceInfo) GetMfaSecret() *MFASecret { + if x != nil { + return x.MfaSecret + } + return nil +} + +func (x *MFADeviceInfo) GetWebauthnCredentials() []*WebAuthnCredential { + if x != nil { + return x.WebauthnCredentials + } + return nil +} + +// UserIdentity represents persistent per-user identity data. +type UserIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + EmailVerified bool `protobuf:"varint,4,opt,name=email_verified,json=emailVerified,proto3" json:"email_verified,omitempty"` + Username string `protobuf:"bytes,5,opt,name=username,proto3" json:"username,omitempty"` + Groups []string `protobuf:"bytes,6,rep,name=groups,proto3" json:"groups,omitempty"` + Consents []*ConsentEntry `protobuf:"bytes,7,rep,name=consents,proto3" json:"consents,omitempty"` + MfaDevices []*MFADeviceInfo `protobuf:"bytes,8,rep,name=mfa_devices,json=mfaDevices,proto3" json:"mfa_devices,omitempty"` + CreatedAt int64 `protobuf:"varint,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastLogin int64 `protobuf:"varint,10,opt,name=last_login,json=lastLogin,proto3" json:"last_login,omitempty"` + BlockedUntil int64 `protobuf:"varint,11,opt,name=blocked_until,json=blockedUntil,proto3" json:"blocked_until,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserIdentity) Reset() { + *x = UserIdentity{} + mi := &file_api_v2_api_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserIdentity) ProtoMessage() {} + +func (x *UserIdentity) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserIdentity.ProtoReflect.Descriptor instead. +func (*UserIdentity) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{58} +} + +func (x *UserIdentity) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *UserIdentity) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +func (x *UserIdentity) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *UserIdentity) GetEmailVerified() bool { + if x != nil { + return x.EmailVerified + } + return false +} + +func (x *UserIdentity) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UserIdentity) GetGroups() []string { + if x != nil { + return x.Groups + } + return nil +} + +func (x *UserIdentity) GetConsents() []*ConsentEntry { + if x != nil { + return x.Consents + } + return nil +} + +func (x *UserIdentity) GetMfaDevices() []*MFADeviceInfo { + if x != nil { + return x.MfaDevices + } + return nil +} + +func (x *UserIdentity) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *UserIdentity) GetLastLogin() int64 { + if x != nil { + return x.LastLogin + } + return 0 +} + +func (x *UserIdentity) GetBlockedUntil() int64 { + if x != nil { + return x.BlockedUntil + } + return 0 +} + +// GetUserIdentityReq is a request to retrieve a user identity. +type GetUserIdentityReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserIdentityReq) Reset() { + *x = GetUserIdentityReq{} + mi := &file_api_v2_api_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserIdentityReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserIdentityReq) ProtoMessage() {} + +func (x *GetUserIdentityReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserIdentityReq.ProtoReflect.Descriptor instead. +func (*GetUserIdentityReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{59} +} + +func (x *GetUserIdentityReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *GetUserIdentityReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +// GetUserIdentityResp returns the user identity details. +type GetUserIdentityResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identity *UserIdentity `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserIdentityResp) Reset() { + *x = GetUserIdentityResp{} + mi := &file_api_v2_api_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserIdentityResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserIdentityResp) ProtoMessage() {} + +func (x *GetUserIdentityResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserIdentityResp.ProtoReflect.Descriptor instead. +func (*GetUserIdentityResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{60} +} + +func (x *GetUserIdentityResp) GetIdentity() *UserIdentity { + if x != nil { + return x.Identity + } + return nil +} + +// ListUserIdentitiesReq is a request to list user identities. +type ListUserIdentitiesReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUserIdentitiesReq) Reset() { + *x = ListUserIdentitiesReq{} + mi := &file_api_v2_api_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserIdentitiesReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserIdentitiesReq) ProtoMessage() {} + +func (x *ListUserIdentitiesReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUserIdentitiesReq.ProtoReflect.Descriptor instead. +func (*ListUserIdentitiesReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{61} +} + +// ListUserIdentitiesResp returns a list of user identities. +type ListUserIdentitiesResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identities []*UserIdentity `protobuf:"bytes,1,rep,name=identities,proto3" json:"identities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUserIdentitiesResp) Reset() { + *x = ListUserIdentitiesResp{} + mi := &file_api_v2_api_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserIdentitiesResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserIdentitiesResp) ProtoMessage() {} + +func (x *ListUserIdentitiesResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUserIdentitiesResp.ProtoReflect.Descriptor instead. +func (*ListUserIdentitiesResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{62} +} + +func (x *ListUserIdentitiesResp) GetIdentities() []*UserIdentity { + if x != nil { + return x.Identities + } + return nil +} + +// DeleteUserIdentityReq is a request to delete a user identity. +// This is a full data purge for GDPR compliance and account deletion. +// It cascades to: auth session, all refresh tokens, offline sessions, and the identity itself. +// Password records must be deleted separately via DeletePassword (linked by email, not user ID). +type DeleteUserIdentityReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteUserIdentityReq) Reset() { + *x = DeleteUserIdentityReq{} + mi := &file_api_v2_api_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserIdentityReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserIdentityReq) ProtoMessage() {} + +func (x *DeleteUserIdentityReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserIdentityReq.ProtoReflect.Descriptor instead. +func (*DeleteUserIdentityReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{63} +} + +func (x *DeleteUserIdentityReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *DeleteUserIdentityReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +// DeleteUserIdentityResp returns the result of deleting a user identity. +type DeleteUserIdentityResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteUserIdentityResp) Reset() { + *x = DeleteUserIdentityResp{} + mi := &file_api_v2_api_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserIdentityResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserIdentityResp) ProtoMessage() {} + +func (x *DeleteUserIdentityResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserIdentityResp.ProtoReflect.Descriptor instead. +func (*DeleteUserIdentityResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{64} +} + +func (x *DeleteUserIdentityResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + +// ResetMFAReq is a request to clear all MFA secrets and WebAuthn credentials for a user. +// Use when a user has lost access to all their MFA devices. +type ResetMFAReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResetMFAReq) Reset() { + *x = ResetMFAReq{} + mi := &file_api_v2_api_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResetMFAReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResetMFAReq) ProtoMessage() {} + +func (x *ResetMFAReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResetMFAReq.ProtoReflect.Descriptor instead. +func (*ResetMFAReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{65} +} + +func (x *ResetMFAReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ResetMFAReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +// ResetMFAResp returns the result of resetting MFA. +type ResetMFAResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResetMFAResp) Reset() { + *x = ResetMFAResp{} + mi := &file_api_v2_api_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResetMFAResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResetMFAResp) ProtoMessage() {} + +func (x *ResetMFAResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResetMFAResp.ProtoReflect.Descriptor instead. +func (*ResetMFAResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{66} +} + +func (x *ResetMFAResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + +// ListMFADevicesReq is a request to list registered MFA authenticators for a user. +type ListMFADevicesReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMFADevicesReq) Reset() { + *x = ListMFADevicesReq{} + mi := &file_api_v2_api_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMFADevicesReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMFADevicesReq) ProtoMessage() {} + +func (x *ListMFADevicesReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMFADevicesReq.ProtoReflect.Descriptor instead. +func (*ListMFADevicesReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{67} +} + +func (x *ListMFADevicesReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ListMFADevicesReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +// ListMFADevicesResp returns MFA device information. +// Secret values and public keys are never included in the response. +type ListMFADevicesResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Devices []*MFADeviceInfo `protobuf:"bytes,1,rep,name=devices,proto3" json:"devices,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMFADevicesResp) Reset() { + *x = ListMFADevicesResp{} + mi := &file_api_v2_api_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMFADevicesResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMFADevicesResp) ProtoMessage() {} + +func (x *ListMFADevicesResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMFADevicesResp.ProtoReflect.Descriptor instead. +func (*ListMFADevicesResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{68} +} + +func (x *ListMFADevicesResp) GetDevices() []*MFADeviceInfo { + if x != nil { + return x.Devices + } + return nil +} + +// DeleteWebAuthnCredentialReq is a request to delete a specific WebAuthn credential. +// Use when a user has lost or wants to deregister a specific security key. +type DeleteWebAuthnCredentialReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + CredentialId []byte `protobuf:"bytes,3,opt,name=credential_id,json=credentialId,proto3" json:"credential_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWebAuthnCredentialReq) Reset() { + *x = DeleteWebAuthnCredentialReq{} + mi := &file_api_v2_api_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWebAuthnCredentialReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWebAuthnCredentialReq) ProtoMessage() {} + +func (x *DeleteWebAuthnCredentialReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWebAuthnCredentialReq.ProtoReflect.Descriptor instead. +func (*DeleteWebAuthnCredentialReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{69} +} + +func (x *DeleteWebAuthnCredentialReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *DeleteWebAuthnCredentialReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +func (x *DeleteWebAuthnCredentialReq) GetCredentialId() []byte { + if x != nil { + return x.CredentialId + } + return nil +} + +// DeleteWebAuthnCredentialResp returns the result of deleting a WebAuthn credential. +type DeleteWebAuthnCredentialResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWebAuthnCredentialResp) Reset() { + *x = DeleteWebAuthnCredentialResp{} + mi := &file_api_v2_api_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWebAuthnCredentialResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWebAuthnCredentialResp) ProtoMessage() {} + +func (x *DeleteWebAuthnCredentialResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWebAuthnCredentialResp.ProtoReflect.Descriptor instead. +func (*DeleteWebAuthnCredentialResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{70} +} + +func (x *DeleteWebAuthnCredentialResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + +// DeleteMFASecretReq is a request to delete a specific MFA authenticator secret. +// Also removes any associated WebAuthn credentials for the same authenticator. +type DeleteMFASecretReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + AuthenticatorId string `protobuf:"bytes,3,opt,name=authenticator_id,json=authenticatorId,proto3" json:"authenticator_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteMFASecretReq) Reset() { + *x = DeleteMFASecretReq{} + mi := &file_api_v2_api_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteMFASecretReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteMFASecretReq) ProtoMessage() {} + +func (x *DeleteMFASecretReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteMFASecretReq.ProtoReflect.Descriptor instead. +func (*DeleteMFASecretReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{71} +} + +func (x *DeleteMFASecretReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *DeleteMFASecretReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +func (x *DeleteMFASecretReq) GetAuthenticatorId() string { + if x != nil { + return x.AuthenticatorId + } + return "" +} + +// DeleteMFASecretResp returns the result of deleting an MFA secret. +type DeleteMFASecretResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteMFASecretResp) Reset() { + *x = DeleteMFASecretResp{} + mi := &file_api_v2_api_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteMFASecretResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteMFASecretResp) ProtoMessage() {} + +func (x *DeleteMFASecretResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[72] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteMFASecretResp.ProtoReflect.Descriptor instead. +func (*DeleteMFASecretResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{72} +} + +func (x *DeleteMFASecretResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + +// RevokeConsentReq is a request to revoke consent for a specific client. +// The user will see the consent screen again on next authorization. +type RevokeConsentReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` + ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeConsentReq) Reset() { + *x = RevokeConsentReq{} + mi := &file_api_v2_api_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeConsentReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeConsentReq) ProtoMessage() {} + +func (x *RevokeConsentReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[73] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeConsentReq.ProtoReflect.Descriptor instead. +func (*RevokeConsentReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{73} +} + +func (x *RevokeConsentReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *RevokeConsentReq) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +func (x *RevokeConsentReq) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +// RevokeConsentResp returns the result of revoking consent. +type RevokeConsentResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeConsentResp) Reset() { + *x = RevokeConsentResp{} + mi := &file_api_v2_api_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeConsentResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeConsentResp) ProtoMessage() {} + +func (x *RevokeConsentResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[74] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeConsentResp.ProtoReflect.Descriptor instead. +func (*RevokeConsentResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{74} +} + +func (x *RevokeConsentResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + var File_api_v2_api_proto protoreflect.FileDescriptor var file_api_v2_api_proto_rawDesc = string([]byte{ @@ -2584,83 +4468,370 @@ var file_api_v2_api_proto_rawDesc = string([]byte{ 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, - 0x6e, 0x64, 0x32, 0x8b, 0x09, 0x0a, 0x03, 0x44, 0x65, 0x78, 0x12, 0x34, 0x0a, 0x09, 0x47, 0x65, - 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, - 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, - 0x12, 0x3d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, - 0x3d, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, - 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, - 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x38, 0x0a, - 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x12, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, - 0x1a, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, - 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, + 0x6e, 0x64, 0x22, 0xbb, 0x01, 0x0a, 0x0f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x75, 0x74, + 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x12, + 0x2f, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x73, + 0x73, 0x75, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x6c, + 0x61, 0x73, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x41, 0x74, + 0x22, 0xd0, 0x02, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0d, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, + 0x61, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x69, + 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x75, 0x73, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x62, 0x73, + 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x61, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x45, 0x78, 0x70, 0x69, + 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x64, 0x6c, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x69, 0x64, 0x6c, 0x65, 0x45, 0x78, 0x70, + 0x69, 0x72, 0x79, 0x22, 0x4f, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x49, 0x64, 0x22, 0x40, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2a, 0x0a, 0x07, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2e, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, + 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, + 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x44, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, + 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2c, + 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x52, 0x0a, 0x14, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, + 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, + 0x22, 0x34, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, + 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, + 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x44, 0x0a, 0x1f, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x53, 0x0a, 0x20, + 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x42, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x12, 0x2f, 0x0a, 0x13, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x74, 0x65, 0x72, + 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, + 0x64, 0x22, 0x35, 0x0a, 0x1a, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x1b, 0x54, 0x65, 0x72, 0x6d, + 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2f, 0x0a, 0x13, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x5f, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x54, 0x65, + 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x64, 0x22, 0x43, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x73, + 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x22, 0x87, 0x01, + 0x0a, 0x09, 0x4d, 0x46, 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x61, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xec, 0x02, 0x0a, 0x12, 0x57, 0x65, 0x62, 0x41, + 0x75, 0x74, 0x68, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x23, + 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, + 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x61, 0x61, 0x67, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, + 0x61, 0x61, 0x67, 0x75, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, 0x6f, 0x6e, 0x65, 0x5f, 0x77, + 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x63, 0x6c, + 0x6f, 0x6e, 0x65, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x5f, 0x65, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0e, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x45, 0x6c, 0x69, 0x67, 0x69, 0x62, 0x6c, + 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4d, 0x46, 0x41, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x29, 0x0a, 0x10, 0x61, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, + 0x72, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x0a, 0x6d, 0x66, 0x61, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x46, + 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x09, 0x6d, 0x66, 0x61, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x12, 0x4a, 0x0a, 0x14, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x5f, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x57, 0x65, 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x13, 0x77, 0x65, 0x62, 0x61, 0x75, + 0x74, 0x68, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0x82, + 0x03, 0x0a, 0x0c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, + 0x69, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x65, 0x6d, 0x61, 0x69, 0x6c, + 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x2d, 0x0a, 0x08, + 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x0b, 0x6d, + 0x66, 0x61, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x46, 0x41, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x6d, 0x66, 0x61, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x23, + 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x55, 0x6e, + 0x74, 0x69, 0x6c, 0x22, 0x50, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x44, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2d, 0x0a, 0x08, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x17, 0x0a, 0x15, 0x4c, + 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x22, 0x4b, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x31, + 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x22, 0x53, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x35, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x49, 0x0a, + 0x0b, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x46, 0x41, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x2b, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, + 0x74, 0x4d, 0x46, 0x41, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, + 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, + 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x4f, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x46, 0x41, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x42, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x46, + 0x41, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2c, 0x0a, 0x07, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x46, 0x41, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x52, 0x07, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x7e, 0x0a, 0x1b, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x57, 0x65, 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x63, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x22, 0x3b, 0x0a, 0x1c, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x57, 0x65, 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, + 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, + 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x7b, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4d, 0x46, 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, + 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x61, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x6f, 0x72, 0x49, 0x64, 0x22, 0x32, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x46, + 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, + 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x6b, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, + 0x6b, 0x65, 0x43, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x30, 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, + 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, + 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, + 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x87, 0x11, 0x0a, 0x03, 0x44, 0x65, 0x78, 0x12, + 0x34, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x11, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, + 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x22, 0x00, 0x12, 0x38, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, - 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, - 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, - 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, - 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x41, - 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, - 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, - 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, - 0x76, 0x65, 0x72, 0x79, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, - 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x69, - 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3a, 0x0a, - 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x13, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, - 0x71, 0x1a, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, - 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x52, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, - 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, - 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x56, - 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, - 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, - 0x42, 0x36, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x6f, 0x73, 0x2e, 0x64, - 0x65, 0x78, 0x2e, 0x61, 0x70, 0x69, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x78, 0x69, 0x64, 0x70, 0x2f, 0x64, 0x65, 0x78, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, + 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0d, 0x4c, + 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x14, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, + 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x17, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, + 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x17, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0c, 0x47, 0x65, 0x74, + 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, + 0x68, 0x12, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, + 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, + 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, + 0x12, 0x43, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, + 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, + 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x49, 0x0a, 0x10, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x00, 0x12, 0x6d, 0x0a, 0x1c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x12, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x25, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x42, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x22, 0x00, 0x12, 0x5e, 0x0a, 0x17, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1f, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x20, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x18, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x12, 0x4c, 0x69, + 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, + 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x12, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x46, 0x41, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, + 0x65, 0x73, 0x65, 0x74, 0x4d, 0x46, 0x41, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x46, 0x41, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, + 0x43, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x46, 0x41, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x46, 0x41, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x46, 0x41, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x22, 0x00, 0x12, 0x61, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x57, 0x65, + 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x12, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x57, 0x65, 0x62, + 0x41, 0x75, 0x74, 0x68, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, + 0x65, 0x71, 0x1a, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x57, + 0x65, 0x62, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4d, 0x46, 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x46, 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x4d, 0x46, 0x41, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, + 0x40, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x74, + 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x6f, 0x6e, + 0x73, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, + 0x00, 0x42, 0x36, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x6f, 0x73, 0x2e, + 0x64, 0x65, 0x78, 0x2e, 0x61, 0x70, 0x69, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x78, 0x69, 0x64, 0x70, 0x2f, 0x64, 0x65, 0x78, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, }) var ( @@ -2675,50 +4846,83 @@ func file_api_v2_api_proto_rawDescGZIP() []byte { return file_api_v2_api_proto_rawDescData } -var file_api_v2_api_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_api_v2_api_proto_msgTypes = make([]protoimpl.MessageInfo, 75) var file_api_v2_api_proto_goTypes = []any{ - (*Client)(nil), // 0: api.Client - (*ClientInfo)(nil), // 1: api.ClientInfo - (*GetClientReq)(nil), // 2: api.GetClientReq - (*GetClientResp)(nil), // 3: api.GetClientResp - (*CreateClientReq)(nil), // 4: api.CreateClientReq - (*CreateClientResp)(nil), // 5: api.CreateClientResp - (*DeleteClientReq)(nil), // 6: api.DeleteClientReq - (*DeleteClientResp)(nil), // 7: api.DeleteClientResp - (*UpdateClientReq)(nil), // 8: api.UpdateClientReq - (*UpdateClientResp)(nil), // 9: api.UpdateClientResp - (*ListClientReq)(nil), // 10: api.ListClientReq - (*ListClientResp)(nil), // 11: api.ListClientResp - (*Password)(nil), // 12: api.Password - (*CreatePasswordReq)(nil), // 13: api.CreatePasswordReq - (*CreatePasswordResp)(nil), // 14: api.CreatePasswordResp - (*UpdatePasswordReq)(nil), // 15: api.UpdatePasswordReq - (*UpdatePasswordResp)(nil), // 16: api.UpdatePasswordResp - (*DeletePasswordReq)(nil), // 17: api.DeletePasswordReq - (*DeletePasswordResp)(nil), // 18: api.DeletePasswordResp - (*ListPasswordReq)(nil), // 19: api.ListPasswordReq - (*ListPasswordResp)(nil), // 20: api.ListPasswordResp - (*Connector)(nil), // 21: api.Connector - (*CreateConnectorReq)(nil), // 22: api.CreateConnectorReq - (*CreateConnectorResp)(nil), // 23: api.CreateConnectorResp - (*GrantTypes)(nil), // 24: api.GrantTypes - (*UpdateConnectorReq)(nil), // 25: api.UpdateConnectorReq - (*UpdateConnectorResp)(nil), // 26: api.UpdateConnectorResp - (*DeleteConnectorReq)(nil), // 27: api.DeleteConnectorReq - (*DeleteConnectorResp)(nil), // 28: api.DeleteConnectorResp - (*ListConnectorReq)(nil), // 29: api.ListConnectorReq - (*ListConnectorResp)(nil), // 30: api.ListConnectorResp - (*VersionReq)(nil), // 31: api.VersionReq - (*VersionResp)(nil), // 32: api.VersionResp - (*DiscoveryReq)(nil), // 33: api.DiscoveryReq - (*DiscoveryResp)(nil), // 34: api.DiscoveryResp - (*RefreshTokenRef)(nil), // 35: api.RefreshTokenRef - (*ListRefreshReq)(nil), // 36: api.ListRefreshReq - (*ListRefreshResp)(nil), // 37: api.ListRefreshResp - (*RevokeRefreshReq)(nil), // 38: api.RevokeRefreshReq - (*RevokeRefreshResp)(nil), // 39: api.RevokeRefreshResp - (*VerifyPasswordReq)(nil), // 40: api.VerifyPasswordReq - (*VerifyPasswordResp)(nil), // 41: api.VerifyPasswordResp + (*Client)(nil), // 0: api.Client + (*ClientInfo)(nil), // 1: api.ClientInfo + (*GetClientReq)(nil), // 2: api.GetClientReq + (*GetClientResp)(nil), // 3: api.GetClientResp + (*CreateClientReq)(nil), // 4: api.CreateClientReq + (*CreateClientResp)(nil), // 5: api.CreateClientResp + (*DeleteClientReq)(nil), // 6: api.DeleteClientReq + (*DeleteClientResp)(nil), // 7: api.DeleteClientResp + (*UpdateClientReq)(nil), // 8: api.UpdateClientReq + (*UpdateClientResp)(nil), // 9: api.UpdateClientResp + (*ListClientReq)(nil), // 10: api.ListClientReq + (*ListClientResp)(nil), // 11: api.ListClientResp + (*Password)(nil), // 12: api.Password + (*CreatePasswordReq)(nil), // 13: api.CreatePasswordReq + (*CreatePasswordResp)(nil), // 14: api.CreatePasswordResp + (*UpdatePasswordReq)(nil), // 15: api.UpdatePasswordReq + (*UpdatePasswordResp)(nil), // 16: api.UpdatePasswordResp + (*DeletePasswordReq)(nil), // 17: api.DeletePasswordReq + (*DeletePasswordResp)(nil), // 18: api.DeletePasswordResp + (*ListPasswordReq)(nil), // 19: api.ListPasswordReq + (*ListPasswordResp)(nil), // 20: api.ListPasswordResp + (*Connector)(nil), // 21: api.Connector + (*CreateConnectorReq)(nil), // 22: api.CreateConnectorReq + (*CreateConnectorResp)(nil), // 23: api.CreateConnectorResp + (*GrantTypes)(nil), // 24: api.GrantTypes + (*UpdateConnectorReq)(nil), // 25: api.UpdateConnectorReq + (*UpdateConnectorResp)(nil), // 26: api.UpdateConnectorResp + (*DeleteConnectorReq)(nil), // 27: api.DeleteConnectorReq + (*DeleteConnectorResp)(nil), // 28: api.DeleteConnectorResp + (*ListConnectorReq)(nil), // 29: api.ListConnectorReq + (*ListConnectorResp)(nil), // 30: api.ListConnectorResp + (*VersionReq)(nil), // 31: api.VersionReq + (*VersionResp)(nil), // 32: api.VersionResp + (*DiscoveryReq)(nil), // 33: api.DiscoveryReq + (*DiscoveryResp)(nil), // 34: api.DiscoveryResp + (*RefreshTokenRef)(nil), // 35: api.RefreshTokenRef + (*ListRefreshReq)(nil), // 36: api.ListRefreshReq + (*ListRefreshResp)(nil), // 37: api.ListRefreshResp + (*RevokeRefreshReq)(nil), // 38: api.RevokeRefreshReq + (*RevokeRefreshResp)(nil), // 39: api.RevokeRefreshResp + (*VerifyPasswordReq)(nil), // 40: api.VerifyPasswordReq + (*VerifyPasswordResp)(nil), // 41: api.VerifyPasswordResp + (*ClientAuthState)(nil), // 42: api.ClientAuthState + (*AuthSession)(nil), // 43: api.AuthSession + (*GetAuthSessionReq)(nil), // 44: api.GetAuthSessionReq + (*GetAuthSessionResp)(nil), // 45: api.GetAuthSessionResp + (*ListAuthSessionsReq)(nil), // 46: api.ListAuthSessionsReq + (*ListAuthSessionsResp)(nil), // 47: api.ListAuthSessionsResp + (*DeleteAuthSessionReq)(nil), // 48: api.DeleteAuthSessionReq + (*DeleteAuthSessionResp)(nil), // 49: api.DeleteAuthSessionResp + (*TerminateSessionsByConnectorReq)(nil), // 50: api.TerminateSessionsByConnectorReq + (*TerminateSessionsByConnectorResp)(nil), // 51: api.TerminateSessionsByConnectorResp + (*TerminateSessionsByUserReq)(nil), // 52: api.TerminateSessionsByUserReq + (*TerminateSessionsByUserResp)(nil), // 53: api.TerminateSessionsByUserResp + (*ConsentEntry)(nil), // 54: api.ConsentEntry + (*MFASecret)(nil), // 55: api.MFASecret + (*WebAuthnCredential)(nil), // 56: api.WebAuthnCredential + (*MFADeviceInfo)(nil), // 57: api.MFADeviceInfo + (*UserIdentity)(nil), // 58: api.UserIdentity + (*GetUserIdentityReq)(nil), // 59: api.GetUserIdentityReq + (*GetUserIdentityResp)(nil), // 60: api.GetUserIdentityResp + (*ListUserIdentitiesReq)(nil), // 61: api.ListUserIdentitiesReq + (*ListUserIdentitiesResp)(nil), // 62: api.ListUserIdentitiesResp + (*DeleteUserIdentityReq)(nil), // 63: api.DeleteUserIdentityReq + (*DeleteUserIdentityResp)(nil), // 64: api.DeleteUserIdentityResp + (*ResetMFAReq)(nil), // 65: api.ResetMFAReq + (*ResetMFAResp)(nil), // 66: api.ResetMFAResp + (*ListMFADevicesReq)(nil), // 67: api.ListMFADevicesReq + (*ListMFADevicesResp)(nil), // 68: api.ListMFADevicesResp + (*DeleteWebAuthnCredentialReq)(nil), // 69: api.DeleteWebAuthnCredentialReq + (*DeleteWebAuthnCredentialResp)(nil), // 70: api.DeleteWebAuthnCredentialResp + (*DeleteMFASecretReq)(nil), // 71: api.DeleteMFASecretReq + (*DeleteMFASecretResp)(nil), // 72: api.DeleteMFASecretResp + (*RevokeConsentReq)(nil), // 73: api.RevokeConsentReq + (*RevokeConsentResp)(nil), // 74: api.RevokeConsentResp } var file_api_v2_api_proto_depIdxs = []int32{ 0, // 0: api.GetClientResp.client:type_name -> api.Client @@ -2731,47 +4935,83 @@ var file_api_v2_api_proto_depIdxs = []int32{ 24, // 7: api.UpdateConnectorReq.new_grant_types:type_name -> api.GrantTypes 21, // 8: api.ListConnectorResp.connectors:type_name -> api.Connector 35, // 9: api.ListRefreshResp.refresh_tokens:type_name -> api.RefreshTokenRef - 2, // 10: api.Dex.GetClient:input_type -> api.GetClientReq - 4, // 11: api.Dex.CreateClient:input_type -> api.CreateClientReq - 8, // 12: api.Dex.UpdateClient:input_type -> api.UpdateClientReq - 6, // 13: api.Dex.DeleteClient:input_type -> api.DeleteClientReq - 10, // 14: api.Dex.ListClients:input_type -> api.ListClientReq - 13, // 15: api.Dex.CreatePassword:input_type -> api.CreatePasswordReq - 15, // 16: api.Dex.UpdatePassword:input_type -> api.UpdatePasswordReq - 17, // 17: api.Dex.DeletePassword:input_type -> api.DeletePasswordReq - 19, // 18: api.Dex.ListPasswords:input_type -> api.ListPasswordReq - 22, // 19: api.Dex.CreateConnector:input_type -> api.CreateConnectorReq - 25, // 20: api.Dex.UpdateConnector:input_type -> api.UpdateConnectorReq - 27, // 21: api.Dex.DeleteConnector:input_type -> api.DeleteConnectorReq - 29, // 22: api.Dex.ListConnectors:input_type -> api.ListConnectorReq - 31, // 23: api.Dex.GetVersion:input_type -> api.VersionReq - 33, // 24: api.Dex.GetDiscovery:input_type -> api.DiscoveryReq - 36, // 25: api.Dex.ListRefresh:input_type -> api.ListRefreshReq - 38, // 26: api.Dex.RevokeRefresh:input_type -> api.RevokeRefreshReq - 40, // 27: api.Dex.VerifyPassword:input_type -> api.VerifyPasswordReq - 3, // 28: api.Dex.GetClient:output_type -> api.GetClientResp - 5, // 29: api.Dex.CreateClient:output_type -> api.CreateClientResp - 9, // 30: api.Dex.UpdateClient:output_type -> api.UpdateClientResp - 7, // 31: api.Dex.DeleteClient:output_type -> api.DeleteClientResp - 11, // 32: api.Dex.ListClients:output_type -> api.ListClientResp - 14, // 33: api.Dex.CreatePassword:output_type -> api.CreatePasswordResp - 16, // 34: api.Dex.UpdatePassword:output_type -> api.UpdatePasswordResp - 18, // 35: api.Dex.DeletePassword:output_type -> api.DeletePasswordResp - 20, // 36: api.Dex.ListPasswords:output_type -> api.ListPasswordResp - 23, // 37: api.Dex.CreateConnector:output_type -> api.CreateConnectorResp - 26, // 38: api.Dex.UpdateConnector:output_type -> api.UpdateConnectorResp - 28, // 39: api.Dex.DeleteConnector:output_type -> api.DeleteConnectorResp - 30, // 40: api.Dex.ListConnectors:output_type -> api.ListConnectorResp - 32, // 41: api.Dex.GetVersion:output_type -> api.VersionResp - 34, // 42: api.Dex.GetDiscovery:output_type -> api.DiscoveryResp - 37, // 43: api.Dex.ListRefresh:output_type -> api.ListRefreshResp - 39, // 44: api.Dex.RevokeRefresh:output_type -> api.RevokeRefreshResp - 41, // 45: api.Dex.VerifyPassword:output_type -> api.VerifyPasswordResp - 28, // [28:46] is the sub-list for method output_type - 10, // [10:28] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 42, // 10: api.AuthSession.client_states:type_name -> api.ClientAuthState + 43, // 11: api.GetAuthSessionResp.session:type_name -> api.AuthSession + 43, // 12: api.ListAuthSessionsResp.sessions:type_name -> api.AuthSession + 55, // 13: api.MFADeviceInfo.mfa_secret:type_name -> api.MFASecret + 56, // 14: api.MFADeviceInfo.webauthn_credentials:type_name -> api.WebAuthnCredential + 54, // 15: api.UserIdentity.consents:type_name -> api.ConsentEntry + 57, // 16: api.UserIdentity.mfa_devices:type_name -> api.MFADeviceInfo + 58, // 17: api.GetUserIdentityResp.identity:type_name -> api.UserIdentity + 58, // 18: api.ListUserIdentitiesResp.identities:type_name -> api.UserIdentity + 57, // 19: api.ListMFADevicesResp.devices:type_name -> api.MFADeviceInfo + 2, // 20: api.Dex.GetClient:input_type -> api.GetClientReq + 4, // 21: api.Dex.CreateClient:input_type -> api.CreateClientReq + 8, // 22: api.Dex.UpdateClient:input_type -> api.UpdateClientReq + 6, // 23: api.Dex.DeleteClient:input_type -> api.DeleteClientReq + 10, // 24: api.Dex.ListClients:input_type -> api.ListClientReq + 13, // 25: api.Dex.CreatePassword:input_type -> api.CreatePasswordReq + 15, // 26: api.Dex.UpdatePassword:input_type -> api.UpdatePasswordReq + 17, // 27: api.Dex.DeletePassword:input_type -> api.DeletePasswordReq + 19, // 28: api.Dex.ListPasswords:input_type -> api.ListPasswordReq + 22, // 29: api.Dex.CreateConnector:input_type -> api.CreateConnectorReq + 25, // 30: api.Dex.UpdateConnector:input_type -> api.UpdateConnectorReq + 27, // 31: api.Dex.DeleteConnector:input_type -> api.DeleteConnectorReq + 29, // 32: api.Dex.ListConnectors:input_type -> api.ListConnectorReq + 31, // 33: api.Dex.GetVersion:input_type -> api.VersionReq + 33, // 34: api.Dex.GetDiscovery:input_type -> api.DiscoveryReq + 36, // 35: api.Dex.ListRefresh:input_type -> api.ListRefreshReq + 38, // 36: api.Dex.RevokeRefresh:input_type -> api.RevokeRefreshReq + 40, // 37: api.Dex.VerifyPassword:input_type -> api.VerifyPasswordReq + 44, // 38: api.Dex.GetAuthSession:input_type -> api.GetAuthSessionReq + 46, // 39: api.Dex.ListAuthSessions:input_type -> api.ListAuthSessionsReq + 48, // 40: api.Dex.DeleteAuthSession:input_type -> api.DeleteAuthSessionReq + 50, // 41: api.Dex.TerminateSessionsByConnector:input_type -> api.TerminateSessionsByConnectorReq + 52, // 42: api.Dex.TerminateSessionsByUser:input_type -> api.TerminateSessionsByUserReq + 59, // 43: api.Dex.GetUserIdentity:input_type -> api.GetUserIdentityReq + 61, // 44: api.Dex.ListUserIdentities:input_type -> api.ListUserIdentitiesReq + 63, // 45: api.Dex.DeleteUserIdentity:input_type -> api.DeleteUserIdentityReq + 65, // 46: api.Dex.ResetMFA:input_type -> api.ResetMFAReq + 67, // 47: api.Dex.ListMFADevices:input_type -> api.ListMFADevicesReq + 69, // 48: api.Dex.DeleteWebAuthnCredential:input_type -> api.DeleteWebAuthnCredentialReq + 71, // 49: api.Dex.DeleteMFASecret:input_type -> api.DeleteMFASecretReq + 73, // 50: api.Dex.RevokeConsent:input_type -> api.RevokeConsentReq + 3, // 51: api.Dex.GetClient:output_type -> api.GetClientResp + 5, // 52: api.Dex.CreateClient:output_type -> api.CreateClientResp + 9, // 53: api.Dex.UpdateClient:output_type -> api.UpdateClientResp + 7, // 54: api.Dex.DeleteClient:output_type -> api.DeleteClientResp + 11, // 55: api.Dex.ListClients:output_type -> api.ListClientResp + 14, // 56: api.Dex.CreatePassword:output_type -> api.CreatePasswordResp + 16, // 57: api.Dex.UpdatePassword:output_type -> api.UpdatePasswordResp + 18, // 58: api.Dex.DeletePassword:output_type -> api.DeletePasswordResp + 20, // 59: api.Dex.ListPasswords:output_type -> api.ListPasswordResp + 23, // 60: api.Dex.CreateConnector:output_type -> api.CreateConnectorResp + 26, // 61: api.Dex.UpdateConnector:output_type -> api.UpdateConnectorResp + 28, // 62: api.Dex.DeleteConnector:output_type -> api.DeleteConnectorResp + 30, // 63: api.Dex.ListConnectors:output_type -> api.ListConnectorResp + 32, // 64: api.Dex.GetVersion:output_type -> api.VersionResp + 34, // 65: api.Dex.GetDiscovery:output_type -> api.DiscoveryResp + 37, // 66: api.Dex.ListRefresh:output_type -> api.ListRefreshResp + 39, // 67: api.Dex.RevokeRefresh:output_type -> api.RevokeRefreshResp + 41, // 68: api.Dex.VerifyPassword:output_type -> api.VerifyPasswordResp + 45, // 69: api.Dex.GetAuthSession:output_type -> api.GetAuthSessionResp + 47, // 70: api.Dex.ListAuthSessions:output_type -> api.ListAuthSessionsResp + 49, // 71: api.Dex.DeleteAuthSession:output_type -> api.DeleteAuthSessionResp + 51, // 72: api.Dex.TerminateSessionsByConnector:output_type -> api.TerminateSessionsByConnectorResp + 53, // 73: api.Dex.TerminateSessionsByUser:output_type -> api.TerminateSessionsByUserResp + 60, // 74: api.Dex.GetUserIdentity:output_type -> api.GetUserIdentityResp + 62, // 75: api.Dex.ListUserIdentities:output_type -> api.ListUserIdentitiesResp + 64, // 76: api.Dex.DeleteUserIdentity:output_type -> api.DeleteUserIdentityResp + 66, // 77: api.Dex.ResetMFA:output_type -> api.ResetMFAResp + 68, // 78: api.Dex.ListMFADevices:output_type -> api.ListMFADevicesResp + 70, // 79: api.Dex.DeleteWebAuthnCredential:output_type -> api.DeleteWebAuthnCredentialResp + 72, // 80: api.Dex.DeleteMFASecret:output_type -> api.DeleteMFASecretResp + 74, // 81: api.Dex.RevokeConsent:output_type -> api.RevokeConsentResp + 51, // [51:82] is the sub-list for method output_type + 20, // [20:51] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_api_v2_api_proto_init() } @@ -2785,7 +5025,7 @@ func file_api_v2_api_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v2_api_proto_rawDesc), len(file_api_v2_api_proto_rawDesc)), NumEnums: 0, - NumMessages: 42, + NumMessages: 75, NumExtensions: 0, NumServices: 1, }, diff --git a/api/v2/api.proto b/api/v2/api.proto index dffe32125c..a256bfbc21 100644 --- a/api/v2/api.proto +++ b/api/v2/api.proto @@ -278,6 +278,233 @@ message VerifyPasswordResp { bool not_found = 2; } +// ClientAuthState represents authentication state for a specific client within a session. +// The user_id and connector_id are on the parent AuthSession message. +message ClientAuthState { + string client_id = 1; + bool active = 2; + int64 expires_at = 3; + int64 last_activity = 4; + int64 last_token_issued_at = 5; +} + +// AuthSession represents a user's authentication session. +message AuthSession { + string user_id = 1; + string connector_id = 2; + repeated ClientAuthState client_states = 3; + int64 created_at = 4; + int64 last_activity = 5; + string ip_address = 6; + string user_agent = 7; + int64 absolute_expiry = 8; + int64 idle_expiry = 9; +} + +// GetAuthSessionReq is a request to retrieve an auth session. +message GetAuthSessionReq { + string user_id = 1; + string connector_id = 2; +} + +// GetAuthSessionResp returns the auth session details. +message GetAuthSessionResp { + AuthSession session = 1; +} + +// ListAuthSessionsReq is a request to list auth sessions. +message ListAuthSessionsReq { + // Optional filter: if set, only sessions for this user are returned. + string user_id = 1; +} + +// ListAuthSessionsResp returns a list of auth sessions. +message ListAuthSessionsResp { + repeated AuthSession sessions = 1; +} + +// DeleteAuthSessionReq is a request to delete an auth session. +// Deleting a session also revokes all associated refresh tokens (consistent with logout behavior). +message DeleteAuthSessionReq { + string user_id = 1; + string connector_id = 2; +} + +// DeleteAuthSessionResp returns the result of deleting an auth session. +message DeleteAuthSessionResp { + bool not_found = 1; +} + +// TerminateSessionsByConnectorReq is a request to terminate all sessions for a connector. +// Use when connector configuration changes or is removed. Also revokes associated refresh tokens. +message TerminateSessionsByConnectorReq { + string connector_id = 1; +} + +// TerminateSessionsByConnectorResp returns the count of terminated sessions. +message TerminateSessionsByConnectorResp { + int64 sessions_terminated = 1; +} + +// TerminateSessionsByUserReq is a request to terminate all sessions for a user. +// Use for account compromise scenarios. Also revokes associated refresh tokens. +message TerminateSessionsByUserReq { + string user_id = 1; +} + +// TerminateSessionsByUserResp returns the count of terminated sessions. +message TerminateSessionsByUserResp { + int64 sessions_terminated = 1; +} + +// ConsentEntry represents approved scopes for a single client. +message ConsentEntry { + string client_id = 1; + repeated string scopes = 2; +} + +// MFASecret represents metadata of an enrolled MFA authenticator. +// The actual secret value is never exposed through the admin API. +message MFASecret { + string authenticator_id = 1; + string type = 2; + bool confirmed = 3; + int64 created_at = 4; +} + +// WebAuthnCredential represents metadata of a registered WebAuthn credential. +// The public key is never exposed through the admin API. +message WebAuthnCredential { + bytes credential_id = 1; + string attestation_type = 2; + bytes aaguid = 3; + uint32 sign_count = 4; + bool clone_warning = 5; + repeated string transport = 6; + bool backup_eligible = 7; + bool backup_state = 8; + string display_name = 9; + int64 created_at = 10; +} + +// MFADeviceInfo groups MFA secret and WebAuthn credentials for one authenticator. +message MFADeviceInfo { + string authenticator_id = 1; + MFASecret mfa_secret = 2; + repeated WebAuthnCredential webauthn_credentials = 3; +} + +// UserIdentity represents persistent per-user identity data. +message UserIdentity { + string user_id = 1; + string connector_id = 2; + string email = 3; + bool email_verified = 4; + string username = 5; + repeated string groups = 6; + repeated ConsentEntry consents = 7; + repeated MFADeviceInfo mfa_devices = 8; + int64 created_at = 9; + int64 last_login = 10; + int64 blocked_until = 11; +} + +// GetUserIdentityReq is a request to retrieve a user identity. +message GetUserIdentityReq { + string user_id = 1; + string connector_id = 2; +} + +// GetUserIdentityResp returns the user identity details. +message GetUserIdentityResp { + UserIdentity identity = 1; +} + +// ListUserIdentitiesReq is a request to list user identities. +message ListUserIdentitiesReq {} + +// ListUserIdentitiesResp returns a list of user identities. +message ListUserIdentitiesResp { + repeated UserIdentity identities = 1; +} + +// DeleteUserIdentityReq is a request to delete a user identity. +// This is a full data purge for GDPR compliance and account deletion. +// It cascades to: auth session, all refresh tokens, offline sessions, and the identity itself. +// Password records must be deleted separately via DeletePassword (linked by email, not user ID). +message DeleteUserIdentityReq { + string user_id = 1; + string connector_id = 2; +} + +// DeleteUserIdentityResp returns the result of deleting a user identity. +message DeleteUserIdentityResp { + bool not_found = 1; +} + +// ResetMFAReq is a request to clear all MFA secrets and WebAuthn credentials for a user. +// Use when a user has lost access to all their MFA devices. +message ResetMFAReq { + string user_id = 1; + string connector_id = 2; +} + +// ResetMFAResp returns the result of resetting MFA. +message ResetMFAResp { + bool not_found = 1; +} + +// ListMFADevicesReq is a request to list registered MFA authenticators for a user. +message ListMFADevicesReq { + string user_id = 1; + string connector_id = 2; +} + +// ListMFADevicesResp returns MFA device information. +// Secret values and public keys are never included in the response. +message ListMFADevicesResp { + repeated MFADeviceInfo devices = 1; +} + +// DeleteWebAuthnCredentialReq is a request to delete a specific WebAuthn credential. +// Use when a user has lost or wants to deregister a specific security key. +message DeleteWebAuthnCredentialReq { + string user_id = 1; + string connector_id = 2; + bytes credential_id = 3; +} + +// DeleteWebAuthnCredentialResp returns the result of deleting a WebAuthn credential. +message DeleteWebAuthnCredentialResp { + bool not_found = 1; +} + +// DeleteMFASecretReq is a request to delete a specific MFA authenticator secret. +// Also removes any associated WebAuthn credentials for the same authenticator. +message DeleteMFASecretReq { + string user_id = 1; + string connector_id = 2; + string authenticator_id = 3; +} + +// DeleteMFASecretResp returns the result of deleting an MFA secret. +message DeleteMFASecretResp { + bool not_found = 1; +} + +// RevokeConsentReq is a request to revoke consent for a specific client. +// The user will see the consent screen again on next authorization. +message RevokeConsentReq { + string user_id = 1; + string connector_id = 2; + string client_id = 3; +} + +// RevokeConsentResp returns the result of revoking consent. +message RevokeConsentResp { + bool not_found = 1; +} + // Dex represents the dex gRPC service. service Dex { // GetClient gets a client. @@ -318,4 +545,31 @@ service Dex { rpc RevokeRefresh(RevokeRefreshReq) returns (RevokeRefreshResp) {}; // VerifyPassword returns whether a password matches a hash for a specific email or not. rpc VerifyPassword(VerifyPasswordReq) returns (VerifyPasswordResp) {}; + // GetAuthSession returns an auth session by user and connector ID. + rpc GetAuthSession(GetAuthSessionReq) returns (GetAuthSessionResp) {}; + // ListAuthSessions lists auth sessions, optionally filtered by user_id. + rpc ListAuthSessions(ListAuthSessionsReq) returns (ListAuthSessionsResp) {}; + // DeleteAuthSession deletes an auth session and revokes associated refresh tokens. + rpc DeleteAuthSession(DeleteAuthSessionReq) returns (DeleteAuthSessionResp) {}; + // TerminateSessionsByConnector terminates all sessions for a connector and revokes associated refresh tokens. + rpc TerminateSessionsByConnector(TerminateSessionsByConnectorReq) returns (TerminateSessionsByConnectorResp) {}; + // TerminateSessionsByUser terminates all sessions for a user and revokes associated refresh tokens. + rpc TerminateSessionsByUser(TerminateSessionsByUserReq) returns (TerminateSessionsByUserResp) {}; + // GetUserIdentity returns a user identity by user and connector ID. + rpc GetUserIdentity(GetUserIdentityReq) returns (GetUserIdentityResp) {}; + // ListUserIdentities lists all user identities. + rpc ListUserIdentities(ListUserIdentitiesReq) returns (ListUserIdentitiesResp) {}; + // DeleteUserIdentity performs a full data purge for GDPR compliance: deletes the identity, + // auth session, refresh tokens, and offline sessions. + rpc DeleteUserIdentity(DeleteUserIdentityReq) returns (DeleteUserIdentityResp) {}; + // ResetMFA clears all MFA secrets and WebAuthn credentials for a user. + rpc ResetMFA(ResetMFAReq) returns (ResetMFAResp) {}; + // ListMFADevices lists registered MFA authenticators for a user. + rpc ListMFADevices(ListMFADevicesReq) returns (ListMFADevicesResp) {}; + // DeleteWebAuthnCredential deletes a specific WebAuthn credential. + rpc DeleteWebAuthnCredential(DeleteWebAuthnCredentialReq) returns (DeleteWebAuthnCredentialResp) {}; + // DeleteMFASecret deletes a specific MFA authenticator and its associated WebAuthn credentials. + rpc DeleteMFASecret(DeleteMFASecretReq) returns (DeleteMFASecretResp) {}; + // RevokeConsent revokes consent for a specific client. + rpc RevokeConsent(RevokeConsentReq) returns (RevokeConsentResp) {}; } diff --git a/api/v2/api_grpc.pb.go b/api/v2/api_grpc.pb.go index 3fe210e6ff..f696b7de10 100644 --- a/api/v2/api_grpc.pb.go +++ b/api/v2/api_grpc.pb.go @@ -19,24 +19,37 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Dex_GetClient_FullMethodName = "/api.Dex/GetClient" - Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient" - Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient" - Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient" - Dex_ListClients_FullMethodName = "/api.Dex/ListClients" - Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword" - Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword" - Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword" - Dex_ListPasswords_FullMethodName = "/api.Dex/ListPasswords" - Dex_CreateConnector_FullMethodName = "/api.Dex/CreateConnector" - Dex_UpdateConnector_FullMethodName = "/api.Dex/UpdateConnector" - Dex_DeleteConnector_FullMethodName = "/api.Dex/DeleteConnector" - Dex_ListConnectors_FullMethodName = "/api.Dex/ListConnectors" - Dex_GetVersion_FullMethodName = "/api.Dex/GetVersion" - Dex_GetDiscovery_FullMethodName = "/api.Dex/GetDiscovery" - Dex_ListRefresh_FullMethodName = "/api.Dex/ListRefresh" - Dex_RevokeRefresh_FullMethodName = "/api.Dex/RevokeRefresh" - Dex_VerifyPassword_FullMethodName = "/api.Dex/VerifyPassword" + Dex_GetClient_FullMethodName = "/api.Dex/GetClient" + Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient" + Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient" + Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient" + Dex_ListClients_FullMethodName = "/api.Dex/ListClients" + Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword" + Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword" + Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword" + Dex_ListPasswords_FullMethodName = "/api.Dex/ListPasswords" + Dex_CreateConnector_FullMethodName = "/api.Dex/CreateConnector" + Dex_UpdateConnector_FullMethodName = "/api.Dex/UpdateConnector" + Dex_DeleteConnector_FullMethodName = "/api.Dex/DeleteConnector" + Dex_ListConnectors_FullMethodName = "/api.Dex/ListConnectors" + Dex_GetVersion_FullMethodName = "/api.Dex/GetVersion" + Dex_GetDiscovery_FullMethodName = "/api.Dex/GetDiscovery" + Dex_ListRefresh_FullMethodName = "/api.Dex/ListRefresh" + Dex_RevokeRefresh_FullMethodName = "/api.Dex/RevokeRefresh" + Dex_VerifyPassword_FullMethodName = "/api.Dex/VerifyPassword" + Dex_GetAuthSession_FullMethodName = "/api.Dex/GetAuthSession" + Dex_ListAuthSessions_FullMethodName = "/api.Dex/ListAuthSessions" + Dex_DeleteAuthSession_FullMethodName = "/api.Dex/DeleteAuthSession" + Dex_TerminateSessionsByConnector_FullMethodName = "/api.Dex/TerminateSessionsByConnector" + Dex_TerminateSessionsByUser_FullMethodName = "/api.Dex/TerminateSessionsByUser" + Dex_GetUserIdentity_FullMethodName = "/api.Dex/GetUserIdentity" + Dex_ListUserIdentities_FullMethodName = "/api.Dex/ListUserIdentities" + Dex_DeleteUserIdentity_FullMethodName = "/api.Dex/DeleteUserIdentity" + Dex_ResetMFA_FullMethodName = "/api.Dex/ResetMFA" + Dex_ListMFADevices_FullMethodName = "/api.Dex/ListMFADevices" + Dex_DeleteWebAuthnCredential_FullMethodName = "/api.Dex/DeleteWebAuthnCredential" + Dex_DeleteMFASecret_FullMethodName = "/api.Dex/DeleteMFASecret" + Dex_RevokeConsent_FullMethodName = "/api.Dex/RevokeConsent" ) // DexClient is the client API for Dex service. @@ -83,6 +96,33 @@ type DexClient interface { RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error) // VerifyPassword returns whether a password matches a hash for a specific email or not. VerifyPassword(ctx context.Context, in *VerifyPasswordReq, opts ...grpc.CallOption) (*VerifyPasswordResp, error) + // GetAuthSession returns an auth session by user and connector ID. + GetAuthSession(ctx context.Context, in *GetAuthSessionReq, opts ...grpc.CallOption) (*GetAuthSessionResp, error) + // ListAuthSessions lists auth sessions, optionally filtered by user_id. + ListAuthSessions(ctx context.Context, in *ListAuthSessionsReq, opts ...grpc.CallOption) (*ListAuthSessionsResp, error) + // DeleteAuthSession deletes an auth session and revokes associated refresh tokens. + DeleteAuthSession(ctx context.Context, in *DeleteAuthSessionReq, opts ...grpc.CallOption) (*DeleteAuthSessionResp, error) + // TerminateSessionsByConnector terminates all sessions for a connector and revokes associated refresh tokens. + TerminateSessionsByConnector(ctx context.Context, in *TerminateSessionsByConnectorReq, opts ...grpc.CallOption) (*TerminateSessionsByConnectorResp, error) + // TerminateSessionsByUser terminates all sessions for a user and revokes associated refresh tokens. + TerminateSessionsByUser(ctx context.Context, in *TerminateSessionsByUserReq, opts ...grpc.CallOption) (*TerminateSessionsByUserResp, error) + // GetUserIdentity returns a user identity by user and connector ID. + GetUserIdentity(ctx context.Context, in *GetUserIdentityReq, opts ...grpc.CallOption) (*GetUserIdentityResp, error) + // ListUserIdentities lists all user identities. + ListUserIdentities(ctx context.Context, in *ListUserIdentitiesReq, opts ...grpc.CallOption) (*ListUserIdentitiesResp, error) + // DeleteUserIdentity performs a full data purge for GDPR compliance: deletes the identity, + // auth session, refresh tokens, and offline sessions. + DeleteUserIdentity(ctx context.Context, in *DeleteUserIdentityReq, opts ...grpc.CallOption) (*DeleteUserIdentityResp, error) + // ResetMFA clears all MFA secrets and WebAuthn credentials for a user. + ResetMFA(ctx context.Context, in *ResetMFAReq, opts ...grpc.CallOption) (*ResetMFAResp, error) + // ListMFADevices lists registered MFA authenticators for a user. + ListMFADevices(ctx context.Context, in *ListMFADevicesReq, opts ...grpc.CallOption) (*ListMFADevicesResp, error) + // DeleteWebAuthnCredential deletes a specific WebAuthn credential. + DeleteWebAuthnCredential(ctx context.Context, in *DeleteWebAuthnCredentialReq, opts ...grpc.CallOption) (*DeleteWebAuthnCredentialResp, error) + // DeleteMFASecret deletes a specific MFA authenticator and its associated WebAuthn credentials. + DeleteMFASecret(ctx context.Context, in *DeleteMFASecretReq, opts ...grpc.CallOption) (*DeleteMFASecretResp, error) + // RevokeConsent revokes consent for a specific client. + RevokeConsent(ctx context.Context, in *RevokeConsentReq, opts ...grpc.CallOption) (*RevokeConsentResp, error) } type dexClient struct { @@ -273,6 +313,136 @@ func (c *dexClient) VerifyPassword(ctx context.Context, in *VerifyPasswordReq, o return out, nil } +func (c *dexClient) GetAuthSession(ctx context.Context, in *GetAuthSessionReq, opts ...grpc.CallOption) (*GetAuthSessionResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAuthSessionResp) + err := c.cc.Invoke(ctx, Dex_GetAuthSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) ListAuthSessions(ctx context.Context, in *ListAuthSessionsReq, opts ...grpc.CallOption) (*ListAuthSessionsResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListAuthSessionsResp) + err := c.cc.Invoke(ctx, Dex_ListAuthSessions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) DeleteAuthSession(ctx context.Context, in *DeleteAuthSessionReq, opts ...grpc.CallOption) (*DeleteAuthSessionResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteAuthSessionResp) + err := c.cc.Invoke(ctx, Dex_DeleteAuthSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) TerminateSessionsByConnector(ctx context.Context, in *TerminateSessionsByConnectorReq, opts ...grpc.CallOption) (*TerminateSessionsByConnectorResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TerminateSessionsByConnectorResp) + err := c.cc.Invoke(ctx, Dex_TerminateSessionsByConnector_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) TerminateSessionsByUser(ctx context.Context, in *TerminateSessionsByUserReq, opts ...grpc.CallOption) (*TerminateSessionsByUserResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TerminateSessionsByUserResp) + err := c.cc.Invoke(ctx, Dex_TerminateSessionsByUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) GetUserIdentity(ctx context.Context, in *GetUserIdentityReq, opts ...grpc.CallOption) (*GetUserIdentityResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetUserIdentityResp) + err := c.cc.Invoke(ctx, Dex_GetUserIdentity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) ListUserIdentities(ctx context.Context, in *ListUserIdentitiesReq, opts ...grpc.CallOption) (*ListUserIdentitiesResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUserIdentitiesResp) + err := c.cc.Invoke(ctx, Dex_ListUserIdentities_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) DeleteUserIdentity(ctx context.Context, in *DeleteUserIdentityReq, opts ...grpc.CallOption) (*DeleteUserIdentityResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteUserIdentityResp) + err := c.cc.Invoke(ctx, Dex_DeleteUserIdentity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) ResetMFA(ctx context.Context, in *ResetMFAReq, opts ...grpc.CallOption) (*ResetMFAResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResetMFAResp) + err := c.cc.Invoke(ctx, Dex_ResetMFA_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) ListMFADevices(ctx context.Context, in *ListMFADevicesReq, opts ...grpc.CallOption) (*ListMFADevicesResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListMFADevicesResp) + err := c.cc.Invoke(ctx, Dex_ListMFADevices_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) DeleteWebAuthnCredential(ctx context.Context, in *DeleteWebAuthnCredentialReq, opts ...grpc.CallOption) (*DeleteWebAuthnCredentialResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteWebAuthnCredentialResp) + err := c.cc.Invoke(ctx, Dex_DeleteWebAuthnCredential_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) DeleteMFASecret(ctx context.Context, in *DeleteMFASecretReq, opts ...grpc.CallOption) (*DeleteMFASecretResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteMFASecretResp) + err := c.cc.Invoke(ctx, Dex_DeleteMFASecret_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) RevokeConsent(ctx context.Context, in *RevokeConsentReq, opts ...grpc.CallOption) (*RevokeConsentResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RevokeConsentResp) + err := c.cc.Invoke(ctx, Dex_RevokeConsent_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // DexServer is the server API for Dex service. // All implementations must embed UnimplementedDexServer // for forward compatibility. @@ -317,6 +487,33 @@ type DexServer interface { RevokeRefresh(context.Context, *RevokeRefreshReq) (*RevokeRefreshResp, error) // VerifyPassword returns whether a password matches a hash for a specific email or not. VerifyPassword(context.Context, *VerifyPasswordReq) (*VerifyPasswordResp, error) + // GetAuthSession returns an auth session by user and connector ID. + GetAuthSession(context.Context, *GetAuthSessionReq) (*GetAuthSessionResp, error) + // ListAuthSessions lists auth sessions, optionally filtered by user_id. + ListAuthSessions(context.Context, *ListAuthSessionsReq) (*ListAuthSessionsResp, error) + // DeleteAuthSession deletes an auth session and revokes associated refresh tokens. + DeleteAuthSession(context.Context, *DeleteAuthSessionReq) (*DeleteAuthSessionResp, error) + // TerminateSessionsByConnector terminates all sessions for a connector and revokes associated refresh tokens. + TerminateSessionsByConnector(context.Context, *TerminateSessionsByConnectorReq) (*TerminateSessionsByConnectorResp, error) + // TerminateSessionsByUser terminates all sessions for a user and revokes associated refresh tokens. + TerminateSessionsByUser(context.Context, *TerminateSessionsByUserReq) (*TerminateSessionsByUserResp, error) + // GetUserIdentity returns a user identity by user and connector ID. + GetUserIdentity(context.Context, *GetUserIdentityReq) (*GetUserIdentityResp, error) + // ListUserIdentities lists all user identities. + ListUserIdentities(context.Context, *ListUserIdentitiesReq) (*ListUserIdentitiesResp, error) + // DeleteUserIdentity performs a full data purge for GDPR compliance: deletes the identity, + // auth session, refresh tokens, and offline sessions. + DeleteUserIdentity(context.Context, *DeleteUserIdentityReq) (*DeleteUserIdentityResp, error) + // ResetMFA clears all MFA secrets and WebAuthn credentials for a user. + ResetMFA(context.Context, *ResetMFAReq) (*ResetMFAResp, error) + // ListMFADevices lists registered MFA authenticators for a user. + ListMFADevices(context.Context, *ListMFADevicesReq) (*ListMFADevicesResp, error) + // DeleteWebAuthnCredential deletes a specific WebAuthn credential. + DeleteWebAuthnCredential(context.Context, *DeleteWebAuthnCredentialReq) (*DeleteWebAuthnCredentialResp, error) + // DeleteMFASecret deletes a specific MFA authenticator and its associated WebAuthn credentials. + DeleteMFASecret(context.Context, *DeleteMFASecretReq) (*DeleteMFASecretResp, error) + // RevokeConsent revokes consent for a specific client. + RevokeConsent(context.Context, *RevokeConsentReq) (*RevokeConsentResp, error) mustEmbedUnimplementedDexServer() } @@ -381,6 +578,45 @@ func (UnimplementedDexServer) RevokeRefresh(context.Context, *RevokeRefreshReq) func (UnimplementedDexServer) VerifyPassword(context.Context, *VerifyPasswordReq) (*VerifyPasswordResp, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifyPassword not implemented") } +func (UnimplementedDexServer) GetAuthSession(context.Context, *GetAuthSessionReq) (*GetAuthSessionResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAuthSession not implemented") +} +func (UnimplementedDexServer) ListAuthSessions(context.Context, *ListAuthSessionsReq) (*ListAuthSessionsResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAuthSessions not implemented") +} +func (UnimplementedDexServer) DeleteAuthSession(context.Context, *DeleteAuthSessionReq) (*DeleteAuthSessionResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteAuthSession not implemented") +} +func (UnimplementedDexServer) TerminateSessionsByConnector(context.Context, *TerminateSessionsByConnectorReq) (*TerminateSessionsByConnectorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method TerminateSessionsByConnector not implemented") +} +func (UnimplementedDexServer) TerminateSessionsByUser(context.Context, *TerminateSessionsByUserReq) (*TerminateSessionsByUserResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method TerminateSessionsByUser not implemented") +} +func (UnimplementedDexServer) GetUserIdentity(context.Context, *GetUserIdentityReq) (*GetUserIdentityResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserIdentity not implemented") +} +func (UnimplementedDexServer) ListUserIdentities(context.Context, *ListUserIdentitiesReq) (*ListUserIdentitiesResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUserIdentities not implemented") +} +func (UnimplementedDexServer) DeleteUserIdentity(context.Context, *DeleteUserIdentityReq) (*DeleteUserIdentityResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteUserIdentity not implemented") +} +func (UnimplementedDexServer) ResetMFA(context.Context, *ResetMFAReq) (*ResetMFAResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ResetMFA not implemented") +} +func (UnimplementedDexServer) ListMFADevices(context.Context, *ListMFADevicesReq) (*ListMFADevicesResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListMFADevices not implemented") +} +func (UnimplementedDexServer) DeleteWebAuthnCredential(context.Context, *DeleteWebAuthnCredentialReq) (*DeleteWebAuthnCredentialResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteWebAuthnCredential not implemented") +} +func (UnimplementedDexServer) DeleteMFASecret(context.Context, *DeleteMFASecretReq) (*DeleteMFASecretResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteMFASecret not implemented") +} +func (UnimplementedDexServer) RevokeConsent(context.Context, *RevokeConsentReq) (*RevokeConsentResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevokeConsent not implemented") +} func (UnimplementedDexServer) mustEmbedUnimplementedDexServer() {} func (UnimplementedDexServer) testEmbeddedByValue() {} @@ -726,6 +962,240 @@ func _Dex_VerifyPassword_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Dex_GetAuthSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAuthSessionReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).GetAuthSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_GetAuthSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).GetAuthSession(ctx, req.(*GetAuthSessionReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_ListAuthSessions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAuthSessionsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).ListAuthSessions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_ListAuthSessions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).ListAuthSessions(ctx, req.(*ListAuthSessionsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_DeleteAuthSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteAuthSessionReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).DeleteAuthSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_DeleteAuthSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).DeleteAuthSession(ctx, req.(*DeleteAuthSessionReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_TerminateSessionsByConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TerminateSessionsByConnectorReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).TerminateSessionsByConnector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_TerminateSessionsByConnector_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).TerminateSessionsByConnector(ctx, req.(*TerminateSessionsByConnectorReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_TerminateSessionsByUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TerminateSessionsByUserReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).TerminateSessionsByUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_TerminateSessionsByUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).TerminateSessionsByUser(ctx, req.(*TerminateSessionsByUserReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_GetUserIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserIdentityReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).GetUserIdentity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_GetUserIdentity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).GetUserIdentity(ctx, req.(*GetUserIdentityReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_ListUserIdentities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUserIdentitiesReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).ListUserIdentities(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_ListUserIdentities_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).ListUserIdentities(ctx, req.(*ListUserIdentitiesReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_DeleteUserIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteUserIdentityReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).DeleteUserIdentity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_DeleteUserIdentity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).DeleteUserIdentity(ctx, req.(*DeleteUserIdentityReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_ResetMFA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResetMFAReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).ResetMFA(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_ResetMFA_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).ResetMFA(ctx, req.(*ResetMFAReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_ListMFADevices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMFADevicesReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).ListMFADevices(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_ListMFADevices_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).ListMFADevices(ctx, req.(*ListMFADevicesReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_DeleteWebAuthnCredential_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteWebAuthnCredentialReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).DeleteWebAuthnCredential(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_DeleteWebAuthnCredential_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).DeleteWebAuthnCredential(ctx, req.(*DeleteWebAuthnCredentialReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_DeleteMFASecret_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteMFASecretReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).DeleteMFASecret(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_DeleteMFASecret_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).DeleteMFASecret(ctx, req.(*DeleteMFASecretReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_RevokeConsent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeConsentReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).RevokeConsent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_RevokeConsent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).RevokeConsent(ctx, req.(*RevokeConsentReq)) + } + return interceptor(ctx, in, info, handler) +} + // Dex_ServiceDesc is the grpc.ServiceDesc for Dex service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -805,6 +1275,58 @@ var Dex_ServiceDesc = grpc.ServiceDesc{ MethodName: "VerifyPassword", Handler: _Dex_VerifyPassword_Handler, }, + { + MethodName: "GetAuthSession", + Handler: _Dex_GetAuthSession_Handler, + }, + { + MethodName: "ListAuthSessions", + Handler: _Dex_ListAuthSessions_Handler, + }, + { + MethodName: "DeleteAuthSession", + Handler: _Dex_DeleteAuthSession_Handler, + }, + { + MethodName: "TerminateSessionsByConnector", + Handler: _Dex_TerminateSessionsByConnector_Handler, + }, + { + MethodName: "TerminateSessionsByUser", + Handler: _Dex_TerminateSessionsByUser_Handler, + }, + { + MethodName: "GetUserIdentity", + Handler: _Dex_GetUserIdentity_Handler, + }, + { + MethodName: "ListUserIdentities", + Handler: _Dex_ListUserIdentities_Handler, + }, + { + MethodName: "DeleteUserIdentity", + Handler: _Dex_DeleteUserIdentity_Handler, + }, + { + MethodName: "ResetMFA", + Handler: _Dex_ResetMFA_Handler, + }, + { + MethodName: "ListMFADevices", + Handler: _Dex_ListMFADevices_Handler, + }, + { + MethodName: "DeleteWebAuthnCredential", + Handler: _Dex_DeleteWebAuthnCredential_Handler, + }, + { + MethodName: "DeleteMFASecret", + Handler: _Dex_DeleteMFASecret_Handler, + }, + { + MethodName: "RevokeConsent", + Handler: _Dex_RevokeConsent_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "api/v2/api.proto", diff --git a/pkg/featureflags/set.go b/pkg/featureflags/set.go index d394297945..a63da72ce0 100644 --- a/pkg/featureflags/set.go +++ b/pkg/featureflags/set.go @@ -24,4 +24,7 @@ var ( // SessionsEnabled enables experimental auth sessions support. SessionsEnabled = newFlag("sessions_enabled", false) + + // APISessionsIdentitiesCRUD allows CRUD operations on auth sessions and user identities through the gRPC API. + APISessionsIdentitiesCRUD = newFlag("api_sessions_identities_crud", false) ) diff --git a/server/api.go b/server/api.go index c5ee787478..0fbadd472b 100644 --- a/server/api.go +++ b/server/api.go @@ -1,11 +1,13 @@ package server import ( + "bytes" "context" "encoding/json" "errors" "fmt" "log/slog" + "slices" "strconv" "golang.org/x/crypto/bcrypt" @@ -18,7 +20,7 @@ import ( // apiVersion increases every time a new call is added to the API. Clients should use this info // to determine if the server supports specific features. -const apiVersion = 3 +const apiVersion = 4 const ( // recCost is the recommended bcrypt cost, which balances hash strength and @@ -617,3 +619,512 @@ func defaultTo[T comparable](v, def T) T { } return v } + +// revokeUserRefreshTokens revokes all refresh tokens for a user/connector pair +// and cleans up offline session references. Errors are logged but not returned +// (best-effort). Uses the shared revokeRefreshTokensFromStorage helper. +func (d dexAPI) revokeUserRefreshTokens(ctx context.Context, userID, connectorID string) { + revokeRefreshTokensFromStorage(ctx, d.s, d.logger, userID, connectorID) +} + +func storageAuthSessionToAPI(s storage.AuthSession) *api.AuthSession { + clientStates := make([]*api.ClientAuthState, 0, len(s.ClientStates)) + for clientID, state := range s.ClientStates { + clientStates = append(clientStates, &api.ClientAuthState{ + ClientId: clientID, + Active: state.Active, + ExpiresAt: state.ExpiresAt.Unix(), + LastActivity: state.LastActivity.Unix(), + LastTokenIssuedAt: state.LastTokenIssuedAt.Unix(), + }) + } + + return &api.AuthSession{ + UserId: s.UserID, + ConnectorId: s.ConnectorID, + ClientStates: clientStates, + CreatedAt: s.CreatedAt.Unix(), + LastActivity: s.LastActivity.Unix(), + IpAddress: s.IPAddress, + UserAgent: s.UserAgent, + AbsoluteExpiry: s.AbsoluteExpiry.Unix(), + IdleExpiry: s.IdleExpiry.Unix(), + } +} + +func storageMFADevicesToAPI(secrets map[string]*storage.MFASecret, credentials map[string][]storage.WebAuthnCredential) []*api.MFADeviceInfo { + // Collect all authenticator IDs from both maps. + authIDs := make(map[string]struct{}) + for id := range secrets { + authIDs[id] = struct{}{} + } + for id := range credentials { + authIDs[id] = struct{}{} + } + + devices := make([]*api.MFADeviceInfo, 0, len(authIDs)) + for authID := range authIDs { + device := &api.MFADeviceInfo{ + AuthenticatorId: authID, + } + + if secret, ok := secrets[authID]; ok { + device.MfaSecret = &api.MFASecret{ + AuthenticatorId: secret.AuthenticatorID, + Type: secret.Type, + Confirmed: secret.Confirmed, + CreatedAt: secret.CreatedAt.Unix(), + } + } + + if creds, ok := credentials[authID]; ok { + apiCreds := make([]*api.WebAuthnCredential, 0, len(creds)) + for _, c := range creds { + apiCreds = append(apiCreds, &api.WebAuthnCredential{ + CredentialId: c.CredentialID, + AttestationType: c.AttestationType, + Aaguid: c.AAGUID, + SignCount: c.SignCount, + CloneWarning: c.CloneWarning, + Transport: c.Transport, + BackupEligible: c.BackupEligible, + BackupState: c.BackupState, + DisplayName: c.DisplayName, + CreatedAt: c.CreatedAt.Unix(), + }) + } + device.WebauthnCredentials = apiCreds + } + + devices = append(devices, device) + } + return devices +} + +func storageUserIdentityToAPI(u storage.UserIdentity) *api.UserIdentity { + consents := make([]*api.ConsentEntry, 0, len(u.Consents)) + for clientID, scopes := range u.Consents { + consents = append(consents, &api.ConsentEntry{ + ClientId: clientID, + Scopes: scopes, + }) + } + + identity := &api.UserIdentity{ + UserId: u.UserID, + ConnectorId: u.ConnectorID, + Email: u.Claims.Email, + EmailVerified: u.Claims.EmailVerified, + Username: u.Claims.Username, + Groups: u.Claims.Groups, + Consents: consents, + MfaDevices: storageMFADevicesToAPI(u.MFASecrets, u.WebAuthnCredentials), + CreatedAt: u.CreatedAt.Unix(), + LastLogin: u.LastLogin.Unix(), + } + + if !u.BlockedUntil.IsZero() { + identity.BlockedUntil = u.BlockedUntil.Unix() + } + + return identity +} + +func (d dexAPI) GetAuthSession(ctx context.Context, req *api.GetAuthSessionReq) (*api.GetAuthSessionResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + + session, err := d.s.GetAuthSession(ctx, req.UserId, req.ConnectorId) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, storage.ErrNotFound + } + d.logger.Error("api: failed to get auth session", "err", err) + return nil, fmt.Errorf("get auth session: %v", err) + } + + return &api.GetAuthSessionResp{ + Session: storageAuthSessionToAPI(session), + }, nil +} + +func (d dexAPI) ListAuthSessions(ctx context.Context, req *api.ListAuthSessionsReq) (*api.ListAuthSessionsResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + sessionList, err := d.s.ListAuthSessions(ctx) + if err != nil { + d.logger.Error("api: failed to list auth sessions", "err", err) + return nil, fmt.Errorf("list auth sessions: %v", err) + } + + sessions := make([]*api.AuthSession, 0, len(sessionList)) + for _, s := range sessionList { + if req.UserId != "" && s.UserID != req.UserId { + continue + } + sessions = append(sessions, storageAuthSessionToAPI(s)) + } + + return &api.ListAuthSessionsResp{ + Sessions: sessions, + }, nil +} + +func (d dexAPI) DeleteAuthSession(ctx context.Context, req *api.DeleteAuthSessionReq) (*api.DeleteAuthSessionResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + + // Revoke refresh tokens (best-effort, consistent with logout flow). + d.revokeUserRefreshTokens(ctx, req.UserId, req.ConnectorId) + + if err := d.s.DeleteAuthSession(ctx, req.UserId, req.ConnectorId); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return &api.DeleteAuthSessionResp{NotFound: true}, nil + } + d.logger.Error("api: failed to delete auth session", "err", err) + return nil, fmt.Errorf("delete auth session: %v", err) + } + + return &api.DeleteAuthSessionResp{}, nil +} + +func (d dexAPI) terminateSessions(ctx context.Context, match func(storage.AuthSession) bool) (int64, error) { + sessionList, err := d.s.ListAuthSessions(ctx) + if err != nil { + d.logger.Error("api: failed to list auth sessions", "err", err) + return 0, fmt.Errorf("list auth sessions: %v", err) + } + + var terminated int64 + for _, s := range sessionList { + if !match(s) { + continue + } + + d.revokeUserRefreshTokens(ctx, s.UserID, s.ConnectorID) + + if err := d.s.DeleteAuthSession(ctx, s.UserID, s.ConnectorID); err != nil { + d.logger.Error("api: failed to delete auth session during batch terminate", + "user_id", s.UserID, "connector_id", s.ConnectorID, "err", err) + continue + } + terminated++ + } + return terminated, nil +} + +func (d dexAPI) TerminateSessionsByConnector(ctx context.Context, req *api.TerminateSessionsByConnectorReq) (*api.TerminateSessionsByConnectorResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + + terminated, err := d.terminateSessions(ctx, func(s storage.AuthSession) bool { + return s.ConnectorID == req.ConnectorId + }) + if err != nil { + return nil, err + } + + return &api.TerminateSessionsByConnectorResp{SessionsTerminated: terminated}, nil +} + +func (d dexAPI) TerminateSessionsByUser(ctx context.Context, req *api.TerminateSessionsByUserReq) (*api.TerminateSessionsByUserResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + + terminated, err := d.terminateSessions(ctx, func(s storage.AuthSession) bool { + return s.UserID == req.UserId + }) + if err != nil { + return nil, err + } + + return &api.TerminateSessionsByUserResp{SessionsTerminated: terminated}, nil +} + +func (d dexAPI) GetUserIdentity(ctx context.Context, req *api.GetUserIdentityReq) (*api.GetUserIdentityResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + + identity, err := d.s.GetUserIdentity(ctx, req.UserId, req.ConnectorId) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, storage.ErrNotFound + } + d.logger.Error("api: failed to get user identity", "err", err) + return nil, fmt.Errorf("get user identity: %v", err) + } + + return &api.GetUserIdentityResp{ + Identity: storageUserIdentityToAPI(identity), + }, nil +} + +func (d dexAPI) ListUserIdentities(ctx context.Context, req *api.ListUserIdentitiesReq) (*api.ListUserIdentitiesResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + identityList, err := d.s.ListUserIdentities(ctx) + if err != nil { + d.logger.Error("api: failed to list user identities", "err", err) + return nil, fmt.Errorf("list user identities: %v", err) + } + + identities := make([]*api.UserIdentity, 0, len(identityList)) + for _, u := range identityList { + identities = append(identities, storageUserIdentityToAPI(u)) + } + + return &api.ListUserIdentitiesResp{ + Identities: identities, + }, nil +} + +func (d dexAPI) DeleteUserIdentity(ctx context.Context, req *api.DeleteUserIdentityReq) (*api.DeleteUserIdentityResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + + // Cascade: delete auth session (ignore not-found). + if err := d.s.DeleteAuthSession(ctx, req.UserId, req.ConnectorId); err != nil && !errors.Is(err, storage.ErrNotFound) { + d.logger.Error("api: failed to delete auth session during identity purge", "err", err) + } + + // Cascade: revoke all refresh tokens. + d.revokeUserRefreshTokens(ctx, req.UserId, req.ConnectorId) + + // Cascade: delete offline sessions. + if err := d.s.DeleteOfflineSessions(ctx, req.UserId, req.ConnectorId); err != nil && !errors.Is(err, storage.ErrNotFound) { + d.logger.Error("api: failed to delete offline sessions during identity purge", "err", err) + } + + // Delete the user identity itself. + if err := d.s.DeleteUserIdentity(ctx, req.UserId, req.ConnectorId); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return &api.DeleteUserIdentityResp{NotFound: true}, nil + } + d.logger.Error("api: failed to delete user identity", "err", err) + return nil, fmt.Errorf("delete user identity: %v", err) + } + + return &api.DeleteUserIdentityResp{}, nil +} + +func (d dexAPI) ResetMFA(ctx context.Context, req *api.ResetMFAReq) (*api.ResetMFAResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + + if err := d.s.UpdateUserIdentity(ctx, req.UserId, req.ConnectorId, func(old storage.UserIdentity) (storage.UserIdentity, error) { + old.MFASecrets = nil + old.WebAuthnCredentials = nil + return old, nil + }); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return &api.ResetMFAResp{NotFound: true}, nil + } + d.logger.Error("api: failed to reset MFA", "err", err) + return nil, fmt.Errorf("reset MFA: %v", err) + } + + return &api.ResetMFAResp{}, nil +} + +func (d dexAPI) ListMFADevices(ctx context.Context, req *api.ListMFADevicesReq) (*api.ListMFADevicesResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + + identity, err := d.s.GetUserIdentity(ctx, req.UserId, req.ConnectorId) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, storage.ErrNotFound + } + d.logger.Error("api: failed to get user identity for MFA devices", "err", err) + return nil, fmt.Errorf("list MFA devices: %v", err) + } + + return &api.ListMFADevicesResp{ + Devices: storageMFADevicesToAPI(identity.MFASecrets, identity.WebAuthnCredentials), + }, nil +} + +func (d dexAPI) DeleteWebAuthnCredential(ctx context.Context, req *api.DeleteWebAuthnCredentialReq) (*api.DeleteWebAuthnCredentialResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + if len(req.CredentialId) == 0 { + return nil, errors.New("no credential_id supplied") + } + + notFound := true + if err := d.s.UpdateUserIdentity(ctx, req.UserId, req.ConnectorId, func(old storage.UserIdentity) (storage.UserIdentity, error) { + for authID, creds := range old.WebAuthnCredentials { + for i, cred := range creds { + if bytes.Equal(cred.CredentialID, req.CredentialId) { + notFound = false + old.WebAuthnCredentials[authID] = slices.Delete(creds, i, i+1) + if len(old.WebAuthnCredentials[authID]) == 0 { + delete(old.WebAuthnCredentials, authID) + } + return old, nil + } + } + } + return old, nil + }); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return &api.DeleteWebAuthnCredentialResp{NotFound: true}, nil + } + d.logger.Error("api: failed to delete WebAuthn credential", "err", err) + return nil, fmt.Errorf("delete WebAuthn credential: %v", err) + } + + if notFound { + return &api.DeleteWebAuthnCredentialResp{NotFound: true}, nil + } + + return &api.DeleteWebAuthnCredentialResp{}, nil +} + +func (d dexAPI) DeleteMFASecret(ctx context.Context, req *api.DeleteMFASecretReq) (*api.DeleteMFASecretResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + if req.AuthenticatorId == "" { + return nil, errors.New("no authenticator_id supplied") + } + + notFound := true + if err := d.s.UpdateUserIdentity(ctx, req.UserId, req.ConnectorId, func(old storage.UserIdentity) (storage.UserIdentity, error) { + if _, ok := old.MFASecrets[req.AuthenticatorId]; !ok { + return old, nil + } + notFound = false + delete(old.MFASecrets, req.AuthenticatorId) + // Also remove associated WebAuthn credentials for the same authenticator. + delete(old.WebAuthnCredentials, req.AuthenticatorId) + return old, nil + }); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return &api.DeleteMFASecretResp{NotFound: true}, nil + } + d.logger.Error("api: failed to delete MFA secret", "err", err) + return nil, fmt.Errorf("delete MFA secret: %v", err) + } + + if notFound { + return &api.DeleteMFASecretResp{NotFound: true}, nil + } + + return &api.DeleteMFASecretResp{}, nil +} + +func (d dexAPI) RevokeConsent(ctx context.Context, req *api.RevokeConsentReq) (*api.RevokeConsentResp, error) { + if !featureflags.APISessionsIdentitiesCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APISessionsIdentitiesCRUD.Name) + } + + if req.UserId == "" { + return nil, errors.New("no user_id supplied") + } + if req.ConnectorId == "" { + return nil, errors.New("no connector_id supplied") + } + if req.ClientId == "" { + return nil, errors.New("no client_id supplied") + } + + notFound := true + if err := d.s.UpdateUserIdentity(ctx, req.UserId, req.ConnectorId, func(old storage.UserIdentity) (storage.UserIdentity, error) { + if _, ok := old.Consents[req.ClientId]; !ok { + return old, nil + } + notFound = false + delete(old.Consents, req.ClientId) + return old, nil + }); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return &api.RevokeConsentResp{NotFound: true}, nil + } + d.logger.Error("api: failed to revoke consent", "err", err) + return nil, fmt.Errorf("revoke consent: %v", err) + } + + if notFound { + return &api.RevokeConsentResp{NotFound: true}, nil + } + + return &api.RevokeConsentResp{}, nil +} diff --git a/server/api_test.go b/server/api_test.go index 09cfa6783f..31e48d4813 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -1,6 +1,7 @@ package server import ( + "bytes" "log/slog" "net" "slices" @@ -924,3 +925,779 @@ func TestListClients(t *testing.T) { } } } + +func TestGetAuthSession(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + session := storage.AuthSession{ + UserID: "user1", + ConnectorID: "conn1", + Nonce: "nonce123", + ClientStates: map[string]*storage.ClientAuthState{ + "client-a": { + Active: true, + ExpiresAt: now.Add(24 * time.Hour), + LastActivity: now, + LastTokenIssuedAt: now, + }, + }, + CreatedAt: now, + LastActivity: now, + IPAddress: "10.0.0.1", + UserAgent: "TestAgent/1.0", + AbsoluteExpiry: now.Add(24 * time.Hour), + IdleExpiry: now.Add(1 * time.Hour), + } + + if err := s.CreateAuthSession(ctx, session); err != nil { + t.Fatalf("create auth session: %v", err) + } + + resp, err := client.GetAuthSession(ctx, &api.GetAuthSessionReq{ + UserId: "user1", + ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("get auth session: %v", err) + } + + if resp.Session.UserId != "user1" { + t.Errorf("expected user_id 'user1', got '%s'", resp.Session.UserId) + } + if resp.Session.IpAddress != "10.0.0.1" { + t.Errorf("expected ip_address '10.0.0.1', got '%s'", resp.Session.IpAddress) + } + if len(resp.Session.ClientStates) != 1 { + t.Fatalf("expected 1 client state, got %d", len(resp.Session.ClientStates)) + } + cs := resp.Session.ClientStates[0] + if cs.ClientId != "client-a" { + t.Errorf("expected client_id 'client-a', got '%s'", cs.ClientId) + } + if !cs.Active { + t.Error("expected client state to be active") + } + + // Not found case. + _, err = client.GetAuthSession(ctx, &api.GetAuthSessionReq{ + UserId: "nonexistent", + ConnectorId: "conn1", + }) + if err == nil { + t.Fatal("expected error for non-existent session") + } +} + +func TestListAuthSessions(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + for _, sess := range []storage.AuthSession{ + {UserID: "user1", ConnectorID: "conn1", Nonce: "n1", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + {UserID: "user1", ConnectorID: "conn2", Nonce: "n2", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + {UserID: "user2", ConnectorID: "conn1", Nonce: "n3", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + } { + if err := s.CreateAuthSession(ctx, sess); err != nil { + t.Fatalf("create auth session: %v", err) + } + } + + // List all. + resp, err := client.ListAuthSessions(ctx, &api.ListAuthSessionsReq{}) + if err != nil { + t.Fatalf("list auth sessions: %v", err) + } + if len(resp.Sessions) != 3 { + t.Fatalf("expected 3 sessions, got %d", len(resp.Sessions)) + } + + // Filter by user_id. + resp, err = client.ListAuthSessions(ctx, &api.ListAuthSessionsReq{UserId: "user1"}) + if err != nil { + t.Fatalf("list auth sessions with filter: %v", err) + } + if len(resp.Sessions) != 2 { + t.Fatalf("expected 2 sessions for user1, got %d", len(resp.Sessions)) + } +} + +func TestDeleteAuthSession(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + + // Create session. + session := storage.AuthSession{ + UserID: "user1", ConnectorID: "conn1", Nonce: "n1", + ClientStates: map[string]*storage.ClientAuthState{}, + CreatedAt: now, + LastActivity: now, + AbsoluteExpiry: now.Add(time.Hour), + IdleExpiry: now.Add(time.Hour), + } + if err := s.CreateAuthSession(ctx, session); err != nil { + t.Fatalf("create auth session: %v", err) + } + + // Create refresh token + offline session to verify cascading revocation. + refreshID := storage.NewID() + if err := s.CreateRefresh(ctx, storage.RefreshToken{ + ID: refreshID, Token: "tok", Nonce: "n", ClientID: "client1", ConnectorID: "conn1", + Scopes: []string{"openid"}, CreatedAt: now, LastUsed: now, + Claims: storage.Claims{UserID: "user1", Username: "test", Email: "test@test.com"}, + }); err != nil { + t.Fatalf("create refresh: %v", err) + } + if err := s.CreateOfflineSessions(ctx, storage.OfflineSessions{ + UserID: "user1", ConnID: "conn1", + Refresh: map[string]*storage.RefreshTokenRef{ + "client1": {ID: refreshID, ClientID: "client1", CreatedAt: now, LastUsed: now}, + }, + }); err != nil { + t.Fatalf("create offline sessions: %v", err) + } + + // Delete session. + resp, err := client.DeleteAuthSession(ctx, &api.DeleteAuthSessionReq{ + UserId: "user1", ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("delete auth session: %v", err) + } + if resp.NotFound { + t.Error("expected session to be found") + } + + // Verify session is gone. + _, err = s.GetAuthSession(ctx, "user1", "conn1") + if err == nil { + t.Error("expected auth session to be deleted") + } + + // Verify refresh token was revoked. + _, err = s.GetRefresh(ctx, refreshID) + if err == nil { + t.Error("expected refresh token to be revoked") + } + + // Not found case. + resp, err = client.DeleteAuthSession(ctx, &api.DeleteAuthSessionReq{ + UserId: "user1", ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("delete auth session: %v", err) + } + if !resp.NotFound { + t.Error("expected not_found for already deleted session") + } +} + +func TestTerminateSessionsByConnector(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + for _, sess := range []storage.AuthSession{ + {UserID: "user1", ConnectorID: "target-conn", Nonce: "n1", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + {UserID: "user2", ConnectorID: "target-conn", Nonce: "n2", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + {UserID: "user3", ConnectorID: "other-conn", Nonce: "n3", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + } { + if err := s.CreateAuthSession(ctx, sess); err != nil { + t.Fatalf("create auth session: %v", err) + } + } + + resp, err := client.TerminateSessionsByConnector(ctx, &api.TerminateSessionsByConnectorReq{ + ConnectorId: "target-conn", + }) + if err != nil { + t.Fatalf("terminate sessions by connector: %v", err) + } + if resp.SessionsTerminated != 2 { + t.Errorf("expected 2 terminated, got %d", resp.SessionsTerminated) + } + + // Verify remaining session is untouched. + remaining, err := s.ListAuthSessions(ctx) + if err != nil { + t.Fatalf("list auth sessions: %v", err) + } + if len(remaining) != 1 || remaining[0].ConnectorID != "other-conn" { + t.Errorf("expected only other-conn session to remain, got %v", remaining) + } +} + +func TestTerminateSessionsByUser(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + for _, sess := range []storage.AuthSession{ + {UserID: "target-user", ConnectorID: "conn1", Nonce: "n1", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + {UserID: "target-user", ConnectorID: "conn2", Nonce: "n2", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + {UserID: "other-user", ConnectorID: "conn1", Nonce: "n3", ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour)}, + } { + if err := s.CreateAuthSession(ctx, sess); err != nil { + t.Fatalf("create auth session: %v", err) + } + } + + resp, err := client.TerminateSessionsByUser(ctx, &api.TerminateSessionsByUserReq{ + UserId: "target-user", + }) + if err != nil { + t.Fatalf("terminate sessions by user: %v", err) + } + if resp.SessionsTerminated != 2 { + t.Errorf("expected 2 terminated, got %d", resp.SessionsTerminated) + } + + remaining, err := s.ListAuthSessions(ctx) + if err != nil { + t.Fatalf("list auth sessions: %v", err) + } + if len(remaining) != 1 || remaining[0].UserID != "other-user" { + t.Errorf("expected only other-user session to remain") + } +} + +func TestGetUserIdentity(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + identity := storage.UserIdentity{ + UserID: "user1", + ConnectorID: "conn1", + Claims: storage.Claims{ + UserID: "user1", + Username: "testuser", + Email: "test@example.com", + EmailVerified: true, + Groups: []string{"admins"}, + }, + Consents: map[string][]string{ + "client-a": {"openid", "email"}, + }, + MFASecrets: map[string]*storage.MFASecret{ + "totp-1": {AuthenticatorID: "totp-1", Type: "TOTP", Secret: "secret123", Confirmed: true, CreatedAt: now}, + }, + WebAuthnCredentials: map[string][]storage.WebAuthnCredential{ + "webauthn-1": {{CredentialID: []byte("cred1"), AttestationType: "none", DisplayName: "YubiKey", CreatedAt: now}}, + }, + CreatedAt: now, + LastLogin: now, + } + + if err := s.CreateUserIdentity(ctx, identity); err != nil { + t.Fatalf("create user identity: %v", err) + } + + resp, err := client.GetUserIdentity(ctx, &api.GetUserIdentityReq{ + UserId: "user1", ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("get user identity: %v", err) + } + + if resp.Identity.Email != "test@example.com" { + t.Errorf("expected email 'test@example.com', got '%s'", resp.Identity.Email) + } + if !resp.Identity.EmailVerified { + t.Error("expected email_verified true") + } + if resp.Identity.Username != "testuser" { + t.Errorf("expected username 'testuser', got '%s'", resp.Identity.Username) + } + if len(resp.Identity.Consents) != 1 { + t.Fatalf("expected 1 consent entry, got %d", len(resp.Identity.Consents)) + } + if len(resp.Identity.MfaDevices) == 0 { + t.Fatal("expected MFA devices") + } +} + +func TestListUserIdentities(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + for _, id := range []storage.UserIdentity{ + {UserID: "user1", ConnectorID: "conn1", Claims: storage.Claims{Email: "a@test.com"}, CreatedAt: now, LastLogin: now}, + {UserID: "user2", ConnectorID: "conn1", Claims: storage.Claims{Email: "b@test.com"}, CreatedAt: now, LastLogin: now}, + } { + if err := s.CreateUserIdentity(ctx, id); err != nil { + t.Fatalf("create user identity: %v", err) + } + } + + resp, err := client.ListUserIdentities(ctx, &api.ListUserIdentitiesReq{}) + if err != nil { + t.Fatalf("list user identities: %v", err) + } + if len(resp.Identities) != 2 { + t.Fatalf("expected 2 identities, got %d", len(resp.Identities)) + } +} + +func TestDeleteUserIdentity(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + + // Create identity + session + offline sessions + refresh token. + if err := s.CreateUserIdentity(ctx, storage.UserIdentity{ + UserID: "user1", ConnectorID: "conn1", + Claims: storage.Claims{Email: "test@test.com"}, CreatedAt: now, LastLogin: now, + }); err != nil { + t.Fatalf("create user identity: %v", err) + } + if err := s.CreateAuthSession(ctx, storage.AuthSession{ + UserID: "user1", ConnectorID: "conn1", Nonce: "n", + ClientStates: map[string]*storage.ClientAuthState{}, CreatedAt: now, LastActivity: now, + AbsoluteExpiry: now.Add(time.Hour), IdleExpiry: now.Add(time.Hour), + }); err != nil { + t.Fatalf("create auth session: %v", err) + } + refreshID := storage.NewID() + if err := s.CreateRefresh(ctx, storage.RefreshToken{ + ID: refreshID, Token: "tok", Nonce: "n", ClientID: "c1", ConnectorID: "conn1", + Scopes: []string{"openid"}, CreatedAt: now, LastUsed: now, + Claims: storage.Claims{UserID: "user1", Email: "test@test.com"}, + }); err != nil { + t.Fatalf("create refresh: %v", err) + } + if err := s.CreateOfflineSessions(ctx, storage.OfflineSessions{ + UserID: "user1", ConnID: "conn1", + Refresh: map[string]*storage.RefreshTokenRef{ + "c1": {ID: refreshID, ClientID: "c1", CreatedAt: now, LastUsed: now}, + }, + }); err != nil { + t.Fatalf("create offline sessions: %v", err) + } + + // Delete identity (cascading). + resp, err := client.DeleteUserIdentity(ctx, &api.DeleteUserIdentityReq{ + UserId: "user1", ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("delete user identity: %v", err) + } + if resp.NotFound { + t.Error("expected identity to be found") + } + + // Verify everything is deleted. + if _, err := s.GetUserIdentity(ctx, "user1", "conn1"); err == nil { + t.Error("expected user identity to be deleted") + } + if _, err := s.GetAuthSession(ctx, "user1", "conn1"); err == nil { + t.Error("expected auth session to be deleted") + } + if _, err := s.GetRefresh(ctx, refreshID); err == nil { + t.Error("expected refresh token to be deleted") + } + if _, err := s.GetOfflineSessions(ctx, "user1", "conn1"); err == nil { + t.Error("expected offline sessions to be deleted") + } + + // Not found case. + resp, err = client.DeleteUserIdentity(ctx, &api.DeleteUserIdentityReq{ + UserId: "user1", ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("delete user identity: %v", err) + } + if !resp.NotFound { + t.Error("expected not_found for already deleted identity") + } +} + +func TestResetMFA(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + if err := s.CreateUserIdentity(ctx, storage.UserIdentity{ + UserID: "user1", ConnectorID: "conn1", + Claims: storage.Claims{Email: "test@test.com"}, CreatedAt: now, LastLogin: now, + MFASecrets: map[string]*storage.MFASecret{ + "totp-1": {AuthenticatorID: "totp-1", Type: "TOTP", Secret: "s", Confirmed: true, CreatedAt: now}, + }, + WebAuthnCredentials: map[string][]storage.WebAuthnCredential{ + "webauthn-1": {{CredentialID: []byte("c1"), CreatedAt: now}}, + }, + }); err != nil { + t.Fatalf("create user identity: %v", err) + } + + resp, err := client.ResetMFA(ctx, &api.ResetMFAReq{ + UserId: "user1", ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("reset MFA: %v", err) + } + if resp.NotFound { + t.Error("expected identity to be found") + } + + // Verify MFA data is cleared. + identity, err := s.GetUserIdentity(ctx, "user1", "conn1") + if err != nil { + t.Fatalf("get user identity: %v", err) + } + if len(identity.MFASecrets) != 0 { + t.Errorf("expected MFASecrets to be cleared, got %d", len(identity.MFASecrets)) + } + if len(identity.WebAuthnCredentials) != 0 { + t.Errorf("expected WebAuthnCredentials to be cleared, got %d", len(identity.WebAuthnCredentials)) + } + // Verify other fields are preserved. + if identity.Claims.Email != "test@test.com" { + t.Errorf("expected email to be preserved, got '%s'", identity.Claims.Email) + } +} + +func TestListMFADevices(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + if err := s.CreateUserIdentity(ctx, storage.UserIdentity{ + UserID: "user1", ConnectorID: "conn1", + Claims: storage.Claims{Email: "test@test.com"}, CreatedAt: now, LastLogin: now, + MFASecrets: map[string]*storage.MFASecret{ + "totp-1": {AuthenticatorID: "totp-1", Type: "TOTP", Secret: "secret123", Confirmed: true, CreatedAt: now}, + }, + WebAuthnCredentials: map[string][]storage.WebAuthnCredential{ + "webauthn-1": { + {CredentialID: []byte("cred1"), PublicKey: []byte("pk1"), DisplayName: "Key1", CreatedAt: now}, + {CredentialID: []byte("cred2"), PublicKey: []byte("pk2"), DisplayName: "Key2", CreatedAt: now}, + }, + }, + }); err != nil { + t.Fatalf("create user identity: %v", err) + } + + resp, err := client.ListMFADevices(ctx, &api.ListMFADevicesReq{ + UserId: "user1", ConnectorId: "conn1", + }) + if err != nil { + t.Fatalf("list MFA devices: %v", err) + } + if len(resp.Devices) != 2 { + t.Fatalf("expected 2 device groups, got %d", len(resp.Devices)) + } + + // Find the TOTP device and verify secret is not exposed. + for _, device := range resp.Devices { + if device.AuthenticatorId == "totp-1" { + if device.MfaSecret == nil { + t.Fatal("expected MFA secret for totp-1") + } + if device.MfaSecret.Type != "TOTP" { + t.Errorf("expected type TOTP, got %s", device.MfaSecret.Type) + } + } + if device.AuthenticatorId == "webauthn-1" { + if len(device.WebauthnCredentials) != 2 { + t.Errorf("expected 2 webauthn credentials, got %d", len(device.WebauthnCredentials)) + } + } + } +} + +func TestDeleteWebAuthnCredential(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + if err := s.CreateUserIdentity(ctx, storage.UserIdentity{ + UserID: "user1", ConnectorID: "conn1", + Claims: storage.Claims{Email: "test@test.com"}, CreatedAt: now, LastLogin: now, + WebAuthnCredentials: map[string][]storage.WebAuthnCredential{ + "auth-1": { + {CredentialID: []byte("cred-to-delete"), DisplayName: "Key1", CreatedAt: now}, + {CredentialID: []byte("cred-to-keep"), DisplayName: "Key2", CreatedAt: now}, + }, + }, + }); err != nil { + t.Fatalf("create user identity: %v", err) + } + + // Delete one credential. + resp, err := client.DeleteWebAuthnCredential(ctx, &api.DeleteWebAuthnCredentialReq{ + UserId: "user1", ConnectorId: "conn1", CredentialId: []byte("cred-to-delete"), + }) + if err != nil { + t.Fatalf("delete webauthn credential: %v", err) + } + if resp.NotFound { + t.Error("expected credential to be found") + } + + // Verify only one credential remains. + identity, err := s.GetUserIdentity(ctx, "user1", "conn1") + if err != nil { + t.Fatalf("get user identity: %v", err) + } + creds := identity.WebAuthnCredentials["auth-1"] + if len(creds) != 1 { + t.Fatalf("expected 1 credential remaining, got %d", len(creds)) + } + if !bytes.Equal(creds[0].CredentialID, []byte("cred-to-keep")) { + t.Error("wrong credential was deleted") + } + + // Not found case. + resp, err = client.DeleteWebAuthnCredential(ctx, &api.DeleteWebAuthnCredentialReq{ + UserId: "user1", ConnectorId: "conn1", CredentialId: []byte("nonexistent"), + }) + if err != nil { + t.Fatalf("delete webauthn credential: %v", err) + } + if !resp.NotFound { + t.Error("expected not_found for nonexistent credential") + } +} + +func TestDeleteMFASecret(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + if err := s.CreateUserIdentity(ctx, storage.UserIdentity{ + UserID: "user1", ConnectorID: "conn1", + Claims: storage.Claims{Email: "test@test.com"}, CreatedAt: now, LastLogin: now, + MFASecrets: map[string]*storage.MFASecret{ + "totp-1": {AuthenticatorID: "totp-1", Type: "TOTP", Secret: "s", Confirmed: true, CreatedAt: now}, + "totp-2": {AuthenticatorID: "totp-2", Type: "TOTP", Secret: "s2", Confirmed: true, CreatedAt: now}, + }, + WebAuthnCredentials: map[string][]storage.WebAuthnCredential{ + "totp-1": {{CredentialID: []byte("c1"), CreatedAt: now}}, + }, + }); err != nil { + t.Fatalf("create user identity: %v", err) + } + + // Delete totp-1 (should also remove associated webauthn credentials). + resp, err := client.DeleteMFASecret(ctx, &api.DeleteMFASecretReq{ + UserId: "user1", ConnectorId: "conn1", AuthenticatorId: "totp-1", + }) + if err != nil { + t.Fatalf("delete MFA secret: %v", err) + } + if resp.NotFound { + t.Error("expected authenticator to be found") + } + + identity, err := s.GetUserIdentity(ctx, "user1", "conn1") + if err != nil { + t.Fatalf("get user identity: %v", err) + } + if _, ok := identity.MFASecrets["totp-1"]; ok { + t.Error("expected totp-1 to be deleted") + } + if _, ok := identity.MFASecrets["totp-2"]; !ok { + t.Error("expected totp-2 to remain") + } + if _, ok := identity.WebAuthnCredentials["totp-1"]; ok { + t.Error("expected webauthn credentials for totp-1 to be deleted") + } + + // Not found case. + resp, err = client.DeleteMFASecret(ctx, &api.DeleteMFASecretReq{ + UserId: "user1", ConnectorId: "conn1", AuthenticatorId: "nonexistent", + }) + if err != nil { + t.Fatalf("delete MFA secret: %v", err) + } + if !resp.NotFound { + t.Error("expected not_found for nonexistent authenticator") + } +} + +func TestRevokeConsent(t *testing.T) { + t.Setenv("DEX_API_SESSIONS_IDENTITIES_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + now := time.Now().UTC().Round(time.Second) + if err := s.CreateUserIdentity(ctx, storage.UserIdentity{ + UserID: "user1", ConnectorID: "conn1", + Claims: storage.Claims{Email: "test@test.com"}, CreatedAt: now, LastLogin: now, + Consents: map[string][]string{ + "client-a": {"openid", "email"}, + "client-b": {"openid"}, + }, + }); err != nil { + t.Fatalf("create user identity: %v", err) + } + + // Revoke consent for client-a. + resp, err := client.RevokeConsent(ctx, &api.RevokeConsentReq{ + UserId: "user1", ConnectorId: "conn1", ClientId: "client-a", + }) + if err != nil { + t.Fatalf("revoke consent: %v", err) + } + if resp.NotFound { + t.Error("expected consent to be found") + } + + // Verify only client-b consent remains. + identity, err := s.GetUserIdentity(ctx, "user1", "conn1") + if err != nil { + t.Fatalf("get user identity: %v", err) + } + if _, ok := identity.Consents["client-a"]; ok { + t.Error("expected client-a consent to be revoked") + } + if _, ok := identity.Consents["client-b"]; !ok { + t.Error("expected client-b consent to remain") + } + + // Not found case. + resp, err = client.RevokeConsent(ctx, &api.RevokeConsentReq{ + UserId: "user1", ConnectorId: "conn1", ClientId: "nonexistent", + }) + if err != nil { + t.Fatalf("revoke consent: %v", err) + } + if !resp.NotFound { + t.Error("expected not_found for nonexistent consent") + } +} + +func TestMissingSessionsIdentitiesCRUDFeatureFlag(t *testing.T) { + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + if _, err := client.GetAuthSession(ctx, &api.GetAuthSessionReq{UserId: "u", ConnectorId: "c"}); err == nil { + t.Error("GetAuthSession should fail without feature flag") + } + if _, err := client.ListAuthSessions(ctx, &api.ListAuthSessionsReq{}); err == nil { + t.Error("ListAuthSessions should fail without feature flag") + } + if _, err := client.DeleteAuthSession(ctx, &api.DeleteAuthSessionReq{UserId: "u", ConnectorId: "c"}); err == nil { + t.Error("DeleteAuthSession should fail without feature flag") + } + if _, err := client.GetUserIdentity(ctx, &api.GetUserIdentityReq{UserId: "u", ConnectorId: "c"}); err == nil { + t.Error("GetUserIdentity should fail without feature flag") + } + if _, err := client.ListUserIdentities(ctx, &api.ListUserIdentitiesReq{}); err == nil { + t.Error("ListUserIdentities should fail without feature flag") + } + if _, err := client.DeleteUserIdentity(ctx, &api.DeleteUserIdentityReq{UserId: "u", ConnectorId: "c"}); err == nil { + t.Error("DeleteUserIdentity should fail without feature flag") + } + if _, err := client.ResetMFA(ctx, &api.ResetMFAReq{UserId: "u", ConnectorId: "c"}); err == nil { + t.Error("ResetMFA should fail without feature flag") + } + if _, err := client.ListMFADevices(ctx, &api.ListMFADevicesReq{UserId: "u", ConnectorId: "c"}); err == nil { + t.Error("ListMFADevices should fail without feature flag") + } + if _, err := client.RevokeConsent(ctx, &api.RevokeConsentReq{UserId: "u", ConnectorId: "c", ClientId: "cl"}); err == nil { + t.Error("RevokeConsent should fail without feature flag") + } +} diff --git a/server/logout.go b/server/logout.go index d48f7a59fe..3c440dba36 100644 --- a/server/logout.go +++ b/server/logout.go @@ -309,49 +309,6 @@ func (s *Server) deleteAuthSession(ctx context.Context, userID, connectorID stri // revokeRefreshTokens deletes all refresh tokens for the given user/connector // and clears the references in the offline session (but keeps the session object). // Returns the connector data from the offline session (for upstream logout). -// -// To avoid a race condition where a new token issued between deletion and the -// OfflineSessions update would have its reference wiped, we: -// 1. Snapshot the token IDs to revoke -// 2. Remove only those specific references from OfflineSessions (the updater -// sees the latest state, so concurrently added refs are preserved) -// 3. Delete the actual refresh tokens func (s *Server) revokeRefreshTokens(ctx context.Context, userID, connectorID string) []byte { - offlineSessions, err := s.storage.GetOfflineSessions(ctx, userID, connectorID) - if err != nil { - if !errors.Is(err, storage.ErrNotFound) { - s.logger.ErrorContext(ctx, "logout: failed to get offline sessions", "err", err) - } - return nil - } - - // Snapshot token IDs to revoke. - tokenIDs := make(map[string]struct{}, len(offlineSessions.Refresh)) - for _, ref := range offlineSessions.Refresh { - tokenIDs[ref.ID] = struct{}{} - } - - // Remove only the snapshotted references — any token added concurrently - // will not be in tokenIDs and will be left untouched. - if err := s.storage.UpdateOfflineSessions(ctx, userID, connectorID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) { - for clientID, ref := range old.Refresh { - if _, ok := tokenIDs[ref.ID]; ok { - delete(old.Refresh, clientID) - } - } - return old, nil - }); err != nil { - s.logger.ErrorContext(ctx, "logout: failed to update offline sessions", "err", err) - } - - // Delete the actual refresh tokens. - for id := range tokenIDs { - if err := s.storage.DeleteRefresh(ctx, id); err != nil { - if !errors.Is(err, storage.ErrNotFound) { - s.logger.ErrorContext(ctx, "logout: failed to delete refresh token", "token_id", id, "err", err) - } - } - } - - return offlineSessions.ConnectorData + return revokeRefreshTokensFromStorage(ctx, s.storage, s.logger, userID, connectorID) }