diff --git a/api/openapi/http.yml b/api/openapi/http.yml index f484c20d1e..f366458bdf 100644 --- a/api/openapi/http.yml +++ b/api/openapi/http.yml @@ -57,6 +57,7 @@ paths: summary: Retrieves service health check info. tags: - health + security: [] responses: "200": $ref: "#/components/responses/HealthRes" diff --git a/api/openapi/readers.yml b/api/openapi/readers.yml index e82de3fb1b..3ffdf7f8b1 100644 --- a/api/openapi/readers.yml +++ b/api/openapi/readers.yml @@ -75,6 +75,7 @@ paths: summary: Retrieves service health check info. tags: - health + security: [] responses: "200": $ref: "#/components/responses/HealthRes" diff --git a/api/openapi/things.yml b/api/openapi/things.yml index 8deae43c9b..ec7329bf34 100644 --- a/api/openapi/things.yml +++ b/api/openapi/things.yml @@ -401,6 +401,35 @@ paths: "500": $ref: "#/components/responses/ServiceError" + /things/verify-connections: + post: + operationId: verifyConnections + summary: Verify Connections + description: | + Verify the connections between a list of channels and things, and provide + a list showing which channels and things are connected and which are disconnected. + tags: + - Things + requestBody: + $ref: "#/components/requestBodies/VerifyConnectionsReq" + security: + - bearerAuth: [] + responses: + "200": + $ref: "#/components/responses/VerifyConnectionsPageRes" + "400": + description: Failed due to malformed JSON. + "401": + description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. + "500": + $ref: "#/components/responses/ServiceError" + /channels/{chanID}/things: get: operationId: listThingsInaChannel @@ -967,6 +996,7 @@ paths: summary: Retrieves service health check info. tags: - health + security: [] responses: "200": $ref: "#/components/responses/HealthRes" @@ -1066,6 +1096,35 @@ components: - user_ids - relation + VerifyReqObj: + type: object + properties: + things_id: + type: array + minItems: 0 + items: + type: string + description: things IDs + example: + [ + "bb7edb32-2eac-4aad-aebe-ed96fe073879", + "bb7edb32-2eac-4aad-aebe-ed96fe073879", + ] + channels_id: + type: array + minItems: 0 + items: + type: string + description: channels IDs + example: + [ + "cb7edb32-2eac-4aad-aebe-ed96fe073879", + "cb7edb32-2eac-4aad-aebe-ed96fe073879", + ] + required: + - things_id + - channels_id + AssignReqObj: type: object properties: @@ -1367,6 +1426,36 @@ components: - total - offset + VerifyConnectionsPage: + type: object + properties: + status: + type: string + example: all_connected + description: All things and channels are connected. + connection_status: + type: array + items: + type: object + properties: + things_id: + type: string + example: "bb7edb32-2eac-4aad-aebe-ed96fe073879" + channels_id: + type: string + example: "cb7edb32-2eac-4aad-aebe-ed96fe073879" + status: + type: string + example: "connected" + required: + - things_id + - channels_id + - status + description: Array of things and channels connection statuses + required: + - status + - connection_status + ChannelsPage: type: object properties: @@ -1738,6 +1827,14 @@ components: schema: $ref: "#/components/schemas/PolicyReqObj" + VerifyConnectionsReq: + description: JSON-formated document listing connected and disconnected things and channels. + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/VerifyReqObj" + AssignReq: description: JSON-formated document describing the policy related to assigning members to a channel required: true @@ -1889,6 +1986,13 @@ components: schema: $ref: "#/components/schemas/ThingsPage" + VerifyConnectionsPageRes: + description: Verified connection between channels and things. + content: + application/json: + schema: + $ref: "#/components/schemas/VerifyConnectionsPage" + ChannelCreateRes: description: Registered new channel. headers: diff --git a/api/openapi/users.yml b/api/openapi/users.yml index d2407ef964..5665ce48e1 100644 --- a/api/openapi/users.yml +++ b/api/openapi/users.yml @@ -397,12 +397,13 @@ paths: "500": $ref: "#/components/responses/ServiceError" - /users/search: + /users/search: get: operationId: searchUsers summary: Search users description: | - Search users by name and identity. + Search users by name and ID when logged in as a normal user. + As a superadmin, search by name, identity, and ID. tags: - Users parameters: @@ -1062,6 +1063,7 @@ paths: summary: Retrieves service health check info. tags: - health + security: [] responses: "200": $ref: "#/components/responses/HealthRes" diff --git a/auth.pb.go b/auth.pb.go index 6c072e2033..53b38e30c8 100644 --- a/auth.pb.go +++ b/auth.pb.go @@ -1984,6 +1984,179 @@ func (x *DeleteEntityPoliciesReq) GetId() string { return "" } +type VerifyConnectionsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ThingIds []string `protobuf:"bytes,1,rep,name=thing_ids,json=thingIds,proto3" json:"thing_ids,omitempty"` + ChannelIds []string `protobuf:"bytes,2,rep,name=channel_ids,json=channelIds,proto3" json:"channel_ids,omitempty"` +} + +func (x *VerifyConnectionsReq) Reset() { + *x = VerifyConnectionsReq{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyConnectionsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyConnectionsReq) ProtoMessage() {} + +func (x *VerifyConnectionsReq) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_msgTypes[26] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyConnectionsReq.ProtoReflect.Descriptor instead. +func (*VerifyConnectionsReq) Descriptor() ([]byte, []int) { + return file_auth_proto_rawDescGZIP(), []int{26} +} + +func (x *VerifyConnectionsReq) GetThingIds() []string { + if x != nil { + return x.ThingIds + } + return nil +} + +func (x *VerifyConnectionsReq) GetChannelIds() []string { + if x != nil { + return x.ChannelIds + } + return nil +} + +type VerifyConnectionsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + ConnectionsStatus []*Connectionstatus `protobuf:"bytes,2,rep,name=connections_status,json=connectionsStatus,proto3" json:"connections_status,omitempty"` +} + +func (x *VerifyConnectionsRes) Reset() { + *x = VerifyConnectionsRes{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyConnectionsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyConnectionsRes) ProtoMessage() {} + +func (x *VerifyConnectionsRes) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_msgTypes[27] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyConnectionsRes.ProtoReflect.Descriptor instead. +func (*VerifyConnectionsRes) Descriptor() ([]byte, []int) { + return file_auth_proto_rawDescGZIP(), []int{27} +} + +func (x *VerifyConnectionsRes) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *VerifyConnectionsRes) GetConnectionsStatus() []*Connectionstatus { + if x != nil { + return x.ConnectionsStatus + } + return nil +} + +type Connectionstatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ThingId string `protobuf:"bytes,1,opt,name=thing_id,json=thingId,proto3" json:"thing_id,omitempty"` + ChannelId string `protobuf:"bytes,2,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *Connectionstatus) Reset() { + *x = Connectionstatus{} + if protoimpl.UnsafeEnabled { + mi := &file_auth_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Connectionstatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Connectionstatus) ProtoMessage() {} + +func (x *Connectionstatus) ProtoReflect() protoreflect.Message { + mi := &file_auth_proto_msgTypes[28] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Connectionstatus.ProtoReflect.Descriptor instead. +func (*Connectionstatus) Descriptor() ([]byte, []int) { + return file_auth_proto_rawDescGZIP(), []int{28} +} + +func (x *Connectionstatus) GetThingId() string { + if x != nil { + return x.ThingId + } + return "" +} + +func (x *Connectionstatus) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +func (x *Connectionstatus) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + var File_auth_proto protoreflect.FileDescriptor var file_auth_proto_rawDesc = []byte{ @@ -2251,89 +2424,114 @@ var file_auth_proto_rawDesc = []byte{ 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x32, 0x51, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x7a, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, - 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x18, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x54, 0x0a, 0x14, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, + 0x1b, 0x0a, 0x09, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x0b, + 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x73, 0x22, 0x7b, 0x0a, + 0x14, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4b, 0x0a, + 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x64, 0x0a, 0x10, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, + 0x0a, 0x08, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x32, 0xac, 0x01, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x41, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x73, 0x22, 0x00, 0x32, 0xac, 0x09, 0x0a, 0x0b, 0x41, - 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x49, 0x73, - 0x73, 0x75, 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, - 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x00, 0x12, 0x36, - 0x0a, 0x07, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x16, 0x2e, 0x6d, 0x61, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, - 0x71, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x08, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x79, 0x12, 0x17, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, - 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x6d, 0x61, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x12, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, - 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x09, 0x41, 0x64, 0x64, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, - 0x1a, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x64, - 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x0b, - 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x1a, 0x2e, 0x6d, 0x61, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x52, + 0x65, 0x73, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x20, 0x2e, 0x6d, 0x61, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x20, 0x2e, 0x6d, 0x61, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x32, + 0xac, 0x09, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x32, 0x0a, 0x05, 0x49, 0x73, 0x73, 0x75, 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x11, + 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x07, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x16, + 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x6c, 0x61, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x08, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x12, 0x17, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, + 0x1a, 0x17, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x09, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x52, + 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x41, + 0x0a, 0x09, 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x21, 0x2e, 0x6d, 0x61, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x1b, - 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4e, 0x0a, - 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, - 0x1d, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1b, - 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x47, 0x0a, - 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x6d, - 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, - 0x6c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, - 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x63, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x6c, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x22, + 0x00, 0x12, 0x47, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, + 0x12, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x64, + 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x6d, + 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x12, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x12, 0x21, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, + 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x69, 0x65, 0x73, 0x12, 0x1d, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, + 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, + 0x22, 0x00, 0x12, 0x47, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x12, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, + 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0e, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1a, 0x2e, + 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0c, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, + 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x1a, 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, + 0x4d, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, - 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4a, - 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1b, - 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6d, 0x61, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x0f, 0x4c, 0x69, - 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1b, 0x2e, - 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6d, 0x61, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x0d, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, - 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x2e, 0x6d, 0x61, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1e, 0x2e, 0x6d, 0x61, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x5a, 0x0a, - 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, - 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1b, 0x2e, 0x6d, 0x61, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2f, 0x6d, - 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x4c, 0x69, 0x73, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, + 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4d, + 0x0a, 0x0d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, + 0x1c, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1c, 0x2e, + 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x53, 0x0a, + 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x1e, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, + 0x1a, 0x1e, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, + 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x6d, 0x61, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, + 0x1b, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x42, 0x0e, + 0x5a, 0x0c, 0x2e, 0x2f, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2348,7 +2546,7 @@ func file_auth_proto_rawDescGZIP() []byte { return file_auth_proto_rawDescData } -var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 29) var file_auth_proto_goTypes = []any{ (*Token)(nil), // 0: magistrala.Token (*IdentityReq)(nil), // 1: magistrala.IdentityReq @@ -2376,49 +2574,55 @@ var file_auth_proto_goTypes = []any{ (*ListPermissionsReq)(nil), // 23: magistrala.ListPermissionsReq (*ListPermissionsRes)(nil), // 24: magistrala.ListPermissionsRes (*DeleteEntityPoliciesReq)(nil), // 25: magistrala.DeleteEntityPoliciesReq + (*VerifyConnectionsReq)(nil), // 26: magistrala.VerifyConnectionsReq + (*VerifyConnectionsRes)(nil), // 27: magistrala.VerifyConnectionsRes + (*Connectionstatus)(nil), // 28: magistrala.Connectionstatus } var file_auth_proto_depIdxs = []int32{ 7, // 0: magistrala.AddPoliciesReq.addPoliciesReq:type_name -> magistrala.AddPolicyReq 13, // 1: magistrala.DeletePoliciesReq.deletePoliciesReq:type_name -> magistrala.DeletePolicyReq - 5, // 2: magistrala.AuthzService.Authorize:input_type -> magistrala.AuthorizeReq - 3, // 3: magistrala.AuthService.Issue:input_type -> magistrala.IssueReq - 4, // 4: magistrala.AuthService.Refresh:input_type -> magistrala.RefreshReq - 1, // 5: magistrala.AuthService.Identify:input_type -> magistrala.IdentityReq - 5, // 6: magistrala.AuthService.Authorize:input_type -> magistrala.AuthorizeReq - 7, // 7: magistrala.AuthService.AddPolicy:input_type -> magistrala.AddPolicyReq - 8, // 8: magistrala.AuthService.AddPolicies:input_type -> magistrala.AddPoliciesReq - 11, // 9: magistrala.AuthService.DeletePolicyFilter:input_type -> magistrala.DeletePolicyFilterReq - 12, // 10: magistrala.AuthService.DeletePolicies:input_type -> magistrala.DeletePoliciesReq - 15, // 11: magistrala.AuthService.ListObjects:input_type -> magistrala.ListObjectsReq - 15, // 12: magistrala.AuthService.ListAllObjects:input_type -> magistrala.ListObjectsReq - 17, // 13: magistrala.AuthService.CountObjects:input_type -> magistrala.CountObjectsReq - 19, // 14: magistrala.AuthService.ListSubjects:input_type -> magistrala.ListSubjectsReq - 19, // 15: magistrala.AuthService.ListAllSubjects:input_type -> magistrala.ListSubjectsReq - 21, // 16: magistrala.AuthService.CountSubjects:input_type -> magistrala.CountSubjectsReq - 23, // 17: magistrala.AuthService.ListPermissions:input_type -> magistrala.ListPermissionsReq - 25, // 18: magistrala.AuthService.DeleteEntityPolicies:input_type -> magistrala.DeleteEntityPoliciesReq - 6, // 19: magistrala.AuthzService.Authorize:output_type -> magistrala.AuthorizeRes - 0, // 20: magistrala.AuthService.Issue:output_type -> magistrala.Token - 0, // 21: magistrala.AuthService.Refresh:output_type -> magistrala.Token - 2, // 22: magistrala.AuthService.Identify:output_type -> magistrala.IdentityRes - 6, // 23: magistrala.AuthService.Authorize:output_type -> magistrala.AuthorizeRes - 9, // 24: magistrala.AuthService.AddPolicy:output_type -> magistrala.AddPolicyRes - 10, // 25: magistrala.AuthService.AddPolicies:output_type -> magistrala.AddPoliciesRes - 14, // 26: magistrala.AuthService.DeletePolicyFilter:output_type -> magistrala.DeletePolicyRes - 14, // 27: magistrala.AuthService.DeletePolicies:output_type -> magistrala.DeletePolicyRes - 16, // 28: magistrala.AuthService.ListObjects:output_type -> magistrala.ListObjectsRes - 16, // 29: magistrala.AuthService.ListAllObjects:output_type -> magistrala.ListObjectsRes - 18, // 30: magistrala.AuthService.CountObjects:output_type -> magistrala.CountObjectsRes - 20, // 31: magistrala.AuthService.ListSubjects:output_type -> magistrala.ListSubjectsRes - 20, // 32: magistrala.AuthService.ListAllSubjects:output_type -> magistrala.ListSubjectsRes - 22, // 33: magistrala.AuthService.CountSubjects:output_type -> magistrala.CountSubjectsRes - 24, // 34: magistrala.AuthService.ListPermissions:output_type -> magistrala.ListPermissionsRes - 14, // 35: magistrala.AuthService.DeleteEntityPolicies:output_type -> magistrala.DeletePolicyRes - 19, // [19:36] is the sub-list for method output_type - 2, // [2:19] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 28, // 2: magistrala.VerifyConnectionsRes.connections_status:type_name -> magistrala.Connectionstatus + 5, // 3: magistrala.AuthzService.Authorize:input_type -> magistrala.AuthorizeReq + 26, // 4: magistrala.AuthzService.VerifyConnections:input_type -> magistrala.VerifyConnectionsReq + 3, // 5: magistrala.AuthService.Issue:input_type -> magistrala.IssueReq + 4, // 6: magistrala.AuthService.Refresh:input_type -> magistrala.RefreshReq + 1, // 7: magistrala.AuthService.Identify:input_type -> magistrala.IdentityReq + 5, // 8: magistrala.AuthService.Authorize:input_type -> magistrala.AuthorizeReq + 7, // 9: magistrala.AuthService.AddPolicy:input_type -> magistrala.AddPolicyReq + 8, // 10: magistrala.AuthService.AddPolicies:input_type -> magistrala.AddPoliciesReq + 11, // 11: magistrala.AuthService.DeletePolicyFilter:input_type -> magistrala.DeletePolicyFilterReq + 12, // 12: magistrala.AuthService.DeletePolicies:input_type -> magistrala.DeletePoliciesReq + 15, // 13: magistrala.AuthService.ListObjects:input_type -> magistrala.ListObjectsReq + 15, // 14: magistrala.AuthService.ListAllObjects:input_type -> magistrala.ListObjectsReq + 17, // 15: magistrala.AuthService.CountObjects:input_type -> magistrala.CountObjectsReq + 19, // 16: magistrala.AuthService.ListSubjects:input_type -> magistrala.ListSubjectsReq + 19, // 17: magistrala.AuthService.ListAllSubjects:input_type -> magistrala.ListSubjectsReq + 21, // 18: magistrala.AuthService.CountSubjects:input_type -> magistrala.CountSubjectsReq + 23, // 19: magistrala.AuthService.ListPermissions:input_type -> magistrala.ListPermissionsReq + 25, // 20: magistrala.AuthService.DeleteEntityPolicies:input_type -> magistrala.DeleteEntityPoliciesReq + 6, // 21: magistrala.AuthzService.Authorize:output_type -> magistrala.AuthorizeRes + 27, // 22: magistrala.AuthzService.VerifyConnections:output_type -> magistrala.VerifyConnectionsRes + 0, // 23: magistrala.AuthService.Issue:output_type -> magistrala.Token + 0, // 24: magistrala.AuthService.Refresh:output_type -> magistrala.Token + 2, // 25: magistrala.AuthService.Identify:output_type -> magistrala.IdentityRes + 6, // 26: magistrala.AuthService.Authorize:output_type -> magistrala.AuthorizeRes + 9, // 27: magistrala.AuthService.AddPolicy:output_type -> magistrala.AddPolicyRes + 10, // 28: magistrala.AuthService.AddPolicies:output_type -> magistrala.AddPoliciesRes + 14, // 29: magistrala.AuthService.DeletePolicyFilter:output_type -> magistrala.DeletePolicyRes + 14, // 30: magistrala.AuthService.DeletePolicies:output_type -> magistrala.DeletePolicyRes + 16, // 31: magistrala.AuthService.ListObjects:output_type -> magistrala.ListObjectsRes + 16, // 32: magistrala.AuthService.ListAllObjects:output_type -> magistrala.ListObjectsRes + 18, // 33: magistrala.AuthService.CountObjects:output_type -> magistrala.CountObjectsRes + 20, // 34: magistrala.AuthService.ListSubjects:output_type -> magistrala.ListSubjectsRes + 20, // 35: magistrala.AuthService.ListAllSubjects:output_type -> magistrala.ListSubjectsRes + 22, // 36: magistrala.AuthService.CountSubjects:output_type -> magistrala.CountSubjectsRes + 24, // 37: magistrala.AuthService.ListPermissions:output_type -> magistrala.ListPermissionsRes + 14, // 38: magistrala.AuthService.DeleteEntityPolicies:output_type -> magistrala.DeletePolicyRes + 21, // [21:39] is the sub-list for method output_type + 3, // [3:21] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_auth_proto_init() } @@ -2739,6 +2943,42 @@ func file_auth_proto_init() { return nil } } + file_auth_proto_msgTypes[26].Exporter = func(v any, i int) any { + switch v := v.(*VerifyConnectionsReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_msgTypes[27].Exporter = func(v any, i int) any { + switch v := v.(*VerifyConnectionsRes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_auth_proto_msgTypes[28].Exporter = func(v any, i int) any { + switch v := v.(*Connectionstatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_auth_proto_msgTypes[0].OneofWrappers = []any{} file_auth_proto_msgTypes[3].OneofWrappers = []any{} @@ -2749,7 +2989,7 @@ func file_auth_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_auth_proto_rawDesc, NumEnums: 0, - NumMessages: 26, + NumMessages: 29, NumExtensions: 0, NumServices: 2, }, diff --git a/auth.proto b/auth.proto index 9f937cba8e..73afc17f72 100644 --- a/auth.proto +++ b/auth.proto @@ -12,6 +12,7 @@ service AuthzService { // Authorize checks if the subject is authorized to perform // the action on the object. rpc Authorize(AuthorizeReq) returns (AuthorizeRes) {} + rpc VerifyConnections(VerifyConnectionsReq) returns (VerifyConnectionsRes) {} } // AuthService is a service that provides authentication and authorization @@ -223,3 +224,19 @@ message DeleteEntityPoliciesReq{ string entity_type = 1; string id = 2; } + +message VerifyConnectionsReq { + repeated string thing_ids = 1; + repeated string channel_ids = 2; +} + +message VerifyConnectionsRes { + string status = 1; + repeated Connectionstatus connections_status = 2; +} + +message Connectionstatus { + string thing_id = 1; + string channel_id = 2; + string status = 3; +} diff --git a/auth_grpc.pb.go b/auth_grpc.pb.go index c6350d3a48..eeab937ca0 100644 --- a/auth_grpc.pb.go +++ b/auth_grpc.pb.go @@ -22,7 +22,8 @@ import ( const _ = grpc.SupportPackageIsVersion8 const ( - AuthzService_Authorize_FullMethodName = "/magistrala.AuthzService/Authorize" + AuthzService_Authorize_FullMethodName = "/magistrala.AuthzService/Authorize" + AuthzService_VerifyConnections_FullMethodName = "/magistrala.AuthzService/VerifyConnections" ) // AuthzServiceClient is the client API for AuthzService service. @@ -35,6 +36,7 @@ type AuthzServiceClient interface { // Authorize checks if the subject is authorized to perform // the action on the object. Authorize(ctx context.Context, in *AuthorizeReq, opts ...grpc.CallOption) (*AuthorizeRes, error) + VerifyConnections(ctx context.Context, in *VerifyConnectionsReq, opts ...grpc.CallOption) (*VerifyConnectionsRes, error) } type authzServiceClient struct { @@ -55,6 +57,16 @@ func (c *authzServiceClient) Authorize(ctx context.Context, in *AuthorizeReq, op return out, nil } +func (c *authzServiceClient) VerifyConnections(ctx context.Context, in *VerifyConnectionsReq, opts ...grpc.CallOption) (*VerifyConnectionsRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VerifyConnectionsRes) + err := c.cc.Invoke(ctx, AuthzService_VerifyConnections_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AuthzServiceServer is the server API for AuthzService service. // All implementations must embed UnimplementedAuthzServiceServer // for forward compatibility @@ -65,6 +77,7 @@ type AuthzServiceServer interface { // Authorize checks if the subject is authorized to perform // the action on the object. Authorize(context.Context, *AuthorizeReq) (*AuthorizeRes, error) + VerifyConnections(context.Context, *VerifyConnectionsReq) (*VerifyConnectionsRes, error) mustEmbedUnimplementedAuthzServiceServer() } @@ -75,6 +88,9 @@ type UnimplementedAuthzServiceServer struct { func (UnimplementedAuthzServiceServer) Authorize(context.Context, *AuthorizeReq) (*AuthorizeRes, error) { return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented") } +func (UnimplementedAuthzServiceServer) VerifyConnections(context.Context, *VerifyConnectionsReq) (*VerifyConnectionsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method VerifyConnections not implemented") +} func (UnimplementedAuthzServiceServer) mustEmbedUnimplementedAuthzServiceServer() {} // UnsafeAuthzServiceServer may be embedded to opt out of forward compatibility for this service. @@ -106,6 +122,24 @@ func _AuthzService_Authorize_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _AuthzService_VerifyConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VerifyConnectionsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthzServiceServer).VerifyConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthzService_VerifyConnections_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthzServiceServer).VerifyConnections(ctx, req.(*VerifyConnectionsReq)) + } + return interceptor(ctx, in, info, handler) +} + // AuthzService_ServiceDesc is the grpc.ServiceDesc for AuthzService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -117,6 +151,10 @@ var AuthzService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Authorize", Handler: _AuthzService_Authorize_Handler, }, + { + MethodName: "VerifyConnections", + Handler: _AuthzService_VerifyConnections_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "auth.proto", diff --git a/bootstrap/events/producer/streams_test.go b/bootstrap/events/producer/streams_test.go index 759c689a2d..4ae6a2f298 100644 --- a/bootstrap/events/producer/streams_test.go +++ b/bootstrap/events/producer/streams_test.go @@ -24,6 +24,7 @@ import ( mgsdk "github.com/absmach/magistrala/pkg/sdk/go" sdkmocks "github.com/absmach/magistrala/pkg/sdk/mocks" "github.com/absmach/magistrala/pkg/uuid" + tauthmocks "github.com/absmach/magistrala/things/mocks" "github.com/go-redis/redis/v8" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -84,24 +85,25 @@ var ( } ) -func newService(t *testing.T, url string) (bootstrap.Service, *mocks.ConfigRepository, *authmocks.AuthClient, *sdkmocks.SDK) { +func newService(t *testing.T, url string) (bootstrap.Service, *mocks.ConfigRepository, *authmocks.AuthClient, *tauthmocks.AuthzClient, *sdkmocks.SDK) { boot := new(mocks.ConfigRepository) auth := new(authmocks.AuthClient) + tauth := new(tauthmocks.AuthzClient) sdk := new(sdkmocks.SDK) idp := uuid.NewMock() - svc := bootstrap.New(auth, boot, sdk, encKey, idp) + svc := bootstrap.New(auth, tauth, boot, sdk, encKey, idp) publisher, err := store.NewPublisher(context.Background(), url, streamID) require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) svc = producer.NewEventStoreMiddleware(svc, publisher) - return svc, boot, auth, sdk + return svc, boot, auth, tauth, sdk } func TestAdd(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, auth, sdk := newService(t, redisURL) + svc, boot, auth, tAuth, sdk := newService(t, redisURL) var channels []string for _, ch := range config.Channels { @@ -113,29 +115,32 @@ func TestAdd(t *testing.T) { invalidConfig.Channels = []bootstrap.Channel{{ID: "empty"}} cases := []struct { - desc string - config bootstrap.Config - token string - id string - domainID string - authResponse *magistrala.AuthorizeRes - authorizeErr error - identifyErr error - thingErr error - channel []bootstrap.Channel - listErr error - saveErr error - err error - event map[string]interface{} + desc string + config bootstrap.Config + token string + id string + domainID string + authResponse *magistrala.AuthorizeRes + authorizeErr error + identifyErr error + thingErr error + channel []bootstrap.Channel + listErr error + saveErr error + err error + event map[string]interface{} + verifyResponse *magistrala.VerifyConnectionsRes + verifyErr error }{ { - desc: "create config successfully", - config: config, - token: validToken, - id: validID, - domainID: domainID, - channel: config.Channels, - authResponse: &magistrala.AuthorizeRes{Authorized: true}, + desc: "create config successfully", + config: config, + token: validToken, + id: validID, + domainID: domainID, + channel: config.Channels, + authResponse: &magistrala.AuthorizeRes{Authorized: true}, + verifyResponse: &magistrala.VerifyConnectionsRes{Status: "all_connected"}, event: map[string]interface{}{ "thing_id": "1", "domain_id": domainID, @@ -208,6 +213,7 @@ func TestAdd(t *testing.T) { authCall1 := auth.On("Authorize", context.Background(), mock.Anything).Return(tc.authResponse, tc.authorizeErr) sdkCall := sdk.On("Thing", tc.config.ThingID, tc.token).Return(mgsdk.Thing{ID: tc.config.ThingID, Credentials: mgsdk.Credentials{Secret: tc.config.ThingKey}}, errors.NewSDKError(tc.thingErr)) repoCall := boot.On("ListExisting", context.Background(), domainID, mock.Anything).Return(tc.config.Channels, tc.listErr) + authCall2 := tAuth.On("VerifyConnections", mock.Anything, mock.Anything).Return(tc.verifyResponse, tc.verifyErr) repoCall1 := boot.On("Save", context.Background(), mock.Anything, mock.Anything).Return(mock.Anything, tc.saveErr) _, err := svc.Add(context.Background(), tc.token, tc.config) @@ -231,6 +237,7 @@ func TestAdd(t *testing.T) { authCall1.Unset() sdkCall.Unset() repoCall.Unset() + authCall2.Unset() repoCall1.Unset() } } @@ -239,7 +246,7 @@ func TestView(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, auth, _ := newService(t, redisURL) + svc, boot, auth, _, _ := newService(t, redisURL) nonExisting := config nonExisting.ThingID = unknownThingID @@ -342,7 +349,7 @@ func TestUpdate(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, auth, _ := newService(t, redisURL) + svc, boot, auth, _, _ := newService(t, redisURL) c := config @@ -462,7 +469,7 @@ func TestUpdateConnections(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, auth, sdk := newService(t, redisURL) + svc, boot, auth, _, sdk := newService(t, redisURL) cases := []struct { desc string @@ -605,7 +612,7 @@ func TestUpdateCert(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, auth, _ := newService(t, redisURL) + svc, boot, auth, _, _ := newService(t, redisURL) cases := []struct { desc string @@ -772,7 +779,7 @@ func TestUpdateCert(t *testing.T) { } func TestList(t *testing.T) { - svc, boot, auth, _ := newService(t, redisURL) + svc, boot, auth, _, _ := newService(t, redisURL) numThings := 101 var c bootstrap.Config saved := make([]bootstrap.Config, 0) @@ -1050,7 +1057,7 @@ func TestRemove(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, auth, _ := newService(t, redisURL) + svc, boot, auth, _, _ := newService(t, redisURL) nonExisting := config nonExisting.ThingID = unknownThingID @@ -1145,7 +1152,7 @@ func TestBootstrap(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, _, _ := newService(t, redisURL) + svc, boot, _, _, _ := newService(t, redisURL) cases := []struct { desc string @@ -1208,7 +1215,7 @@ func TestChangeState(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, auth, sdk := newService(t, redisURL) + svc, boot, auth, _, sdk := newService(t, redisURL) cases := []struct { desc string @@ -1319,7 +1326,7 @@ func TestUpdateChannelHandler(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, _, _ := newService(t, redisURL) + svc, boot, _, _, _ := newService(t, redisURL) cases := []struct { desc string @@ -1401,7 +1408,7 @@ func TestRemoveChannelHandler(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, _, _ := newService(t, redisURL) + svc, boot, _, _, _ := newService(t, redisURL) cases := []struct { desc string @@ -1463,7 +1470,7 @@ func TestRemoveConfigHandler(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, _, _ := newService(t, redisURL) + svc, boot, _, _, _ := newService(t, redisURL) cases := []struct { desc string @@ -1525,7 +1532,7 @@ func TestConnectThingHandler(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, _, _ := newService(t, redisURL) + svc, boot, _, _, _ := newService(t, redisURL) cases := []struct { desc string @@ -1599,7 +1606,7 @@ func TestDisconnectThingHandler(t *testing.T) { err := redisClient.FlushAll(context.Background()).Err() assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - svc, boot, _, _ := newService(t, redisURL) + svc, boot, _, _, _ := newService(t, redisURL) cases := []struct { desc string diff --git a/bootstrap/service.go b/bootstrap/service.go index e3a713625d..bdad3c1a2a 100644 --- a/bootstrap/service.go +++ b/bootstrap/service.go @@ -12,6 +12,7 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" svcerr "github.com/absmach/magistrala/pkg/errors/service" @@ -120,6 +121,7 @@ type ConfigReader interface { type bootstrapService struct { auth magistrala.AuthServiceClient + tauth magistrala.AuthzServiceClient configs ConfigRepository sdk mgsdk.SDK encKey []byte @@ -127,11 +129,12 @@ type bootstrapService struct { } // New returns new Bootstrap service. -func New(uauth magistrala.AuthServiceClient, configs ConfigRepository, sdk mgsdk.SDK, encKey []byte, idp magistrala.IDProvider) Service { +func New(uauth magistrala.AuthServiceClient, tauth magistrala.AuthzServiceClient, configs ConfigRepository, sdk mgsdk.SDK, encKey []byte, idp magistrala.IDProvider) Service { return &bootstrapService{ configs: configs, sdk: sdk, auth: uauth, + tauth: tauth, encKey: encKey, idProvider: idp, } @@ -171,9 +174,21 @@ func (bs bootstrapService) Add(ctx context.Context, token string, cfg Config) (C } } + resp, err := bs.tauth.VerifyConnections(ctx, &magistrala.VerifyConnectionsReq{ + ThingIds: []string{cfg.ThingID}, + ChannelIds: bs.toIDList(cfg.Channels), + }) + if err != nil { + return Config{}, err + } + State := Inactive + if resp.Status == clients.AllConnectedState.String() { + State = Active + } + cfg.ThingID = mgThing.ID cfg.DomainID = user.GetDomainId() - cfg.State = Inactive + cfg.State = State cfg.ThingKey = mgThing.Credentials.Secret saved, err := bs.configs.Save(ctx, cfg, toConnect) diff --git a/bootstrap/service_test.go b/bootstrap/service_test.go index 63960a841f..2986d2d453 100644 --- a/bootstrap/service_test.go +++ b/bootstrap/service_test.go @@ -20,11 +20,13 @@ import ( "github.com/absmach/magistrala/bootstrap" "github.com/absmach/magistrala/bootstrap/mocks" "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" mgsdk "github.com/absmach/magistrala/pkg/sdk/go" sdkmocks "github.com/absmach/magistrala/pkg/sdk/mocks" "github.com/absmach/magistrala/pkg/uuid" + tauthmocks "github.com/absmach/magistrala/things/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -38,6 +40,8 @@ const ( channelsNum = 3 instanceID = "5de9b29a-feb9-11ed-be56-0242ac120002" validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22" + allConn = "all_connected" + allDisConn = "all_disconnected" ) var ( @@ -59,13 +63,14 @@ var ( } ) -func newService() (bootstrap.Service, *mocks.ConfigRepository, *authmocks.AuthClient, *sdkmocks.SDK) { +func newService() (bootstrap.Service, *mocks.ConfigRepository, *authmocks.AuthClient, *tauthmocks.AuthzClient, *sdkmocks.SDK) { boot := new(mocks.ConfigRepository) auth := new(authmocks.AuthClient) + tauth := new(tauthmocks.AuthzClient) sdk := new(sdkmocks.SDK) idp := uuid.NewMock() - return bootstrap.New(auth, boot, sdk, encKey, idp), boot, auth, sdk + return bootstrap.New(auth, tauth, boot, sdk, encKey, idp), boot, auth, tauth, sdk } func enc(in []byte) ([]byte, error) { @@ -84,7 +89,7 @@ func enc(in []byte) ([]byte, error) { } func TestAdd(t *testing.T) { - c, boot, auth, sdk := newService() + c, boot, auth, tAuth, sdk := newService() neID := config neID.ThingID = "non-existent" @@ -100,6 +105,8 @@ func TestAdd(t *testing.T) { userID string domainID string authResponse *magistrala.AuthorizeRes + verifyResponse *magistrala.VerifyConnectionsRes + verifyErr error authorizeErr error identifyErr error thingErr error @@ -111,13 +118,39 @@ func TestAdd(t *testing.T) { err error }{ { - desc: "add a new config", + desc: "add a new config with active state", config: config, token: validToken, userID: validID, domainID: domainID, authResponse: &magistrala.AuthorizeRes{Authorized: true}, - err: nil, + verifyResponse: &magistrala.VerifyConnectionsRes{ + Status: allConn, + }, + err: nil, + }, + { + desc: "add a new config with inactive state", + config: config, + token: validToken, + userID: validID, + domainID: domainID, + authResponse: &magistrala.AuthorizeRes{Authorized: true}, + verifyResponse: &magistrala.VerifyConnectionsRes{ + Status: allDisConn, + }, + err: nil, + }, + { + desc: "add a new config with failed verify connections", + config: config, + token: validToken, + userID: validID, + domainID: domainID, + authResponse: &magistrala.AuthorizeRes{Authorized: true}, + verifyResponse: &magistrala.VerifyConnectionsRes{}, + verifyErr: svcerr.ErrNotFound, + err: svcerr.ErrNotFound, }, { desc: "add a config with an invalid ID", @@ -154,12 +187,14 @@ func TestAdd(t *testing.T) { err: svcerr.ErrMalformedEntity, }, { - desc: "add empty config", - config: bootstrap.Config{}, - token: validToken, - userID: validID, - domainID: domainID, - authResponse: &magistrala.AuthorizeRes{Authorized: true}, + desc: "add empty config", + config: bootstrap.Config{}, + token: validToken, + userID: validID, + domainID: domainID, + authResponse: &magistrala.AuthorizeRes{Authorized: true}, + listExistingErr: apiutil.ErrMissingID, + err: apiutil.ErrMissingID, }, { desc: "add a config without authorization", @@ -198,6 +233,7 @@ func TestAdd(t *testing.T) { repoCall1 := sdk.On("CreateThing", mock.Anything, tc.token).Return(mgsdk.Thing{}, tc.createThingErr) repoCall2 := sdk.On("DeleteThing", tc.config.ThingID, tc.token).Return(tc.deleteThingErr) repoCall3 := boot.On("ListExisting", context.Background(), tc.domainID, mock.Anything).Return(tc.config.Channels, tc.listExistingErr) + authCall2 := tAuth.On("VerifyConnections", mock.Anything, mock.Anything).Return(tc.verifyResponse, tc.verifyErr) repoCall4 := boot.On("Save", context.Background(), mock.Anything, mock.Anything).Return(mock.Anything, tc.saveErr) _, err := c.Add(context.Background(), tc.token, tc.config) @@ -209,12 +245,13 @@ func TestAdd(t *testing.T) { repoCall1.Unset() repoCall2.Unset() repoCall3.Unset() + authCall2.Unset() repoCall4.Unset() } } func TestView(t *testing.T) { - svc, boot, auth, _ := newService() + svc, boot, auth, _, _ := newService() cases := []struct { desc string @@ -308,7 +345,7 @@ func TestView(t *testing.T) { } func TestUpdate(t *testing.T) { - svc, boot, auth, _ := newService() + svc, boot, auth, _, _ := newService() c := config ch := channel @@ -395,7 +432,7 @@ func TestUpdate(t *testing.T) { } func TestUpdateCert(t *testing.T) { - svc, boot, auth, _ := newService() + svc, boot, auth, _, _ := newService() c := config ch := channel @@ -506,7 +543,7 @@ func TestUpdateCert(t *testing.T) { } func TestUpdateConnections(t *testing.T) { - svc, boot, auth, sdk := newService() + svc, boot, auth, _, sdk := newService() c := config c.State = bootstrap.Inactive @@ -620,7 +657,7 @@ func TestUpdateConnections(t *testing.T) { } func TestList(t *testing.T) { - svc, boot, auth, _ := newService() + svc, boot, auth, _, _ := newService() numThings := 101 var saved []bootstrap.Config for i := 0; i < numThings; i++ { @@ -981,7 +1018,7 @@ func TestList(t *testing.T) { } func TestRemove(t *testing.T) { - svc, boot, auth, _ := newService() + svc, boot, auth, _, _ := newService() c := config cases := []struct { desc string @@ -1064,7 +1101,7 @@ func TestRemove(t *testing.T) { } func TestBootstrap(t *testing.T) { - svc, boot, _, _ := newService() + svc, boot, _, _, _ := newService() c := config e, err := enc([]byte(c.ExternalKey)) assert.Nil(t, err, fmt.Sprintf("Encrypting external key expected to succeed: %s.\n", err)) @@ -1131,7 +1168,7 @@ func TestBootstrap(t *testing.T) { } func TestChangeState(t *testing.T) { - svc, boot, auth, sdk := newService() + svc, boot, auth, _, sdk := newService() c := config cases := []struct { @@ -1232,7 +1269,7 @@ func TestChangeState(t *testing.T) { } func TestUpdateChannelHandler(t *testing.T) { - svc, boot, _, _ := newService() + svc, boot, _, _, _ := newService() ch := bootstrap.Channel{ ID: channel.ID, Name: "new name", @@ -1265,7 +1302,7 @@ func TestUpdateChannelHandler(t *testing.T) { } func TestRemoveChannelHandler(t *testing.T) { - svc, boot, _, _ := newService() + svc, boot, _, _, _ := newService() cases := []struct { desc string @@ -1293,7 +1330,7 @@ func TestRemoveChannelHandler(t *testing.T) { } func TestRemoveConfigHandler(t *testing.T) { - svc, boot, _, _ := newService() + svc, boot, _, _, _ := newService() cases := []struct { desc string @@ -1321,7 +1358,7 @@ func TestRemoveConfigHandler(t *testing.T) { } func TestConnectThingsHandler(t *testing.T) { - svc, boot, _, _ := newService() + svc, boot, _, _, _ := newService() cases := []struct { desc string thingID string @@ -1351,7 +1388,7 @@ func TestConnectThingsHandler(t *testing.T) { } func TestDisconnectThingsHandler(t *testing.T) { - svc, boot, _, _ := newService() + svc, boot, _, _, _ := newService() cases := []struct { desc string thingID string diff --git a/cli/commands_test.go b/cli/commands_test.go index 068eee4ceb..803ca93e2b 100644 --- a/cli/commands_test.go +++ b/cli/commands_test.go @@ -34,6 +34,7 @@ const ( disconnCmd = "disconnect" shrCmd = "share" unshrCmd = "unshare" + verifyCmd = "verify" ) // Groups and channels commands diff --git a/cli/things.go b/cli/things.go index e37e6f24b3..bae8fe3aa9 100644 --- a/cli/things.go +++ b/cli/things.go @@ -317,6 +317,39 @@ var cmdThings = []cobra.Command{ logJSONCmd(*cmd, cl) }, }, + { + Use: "verify ", + Short: "Verify Thing-Channel Connections", + Long: "Check the connection status between specified things and channels\n" + + "Usage:\n" + + "\tmagistrala-cli verify ", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 3 { + logUsageCmd(*cmd, cmd.Use) + return + } + conns := mgxsdk.Connections{} + if err := json.Unmarshal([]byte(args[0]), &conns.ThingIDs); err != nil { + logErrorCmd(*cmd, err) + return + } + if err := json.Unmarshal([]byte(args[1]), &conns.ChannelIDs); err != nil { + logErrorCmd(*cmd, err) + return + } + pm := mgxsdk.PageMetadata{ + ThingIDs: conns.ThingIDs, + ChannelIDs: conns.ChannelIDs, + } + cp, err := sdk.VerifyConnections(pm, args[2]) + if err != nil { + logErrorCmd(*cmd, err) + return + } + + logJSONCmd(*cmd, cp) + }, + }, { Use: "users ", Short: "List users", @@ -346,9 +379,9 @@ var cmdThings = []cobra.Command{ // NewThingsCmd returns things command. func NewThingsCmd() *cobra.Command { cmd := cobra.Command{ - Use: "things [create | get | update | delete | share | connect | disconnect | connections | not-connected | users ]", + Use: "things [create | get | update | delete | share | connect | disconnect | connections | not-connected | users | verify]", Short: "Things management", - Long: `Things management: create, get, update, delete or share Thing, connect or disconnect Thing from Channel and get the list of Channels connected or disconnected from a Thing`, + Long: `Things management: create, get, update, delete or share Thing, connect or disconnect Thing from Channel, get the list of Channels connected to or disconnected from a Thing and verify connections between things and channels`, } for i := range cmdThings { diff --git a/cli/things_test.go b/cli/things_test.go index 2d56da61e5..6218297bc2 100644 --- a/cli/things_test.go +++ b/cli/things_test.go @@ -13,6 +13,7 @@ import ( "github.com/absmach/magistrala/cli" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/clients" mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" @@ -1047,6 +1048,111 @@ func TestListConnectionCmd(t *testing.T) { } } +func TestVerifyConnectionsCmd(t *testing.T) { + sdkMock := new(sdkmocks.SDK) + cli.SetSDK(sdkMock) + thingsCmd := cli.NewThingsCmd() + rootCmd := setFlags(thingsCmd) + + channels := fmt.Sprintf(`["%s"]`, channel.ID) + things := fmt.Sprintf(`["%s"]`, thing.ID) + + cp := mgsdk.ConnectionsPage{} + cases := []struct { + desc string + args []string + logType outputLog + page mgsdk.ConnectionsPage + sdkErr errors.SDKError + errLogMessage string + }{ + { + desc: "verify connections successfully", + args: []string{ + things, + channels, + token, + }, + logType: entityLog, + page: mgsdk.ConnectionsPage{ + Status: clients.AllConnectedState.String(), + Connections: []mgsdk.ConnectionStatus{ + { + ChannelID: channel.ID, + ThingID: thing.ID, + Status: clients.Connected.String(), + }, + }, + }, + }, + { + desc: "verify connections with invalid args", + args: []string{ + things, + channels, + token, + extraArg, + }, + logType: usageLog, + }, + { + desc: "verify connections with invalid token", + args: []string{ + things, + channels, + invalidToken, + }, + sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized), + errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)), + logType: errLog, + }, + { + desc: "verify connections with invalid things json", + args: []string{ + fmt.Sprintf(`["%s"`, thing.ID), + channels, + token, + }, + sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")), + errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")), + logType: errLog, + }, + { + desc: "verify connections with invalid channels json", + args: []string{ + things, + fmt.Sprintf(`["%s"`, channel.ID), + token, + }, + sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")), + errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")), + logType: errLog, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + sdkCall := sdkMock.On("VerifyConnections", mock.Anything, tc.args[2]).Return(tc.page, tc.sdkErr) + out := executeCommand(t, rootCmd, append([]string{verifyCmd}, tc.args...)...) + + switch tc.logType { + case entityLog: + err := json.Unmarshal([]byte(out), &cp) + if err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + assert.Equal(t, tc.page, cp, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, cp)) + case usageLog: + assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out)) + case errLog: + assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out)) + } + + sdkCall.Unset() + }) + } +} + func TestShareThingCmd(t *testing.T) { sdkMock := new(sdkmocks.SDK) cli.SetSDK(sdkMock) diff --git a/cmd/bootstrap/main.go b/cmd/bootstrap/main.go index 71a6163ba6..e9fd5fb3f9 100644 --- a/cmd/bootstrap/main.go +++ b/cmd/bootstrap/main.go @@ -43,6 +43,7 @@ const ( envPrefixDB = "MG_BOOTSTRAP_DB_" envPrefixHTTP = "MG_BOOTSTRAP_HTTP_" envPrefixAuth = "MG_AUTH_GRPC_" + envPrefixAuthz = "MG_THINGS_AUTH_GRPC_" defDB = "bootstrap" defSvcHTTPPort = "9013" @@ -116,6 +117,21 @@ func main() { defer authHandler.Close() logger.Info("Successfully connected to auth grpc server " + authHandler.Secure()) + tauthConfig := auth.Config{} + if err := env.ParseWithOptions(&tauthConfig, env.Options{Prefix: envPrefixAuthz}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) + exitCode = 1 + return + } + tauthClient, tauthHandler, err := auth.SetupAuthz(ctx, tauthConfig) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer tauthHandler.Close() + logger.Info("Successfully connected to things grpc server " + tauthHandler.Secure()) + tp, err := jaeger.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) if err != nil { logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err)) @@ -130,7 +146,7 @@ func main() { tracer := tp.Tracer(svcName) // Create new service - svc, err := newService(ctx, authClient, db, tracer, logger, cfg, dbConfig) + svc, err := newService(ctx, authClient, tauthClient, db, tracer, logger, cfg, dbConfig) if err != nil { logger.Error(fmt.Sprintf("failed to create %s service: %s", svcName, err)) exitCode = 1 @@ -171,7 +187,7 @@ func main() { } } -func newService(ctx context.Context, authClient magistrala.AuthServiceClient, db *sqlx.DB, tracer trace.Tracer, logger *slog.Logger, cfg config, dbConfig pgclient.Config) (bootstrap.Service, error) { +func newService(ctx context.Context, authClient magistrala.AuthServiceClient, tauthClient magistrala.AuthzServiceClient, db *sqlx.DB, tracer trace.Tracer, logger *slog.Logger, cfg config, dbConfig pgclient.Config) (bootstrap.Service, error) { database := postgres.NewDatabase(db, dbConfig, tracer) repoConfig := bootstrappg.NewConfigRepository(database, logger) @@ -183,7 +199,7 @@ func newService(ctx context.Context, authClient magistrala.AuthServiceClient, db sdk := mgsdk.NewSDK(config) idp := uuid.New() - svc := bootstrap.New(authClient, repoConfig, sdk, []byte(cfg.EncKey), idp) + svc := bootstrap.New(authClient, tauthClient, repoConfig, sdk, []byte(cfg.EncKey), idp) publisher, err := store.NewPublisher(ctx, cfg.ESURL, streamID) if err != nil { diff --git a/docker/addons/bootstrap/docker-compose.yml b/docker/addons/bootstrap/docker-compose.yml index 9792972712..7e60df958f 100644 --- a/docker/addons/bootstrap/docker-compose.yml +++ b/docker/addons/bootstrap/docker-compose.yml @@ -58,6 +58,11 @@ services: MG_AUTH_GRPC_CLIENT_CERT: ${MG_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} MG_AUTH_GRPC_CLIENT_KEY: ${MG_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key} MG_AUTH_GRPC_SERVER_CA_CERTS: ${MG_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + MG_THINGS_AUTH_GRPC_URL: ${MG_THINGS_AUTH_GRPC_URL} + MG_THINGS_AUTH_GRPC_TIMEOUT: ${MG_THINGS_AUTH_GRPC_TIMEOUT} + MG_THINGS_AUTH_GRPC_CLIENT_CERT: ${MG_THINGS_AUTH_GRPC_CLIENT_CERT:+/things-grpc-client.crt} + MG_THINGS_AUTH_GRPC_CLIENT_KEY: ${MG_THINGS_AUTH_GRPC_CLIENT_KEY:+/things-grpc-client.key} + MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS: ${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+/things-grpc-server-ca.crt} MG_THINGS_URL: ${MG_THINGS_URL} MG_JAEGER_URL: ${MG_JAEGER_URL} MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} diff --git a/http/api/endpoint_test.go b/http/api/endpoint_test.go index c1c48f44bf..ab001c079d 100644 --- a/http/api/endpoint_test.go +++ b/http/api/endpoint_test.go @@ -12,12 +12,12 @@ import ( "testing" "github.com/absmach/magistrala" - authmocks "github.com/absmach/magistrala/auth/mocks" server "github.com/absmach/magistrala/http" "github.com/absmach/magistrala/http/api" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" pubsub "github.com/absmach/magistrala/pkg/messaging/mocks" + thmocks "github.com/absmach/magistrala/things/mocks" "github.com/absmach/mproxy" mproxyhttp "github.com/absmach/mproxy/pkg/http" "github.com/absmach/mproxy/pkg/session" @@ -81,7 +81,7 @@ func (tr testRequest) make() (*http.Response, error) { } func TestPublish(t *testing.T) { - auth := new(authmocks.AuthClient) + tauth := new(thmocks.AuthzClient) chanID := "1" ctSenmlJSON := "application/senml+json" ctSenmlCBOR := "application/senml+cbor" @@ -91,7 +91,7 @@ func TestPublish(t *testing.T) { msg := `[{"n":"current","t":-1,"v":1.6}]` msgJSON := `{"field1":"val1","field2":"val2"}` msgCBOR := `81A3616E6763757272656E746174206176FB3FF999999999999A` - svc, pub := newService(auth) + svc, pub := newService(tauth) target := newTargetHTTPServer() defer target.Close() ts, err := newProxyHTPPServer(svc, target) @@ -99,8 +99,8 @@ func TestPublish(t *testing.T) { defer ts.Close() - auth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{Subject: thingKey, Object: chanID, Domain: "", SubjectType: "thing", Permission: "publish", ObjectType: "group"}).Return(&magistrala.AuthorizeRes{Authorized: true, Id: ""}, nil) - auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false, Id: ""}, nil) + tauth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{Subject: thingKey, Object: chanID, Domain: "", SubjectType: "thing", Permission: "publish", ObjectType: "group"}).Return(&magistrala.AuthorizeRes{Authorized: true, Id: ""}, nil) + tauth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false, Id: ""}, nil) cases := map[string]struct { chanID string diff --git a/internal/api/common.go b/internal/api/common.go index 2d7de5cbf3..e533a470c7 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -162,7 +162,9 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { errors.Contains(err, apiutil.ErrInvalidTimeFormat), errors.Contains(err, svcerr.ErrSearch), errors.Contains(err, apiutil.ErrEmptySearchQuery), - errors.Contains(err, apiutil.ErrLenSearchQuery): + errors.Contains(err, apiutil.ErrLenSearchQuery), + errors.Contains(err, apiutil.ErrMissingThingIDs), + errors.Contains(err, apiutil.ErrMissingChannelIDs): err = unwrap(err) w.WriteHeader(http.StatusBadRequest) diff --git a/mqtt/handler_test.go b/mqtt/handler_test.go index 2beb3f4bdb..da1f6c094a 100644 --- a/mqtt/handler_test.go +++ b/mqtt/handler_test.go @@ -11,13 +11,13 @@ import ( "testing" "github.com/absmach/magistrala" - authmocks "github.com/absmach/magistrala/auth/mocks" "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/mqtt" "github.com/absmach/magistrala/mqtt/mocks" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" + thmocks "github.com/absmach/magistrala/things/mocks" "github.com/absmach/mproxy/pkg/session" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -450,12 +450,12 @@ func TestDisconnect(t *testing.T) { } } -func newHandler() (session.Handler, *authmocks.AuthClient, *mocks.EventStore) { +func newHandler() (session.Handler, *thmocks.AuthzClient, *mocks.EventStore) { logger, err := mglog.New(&logBuffer, "debug") if err != nil { log.Fatalf("failed to create logger: %s", err) } - auth := new(authmocks.AuthClient) + tauth := new(thmocks.AuthzClient) eventStore := new(mocks.EventStore) - return mqtt.NewHandler(mocks.NewPublisher(), eventStore, logger, auth), auth, eventStore + return mqtt.NewHandler(mocks.NewPublisher(), eventStore, logger, tauth), tauth, eventStore } diff --git a/pkg/apiutil/errors.go b/pkg/apiutil/errors.go index cceba23528..7bb5713309 100644 --- a/pkg/apiutil/errors.go +++ b/pkg/apiutil/errors.go @@ -185,4 +185,10 @@ var ( // ErrLenSearchQuery indicates search query length. ErrLenSearchQuery = errors.New("search query must be at least 3 characters") + + // ErrMissingChannelIDs indicates missing channel IDs. + ErrMissingChannelIDs = errors.New("missing channels ids") + + // ErrMissingThingIDs indicates missing thing IDs. + ErrMissingThingIDs = errors.New("missing things ids") ) diff --git a/pkg/clients/clients.go b/pkg/clients/clients.go index 926260c375..73698c237e 100644 --- a/pkg/clients/clients.go +++ b/pkg/clients/clients.go @@ -5,6 +5,7 @@ package clients import ( "context" + "encoding/json" "fmt" "regexp" "strings" @@ -23,6 +24,72 @@ const ( dotSeparator = "." ) +const ( + connected = "connected" + disconnected = "disconnected" + allConnected = "all_connected" + allDisconnected = "all_disconnected" + partConnected = "partially_connected" +) + +type AllState int + +const ( + AllConnectedState AllState = iota + AllDisconnectedState + PartConnectedState +) + +func (s AllState) String() string { + switch s { + case AllConnectedState: + return allConnected + case AllDisconnectedState: + return allDisconnected + case PartConnectedState: + return partConnected + default: + return Unknown + } +} + +// State represents connection state. +type State int + +const ( + // Disconnected represents disabled connection. + Disconnected State = iota + // Connected represents enabled connection. + Connected +) + +// String returns string representation of State. +func (s State) String() string { + switch s { + case Disconnected: + return disconnected + case Connected: + return connected + default: + return Unknown + } +} + +func (s State) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +type ConnectionStatus struct { + ChannelId string `json:"channel_id"` + ThingId string `json:"thing_id"` + Status State `json:"status"` +} + +type ConnectionsPage struct { + Status AllState `json:"status"` + Connections []ConnectionStatus `json:"connections_status"` +} + var ( userRegexp = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$") hostRegexp = regexp.MustCompile(`^[^\s]+\.[^\s]+$`) diff --git a/pkg/sdk/go/message_test.go b/pkg/sdk/go/message_test.go index ccf13fbb4a..32c184be89 100644 --- a/pkg/sdk/go/message_test.go +++ b/pkg/sdk/go/message_test.go @@ -30,10 +30,10 @@ import ( "github.com/stretchr/testify/mock" ) -func setupMessages() (*httptest.Server, *authmocks.AuthClient, *pubsub.PubSub) { - auth := new(authmocks.AuthClient) +func setupMessages() (*httptest.Server, *thmocks.AuthzClient, *pubsub.PubSub) { + tauth := new(thmocks.AuthzClient) pub := new(pubsub.PubSub) - handler := adapter.NewHandler(pub, mglog.NewMock(), auth) + handler := adapter.NewHandler(pub, mglog.NewMock(), tauth) mux := api.MakeHandler(mglog.NewMock(), "") target := httptest.NewServer(mux) @@ -47,20 +47,20 @@ func setupMessages() (*httptest.Server, *authmocks.AuthClient, *pubsub.PubSub) { return nil, nil, nil } - return httptest.NewServer(http.HandlerFunc(mp.ServeHTTP)), auth, pub + return httptest.NewServer(http.HandlerFunc(mp.ServeHTTP)), tauth, pub } func setupReader() (*httptest.Server, *authmocks.AuthClient, *readersmocks.MessageRepository) { repo := new(readersmocks.MessageRepository) auth := new(authmocks.AuthClient) - tauth := new(thmocks.ThingAuthzService) + tauth := new(thmocks.AuthzClient) mux := readersapi.MakeHandler(repo, auth, tauth, "test", "") return httptest.NewServer(mux), auth, repo } func TestSendMessage(t *testing.T) { - ts, auth, pub := setupMessages() + ts, tauth, pub := setupMessages() defer ts.Close() msg := `[{"n":"current","t":-1,"v":1.6}]` @@ -148,7 +148,7 @@ func TestSendMessage(t *testing.T) { } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { - authCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authRes, tc.authErr) + authCall := tauth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authRes, tc.authErr) svcCall := pub.On("Publish", mock.Anything, channelID, mock.Anything).Return(tc.svcErr) err := mgsdk.SendMessage(tc.chanName, tc.msg, tc.thingKey) assert.Equal(t, tc.err, err) @@ -196,7 +196,7 @@ func TestSetContentType(t *testing.T) { } func TestReadMessages(t *testing.T) { - ts, auth, repo := setupReader() + ts, tauth, repo := setupReader() defer ts.Close() channelID := "channelID" @@ -379,7 +379,7 @@ func TestReadMessages(t *testing.T) { } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { - authCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authRes, tc.authErr) + authCall := tauth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authRes, tc.authErr) repoCall := repo.On("ReadAll", channelID, mock.Anything).Return(tc.repoRes, tc.repoErr) response, err := mgsdk.ReadMessages(tc.messagePageMeta, tc.chanName, tc.token) assert.Equal(t, tc.err, err) diff --git a/pkg/sdk/go/responses.go b/pkg/sdk/go/responses.go index c51f0426f0..4e3e0dce80 100644 --- a/pkg/sdk/go/responses.go +++ b/pkg/sdk/go/responses.go @@ -31,6 +31,11 @@ type ChannelsPage struct { PageRes } +type ConnectionsPage struct { + Status string `json:"status"` + Connections []ConnectionStatus `json:"connections_status"` +} + // MessagesPage contains list of messages in a page with proper metadata. type MessagesPage struct { Messages []senml.Message `json:"messages,omitempty"` diff --git a/pkg/sdk/go/sdk.go b/pkg/sdk/go/sdk.go index 7ea46c298b..36705a63cc 100644 --- a/pkg/sdk/go/sdk.go +++ b/pkg/sdk/go/sdk.go @@ -122,6 +122,8 @@ type PageMetadata struct { WithMetadata bool `json:"with_metadata,omitempty"` WithAttributes bool `json:"with_attributes,omitempty"` ID string `json:"id,omitempty"` + ThingIDs []string `json:"thing_ids,omitempty"` + ChannelIDs []string `json:"channel_ids,omitempty"` } // Credentials represent client credentials: it contains @@ -406,6 +408,17 @@ type SDK interface { // fmt.Println(things) ThingsByChannel(chanID string, pm PageMetadata, token string) (ThingsPage, errors.SDKError) + // VerifyConnections returns page of things and channels that are connected or disconnected. + // + // example: + // pm := sdk.PageMetadata{ + // ThingsIds: []string{"df7f3e08-d234-4142-860e-4aebfb557fc5"}, + // ChannelsIds: []string{"c9091851-bdcc-43aa-878a-7fe75794cf37"} + // } + // connections, _ := sdk.VerifyConnections(pm, "token") + // fmt.Println(connections) + VerifyConnections(pm PageMetadata, token string) (ConnectionsPage, errors.SDKError) + // Thing returns thing object by id. // // example: diff --git a/pkg/sdk/go/things.go b/pkg/sdk/go/things.go index 8b598898bd..9ec4e81647 100644 --- a/pkg/sdk/go/things.go +++ b/pkg/sdk/go/things.go @@ -37,6 +37,17 @@ type Thing struct { Permissions []string `json:"permissions,omitempty"` } +type Connections struct { + ChannelIDs []string `json:"channel_ids,omitempty"` + ThingIDs []string `json:"thing_ids,omitempty"` +} + +type ConnectionStatus struct { + ChannelID string `json:"channel_id"` + ThingID string `json:"thing_id"` + Status string `json:"status"` +} + func (sdk mgSDK) CreateThing(thing Thing, token string) (Thing, errors.SDKError) { data, err := json.Marshal(thing) if err != nil { @@ -117,6 +128,27 @@ func (sdk mgSDK) ThingsByChannel(chanID string, pm PageMetadata, token string) ( return tp, nil } +func (sdk mgSDK) VerifyConnections(pm PageMetadata, token string) (ConnectionsPage, errors.SDKError) { + data, err := json.Marshal(pm) + if err != nil { + return ConnectionsPage{}, errors.NewSDKError(err) + } + + url := fmt.Sprintf("%s/%s/verify-connections", sdk.thingsURL, thingsEndpoint) + + _, body, sdkerr := sdk.processRequest(http.MethodPost, url, token, data, nil, http.StatusOK) + if sdkerr != nil { + return ConnectionsPage{}, sdkerr + } + + var cp ConnectionsPage + if err := json.Unmarshal(body, &cp); err != nil { + return ConnectionsPage{}, errors.NewSDKError(err) + } + + return cp, nil +} + func (sdk mgSDK) Thing(id, token string) (Thing, errors.SDKError) { if id == "" { return Thing{}, errors.NewSDKError(apiutil.ErrMissingID) diff --git a/pkg/sdk/go/things_test.go b/pkg/sdk/go/things_test.go index fc34743f06..f41489808f 100644 --- a/pkg/sdk/go/things_test.go +++ b/pkg/sdk/go/things_test.go @@ -15,6 +15,7 @@ import ( "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/clients" mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" @@ -727,6 +728,90 @@ func TestListThingsByChannel(t *testing.T) { } } +func TestVerifyConnections(t *testing.T) { + ts, tsvc := setupThings() + defer ts.Close() + + conf := sdk.Config{ + ThingsURL: ts.URL, + } + mgsdk := sdk.NewSDK(conf) + pm := sdk.PageMetadata{ + ThingIDs: []string{testsutil.GenerateUUID(t)}, + ChannelIDs: []string{testsutil.GenerateUUID(t)}, + } + + connsSDK := []sdk.ConnectionStatus{ + { + ChannelID: pm.ChannelIDs[0], + ThingID: pm.ThingIDs[0], + Status: clients.Connected.String(), + }, + } + connsClients := []mgclients.ConnectionStatus{ + { + ChannelId: pm.ChannelIDs[0], + ThingId: pm.ThingIDs[0], + Status: clients.Connected, + }, + } + + cases := []struct { + desc string + token string + pageMeta sdk.PageMetadata + svcRes mgclients.ConnectionsPage + response sdk.ConnectionsPage + err errors.SDKError + svcErr error + }{ + { + desc: "verify connections successfully", + token: validToken, + pageMeta: pm, + svcRes: mgclients.ConnectionsPage{ + Status: clients.AllConnectedState, + Connections: connsClients, + }, + response: sdk.ConnectionsPage{ + Status: clients.AllConnectedState.String(), + Connections: connsSDK, + }, + }, + { + desc: "verify connections with an invalid token", + token: invalidToken, + pageMeta: pm, + svcErr: svcerr.ErrAuthentication, + err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, http.StatusUnauthorized), + }, + { + desc: "verify connections with empty token", + token: "", + err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrBearerToken), http.StatusUnauthorized), + }, + { + desc: "verify connection with that can't be marshalled", + token: validToken, + pageMeta: sdk.PageMetadata{ + Metadata: sdk.Metadata{ + "test": make(chan int), + }, + }, + err: errors.NewSDKError(errors.New("json: unsupported type: chan int")), + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + svcCall := tsvc.On("VerifyConnectionsWithAuth", mock.Anything, tc.token, tc.pageMeta.ThingIDs, tc.pageMeta.ChannelIDs).Return(tc.svcRes, tc.svcErr) + resp, err := mgsdk.VerifyConnections(tc.pageMeta, tc.token) + assert.Equal(t, tc.err, err) + assert.Equal(t, tc.response, resp) + svcCall.Unset() + }) + } +} + func TestViewThing(t *testing.T) { ts, tsvc := setupThings() defer ts.Close() diff --git a/pkg/sdk/mocks/sdk.go b/pkg/sdk/mocks/sdk.go index 535f44a0d4..5d8f5932b2 100644 --- a/pkg/sdk/mocks/sdk.go +++ b/pkg/sdk/mocks/sdk.go @@ -2788,6 +2788,36 @@ func (_m *SDK) Users(pm sdk.PageMetadata, token string) (sdk.UsersPage, errors.S return r0, r1 } +// VerifyConnections provides a mock function with given fields: pm, token +func (_m *SDK) VerifyConnections(pm sdk.PageMetadata, token string) (sdk.ConnectionsPage, errors.SDKError) { + ret := _m.Called(pm, token) + + if len(ret) == 0 { + panic("no return value specified for VerifyConnections") + } + + var r0 sdk.ConnectionsPage + var r1 errors.SDKError + if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string) (sdk.ConnectionsPage, errors.SDKError)); ok { + return rf(pm, token) + } + if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string) sdk.ConnectionsPage); ok { + r0 = rf(pm, token) + } else { + r0 = ret.Get(0).(sdk.ConnectionsPage) + } + + if rf, ok := ret.Get(1).(func(sdk.PageMetadata, string) errors.SDKError); ok { + r1 = rf(pm, token) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.SDKError) + } + } + + return r0, r1 +} + // ViewBootstrap provides a mock function with given fields: id, token func (_m *SDK) ViewBootstrap(id string, token string) (sdk.BootstrapConfig, errors.SDKError) { ret := _m.Called(id, token) diff --git a/readers/api/endpoint_test.go b/readers/api/endpoint_test.go index 87e7f2d330..7d7566c82d 100644 --- a/readers/api/endpoint_test.go +++ b/readers/api/endpoint_test.go @@ -50,7 +50,7 @@ var ( sum float64 = 42 ) -func newServer(repo *mocks.MessageRepository, ac *authmocks.AuthClient, tc *thmocks.ThingAuthzService) *httptest.Server { +func newServer(repo *mocks.MessageRepository, ac *authmocks.AuthClient, tc *thmocks.AuthzClient) *httptest.Server { mux := api.MakeHandler(repo, ac, tc, svcName, instanceID) return httptest.NewServer(mux) } @@ -130,7 +130,7 @@ func TestReadAll(t *testing.T) { repo := new(mocks.MessageRepository) auth := new(authmocks.AuthClient) - tauth := new(thmocks.ThingAuthzService) + tauth := new(thmocks.AuthzClient) ts := newServer(repo, auth, tauth) defer ts.Close() diff --git a/things/api/grpc/client.go b/things/api/grpc/client.go index db1c31abb7..eeb9cf5f2f 100644 --- a/things/api/grpc/client.go +++ b/things/api/grpc/client.go @@ -23,8 +23,9 @@ const svcName = "magistrala.AuthzService" var _ magistrala.AuthzServiceClient = (*grpcClient)(nil) type grpcClient struct { - timeout time.Duration - authorize endpoint.Endpoint + timeout time.Duration + authorize endpoint.Endpoint + verifyConnections endpoint.Endpoint } // NewClient returns new gRPC client instance. @@ -38,6 +39,14 @@ func NewClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.AuthzSer decodeAuthorizeResponse, magistrala.AuthorizeRes{}, ).Endpoint(), + verifyConnections: kitgrpc.NewClient( + conn, + svcName, + "VerifyConnections", + encodeVerifyConnectionsRequest, + decodeVerifyConnectionsResponse, + magistrala.VerifyConnectionsRes{}, + ).Endpoint(), timeout: timeout, } @@ -75,6 +84,55 @@ func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{} }, nil } +func (client grpcClient) VerifyConnections(ctx context.Context, req *magistrala.VerifyConnectionsReq, opts ...grpc.CallOption) (*magistrala.VerifyConnectionsRes, error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + res, err := client.verifyConnections(ctx, req) + if err != nil { + return &magistrala.VerifyConnectionsRes{}, decodeError(err) + } + + vc := res.(verifyConnectionsRes) + connections := []*magistrala.Connectionstatus{} + for _, rq := range vc.Connections { + connections = append(connections, &magistrala.Connectionstatus{ + ThingId: rq.ThingId, + ChannelId: rq.ChannelId, + Status: rq.Status, + }) + } + return &magistrala.VerifyConnectionsRes{ + Status: vc.Status, + ConnectionsStatus: connections, + }, nil +} + +func decodeVerifyConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(*magistrala.VerifyConnectionsRes) + connections := []connectionStatus{} + + for _, rs := range res.GetConnectionsStatus() { + connections = append(connections, connectionStatus{ + ThingId: rs.ThingId, + ChannelId: rs.ChannelId, + Status: rs.Status, + }) + } + return verifyConnectionsRes{ + Status: res.Status, + Connections: connections, + }, nil +} + +func encodeVerifyConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + reqs := grpcReq.(*magistrala.VerifyConnectionsReq) + return &magistrala.VerifyConnectionsReq{ + ThingIds: reqs.GetThingIds(), + ChannelIds: reqs.GetChannelIds(), + }, nil +} + func decodeError(err error) error { if st, ok := status.FromError(err); ok { switch st.Code() { diff --git a/things/api/grpc/endpoint.go b/things/api/grpc/endpoint.go index e3f3527cba..cc71fff056 100644 --- a/things/api/grpc/endpoint.go +++ b/things/api/grpc/endpoint.go @@ -7,6 +7,7 @@ import ( "context" "github.com/absmach/magistrala" + "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/things" "github.com/go-kit/kit/endpoint" ) @@ -25,3 +26,30 @@ func authorizeEndpoint(svc things.Service) endpoint.Endpoint { }, err } } + +func verifyConnectionsEndpoint(svc things.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(*magistrala.VerifyConnectionsReq) + + if len(req.GetThingIds()) == 0 { + return verifyConnectionsRes{}, apiutil.ErrMissingThingIDs + } + if len(req.GetChannelIds()) == 0 { + return verifyConnectionsRes{}, apiutil.ErrMissingChannelIDs + } + + conns, err := svc.VerifyConnections(ctx, req.GetThingIds(), req.GetChannelIds()) + if err != nil { + return verifyConnectionsRes{}, err + } + cs := []connectionStatus{} + for _, c := range conns.Connections { + cs = append(cs, connectionStatus{ + ThingId: c.ThingId, + ChannelId: c.ChannelId, + Status: c.Status.String(), + }) + } + return verifyConnectionsRes{Status: conns.Status.String(), Connections: cs}, nil + } +} diff --git a/things/api/grpc/endpoint_test.go b/things/api/grpc/endpoint_test.go index 7e681466c8..369e7d7afa 100644 --- a/things/api/grpc/endpoint_test.go +++ b/things/api/grpc/endpoint_test.go @@ -7,12 +7,15 @@ import ( "context" "fmt" "net" + "os" "testing" "time" "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/apiutil" + mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" grpcapi "github.com/absmach/magistrala/things/api/grpc" @@ -24,7 +27,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -const port = 7000 +const port = 7070 var ( thingID = "testID" @@ -33,6 +36,8 @@ var ( valid = "valid" ) +var svc *mocks.Service + func startGRPCServer(svc *mocks.Service, port int) { listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { @@ -47,9 +52,16 @@ func startGRPCServer(svc *mocks.Service, port int) { }() } -func TestAuthorize(t *testing.T) { - svc := new(mocks.Service) +func TestMain(m *testing.M) { + svc = new(mocks.Service) startGRPCServer(svc, port) + + code := m.Run() + + os.Exit(code) +} + +func TestAuthorize(t *testing.T) { authAddr := fmt.Sprintf("localhost:%d", port) conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := grpcapi.NewClient(conn, time.Second) @@ -178,3 +190,65 @@ func TestAuthorize(t *testing.T) { svcCall.Unset() } } + +func TestVerifyConnections(t *testing.T) { + authAddr := fmt.Sprintf("localhost:%d", port) + conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err)) + client := grpcapi.NewClient(conn, time.Second) + + thingIds := []string{testsutil.GenerateUUID(t)} + channelIds := []string{testsutil.GenerateUUID(t)} + cases := []struct { + desc string + verifyConnectionsReq *magistrala.VerifyConnectionsReq + verifyConnectionsRes *magistrala.VerifyConnectionsRes + verifyPage mgclients.ConnectionsPage + err error + }{ + { + desc: "verify valid connection", + verifyConnectionsReq: &magistrala.VerifyConnectionsReq{ + ThingIds: thingIds, + ChannelIds: channelIds, + }, + verifyConnectionsRes: &magistrala.VerifyConnectionsRes{ + Status: mgclients.AllConnectedState.String(), + ConnectionsStatus: []*magistrala.Connectionstatus{ + { + ThingId: thingIds[0], + ChannelId: channelIds[0], + Status: mgclients.Connected.String(), + }, + }, + }, + verifyPage: mgclients.ConnectionsPage{ + Status: mgclients.AllConnectedState, + Connections: []mgclients.ConnectionStatus{ + { + ThingId: thingIds[0], + ChannelId: channelIds[0], + Status: mgclients.Connected, + }, + }, + }, + err: nil, + }, + { + desc: "verify with invalid thing id", + verifyConnectionsReq: &magistrala.VerifyConnectionsReq{ + ThingIds: []string{"invalid"}, + ChannelIds: channelIds, + }, + verifyConnectionsRes: &magistrala.VerifyConnectionsRes{}, + err: svcerr.ErrMalformedEntity, + }, + } + for _, tc := range cases { + svcCall := svc.On("VerifyConnections", mock.Anything, mock.Anything, mock.Anything).Return(tc.verifyPage, tc.err) + vc, err := client.VerifyConnections(context.Background(), tc.verifyConnectionsReq) + assert.Equal(t, tc.verifyConnectionsRes.GetConnectionsStatus(), vc.GetConnectionsStatus(), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.verifyConnectionsRes.GetConnectionsStatus(), vc.GetConnectionsStatus())) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + } +} diff --git a/things/api/grpc/responses.go b/things/api/grpc/responses.go index 8e11f1273f..ed703546d8 100644 --- a/things/api/grpc/responses.go +++ b/things/api/grpc/responses.go @@ -7,3 +7,14 @@ type authorizeRes struct { id string authorized bool } + +type verifyConnectionsRes struct { + Status string `json:"status"` + Connections []connectionStatus `json:"connections_status"` +} + +type connectionStatus struct { + ThingId string `json:"thing_id"` + ChannelId string `json:"channel_id"` + Status string `json:"status"` +} diff --git a/things/api/grpc/server.go b/things/api/grpc/server.go index 01cc0e99e4..cb5e7266ef 100644 --- a/things/api/grpc/server.go +++ b/things/api/grpc/server.go @@ -21,7 +21,8 @@ var _ magistrala.AuthzServiceServer = (*grpcServer)(nil) type grpcServer struct { magistrala.UnimplementedAuthzServiceServer - authorize kitgrpc.Handler + authorize kitgrpc.Handler + verifyConnections kitgrpc.Handler } // NewServer returns new AuthServiceServer instance. @@ -32,6 +33,11 @@ func NewServer(svc things.Service) magistrala.AuthzServiceServer { decodeAuthorizeRequest, encodeAuthorizeResponse, ), + verifyConnections: kitgrpc.NewServer( + (verifyConnectionsEndpoint(svc)), + decodeVerifyConnectionsRequest, + encodeVerifyConnectionsResponse, + ), } } @@ -53,6 +59,35 @@ func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{ return &magistrala.AuthorizeRes{Authorized: res.authorized, Id: res.id}, nil } +func (s *grpcServer) VerifyConnections(ctx context.Context, req *magistrala.VerifyConnectionsReq) (*magistrala.VerifyConnectionsRes, error) { + _, res, err := s.verifyConnections.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*magistrala.VerifyConnectionsRes), nil +} + +func decodeVerifyConnectionsRequest(_ context.Context, grpcreq interface{}) (interface{}, error) { + req := grpcreq.(*magistrala.VerifyConnectionsReq) + return req, nil +} + +func encodeVerifyConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(verifyConnectionsRes) + connections := []*magistrala.Connectionstatus{} + for _, conn := range res.Connections { + connections = append(connections, &magistrala.Connectionstatus{ + ThingId: conn.ThingId, + ChannelId: conn.ChannelId, + Status: conn.Status, + }) + } + return &magistrala.VerifyConnectionsRes{ + Status: res.Status, + ConnectionsStatus: connections, + }, nil +} + func encodeError(err error) error { switch { case errors.Contains(err, nil): @@ -63,7 +98,9 @@ func encodeError(err error) error { err == apiutil.ErrMissingMemberType, err == apiutil.ErrMissingPolicySub, err == apiutil.ErrMissingPolicyObj, - err == apiutil.ErrMalformedPolicyAct: + err == apiutil.ErrMalformedPolicyAct, + err == apiutil.ErrMissingThingIDs, + err == apiutil.ErrMissingChannelIDs: return status.Error(codes.InvalidArgument, err.Error()) case errors.Contains(err, svcerr.ErrAuthentication), errors.Contains(err, auth.ErrKeyExpired), diff --git a/things/api/http/clients.go b/things/api/http/clients.go index 24652c4680..bc122b1ed2 100644 --- a/things/api/http/clients.go +++ b/things/api/http/clients.go @@ -129,6 +129,13 @@ func clientsHandler(svc things.Service, r *chi.Mux, logger *slog.Logger) http.Ha opts..., ), "list_things_by_channel_id").ServeHTTP) + r.Post("/things/verify-connections", otelhttp.NewHandler(kithttp.NewServer( + verifyConnectionsEndpoint(svc), + decodeVerifyConnectionRequest, + api.EncodeResponse, + opts..., + ), "verify_connections").ServeHTTP) + r.Get("/users/{userID}/things", otelhttp.NewHandler(kithttp.NewServer( listClientsEndpoint(svc), decodeListClients, @@ -346,6 +353,20 @@ func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, return req, nil } +func decodeVerifyConnectionRequest(_ context.Context, r *http.Request) (interface{}, error) { + if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { + return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) + } + req := verifyConnectionReq{ + token: apiutil.ExtractBearerToken(r), + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) + } + + return req, nil +} + func decodeThingShareRequest(_ context.Context, r *http.Request) (interface{}, error) { if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) diff --git a/things/api/http/endpoints.go b/things/api/http/endpoints.go index ff70689a3a..1f49f4d88e 100644 --- a/things/api/http/endpoints.go +++ b/things/api/http/endpoints.go @@ -148,6 +148,26 @@ func listMembersEndpoint(svc things.Service) endpoint.Endpoint { } } +func verifyConnectionsEndpoint(svc things.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(verifyConnectionReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + conn, err := svc.VerifyConnectionsWithAuth(ctx, req.token, req.ThingIds, req.ChannelIds) + if err != nil { + return nil, err + } + + res := verifyConnectionRes{ + Status: conn.Status.String(), + Connections: conn.Connections, + } + + return res, nil + } +} + func updateClientEndpoint(svc things.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(updateClientReq) diff --git a/things/api/http/endpoints_test.go b/things/api/http/endpoints_test.go index a0346ac436..4d7977252c 100644 --- a/things/api/http/endpoints_test.go +++ b/things/api/http/endpoints_test.go @@ -195,28 +195,30 @@ func TestCreateThing(t *testing.T) { } for _, tc := range cases { - data := toJSON(tc.client) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/things/", ts.URL), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - - svcCall := svc.On("CreateThings", mock.Anything, tc.token, tc.client).Return([]mgclients.Client{tc.client}, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.client) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/things/", ts.URL), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(data), + } + + svcCall := svc.On("CreateThings", mock.Anything, tc.token, tc.client).Return([]mgclients.Client{tc.client}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var errRes respBody + err = json.NewDecoder(res.Body).Decode(&errRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if errRes.Err != "" || errRes.Message != "" { + err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -337,30 +339,32 @@ func TestCreateThings(t *testing.T) { } for _, tc := range cases { - data := toJSON(tc.client) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/things/bulk", ts.URL), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(data), - } - - svcCall := svc.On("CreateThings", mock.Anything, tc.token, mock.Anything, mock.Anything, mock.Anything).Return(tc.client, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - - var bodyRes respBody - err = json.NewDecoder(res.Body).Decode(&bodyRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if bodyRes.Err != "" || bodyRes.Message != "" { - err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.len, bodyRes.Total, fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.len, bodyRes.Total)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.client) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/things/bulk", ts.URL), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(data), + } + + svcCall := svc.On("CreateThings", mock.Anything, tc.token, mock.Anything, mock.Anything, mock.Anything).Return(tc.client, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + + var bodyRes respBody + err = json.NewDecoder(res.Body).Decode(&bodyRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if bodyRes.Err != "" || bodyRes.Message != "" { + err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.len, bodyRes.Total, fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.len, bodyRes.Total)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -614,27 +618,29 @@ func TestListThings(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: ts.URL + "/things?" + tc.query, - contentType: contentType, - token: tc.token, - } - - svcCall := svc.On("ListClients", mock.Anything, tc.token, "", mock.Anything).Return(tc.listThingsResponse, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - - var bodyRes respBody - err = json.NewDecoder(res.Body).Decode(&bodyRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if bodyRes.Err != "" || bodyRes.Message != "" { - err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodGet, + url: ts.URL + "/things?" + tc.query, + contentType: contentType, + token: tc.token, + } + + svcCall := svc.On("ListClients", mock.Anything, tc.token, "", mock.Anything).Return(tc.listThingsResponse, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + + var bodyRes respBody + err = json.NewDecoder(res.Body).Decode(&bodyRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if bodyRes.Err != "" || bodyRes.Message != "" { + err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -673,25 +679,27 @@ func TestViewThing(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: fmt.Sprintf("%s/things/%s", ts.URL, tc.id), - token: tc.token, - } - - svcCall := svc.On("ViewClient", mock.Anything, tc.token, tc.id).Return(mgclients.Client{}, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var errRes respBody - err = json.NewDecoder(res.Body).Decode(&errRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if errRes.Err != "" || errRes.Message != "" { - err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/things/%s", ts.URL, tc.id), + token: tc.token, + } + + svcCall := svc.On("ViewClient", mock.Anything, tc.token, tc.id).Return(mgclients.Client{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var errRes respBody + err = json.NewDecoder(res.Body).Decode(&errRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if errRes.Err != "" || errRes.Message != "" { + err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -742,26 +750,28 @@ func TestViewThingPerms(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: fmt.Sprintf("%s/things/%s/permissions", ts.URL, tc.thingID), - token: tc.token, - } - - svcCall := svc.On("ViewClientPerms", mock.Anything, tc.token, tc.thingID).Return(tc.response, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var resBody respBody - err = json.NewDecoder(res.Body).Decode(&resBody) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if resBody.Err != "" || resBody.Message != "" { - err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) - } - assert.Equal(t, len(tc.response), len(resBody.Permissions), fmt.Sprintf("%s: expected %d got %d", tc.desc, len(tc.response), len(resBody.Permissions))) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/things/%s/permissions", ts.URL, tc.thingID), + token: tc.token, + } + + svcCall := svc.On("ViewClientPerms", mock.Anything, tc.token, tc.thingID).Return(tc.response, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.Equal(t, len(tc.response), len(resBody.Permissions), fmt.Sprintf("%s: expected %d got %d", tc.desc, len(tc.response), len(resBody.Permissions))) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -855,32 +865,34 @@ func TestUpdateThing(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodPatch, - url: fmt.Sprintf("%s/things/%s", ts.URL, tc.id), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(tc.data), - } - - svcCall := svc.On("UpdateClient", mock.Anything, tc.token, mock.Anything).Return(tc.clientResponse, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - - var resBody respBody - err = json.NewDecoder(res.Body).Decode(&resBody) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if resBody.Err != "" || resBody.Message != "" { - err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) - } - - if err == nil { - assert.Equal(t, tc.clientResponse.ID, resBody.ID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.clientResponse, resBody.ID)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/things/%s", ts.URL, tc.id), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + svcCall := svc.On("UpdateClient", mock.Anything, tc.token, mock.Anything).Return(tc.clientResponse, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + + if err == nil { + assert.Equal(t, tc.clientResponse.ID, resBody.ID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.clientResponse, resBody.ID)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -970,27 +982,29 @@ func TestUpdateThingsTags(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodPatch, - url: fmt.Sprintf("%s/things/%s/tags", ts.URL, tc.id), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(tc.data), - } - - svcCall := svc.On("UpdateClientTags", mock.Anything, tc.token, mock.Anything).Return(tc.clientResponse, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var resBody respBody - err = json.NewDecoder(res.Body).Decode(&resBody) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if resBody.Err != "" || resBody.Message != "" { - err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/things/%s/tags", ts.URL, tc.id), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + svcCall := svc.On("UpdateClientTags", mock.Anything, tc.token, mock.Anything).Return(tc.clientResponse, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -1115,28 +1129,30 @@ func TestUpdateClientSecret(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodPatch, - url: fmt.Sprintf("%s/things/%s/secret", ts.URL, tc.client.ID), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(tc.data), - } - - svcCall := svc.On("UpdateClientSecret", mock.Anything, tc.token, tc.client.ID, mock.Anything).Return(tc.client, tc.err) - - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var resBody respBody - err = json.NewDecoder(res.Body).Decode(&resBody) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if resBody.Err != "" || resBody.Message != "" { - err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/things/%s/secret", ts.URL, tc.client.ID), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + svcCall := svc.On("UpdateClientSecret", mock.Anything, tc.token, tc.client.ID, mock.Anything).Return(tc.client, tc.err) + + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -1191,31 +1207,33 @@ func TestEnableThing(t *testing.T) { } for _, tc := range cases { - data := toJSON(tc.client) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/things/%s/enable", ts.URL, tc.client.ID), - contentType: contentType, - token: tc.token, - body: strings.NewReader(data), - } - - svcCall := svc.On("EnableClient", mock.Anything, tc.token, tc.client.ID).Return(tc.response, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var resBody respBody - err = json.NewDecoder(res.Body).Decode(&resBody) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if resBody.Err != "" || resBody.Message != "" { - err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) - } - if err == nil { - assert.Equal(t, tc.response.Status, resBody.Status, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.response.Status, resBody.Status)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.client) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/things/%s/enable", ts.URL, tc.client.ID), + contentType: contentType, + token: tc.token, + body: strings.NewReader(data), + } + + svcCall := svc.On("EnableClient", mock.Anything, tc.token, tc.client.ID).Return(tc.response, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + if err == nil { + assert.Equal(t, tc.response.Status, resBody.Status, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.response.Status, resBody.Status)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -1270,31 +1288,33 @@ func TestDisableThing(t *testing.T) { } for _, tc := range cases { - data := toJSON(tc.client) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/things/%s/disable", ts.URL, tc.client.ID), - contentType: contentType, - token: tc.token, - body: strings.NewReader(data), - } - - svcCall := svc.On("DisableClient", mock.Anything, tc.token, tc.client.ID).Return(tc.response, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - var resBody respBody - err = json.NewDecoder(res.Body).Decode(&resBody) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if resBody.Err != "" || resBody.Message != "" { - err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) - } - if err == nil { - assert.Equal(t, tc.response.Status, resBody.Status, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.response.Status, resBody.Status)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.client) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/things/%s/disable", ts.URL, tc.client.ID), + contentType: contentType, + token: tc.token, + body: strings.NewReader(data), + } + + svcCall := svc.On("DisableClient", mock.Anything, tc.token, tc.client.ID).Return(tc.response, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + if err == nil { + assert.Equal(t, tc.response.Status, resBody.Status, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.response.Status, resBody.Status)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -1404,20 +1424,22 @@ func TestShareThing(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/things/%s/share", ts.URL, tc.thingID), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(tc.data), - } - - svcCall := svc.On("Share", mock.Anything, tc.token, tc.thingID, mock.Anything, mock.Anything, mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/things/%s/share", ts.URL, tc.thingID), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + svcCall := svc.On("Share", mock.Anything, tc.token, tc.thingID, mock.Anything, mock.Anything, mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -1527,20 +1549,22 @@ func TestUnShareThing(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/things/%s/unshare", ts.URL, tc.thingID), - contentType: tc.contentType, - token: tc.token, - body: strings.NewReader(tc.data), - } - - svcCall := svc.On("Unshare", mock.Anything, tc.token, tc.thingID, mock.Anything, mock.Anything, mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/things/%s/unshare", ts.URL, tc.thingID), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + svcCall := svc.On("Unshare", mock.Anything, tc.token, tc.thingID, mock.Anything, mock.Anything, mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -1593,18 +1617,20 @@ func TestDeleteThing(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodDelete, - url: fmt.Sprintf("%s/things/%s", ts.URL, tc.id), - token: tc.token, - } - - svcCall := svc.On("DeleteClient", mock.Anything, tc.token, tc.id).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodDelete, + url: fmt.Sprintf("%s/things/%s", ts.URL, tc.id), + token: tc.token, + } + + svcCall := svc.On("DeleteClient", mock.Anything, tc.token, tc.id).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -1916,27 +1942,113 @@ func TestListMembers(t *testing.T) { } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodGet, - url: ts.URL + fmt.Sprintf("/channels/%s/things?", tc.groupdID) + tc.query, - contentType: contentType, - token: tc.token, - } + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodGet, + url: ts.URL + fmt.Sprintf("/channels/%s/things?", tc.groupdID) + tc.query, + contentType: contentType, + token: tc.token, + } + + svcCall := svc.On("ListClientsByGroup", mock.Anything, tc.token, mock.Anything, mock.Anything).Return(tc.listMembersResponse, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + + var bodyRes respBody + err = json.NewDecoder(res.Body).Decode(&bodyRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if bodyRes.Err != "" || bodyRes.Message != "" { + err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) + } +} + +func TestVerifyConnections(t *testing.T) { + ts, svc, _ := newThingsServer() + defer ts.Close() - svcCall := svc.On("ListClientsByGroup", mock.Anything, tc.token, mock.Anything, mock.Anything).Return(tc.listMembersResponse, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + cs := mgclients.ConnectionStatus{ + ChannelId: testsutil.GenerateUUID(t), + ThingId: testsutil.GenerateUUID(t), + Status: mgclients.Connected, + } - var bodyRes respBody - err = json.NewDecoder(res.Body).Decode(&bodyRes) - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) - if bodyRes.Err != "" || bodyRes.Message != "" { - err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) - } - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + cases := []struct { + desc string + token string + reqBody string + response mgclients.ConnectionsPage + contentType string + status int + err error + }{ + { + desc: "verify connection with valid token", + token: validToken, + reqBody: fmt.Sprintf(`{"thing_ids": ["%s"], "channel_ids": ["%s"]}`, cs.ThingId, cs.ChannelId), + response: mgclients.ConnectionsPage{ + Status: mgclients.AllConnectedState, + Connections: []mgclients.ConnectionStatus{cs}, + }, + status: http.StatusOK, + contentType: contentType, + }, + { + desc: "verify connection with empty token", + token: "", + reqBody: fmt.Sprintf(`{"things_id": ["%s"], "channels_id": ["%s"]}`, cs.ThingId, cs.ChannelId), + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + contentType: contentType, + }, + { + desc: "verify connection with empty thing ids", + token: validToken, + reqBody: fmt.Sprintf(`{"things_id": ["%s"], "channels_id": ["%s"]}`, "", cs.ChannelId), + status: http.StatusBadRequest, + err: apiutil.ErrMissingID, + contentType: contentType, + }, + { + desc: "verify connection with empty content type", + token: validToken, + reqBody: fmt.Sprintf(`{"things_id": ["%s"], "channels_id": ["%s"]}`, cs.ThingId, cs.ChannelId), + contentType: "", + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrUnsupportedContentType, + }, + { + desc: "verify connection with invalid json", + token: validToken, + reqBody: fmt.Sprintf(`{"things_id": ["%s"], "channels_id": ["%s"]`, cs.ThingId, cs.ChannelId), + contentType: contentType, + status: http.StatusBadRequest, + err: errors.ErrMalformedEntity, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: ts.URL + "/things/verify-connections", + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.reqBody), + } + + svcCall := svc.On("VerifyConnectionsWithAuth", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.response, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2051,21 +2163,23 @@ func TestAssignUsers(t *testing.T) { } for _, tc := range cases { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/channels/%s/users/assign", ts.URL, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - svcCall := gsvc.On("Assign", mock.Anything, tc.token, tc.groupID, mock.Anything, "users", mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.reqBody) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/channels/%s/users/assign", ts.URL, tc.groupID), + token: tc.token, + contentType: tc.contentType, + body: strings.NewReader(data), + } + + svcCall := gsvc.On("Assign", mock.Anything, tc.token, tc.groupID, mock.Anything, "users", mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2180,21 +2294,23 @@ func TestUnassignUsers(t *testing.T) { } for _, tc := range cases { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/channels/%s/users/unassign", ts.URL, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - svcCall := gsvc.On("Unassign", mock.Anything, tc.token, tc.groupID, mock.Anything, "users", mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.reqBody) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/channels/%s/users/unassign", ts.URL, tc.groupID), + token: tc.token, + contentType: tc.contentType, + body: strings.NewReader(data), + } + + svcCall := gsvc.On("Unassign", mock.Anything, tc.token, tc.groupID, mock.Anything, "users", mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2290,21 +2406,23 @@ func TestAssignGroupsToChannel(t *testing.T) { }, } for _, tc := range cases { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/channels/%s/groups/assign", ts.URL, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - svcCall := gsvc.On("Assign", mock.Anything, tc.token, tc.groupID, mock.Anything, "channels", mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.reqBody) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/channels/%s/groups/assign", ts.URL, tc.groupID), + token: tc.token, + contentType: tc.contentType, + body: strings.NewReader(data), + } + + svcCall := gsvc.On("Assign", mock.Anything, tc.token, tc.groupID, mock.Anything, "channels", mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2400,21 +2518,23 @@ func TestUnassignGroupsFromChannel(t *testing.T) { }, } for _, tc := range cases { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/channels/%s/groups/unassign", ts.URL, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - svcCall := gsvc.On("Unassign", mock.Anything, tc.token, tc.groupID, mock.Anything, "channels", mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.reqBody) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/channels/%s/groups/unassign", ts.URL, tc.groupID), + token: tc.token, + contentType: tc.contentType, + body: strings.NewReader(data), + } + + svcCall := gsvc.On("Unassign", mock.Anything, tc.token, tc.groupID, mock.Anything, "channels", mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2469,19 +2589,21 @@ func TestConnectThingToChannel(t *testing.T) { }, } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/channels/%s/things/%s/connect", ts.URL, tc.channelID, tc.thingID), - token: tc.token, - contentType: tc.contentType, - } - - svcCall := gsvc.On("Assign", mock.Anything, tc.token, tc.channelID, "group", "things", []string{tc.thingID}).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/channels/%s/things/%s/connect", ts.URL, tc.channelID, tc.thingID), + token: tc.token, + contentType: tc.contentType, + } + + svcCall := gsvc.On("Assign", mock.Anything, tc.token, tc.channelID, "group", "things", []string{tc.thingID}).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2536,19 +2658,21 @@ func TestDisconnectThingFromChannel(t *testing.T) { }, } for _, tc := range cases { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/channels/%s/things/%s/disconnect", ts.URL, tc.channelID, tc.thingID), - token: tc.token, - contentType: tc.contentType, - } - - svcCall := gsvc.On("Unassign", mock.Anything, tc.token, tc.channelID, "group", "things", []string{tc.thingID}).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/channels/%s/things/%s/disconnect", ts.URL, tc.channelID, tc.thingID), + token: tc.token, + contentType: tc.contentType, + } + + svcCall := gsvc.On("Unassign", mock.Anything, tc.token, tc.channelID, "group", "things", []string{tc.thingID}).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2631,21 +2755,23 @@ func TestConnect(t *testing.T) { }, } for _, tc := range cases { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/connect", ts.URL), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - svcCall := gsvc.On("Assign", mock.Anything, tc.token, mock.Anything, "group", "things", mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.reqBody) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/connect", ts.URL), + token: tc.token, + contentType: tc.contentType, + body: strings.NewReader(data), + } + + svcCall := gsvc.On("Assign", mock.Anything, tc.token, mock.Anything, "group", "things", mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } @@ -2728,21 +2854,23 @@ func TestDisconnect(t *testing.T) { }, } for _, tc := range cases { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/disconnect", ts.URL), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - svcCall := gsvc.On("Unassign", mock.Anything, tc.token, mock.Anything, "group", "things", mock.Anything).Return(tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + data := toJSON(tc.reqBody) + req := testRequest{ + client: ts.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/disconnect", ts.URL), + token: tc.token, + contentType: tc.contentType, + body: strings.NewReader(data), + } + + svcCall := gsvc.On("Unassign", mock.Anything, tc.token, mock.Anything, "group", "things", mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + svcCall.Unset() + }) } } diff --git a/things/api/http/requests.go b/things/api/http/requests.go index acf7fe116b..836c256c70 100644 --- a/things/api/http/requests.go +++ b/things/api/http/requests.go @@ -133,6 +133,25 @@ func (req listMembersReq) validate() error { return nil } +type verifyConnectionReq struct { + token string + ThingIds []string `json:"thing_ids"` + ChannelIds []string `json:"channel_ids"` +} + +func (req verifyConnectionReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if len(req.ThingIds) == 0 { + return apiutil.ErrMissingThingIDs + } + if len(req.ChannelIds) == 0 { + return apiutil.ErrMissingChannelIDs + } + return nil +} + type updateClientReq struct { token string id string diff --git a/things/api/http/responses.go b/things/api/http/responses.go index de7d8cb81e..16bc37af14 100644 --- a/things/api/http/responses.go +++ b/things/api/http/responses.go @@ -123,6 +123,23 @@ func (res clientsPageRes) Empty() bool { return false } +type verifyConnectionRes struct { + Status string `json:"status"` + Connections []mgclients.ConnectionStatus `json:"connections_status"` +} + +func (res verifyConnectionRes) Code() int { + return http.StatusOK +} + +func (res verifyConnectionRes) Headers() map[string]string { + return map[string]string{} +} + +func (res verifyConnectionRes) Empty() bool { + return false +} + type viewMembersRes struct { mgclients.Client } diff --git a/things/api/logging.go b/things/api/logging.go index 164b0ecdfb..e22608d37b 100644 --- a/things/api/logging.go +++ b/things/api/logging.go @@ -214,6 +214,23 @@ func (lm *loggingMiddleware) ListClientsByGroup(ctx context.Context, token, chan return lm.svc.ListClientsByGroup(ctx, token, channelID, cp) } +func (lm *loggingMiddleware) VerifyConnectionsWithAuth(ctx context.Context, token string, thingIds, groupIds []string) (cp mgclients.ConnectionsPage, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Any("thing_ids", thingIds), + slog.Any("channel_ids", groupIds), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Verify connections via http failed", args...) + return + } + lm.logger.Info("Verify connections completed successfully", args...) + }(time.Now()) + return lm.svc.VerifyConnectionsWithAuth(ctx, token, thingIds, groupIds) +} + func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) { defer func(begin time.Time) { args := []any{ @@ -300,3 +317,20 @@ func (lm *loggingMiddleware) DeleteClient(ctx context.Context, token, id string) }(time.Now()) return lm.svc.DeleteClient(ctx, token, id) } + +func (lm *loggingMiddleware) VerifyConnections(ctx context.Context, thingIds, groupIds []string) (cp mgclients.ConnectionsPage, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Any("thing_ids", thingIds), + slog.Any("channel_ids", groupIds), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Verify connections failed", args...) + return + } + lm.logger.Info("Verify connections completed successfully", args...) + }(time.Now()) + return lm.svc.VerifyConnections(ctx, thingIds, groupIds) +} diff --git a/things/api/metrics.go b/things/api/metrics.go index 24976d39a8..626a617122 100644 --- a/things/api/metrics.go +++ b/things/api/metrics.go @@ -110,6 +110,14 @@ func (ms *metricsMiddleware) ListClientsByGroup(ctx context.Context, token, grou return ms.svc.ListClientsByGroup(ctx, token, groupID, pm) } +func (ms *metricsMiddleware) VerifyConnectionsWithAuth(ctx context.Context, token string, thingIds, groupIds []string) (mc mgclients.ConnectionsPage, err error) { + defer func(begin time.Time) { + ms.counter.With("method", "verify_connections_with_auth").Add(1) + ms.latency.With("method", "verify_connections_with_auth").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.VerifyConnectionsWithAuth(ctx, token, thingIds, groupIds) +} + func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string, error) { defer func(begin time.Time) { ms.counter.With("method", "identify_thing").Add(1) @@ -149,3 +157,11 @@ func (ms *metricsMiddleware) DeleteClient(ctx context.Context, token, id string) }(time.Now()) return ms.svc.DeleteClient(ctx, token, id) } + +func (ms *metricsMiddleware) VerifyConnections(ctx context.Context, thingIds, groupIds []string) (mgclients.ConnectionsPage, error) { + defer func(begin time.Time) { + ms.counter.With("method", "verify_connections").Add(1) + ms.latency.With("method", "verify_connections").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.VerifyConnections(ctx, thingIds, groupIds) +} diff --git a/things/events/events.go b/things/events/events.go index 56b68b6ddc..736b9ee22b 100644 --- a/things/events/events.go +++ b/things/events/events.go @@ -22,6 +22,7 @@ const ( clientListByGroup = clientPrefix + "list_by_channel" clientIdentify = clientPrefix + "identify" clientAuthorize = clientPrefix + "authorize" + verifyConnections = clientPrefix + "verify" ) var ( @@ -275,6 +276,23 @@ func (lcge listClientByGroupEvent) Encode() (map[string]interface{}, error) { return val, nil } +type verifyConnectionEvent struct { + page mgclients.ConnectionsPage + thingIDs []string + groupIDs []string +} + +func (vce verifyConnectionEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": verifyConnections, + "thing_ids": vce.thingIDs, + "channel_ids": vce.groupIDs, + "page": vce.page, + } + + return val, nil +} + type identifyClientEvent struct { thingID string } diff --git a/things/events/streams.go b/things/events/streams.go index ca6a392fb2..9e471f0c0b 100644 --- a/things/events/streams.go +++ b/things/events/streams.go @@ -156,6 +156,23 @@ func (es *eventStore) ListClientsByGroup(ctx context.Context, token, chID string return mp, nil } +func (es *eventStore) VerifyConnectionsWithAuth(ctx context.Context, token string, thingIds, groupIds []string) (cp mgclients.ConnectionsPage, err error) { + mc, err := es.svc.VerifyConnectionsWithAuth(ctx, token, thingIds, groupIds) + if err != nil { + return mc, err + } + event := verifyConnectionEvent{ + page: cp, + thingIDs: thingIds, + groupIDs: groupIds, + } + if err := es.Publish(ctx, event); err != nil { + return mc, err + } + + return mc, nil +} + func (es *eventStore) EnableClient(ctx context.Context, token, id string) (mgclients.Client, error) { cli, err := es.svc.EnableClient(ctx, token, id) if err != nil { @@ -265,3 +282,20 @@ func (es *eventStore) DeleteClient(ctx context.Context, token, id string) error return nil } + +func (es *eventStore) VerifyConnections(ctx context.Context, thingIds, groupIds []string) (mgclients.ConnectionsPage, error) { + page, err := es.svc.VerifyConnections(ctx, thingIds, groupIds) + if err != nil { + return page, err + } + + event := verifyConnectionEvent{ + page: page, + thingIDs: thingIds, + groupIDs: groupIds, + } + if err := es.Publish(ctx, event); err != nil { + return page, err + } + return page, nil +} diff --git a/things/mocks/auth.go b/things/mocks/auth.go index 24c84a593c..944b0e53c8 100644 --- a/things/mocks/auth.go +++ b/things/mocks/auth.go @@ -1,33 +1,192 @@ // Copyright (c) Abstract Machines + // SPDX-License-Identifier: Apache-2.0 +// Code generated by mockery v2.43.2. DO NOT EDIT. + package mocks import ( - "context" + context "context" - "github.com/absmach/magistrala" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" -) + grpc "google.golang.org/grpc" -const WrongID = "wrongID" + magistrala "github.com/absmach/magistrala" -var _ magistrala.AuthzServiceClient = (*ThingAuthzService)(nil) + mock "github.com/stretchr/testify/mock" +) -type ThingAuthzService struct { +// AuthzClient is an autogenerated mock type for the AuthzServiceClient type +type AuthzClient struct { mock.Mock } -func (m *ThingAuthzService) Authorize(ctx context.Context, in *magistrala.AuthorizeReq, opts ...grpc.CallOption) (*magistrala.AuthorizeRes, error) { - ret := m.Called(ctx, in) - if in.GetSubject() == WrongID || in.GetSubject() == "" { - return &magistrala.AuthorizeRes{}, svcerr.ErrAuthorization +type AuthzClient_Expecter struct { + mock *mock.Mock +} + +func (_m *AuthzClient) EXPECT() *AuthzClient_Expecter { + return &AuthzClient_Expecter{mock: &_m.Mock} +} + +// Authorize provides a mock function with given fields: ctx, in, opts +func (_m *AuthzClient) Authorize(ctx context.Context, in *magistrala.AuthorizeReq, opts ...grpc.CallOption) (*magistrala.AuthorizeRes, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Authorize") } - if in.GetObject() == WrongID || in.GetObject() == "" { - return &magistrala.AuthorizeRes{}, svcerr.ErrAuthorization + + var r0 *magistrala.AuthorizeRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *magistrala.AuthorizeReq, ...grpc.CallOption) (*magistrala.AuthorizeRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *magistrala.AuthorizeReq, ...grpc.CallOption) *magistrala.AuthorizeRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*magistrala.AuthorizeRes) + } } - return ret.Get(0).(*magistrala.AuthorizeRes), ret.Error(1) + if rf, ok := ret.Get(1).(func(context.Context, *magistrala.AuthorizeReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AuthzClient_Authorize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authorize' +type AuthzClient_Authorize_Call struct { + *mock.Call +} + +// Authorize is a helper method to define mock.On call +// - ctx context.Context +// - in *magistrala.AuthorizeReq +// - opts ...grpc.CallOption +func (_e *AuthzClient_Expecter) Authorize(ctx interface{}, in interface{}, opts ...interface{}) *AuthzClient_Authorize_Call { + return &AuthzClient_Authorize_Call{Call: _e.mock.On("Authorize", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *AuthzClient_Authorize_Call) Run(run func(ctx context.Context, in *magistrala.AuthorizeReq, opts ...grpc.CallOption)) *AuthzClient_Authorize_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*magistrala.AuthorizeReq), variadicArgs...) + }) + return _c +} + +func (_c *AuthzClient_Authorize_Call) Return(_a0 *magistrala.AuthorizeRes, _a1 error) *AuthzClient_Authorize_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AuthzClient_Authorize_Call) RunAndReturn(run func(context.Context, *magistrala.AuthorizeReq, ...grpc.CallOption) (*magistrala.AuthorizeRes, error)) *AuthzClient_Authorize_Call { + _c.Call.Return(run) + return _c +} + +// VerifyConnections provides a mock function with given fields: ctx, in, opts +func (_m *AuthzClient) VerifyConnections(ctx context.Context, in *magistrala.VerifyConnectionsReq, opts ...grpc.CallOption) (*magistrala.VerifyConnectionsRes, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for VerifyConnections") + } + + var r0 *magistrala.VerifyConnectionsRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *magistrala.VerifyConnectionsReq, ...grpc.CallOption) (*magistrala.VerifyConnectionsRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *magistrala.VerifyConnectionsReq, ...grpc.CallOption) *magistrala.VerifyConnectionsRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*magistrala.VerifyConnectionsRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *magistrala.VerifyConnectionsReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AuthzClient_VerifyConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyConnections' +type AuthzClient_VerifyConnections_Call struct { + *mock.Call +} + +// VerifyConnections is a helper method to define mock.On call +// - ctx context.Context +// - in *magistrala.VerifyConnectionsReq +// - opts ...grpc.CallOption +func (_e *AuthzClient_Expecter) VerifyConnections(ctx interface{}, in interface{}, opts ...interface{}) *AuthzClient_VerifyConnections_Call { + return &AuthzClient_VerifyConnections_Call{Call: _e.mock.On("VerifyConnections", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *AuthzClient_VerifyConnections_Call) Run(run func(ctx context.Context, in *magistrala.VerifyConnectionsReq, opts ...grpc.CallOption)) *AuthzClient_VerifyConnections_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*magistrala.VerifyConnectionsReq), variadicArgs...) + }) + return _c +} + +func (_c *AuthzClient_VerifyConnections_Call) Return(_a0 *magistrala.VerifyConnectionsRes, _a1 error) *AuthzClient_VerifyConnections_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AuthzClient_VerifyConnections_Call) RunAndReturn(run func(context.Context, *magistrala.VerifyConnectionsReq, ...grpc.CallOption) (*magistrala.VerifyConnectionsRes, error)) *AuthzClient_VerifyConnections_Call { + _c.Call.Return(run) + return _c +} + +// NewAuthzClient creates a new instance of AuthzClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAuthzClient(t interface { + mock.TestingT + Cleanup(func()) +}) *AuthzClient { + mock := &AuthzClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/things/mocks/service.go b/things/mocks/service.go index efae25fdc6..ef1a675b45 100644 --- a/things/mocks/service.go +++ b/things/mocks/service.go @@ -376,6 +376,62 @@ func (_m *Service) UpdateClientTags(ctx context.Context, token string, client cl return r0, r1 } +// VerifyConnections provides a mock function with given fields: ctx, thingIds, groupIds +func (_m *Service) VerifyConnections(ctx context.Context, thingIds []string, groupIds []string) (clients.ConnectionsPage, error) { + ret := _m.Called(ctx, thingIds, groupIds) + + if len(ret) == 0 { + panic("no return value specified for VerifyConnections") + } + + var r0 clients.ConnectionsPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string, []string) (clients.ConnectionsPage, error)); ok { + return rf(ctx, thingIds, groupIds) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, []string) clients.ConnectionsPage); ok { + r0 = rf(ctx, thingIds, groupIds) + } else { + r0 = ret.Get(0).(clients.ConnectionsPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, []string) error); ok { + r1 = rf(ctx, thingIds, groupIds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// VerifyConnectionsWithAuth provides a mock function with given fields: ctx, token, thingIds, groupIds +func (_m *Service) VerifyConnectionsWithAuth(ctx context.Context, token string, thingIds []string, groupIds []string) (clients.ConnectionsPage, error) { + ret := _m.Called(ctx, token, thingIds, groupIds) + + if len(ret) == 0 { + panic("no return value specified for VerifyConnectionsWithAuth") + } + + var r0 clients.ConnectionsPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string, []string) (clients.ConnectionsPage, error)); ok { + return rf(ctx, token, thingIds, groupIds) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string, []string) clients.ConnectionsPage); ok { + r0 = rf(ctx, token, thingIds, groupIds) + } else { + r0 = ret.Get(0).(clients.ConnectionsPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string, []string) error); ok { + r1 = rf(ctx, token, thingIds, groupIds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ViewClient provides a mock function with given fields: ctx, token, id func (_m *Service) ViewClient(ctx context.Context, token string, id string) (clients.Client, error) { ret := _m.Called(ctx, token, id) diff --git a/things/service.go b/things/service.go index 5c85f4da5a..0cd5860013 100644 --- a/things/service.go +++ b/things/service.go @@ -532,6 +532,59 @@ func (svc service) ListClientsByGroup(ctx context.Context, token, groupID string }, nil } +func (svc service) VerifyConnectionsWithAuth(ctx context.Context, token string, thingIDs, groupIDs []string) (mgclients.ConnectionsPage, error) { + res, err := svc.identify(ctx, token) + if err != nil { + return mgclients.ConnectionsPage{}, err + } + + eCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + g, _ := errgroup.WithContext(eCtx) + + for _, thID := range thingIDs { + thID := thID + g.Go(func() error { + _, err := svc.authorize(ctx, res.GetDomainId(), auth.UserType, auth.UsersKind, res.GetId(), auth.ViewPermission, auth.ThingType, thID) + return err + }) + } + + for _, grpID := range groupIDs { + grpID := grpID + g.Go(func() error { + _, err := svc.authorize(ctx, res.GetDomainId(), auth.UserType, auth.UsersKind, res.GetId(), auth.ViewPermission, auth.GroupType, grpID) + return err + }) + } + + if err := g.Wait(); err != nil { + return mgclients.ConnectionsPage{}, err + } + resp, err := svc.VerifyConnections(ctx, thingIDs, groupIDs) + if err != nil { + return mgclients.ConnectionsPage{}, err + } + + cs := make([]mgclients.ConnectionStatus, len(resp.Connections)) + for i, c := range resp.Connections { + st := mgclients.Disconnected + if c.Status == mgclients.Connected { + st = mgclients.Connected + } + cs[i] = mgclients.ConnectionStatus{ + ThingId: c.ThingId, + ChannelId: c.ChannelId, + Status: st, + } + } + + return mgclients.ConnectionsPage{ + Status: resp.Status, + Connections: cs, + }, nil +} + func (svc service) Identify(ctx context.Context, key string) (string, error) { id, err := svc.clientCache.ID(ctx, key) if err == nil { @@ -549,6 +602,90 @@ func (svc service) Identify(ctx context.Context, key string) (string, error) { return client.ID, nil } +func (svc service) VerifyConnections(ctx context.Context, thingIds, groupIds []string) (mgclients.ConnectionsPage, error) { + uniqueThings := getUniqueValues(thingIds) + uniqueChannels := getUniqueValues(groupIds) + totalConnectionsCnt := len(uniqueChannels) * len(uniqueThings) + g, ctx := errgroup.WithContext(ctx) + + connections := make([]mgclients.ConnectionStatus, 0, totalConnectionsCnt) + + for _, th := range uniqueThings { + for _, ch := range uniqueChannels { + func(thing, channel string) { + g.Go(func() error { + authReq := &magistrala.AuthorizeReq{ + Subject: channel, + SubjectType: auth.GroupType, + Permission: auth.GroupRelation, + Object: thing, + ObjectType: auth.ThingType, + } + + _, err := svc.auth.Authorize(ctx, authReq) + var status mgclients.State + switch { + case err == nil: + status = mgclients.Connected + case errors.Contains(err, svcerr.ErrAuthorization): + status = mgclients.Disconnected + default: + return errors.Wrap(svcerr.ErrMalformedEntity, err) + } + + connections = append(connections, mgclients.ConnectionStatus{ + ThingId: thing, + ChannelId: channel, + Status: status, + }) + + return nil + }) + }(th, ch) + } + } + + if err := g.Wait(); err != nil { + return mgclients.ConnectionsPage{}, err + } + + totalConnectedCnt := 0 + for _, conn := range connections { + if conn.Status == 1 { + totalConnectedCnt++ + } + } + + var status mgclients.AllState + switch { + case totalConnectedCnt == totalConnectionsCnt: + status = mgclients.AllConnectedState + case totalConnectedCnt == 0: + status = mgclients.AllDisconnectedState + default: + status = mgclients.PartConnectedState + } + + return mgclients.ConnectionsPage{ + Status: status, + Connections: connections, + }, nil +} + +func getUniqueValues(slice []string) []string { + uniqueMap := make(map[string]bool) + var result []string + + for _, value := range slice { + if _, exists := uniqueMap[value]; !exists { + uniqueMap[value] = true + result = append(result, value) + } + } + + return result +} + func (svc service) identify(ctx context.Context, token string) (*magistrala.IdentityRes, error) { res, err := svc.auth.Identify(ctx, &magistrala.IdentityReq{Token: token}) if err != nil { diff --git a/things/service_test.go b/things/service_test.go index 61256cd2bd..f34440506b 100644 --- a/things/service_test.go +++ b/things/service_test.go @@ -327,27 +327,29 @@ func TestCreateThings(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, tc.identifyErr) - authcall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authResponse, tc.authorizeErr) - repoCall1 := cRepo.On("Save", context.Background(), mock.Anything).Return([]mgclients.Client{tc.thing}, tc.saveErr) - authCall1 := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPolicyResponse, tc.addPolicyErr) - authCall2 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePolicyRes, tc.deletePolicyErr) - expected, err := svc.CreateThings(context.Background(), tc.token, tc.thing) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - tc.thing.ID = expected[0].ID - tc.thing.CreatedAt = expected[0].CreatedAt - tc.thing.UpdatedAt = expected[0].UpdatedAt - tc.thing.Credentials.Secret = expected[0].Credentials.Secret - tc.thing.Domain = expected[0].Domain - tc.thing.UpdatedBy = expected[0].UpdatedBy - assert.Equal(t, tc.thing, expected[0], fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.thing, expected[0])) - } - repoCall.Unset() - authcall.Unset() - repoCall1.Unset() - authCall1.Unset() - authCall2.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, tc.identifyErr) + authcall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authResponse, tc.authorizeErr) + repoCall1 := cRepo.On("Save", context.Background(), mock.Anything).Return([]mgclients.Client{tc.thing}, tc.saveErr) + authCall1 := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPolicyResponse, tc.addPolicyErr) + authCall2 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePolicyRes, tc.deletePolicyErr) + expected, err := svc.CreateThings(context.Background(), tc.token, tc.thing) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + tc.thing.ID = expected[0].ID + tc.thing.CreatedAt = expected[0].CreatedAt + tc.thing.UpdatedAt = expected[0].UpdatedAt + tc.thing.Credentials.Secret = expected[0].Credentials.Secret + tc.thing.Domain = expected[0].Domain + tc.thing.UpdatedBy = expected[0].UpdatedBy + assert.Equal(t, tc.thing, expected[0], fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.thing, expected[0])) + } + repoCall.Unset() + authcall.Unset() + repoCall1.Unset() + authCall1.Unset() + authCall2.Unset() + }) } } @@ -402,13 +404,15 @@ func TestViewClient(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.response, tc.err) - rClient, err := svc.ViewClient(context.Background(), tc.token, tc.clientID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, rClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, rClient)) - repoCall.Unset() - repoCall1.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.response, tc.err) + rClient, err := svc.ViewClient(context.Background(), tc.token, tc.clientID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.response, rClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, rClient)) + repoCall.Unset() + repoCall1.Unset() + }) } } @@ -605,36 +609,38 @@ func TestListClients(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) - authorizeCall := auth.On("Authorize", context.Background(), &magistrala.AuthorizeReq{ - SubjectType: authsvc.UserType, - Subject: tc.identifyResponse.UserId, - Permission: authsvc.AdminPermission, - ObjectType: authsvc.PlatformType, - Object: authsvc.MagistralaObject, - }).Return(tc.authorizeResponse, tc.authorizeErr) - authorizeCall2 := auth.On("Authorize", context.Background(), &magistrala.AuthorizeReq{ - Domain: "", - SubjectType: authsvc.UserType, - SubjectKind: authsvc.UsersKind, - Subject: tc.identifyResponse.UserId, - Permission: "membership", - ObjectType: "domain", - Object: tc.identifyResponse.DomainId, - }).Return(tc.authorizeResponse1, tc.authorizeErr1) - listAllObjectsCall := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(tc.listObjectsResponse, tc.listObjectsErr) - retrieveAllCall := cRepo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) - listPermissionsCall := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) - - page, err := svc.ListClients(context.Background(), tc.token, tc.id, tc.page) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) - repoCall.Unset() - authorizeCall.Unset() - authorizeCall2.Unset() - listAllObjectsCall.Unset() - retrieveAllCall.Unset() - listPermissionsCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + authorizeCall := auth.On("Authorize", context.Background(), &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + Subject: tc.identifyResponse.UserId, + Permission: authsvc.AdminPermission, + ObjectType: authsvc.PlatformType, + Object: authsvc.MagistralaObject, + }).Return(tc.authorizeResponse, tc.authorizeErr) + authorizeCall2 := auth.On("Authorize", context.Background(), &magistrala.AuthorizeReq{ + Domain: "", + SubjectType: authsvc.UserType, + SubjectKind: authsvc.UsersKind, + Subject: tc.identifyResponse.UserId, + Permission: "membership", + ObjectType: "domain", + Object: tc.identifyResponse.DomainId, + }).Return(tc.authorizeResponse1, tc.authorizeErr1) + listAllObjectsCall := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(tc.listObjectsResponse, tc.listObjectsErr) + retrieveAllCall := cRepo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) + listPermissionsCall := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) + + page, err := svc.ListClients(context.Background(), tc.token, tc.id, tc.page) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) + repoCall.Unset() + authorizeCall.Unset() + authorizeCall2.Unset() + listAllObjectsCall.Unset() + retrieveAllCall.Unset() + listPermissionsCall.Unset() + }) } cases2 := []struct { @@ -795,32 +801,34 @@ func TestListClients(t *testing.T) { } for _, tc := range cases2 { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) - authorizeCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - listAllObjectsCall := auth.On("ListAllObjects", context.Background(), &magistrala.ListObjectsReq{ - SubjectType: authsvc.UserType, - Subject: tc.identifyResponse.DomainId + "_" + adminID, - Permission: "", - ObjectType: authsvc.ThingType, - }).Return(tc.listObjectsResponse, tc.listObjectsErr) - listAllObjectsCall2 := auth.On("ListAllObjects", context.Background(), &magistrala.ListObjectsReq{ - SubjectType: authsvc.UserType, - Subject: tc.identifyResponse.Id, - Permission: "", - ObjectType: authsvc.ThingType, - }).Return(tc.listObjectsResponse1, tc.listObjectsErr1) - retrieveAllCall := cRepo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) - listPermissionsCall := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) - - page, err := svc.ListClients(context.Background(), tc.token, tc.id, tc.page) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) - repoCall.Unset() - authorizeCall.Unset() - listAllObjectsCall.Unset() - listAllObjectsCall2.Unset() - retrieveAllCall.Unset() - listPermissionsCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + authorizeCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + listAllObjectsCall := auth.On("ListAllObjects", context.Background(), &magistrala.ListObjectsReq{ + SubjectType: authsvc.UserType, + Subject: tc.identifyResponse.DomainId + "_" + adminID, + Permission: "", + ObjectType: authsvc.ThingType, + }).Return(tc.listObjectsResponse, tc.listObjectsErr) + listAllObjectsCall2 := auth.On("ListAllObjects", context.Background(), &magistrala.ListObjectsReq{ + SubjectType: authsvc.UserType, + Subject: tc.identifyResponse.Id, + Permission: "", + ObjectType: authsvc.ThingType, + }).Return(tc.listObjectsResponse1, tc.listObjectsErr1) + retrieveAllCall := cRepo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) + listPermissionsCall := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) + + page, err := svc.ListClients(context.Background(), tc.token, tc.id, tc.page) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) + repoCall.Unset() + authorizeCall.Unset() + listAllObjectsCall.Unset() + listAllObjectsCall2.Unset() + retrieveAllCall.Unset() + listPermissionsCall.Unset() + }) } } @@ -909,13 +917,15 @@ func TestUpdateClient(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall1 := cRepo.On("Update", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) - updatedClient, err := svc.UpdateClient(context.Background(), tc.token, tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.updateResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedClient)) - repoCall.Unset() - repoCall1.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall1 := cRepo.On("Update", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) + updatedClient, err := svc.UpdateClient(context.Background(), tc.token, tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.updateResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedClient)) + repoCall.Unset() + repoCall1.Unset() + }) } } @@ -975,13 +985,15 @@ func TestUpdateClientTags(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall1 := cRepo.On("UpdateTags", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) - updatedClient, err := svc.UpdateClientTags(context.Background(), tc.token, tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.updateResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedClient)) - repoCall.Unset() - repoCall1.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall1 := cRepo.On("UpdateTags", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) + updatedClient, err := svc.UpdateClientTags(context.Background(), tc.token, tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.updateResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedClient)) + repoCall.Unset() + repoCall1.Unset() + }) } } @@ -1049,13 +1061,15 @@ func TestUpdateClientSecret(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall1 := cRepo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.updateSecretResponse, tc.updateErr) - updatedClient, err := svc.UpdateClientSecret(context.Background(), tc.token, tc.client.ID, tc.newSecret) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.updateSecretResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateSecretResponse, updatedClient)) - repoCall.Unset() - repoCall1.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall1 := cRepo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.updateSecretResponse, tc.updateErr) + updatedClient, err := svc.UpdateClientSecret(context.Background(), tc.token, tc.client.ID, tc.newSecret) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.updateSecretResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateSecretResponse, updatedClient)) + repoCall.Unset() + repoCall1.Unset() + }) } } @@ -1137,14 +1151,16 @@ func TestEnableClient(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) - repoCall2 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) - _, err := svc.EnableClient(context.Background(), tc.token, tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) + repoCall2 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) + _, err := svc.EnableClient(context.Background(), tc.token, tc.id) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + }) } cases2 := []struct { @@ -1195,23 +1211,25 @@ func TestEnableClient(t *testing.T) { } for _, tc := range cases2 { - pm := mgclients.Page{ - Offset: 0, - Limit: 100, - Status: tc.status, - } - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - repoCall2 := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(&magistrala.ListObjectsRes{Policies: getIDs(tc.response.Clients)}, nil) - repoCall3 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, nil) - page, err := svc.ListClients(context.Background(), validToken, "", pm) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - size := uint64(len(page.Clients)) - assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", tc.desc, tc.size, size)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() - repoCall3.Unset() + t.Run(tc.desc, func(t *testing.T) { + pm := mgclients.Page{ + Offset: 0, + Limit: 100, + Status: tc.status, + } + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) + repoCall2 := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(&magistrala.ListObjectsRes{Policies: getIDs(tc.response.Clients)}, nil) + repoCall3 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, nil) + page, err := svc.ListClients(context.Background(), validToken, "", pm) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + size := uint64(len(page.Clients)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", tc.desc, tc.size, size)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + repoCall3.Unset() + }) } } @@ -1305,16 +1323,18 @@ func TestDisableClient(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) - repoCall2 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) - repoCall3 := cache.On("Remove", mock.Anything, mock.Anything).Return(tc.removeErr) - _, err := svc.DisableClient(context.Background(), tc.token, tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() - repoCall3.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) + repoCall2 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) + repoCall3 := cache.On("Remove", mock.Anything, mock.Anything).Return(tc.removeErr) + _, err := svc.DisableClient(context.Background(), tc.token, tc.id) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + repoCall3.Unset() + }) } cases2 := []struct { @@ -1365,23 +1385,25 @@ func TestDisableClient(t *testing.T) { } for _, tc := range cases2 { - pm := mgclients.Page{ - Offset: 0, - Limit: 100, - Status: tc.status, - } - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - repoCall2 := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(&magistrala.ListObjectsRes{Policies: getIDs(tc.response.Clients)}, nil) - repoCall3 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, nil) - page, err := svc.ListClients(context.Background(), validToken, "", pm) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - size := uint64(len(page.Clients)) - assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", tc.desc, tc.size, size)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() - repoCall3.Unset() + t.Run(tc.desc, func(t *testing.T) { + pm := mgclients.Page{ + Offset: 0, + Limit: 100, + Status: tc.status, + } + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID, DomainId: testsutil.GenerateUUID(t)}, nil) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) + repoCall2 := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(&magistrala.ListObjectsRes{Policies: getIDs(tc.response.Clients)}, nil) + repoCall3 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, nil) + page, err := svc.ListClients(context.Background(), validToken, "", pm) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + size := uint64(len(page.Clients)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", tc.desc, tc.size, size)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + repoCall3.Unset() + }) } } @@ -1587,19 +1609,158 @@ func TestListMembers(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall2 := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(tc.listObjectsResponse, tc.listObjectsErr) - repoCall3 := cRepo.On("RetrieveAllByIDs", context.Background(), tc.page).Return(tc.retreiveAllByIDsResponse, tc.retreiveAllByIDsErr) - repoCall4 := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) - page, err := svc.ListClientsByGroup(context.Background(), tc.token, tc.groupID, tc.page) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() - repoCall3.Unset() - repoCall4.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := auth.On("ListAllObjects", mock.Anything, mock.Anything).Return(tc.listObjectsResponse, tc.listObjectsErr) + repoCall3 := cRepo.On("RetrieveAllByIDs", context.Background(), tc.page).Return(tc.retreiveAllByIDsResponse, tc.retreiveAllByIDsErr) + repoCall4 := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) + page, err := svc.ListClientsByGroup(context.Background(), tc.token, tc.groupID, tc.page) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + repoCall3.Unset() + repoCall4.Unset() + }) + } +} + +func TestVerifyConnectionsWithAuth(t *testing.T) { + svc, _, auth, _ := newService() + + th := []string{ + testsutil.GenerateUUID(t), + } + ch := []string{ + testsutil.GenerateUUID(t), + } + domainID := testsutil.GenerateUUID(t) + cases := []struct { + desc string + token string + thingIds []string + groupIds []string + response mgclients.ConnectionsPage + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + authorizeResponse1 *magistrala.AuthorizeRes + authorizeResponse2 *magistrala.AuthorizeRes + err error + identifyErr error + authorizeErr error + authorizeErr1 error + authorizeErr2 error + }{ + { + desc: "verify connections with connected thing and channel", + token: validToken, + thingIds: th, + groupIds: ch, + identifyResponse: &magistrala.IdentityRes{Id: validID, DomainId: domainID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + authorizeResponse1: &magistrala.AuthorizeRes{Authorized: true}, + authorizeResponse2: &magistrala.AuthorizeRes{Authorized: true}, + response: mgclients.ConnectionsPage{ + Status: mgclients.AllConnectedState, + Connections: []mgclients.ConnectionStatus{ + { + ChannelId: ch[0], + ThingId: th[0], + Status: mgclients.Connected, + }, + }, + }, + }, + { + desc: "verify connections with disconnected thing and channel", + token: validToken, + thingIds: th, + groupIds: ch, + identifyResponse: &magistrala.IdentityRes{Id: validID, DomainId: domainID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + authorizeResponse1: &magistrala.AuthorizeRes{Authorized: true}, + authorizeErr2: svcerr.ErrAuthorization, + response: mgclients.ConnectionsPage{ + Status: mgclients.AllDisconnectedState, + Connections: []mgclients.ConnectionStatus{ + { + ChannelId: ch[0], + ThingId: th[0], + Status: mgclients.Disconnected, + }, + }, + }, + }, + { + desc: "verify connections with unauthorized token", + token: validToken, + thingIds: th, + groupIds: ch, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "verify connections with unauthorized thing", + token: validToken, + thingIds: th, + groupIds: ch, + identifyResponse: &magistrala.IdentityRes{Id: validID, DomainId: domainID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + authorizeErr: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, + }, + { + desc: "verify connections with unauthorized channel", + token: validToken, + thingIds: th, + groupIds: ch, + identifyResponse: &magistrala.IdentityRes{Id: validID, DomainId: domainID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + authorizeResponse1: &magistrala.AuthorizeRes{Authorized: false}, + authorizeErr1: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{ + Domain: domainID, + SubjectType: authsvc.UserType, + SubjectKind: authsvc.UsersKind, + Subject: validID, + Permission: authsvc.ViewPermission, + ObjectType: authsvc.ThingType, + Object: tc.thingIds[0], + }).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := auth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{ + Domain: domainID, + SubjectType: authsvc.UserType, + SubjectKind: authsvc.UsersKind, + Subject: validID, + Permission: authsvc.ViewPermission, + ObjectType: authsvc.GroupType, + Object: tc.groupIds[0], + }).Return(tc.authorizeResponse1, tc.authorizeErr1) + repoCall3 := auth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{ + Subject: tc.groupIds[0], + SubjectType: authsvc.GroupType, + Permission: authsvc.GroupRelation, + Object: tc.thingIds[0], + ObjectType: authsvc.ThingType, + }).Return(tc.authorizeResponse2, tc.authorizeErr2) + page, err := svc.VerifyConnectionsWithAuth(context.Background(), tc.token, tc.thingIds, tc.groupIds) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + repoCall3.Unset() + }) } } @@ -1690,21 +1851,23 @@ func TestDeleteClient(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall2 := cache.On("Remove", mock.Anything, tc.clientID).Return(tc.removeErr) - repoCall3 := auth.On("DeleteEntityPolicies", context.Background(), &magistrala.DeleteEntityPoliciesReq{ - EntityType: authsvc.ThingType, - Id: tc.clientID, - }).Return(tc.deletePolicyResponse, tc.deletePolicyErr) - repoCall4 := cRepo.On("Delete", context.Background(), tc.clientID).Return(tc.deleteErr) - err := svc.DeleteClient(context.Background(), tc.token, tc.clientID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() - repoCall3.Unset() - repoCall4.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cache.On("Remove", mock.Anything, tc.clientID).Return(tc.removeErr) + repoCall3 := auth.On("DeleteEntityPolicies", context.Background(), &magistrala.DeleteEntityPoliciesReq{ + EntityType: authsvc.ThingType, + Id: tc.clientID, + }).Return(tc.deletePolicyResponse, tc.deletePolicyErr) + repoCall4 := cRepo.On("Delete", context.Background(), tc.clientID).Return(tc.deleteErr) + err := svc.DeleteClient(context.Background(), tc.token, tc.clientID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + repoCall3.Unset() + repoCall4.Unset() + }) } } @@ -1775,14 +1938,16 @@ func TestShare(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall2 := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPoliciesResponse, tc.addPoliciesErr) - err := svc.Share(context.Background(), tc.token, tc.clientID, tc.relation, tc.userID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPoliciesResponse, tc.addPoliciesErr) + err := svc.Share(context.Background(), tc.token, tc.clientID, tc.relation, tc.userID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + }) } } @@ -1853,14 +2018,16 @@ func TestUnShare(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall2 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesResponse, tc.deletePoliciesErr) - err := svc.Unshare(context.Background(), tc.token, tc.clientID, tc.relation, tc.userID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesResponse, tc.deletePoliciesErr) + err := svc.Unshare(context.Background(), tc.token, tc.clientID, tc.relation, tc.userID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + }) } } @@ -1925,14 +2092,16 @@ func TestViewClientPerms(t *testing.T) { } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) - repoCall2 := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermResponse, tc.listPermErr) - _, err := svc.ViewClientPerms(context.Background(), tc.token, tc.thingID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermResponse, tc.listPermErr) + _, err := svc.ViewClientPerms(context.Background(), tc.token, tc.thingID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + }) } } @@ -1986,14 +2155,16 @@ func TestIdentify(t *testing.T) { } for _, tc := range cases { - repoCall := cache.On("ID", mock.Anything, tc.key).Return(tc.cacheIDResponse, tc.cacheIDErr) - repoCall1 := cRepo.On("RetrieveBySecret", mock.Anything, mock.Anything).Return(tc.repoIDResponse, tc.retrieveBySecretErr) - repoCall2 := cache.On("Save", mock.Anything, mock.Anything, mock.Anything).Return(tc.saveErr) - _, err := svc.Identify(context.Background(), tc.key) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repoCall.Unset() - repoCall1.Unset() - repoCall2.Unset() + t.Run(tc.desc, func(t *testing.T) { + repoCall := cache.On("ID", mock.Anything, tc.key).Return(tc.cacheIDResponse, tc.cacheIDErr) + repoCall1 := cRepo.On("RetrieveBySecret", mock.Anything, mock.Anything).Return(tc.repoIDResponse, tc.retrieveBySecretErr) + repoCall2 := cache.On("Save", mock.Anything, mock.Anything, mock.Anything).Return(tc.saveErr) + _, err := svc.Identify(context.Background(), tc.key) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + }) } } @@ -2078,19 +2249,21 @@ func TestAuthorize(t *testing.T) { } for _, tc := range cases { - cacheCall := cache.On("ID", context.Background(), tc.request.GetSubject()).Return(tc.cacheIDRes, tc.cacheIDErr) - repoCall := cRepo.On("RetrieveBySecret", context.Background(), tc.request.GetSubject()).Return(tc.retrieveBySecretRes, tc.retrieveBySecretErr) - cacheCall1 := cache.On("Save", context.Background(), tc.request.GetSubject(), tc.retrieveBySecretRes.ID).Return(tc.cacheSaveErr) - authCall := auth.On("Authorize", context.Background(), mock.Anything).Return(tc.authorizeRes, tc.authErr) - id, err := svc.Authorize(context.Background(), tc.request) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if tc.err == nil { - assert.Equal(t, tc.id, id, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.id, id)) - } - cacheCall.Unset() - cacheCall1.Unset() - repoCall.Unset() - authCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + cacheCall := cache.On("ID", context.Background(), tc.request.GetSubject()).Return(tc.cacheIDRes, tc.cacheIDErr) + repoCall := cRepo.On("RetrieveBySecret", context.Background(), tc.request.GetSubject()).Return(tc.retrieveBySecretRes, tc.retrieveBySecretErr) + cacheCall1 := cache.On("Save", context.Background(), tc.request.GetSubject(), tc.retrieveBySecretRes.ID).Return(tc.cacheSaveErr) + authCall := auth.On("Authorize", context.Background(), mock.Anything).Return(tc.authorizeRes, tc.authErr) + id, err := svc.Authorize(context.Background(), tc.request) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if tc.err == nil { + assert.Equal(t, tc.id, id, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.id, id)) + } + cacheCall.Unset() + cacheCall1.Unset() + repoCall.Unset() + authCall.Unset() + }) } } diff --git a/things/standalone/standalone.go b/things/standalone/standalone.go index adaab18bb6..5060fc14d1 100644 --- a/things/standalone/standalone.go +++ b/things/standalone/standalone.go @@ -101,3 +101,7 @@ func (repo singleUserRepo) ListPermissions(ctx context.Context, in *magistrala.L func (repo singleUserRepo) DeleteEntityPolicies(ctx context.Context, in *magistrala.DeleteEntityPoliciesReq, opts ...grpc.CallOption) (*magistrala.DeletePolicyRes, error) { return nil, nil } + +func (repo singleUserRepo) VerifyConnections(ctx context.Context, in *magistrala.VerifyConnectionsReq, opts ...grpc.CallOption) (*magistrala.VerifyConnectionsRes, error) { + return nil, nil +} diff --git a/things/things.go b/things/things.go index 270d59e131..4041685236 100644 --- a/things/things.go +++ b/things/things.go @@ -33,6 +33,9 @@ type Service interface { // the provided key. ListClientsByGroup(ctx context.Context, token, groupID string, pm clients.Page) (clients.MembersPage, error) + // VerifyConnectionsWithAuth verifies if a list of things is connected to a list of channels. + VerifyConnectionsWithAuth(ctx context.Context, token string, thingIds, groupIds []string) (clients.ConnectionsPage, error) + // UpdateClient updates the client's name and metadata. UpdateClient(ctx context.Context, token string, client clients.Client) (clients.Client, error) @@ -62,6 +65,9 @@ type Service interface { // DeleteClient deletes client with given ID. DeleteClient(ctx context.Context, token, id string) error + + // VerifyConnections verifies connections between things and channels. + VerifyConnections(ctx context.Context, thingIds, groupIds []string) (clients.ConnectionsPage, error) } // Cache contains thing caching interface. diff --git a/things/tracing/tracing.go b/things/tracing/tracing.go index f097163279..88b50ab516 100644 --- a/things/tracing/tracing.go +++ b/things/tracing/tracing.go @@ -105,6 +105,17 @@ func (tm *tracingMiddleware) ListClientsByGroup(ctx context.Context, token, grou return tm.svc.ListClientsByGroup(ctx, token, groupID, pm) } +// VerifyConnections traces the "VerifyConnections" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) VerifyConnectionsWithAuth(ctx context.Context, token string, thingIds, groupIds []string) (mgclients.ConnectionsPage, error) { + ctx, span := tm.tracer.Start(ctx, "svc_verify_connection_http", trace.WithAttributes( + attribute.StringSlice("thingIds", thingIds), + attribute.StringSlice("channelIds", groupIds), + )) + defer span.End() + + return tm.svc.VerifyConnectionsWithAuth(ctx, token, thingIds, groupIds) +} + // ListMemberships traces the "ListMemberships" operation of the wrapped policies.Service. func (tm *tracingMiddleware) Identify(ctx context.Context, key string) (string, error) { ctx, span := tm.tracer.Start(ctx, "svc_identify", trace.WithAttributes(attribute.String("key", key))) @@ -140,3 +151,12 @@ func (tm *tracingMiddleware) DeleteClient(ctx context.Context, token, id string) defer span.End() return tm.svc.DeleteClient(ctx, token, id) } + +func (tm *tracingMiddleware) VerifyConnections(ctx context.Context, thingIds, groupIds []string) (mgclients.ConnectionsPage, error) { + ctx, span := tm.tracer.Start(ctx, "verify_connections", trace.WithAttributes( + attribute.StringSlice("thing_ids", thingIds), + attribute.StringSlice("channel_ids", groupIds), + )) + defer span.End() + return tm.svc.VerifyConnections(ctx, thingIds, groupIds) +} diff --git a/tools/config/.golangci.yml b/tools/config/.golangci.yml index 767ec2af74..1689297337 100644 --- a/tools/config/.golangci.yml +++ b/tools/config/.golangci.yml @@ -16,6 +16,12 @@ issues: - path: cli/commands_test.go linters: - godot + - path: bootstrap/service_test.go + linters: + - dogsled + - path: bootstrap/events/producer/streams_test.go + linters: + - dogsled linters-settings: importas: diff --git a/tools/config/mockery.yaml b/tools/config/mockery.yaml index 7de20b2236..7debbbdb5c 100644 --- a/tools/config/mockery.yaml +++ b/tools/config/mockery.yaml @@ -13,3 +13,9 @@ packages: dir: "./auth/mocks" mockname: "AuthClient" filename: "auth_client.go" + AuthzServiceClient: + config: + dir: "./things/mocks" + mockname: "AuthzClient" + filename: "auth.go" + diff --git a/ws/adapter_test.go b/ws/adapter_test.go index 4bccf87f62..c01694c688 100644 --- a/ws/adapter_test.go +++ b/ws/adapter_test.go @@ -9,11 +9,11 @@ import ( "testing" "github.com/absmach/magistrala" - authmocks "github.com/absmach/magistrala/auth/mocks" "github.com/absmach/magistrala/internal/testsutil" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" "github.com/absmach/magistrala/pkg/messaging/mocks" + thmocks "github.com/absmach/magistrala/things/mocks" "github.com/absmach/magistrala/ws" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -37,15 +37,15 @@ var msg = messaging.Message{ Payload: []byte(`[{"n":"current","t":-5,"v":1.2}]`), } -func newService() (ws.Service, *mocks.PubSub, *authmocks.AuthClient) { +func newService() (ws.Service, *mocks.PubSub, *thmocks.AuthzClient) { pubsub := new(mocks.PubSub) - auth := new(authmocks.AuthClient) + tauth := new(thmocks.AuthzClient) - return ws.New(auth, pubsub), pubsub, auth + return ws.New(tauth, pubsub), pubsub, tauth } func TestSubscribe(t *testing.T) { - svc, pubsub, auth := newService() + svc, pubsub, tauth := newService() c := ws.NewClient(nil) @@ -115,7 +115,7 @@ func TestSubscribe(t *testing.T) { Handler: c, } repocall := pubsub.On("Subscribe", mock.Anything, subConfig).Return(tc.err) - repocall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true, Id: thingID}, nil) + repocall1 := tauth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true, Id: thingID}, nil) err := svc.Subscribe(context.Background(), tc.thingKey, tc.chanID, tc.subtopic, c) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) repocall1.Parent.AssertCalled(t, "Authorize", mock.Anything, mock.Anything) diff --git a/ws/api/endpoint_test.go b/ws/api/endpoint_test.go index 25963522d1..4d225466de 100644 --- a/ws/api/endpoint_test.go +++ b/ws/api/endpoint_test.go @@ -13,9 +13,9 @@ import ( "testing" "github.com/absmach/magistrala" - authmocks "github.com/absmach/magistrala/auth/mocks" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/messaging/mocks" + thmocks "github.com/absmach/magistrala/things/mocks" "github.com/absmach/magistrala/ws" "github.com/absmach/magistrala/ws/api" "github.com/absmach/mproxy/pkg/session" @@ -90,17 +90,17 @@ func handshake(tsURL, chanID, subtopic, thingKey string, addHeader bool) (*webso } func TestHandshake(t *testing.T) { - auth := new(authmocks.AuthClient) - svc, pubsub := newService(auth) + tauth := new(thmocks.AuthzClient) + svc, pubsub := newService(tauth) target := newHTTPServer(svc) defer target.Close() - handler := ws.NewHandler(pubsub, mglog.NewMock(), auth) + handler := ws.NewHandler(pubsub, mglog.NewMock(), tauth) ts, err := newProxyHTPPServer(handler, target) require.Nil(t, err) defer ts.Close() - auth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{Subject: thingKey, Object: id, Domain: "", SubjectType: "thing", Permission: "publish", ObjectType: "group"}).Return(&magistrala.AuthorizeRes{Authorized: true, Id: "1"}, nil) - auth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{Subject: thingKey, Object: id, Domain: "", SubjectType: "thing", Permission: "subscribe", ObjectType: "group"}).Return(&magistrala.AuthorizeRes{Authorized: true, Id: "2"}, nil) - auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false, Id: "3"}, nil) + tauth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{Subject: thingKey, Object: id, Domain: "", SubjectType: "thing", Permission: "publish", ObjectType: "group"}).Return(&magistrala.AuthorizeRes{Authorized: true, Id: "1"}, nil) + tauth.On("Authorize", mock.Anything, &magistrala.AuthorizeReq{Subject: thingKey, Object: id, Domain: "", SubjectType: "thing", Permission: "subscribe", ObjectType: "group"}).Return(&magistrala.AuthorizeRes{Authorized: true, Id: "2"}, nil) + tauth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false, Id: "3"}, nil) pubsub.On("Subscribe", mock.Anything, mock.Anything).Return(nil) pubsub.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)