diff --git a/Makefile b/Makefile index 3819259b0e..67ddba77ce 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 MG_DOCKER_IMAGE_NAME_PREFIX ?= magistrala -BUILD_DIR = build -SERVICES = auth users things http coap ws postgres-writer postgres-reader timescale-writer \ +BUILD_DIR ?= build +SERVICES = auth users things groups channels domains http coap ws postgres-writer postgres-reader timescale-writer \ timescale-reader cli bootstrap mqtt provision certs invitations journal TEST_API_SERVICES = journal auth bootstrap certs http invitations notifiers provision readers things users TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES)) @@ -19,10 +19,14 @@ empty:= space:= $(empty) $(empty) # Docker compose project name should follow this guidelines: https://docs.docker.com/compose/reference/#use--p-to-specify-a-project-name DOCKER_PROJECT ?= $(shell echo $(subst $(space),,$(USER_REPO)) | tr -c -s '[:alnum:][=-=]' '_' | tr '[:upper:]' '[:lower:]') -DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config +DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config restart DEFAULT_DOCKER_COMPOSE_COMMAND := up GRPC_MTLS_CERT_FILES_EXISTS = 0 MOCKERY_VERSION=v2.43.2 +INTERNAL_PROTO_GEN_OUT_DIR=internal/grpc +INTERNAL_PROTO_DIR=internal/proto +INTERNAL_PROTO_FILES := $(shell find $(INTERNAL_PROTO_DIR) -name "*.proto" | sed 's|$(INTERNAL_PROTO_DIR)/||') + ifneq ($(MG_MESSAGE_BROKER_TYPE),) MG_MESSAGE_BROKER_TYPE := $(MG_MESSAGE_BROKER_TYPE) else @@ -179,7 +183,8 @@ $(TEST_API): proto: protoc -I. --go_out=. --go_opt=paths=source_relative pkg/messaging/*.proto - protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./*.proto + mkdir -p $(INTERNAL_PROTO_GEN_OUT_DIR) + protoc -I $(INTERNAL_PROTO_DIR) --go_out=$(INTERNAL_PROTO_GEN_OUT_DIR) --go_opt=paths=source_relative --go-grpc_out=$(INTERNAL_PROTO_GEN_OUT_DIR) --go-grpc_opt=paths=source_relative $(INTERNAL_PROTO_FILES) $(FILTERED_SERVICES): $(call compile_service,$(@)) @@ -257,3 +262,6 @@ run_addons: check_certs @for SVC in $(RUN_ADDON_ARGS); do \ MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/addons/$$SVC/docker-compose.yml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \ done + +run_live: check_certs + GOPATH=$(go env GOPATH) docker compose -f docker/docker-compose.yml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) diff --git a/auth.pb.go b/auth.pb.go deleted file mode 100644 index 34166ebd10..0000000000 --- a/auth.pb.go +++ /dev/null @@ -1,992 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.1 -// source: auth.proto - -package magistrala - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// If a token is not carrying any information itself, the type -// field can be used to determine how to validate the token. -// Also, different tokens can be encoded in different ways. -type Token struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - AccessToken string `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"` - RefreshToken *string `protobuf:"bytes,2,opt,name=refreshToken,proto3,oneof" json:"refreshToken,omitempty"` - AccessType string `protobuf:"bytes,3,opt,name=accessType,proto3" json:"accessType,omitempty"` -} - -func (x *Token) Reset() { - *x = Token{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Token) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Token) ProtoMessage() {} - -func (x *Token) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[0] - 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 Token.ProtoReflect.Descriptor instead. -func (*Token) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{0} -} - -func (x *Token) GetAccessToken() string { - if x != nil { - return x.AccessToken - } - return "" -} - -func (x *Token) GetRefreshToken() string { - if x != nil && x.RefreshToken != nil { - return *x.RefreshToken - } - return "" -} - -func (x *Token) GetAccessType() string { - if x != nil { - return x.AccessType - } - return "" -} - -type AuthNReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` -} - -func (x *AuthNReq) Reset() { - *x = AuthNReq{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AuthNReq) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuthNReq) ProtoMessage() {} - -func (x *AuthNReq) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[1] - 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 AuthNReq.ProtoReflect.Descriptor instead. -func (*AuthNReq) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{1} -} - -func (x *AuthNReq) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -type AuthNRes struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // IMPROVEMENT NOTE: change name from "id" to "subject" , sub in jwt = user id + domain id // - UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // user id - DomainId string `protobuf:"bytes,3,opt,name=domain_id,json=domainId,proto3" json:"domain_id,omitempty"` // domain id -} - -func (x *AuthNRes) Reset() { - *x = AuthNRes{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AuthNRes) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuthNRes) ProtoMessage() {} - -func (x *AuthNRes) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[2] - 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 AuthNRes.ProtoReflect.Descriptor instead. -func (*AuthNRes) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{2} -} - -func (x *AuthNRes) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *AuthNRes) GetUserId() string { - if x != nil { - return x.UserId - } - return "" -} - -func (x *AuthNRes) GetDomainId() string { - if x != nil { - return x.DomainId - } - return "" -} - -type IssueReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - Type uint32 `protobuf:"varint,2,opt,name=type,proto3" json:"type,omitempty"` -} - -func (x *IssueReq) Reset() { - *x = IssueReq{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *IssueReq) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*IssueReq) ProtoMessage() {} - -func (x *IssueReq) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[3] - 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 IssueReq.ProtoReflect.Descriptor instead. -func (*IssueReq) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{3} -} - -func (x *IssueReq) GetUserId() string { - if x != nil { - return x.UserId - } - return "" -} - -func (x *IssueReq) GetType() uint32 { - if x != nil { - return x.Type - } - return 0 -} - -type RefreshReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - RefreshToken string `protobuf:"bytes,1,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` -} - -func (x *RefreshReq) Reset() { - *x = RefreshReq{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *RefreshReq) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RefreshReq) ProtoMessage() {} - -func (x *RefreshReq) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[4] - 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 RefreshReq.ProtoReflect.Descriptor instead. -func (*RefreshReq) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{4} -} - -func (x *RefreshReq) GetRefreshToken() string { - if x != nil { - return x.RefreshToken - } - return "" -} - -type AuthZReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` // Domain - SubjectType string `protobuf:"bytes,2,opt,name=subject_type,json=subjectType,proto3" json:"subject_type,omitempty"` // Thing or User - SubjectKind string `protobuf:"bytes,3,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` // ID or Token - SubjectRelation string `protobuf:"bytes,4,opt,name=subject_relation,json=subjectRelation,proto3" json:"subject_relation,omitempty"` // Subject relation - Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"` // Subject value (id or token, depending on kind) - Relation string `protobuf:"bytes,6,opt,name=relation,proto3" json:"relation,omitempty"` // Relation to filter - Permission string `protobuf:"bytes,7,opt,name=permission,proto3" json:"permission,omitempty"` // Action - Object string `protobuf:"bytes,8,opt,name=object,proto3" json:"object,omitempty"` // Object ID - ObjectType string `protobuf:"bytes,9,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"` // Thing, User, Group -} - -func (x *AuthZReq) Reset() { - *x = AuthZReq{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AuthZReq) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuthZReq) ProtoMessage() {} - -func (x *AuthZReq) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[5] - 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 AuthZReq.ProtoReflect.Descriptor instead. -func (*AuthZReq) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{5} -} - -func (x *AuthZReq) GetDomain() string { - if x != nil { - return x.Domain - } - return "" -} - -func (x *AuthZReq) GetSubjectType() string { - if x != nil { - return x.SubjectType - } - return "" -} - -func (x *AuthZReq) GetSubjectKind() string { - if x != nil { - return x.SubjectKind - } - return "" -} - -func (x *AuthZReq) GetSubjectRelation() string { - if x != nil { - return x.SubjectRelation - } - return "" -} - -func (x *AuthZReq) GetSubject() string { - if x != nil { - return x.Subject - } - return "" -} - -func (x *AuthZReq) GetRelation() string { - if x != nil { - return x.Relation - } - return "" -} - -func (x *AuthZReq) GetPermission() string { - if x != nil { - return x.Permission - } - return "" -} - -func (x *AuthZReq) GetObject() string { - if x != nil { - return x.Object - } - return "" -} - -func (x *AuthZReq) GetObjectType() string { - if x != nil { - return x.ObjectType - } - return "" -} - -type AuthZRes struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` - Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` -} - -func (x *AuthZRes) Reset() { - *x = AuthZRes{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AuthZRes) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AuthZRes) ProtoMessage() {} - -func (x *AuthZRes) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[6] - 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 AuthZRes.ProtoReflect.Descriptor instead. -func (*AuthZRes) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{6} -} - -func (x *AuthZRes) GetAuthorized() bool { - if x != nil { - return x.Authorized - } - return false -} - -func (x *AuthZRes) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type DeleteUserRes struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` -} - -func (x *DeleteUserRes) Reset() { - *x = DeleteUserRes{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DeleteUserRes) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteUserRes) ProtoMessage() {} - -func (x *DeleteUserRes) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[7] - 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 DeleteUserRes.ProtoReflect.Descriptor instead. -func (*DeleteUserRes) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{7} -} - -func (x *DeleteUserRes) GetDeleted() bool { - if x != nil { - return x.Deleted - } - return false -} - -type DeleteUserReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` -} - -func (x *DeleteUserReq) Reset() { - *x = DeleteUserReq{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DeleteUserReq) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteUserReq) ProtoMessage() {} - -func (x *DeleteUserReq) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[8] - 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 DeleteUserReq.ProtoReflect.Descriptor instead. -func (*DeleteUserReq) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{8} -} - -func (x *DeleteUserReq) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type ThingsAuthzReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ChannelID string `protobuf:"bytes,1,opt,name=channelID,proto3" json:"channelID,omitempty"` - ThingID string `protobuf:"bytes,2,opt,name=thingID,proto3" json:"thingID,omitempty"` - ThingKey string `protobuf:"bytes,3,opt,name=thingKey,proto3" json:"thingKey,omitempty"` - Permission string `protobuf:"bytes,4,opt,name=permission,proto3" json:"permission,omitempty"` -} - -func (x *ThingsAuthzReq) Reset() { - *x = ThingsAuthzReq{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ThingsAuthzReq) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ThingsAuthzReq) ProtoMessage() {} - -func (x *ThingsAuthzReq) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[9] - 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 ThingsAuthzReq.ProtoReflect.Descriptor instead. -func (*ThingsAuthzReq) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{9} -} - -func (x *ThingsAuthzReq) GetChannelID() string { - if x != nil { - return x.ChannelID - } - return "" -} - -func (x *ThingsAuthzReq) GetThingID() string { - if x != nil { - return x.ThingID - } - return "" -} - -func (x *ThingsAuthzReq) GetThingKey() string { - if x != nil { - return x.ThingKey - } - return "" -} - -func (x *ThingsAuthzReq) GetPermission() string { - if x != nil { - return x.Permission - } - return "" -} - -type ThingsAuthzRes struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` - Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` -} - -func (x *ThingsAuthzRes) Reset() { - *x = ThingsAuthzRes{} - if protoimpl.UnsafeEnabled { - mi := &file_auth_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ThingsAuthzRes) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ThingsAuthzRes) ProtoMessage() {} - -func (x *ThingsAuthzRes) ProtoReflect() protoreflect.Message { - mi := &file_auth_proto_msgTypes[10] - 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 ThingsAuthzRes.ProtoReflect.Descriptor instead. -func (*ThingsAuthzRes) Descriptor() ([]byte, []int) { - return file_auth_proto_rawDescGZIP(), []int{10} -} - -func (x *ThingsAuthzRes) GetAuthorized() bool { - if x != nil { - return x.Authorized - } - return false -} - -func (x *ThingsAuthzRes) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -var File_auth_proto protoreflect.FileDescriptor - -var file_auth_proto_rawDesc = []byte{ - 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x6d, 0x61, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x05, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x27, 0x0a, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x72, 0x65, - 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1e, 0x0a, - 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x42, 0x0f, 0x0a, - 0x0d, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x20, - 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x22, 0x50, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, - 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x49, 0x64, 0x22, 0x37, 0x0a, 0x08, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x71, 0x12, 0x17, - 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x31, 0x0a, 0x0a, 0x52, - 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, - 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa2, - 0x02, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x5a, 0x52, 0x65, 0x71, 0x12, 0x16, 0x0a, 0x06, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1a, - 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, - 0x79, 0x70, 0x65, 0x22, 0x3a, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x5a, 0x52, 0x65, 0x73, 0x12, - 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, - 0x29, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x0d, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x84, 0x01, 0x0a, 0x0e, - 0x54, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, 0x12, 0x1c, - 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, - 0x74, 0x68, 0x69, 0x6e, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, - 0x68, 0x69, 0x6e, 0x67, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x4b, - 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x4b, - 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x22, 0x40, 0x0a, 0x0e, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x41, 0x75, 0x74, 0x68, - 0x7a, 0x52, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x32, 0x56, 0x0a, 0x0d, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x12, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, - 0x54, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, 0x1a, 0x1a, - 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x54, 0x68, 0x69, 0x6e, - 0x67, 0x73, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x73, 0x22, 0x00, 0x32, 0x7a, 0x0a, 0x0c, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 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, 0x32, 0x86, 0x01, 0x0a, 0x0b, 0x41, 0x75, 0x74, - 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, - 0x6c, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x5a, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x6d, 0x61, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x5a, 0x52, 0x65, - 0x73, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x6d, 0x61, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x73, 0x22, - 0x00, 0x32, 0x61, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x19, 0x2e, 0x6d, - 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x19, 0x2e, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x6c, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 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 ( - file_auth_proto_rawDescOnce sync.Once - file_auth_proto_rawDescData = file_auth_proto_rawDesc -) - -func file_auth_proto_rawDescGZIP() []byte { - file_auth_proto_rawDescOnce.Do(func() { - file_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_auth_proto_rawDescData) - }) - return file_auth_proto_rawDescData -} - -var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_auth_proto_goTypes = []any{ - (*Token)(nil), // 0: magistrala.Token - (*AuthNReq)(nil), // 1: magistrala.AuthNReq - (*AuthNRes)(nil), // 2: magistrala.AuthNRes - (*IssueReq)(nil), // 3: magistrala.IssueReq - (*RefreshReq)(nil), // 4: magistrala.RefreshReq - (*AuthZReq)(nil), // 5: magistrala.AuthZReq - (*AuthZRes)(nil), // 6: magistrala.AuthZRes - (*DeleteUserRes)(nil), // 7: magistrala.DeleteUserRes - (*DeleteUserReq)(nil), // 8: magistrala.DeleteUserReq - (*ThingsAuthzReq)(nil), // 9: magistrala.ThingsAuthzReq - (*ThingsAuthzRes)(nil), // 10: magistrala.ThingsAuthzRes -} -var file_auth_proto_depIdxs = []int32{ - 9, // 0: magistrala.ThingsService.Authorize:input_type -> magistrala.ThingsAuthzReq - 3, // 1: magistrala.TokenService.Issue:input_type -> magistrala.IssueReq - 4, // 2: magistrala.TokenService.Refresh:input_type -> magistrala.RefreshReq - 5, // 3: magistrala.AuthService.Authorize:input_type -> magistrala.AuthZReq - 1, // 4: magistrala.AuthService.Authenticate:input_type -> magistrala.AuthNReq - 8, // 5: magistrala.DomainsService.DeleteUserFromDomains:input_type -> magistrala.DeleteUserReq - 10, // 6: magistrala.ThingsService.Authorize:output_type -> magistrala.ThingsAuthzRes - 0, // 7: magistrala.TokenService.Issue:output_type -> magistrala.Token - 0, // 8: magistrala.TokenService.Refresh:output_type -> magistrala.Token - 6, // 9: magistrala.AuthService.Authorize:output_type -> magistrala.AuthZRes - 2, // 10: magistrala.AuthService.Authenticate:output_type -> magistrala.AuthNRes - 7, // 11: magistrala.DomainsService.DeleteUserFromDomains:output_type -> magistrala.DeleteUserRes - 6, // [6:12] is the sub-list for method output_type - 0, // [0:6] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_auth_proto_init() } -func file_auth_proto_init() { - if File_auth_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_auth_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Token); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*AuthNReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*AuthNRes); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*IssueReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*RefreshReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*AuthZReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[6].Exporter = func(v any, i int) any { - switch v := v.(*AuthZRes); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[7].Exporter = func(v any, i int) any { - switch v := v.(*DeleteUserRes); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[8].Exporter = func(v any, i int) any { - switch v := v.(*DeleteUserReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[9].Exporter = func(v any, i int) any { - switch v := v.(*ThingsAuthzReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_auth_proto_msgTypes[10].Exporter = func(v any, i int) any { - switch v := v.(*ThingsAuthzRes); 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{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_auth_proto_rawDesc, - NumEnums: 0, - NumMessages: 11, - NumExtensions: 0, - NumServices: 4, - }, - GoTypes: file_auth_proto_goTypes, - DependencyIndexes: file_auth_proto_depIdxs, - MessageInfos: file_auth_proto_msgTypes, - }.Build() - File_auth_proto = out.File - file_auth_proto_rawDesc = nil - file_auth_proto_goTypes = nil - file_auth_proto_depIdxs = nil -} diff --git a/auth.proto b/auth.proto deleted file mode 100644 index d597071d6c..0000000000 --- a/auth.proto +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -syntax = "proto3"; - -package magistrala; -option go_package = "./magistrala"; - -// ThingsService is a service that provides things authorization functionalities -// for magistrala services. -service ThingsService { - // Authorize checks if the thing is authorized to perform - // the action on the channel. - rpc Authorize(ThingsAuthzReq) returns (ThingsAuthzRes) {} -} - -service TokenService { - rpc Issue(IssueReq) returns (Token) {} - rpc Refresh(RefreshReq) returns (Token) {} -} - -// AuthService is a service that provides authentication and authorization -// functionalities for magistrala services. -service AuthService { - rpc Authorize(AuthZReq) returns (AuthZRes) {} - rpc Authenticate(AuthNReq) returns (AuthNRes) {} -} - -// DomainsService is a service that provides access to domains -// functionalities for magistrala services. -service DomainsService { - rpc DeleteUserFromDomains(DeleteUserReq) returns (DeleteUserRes) {} -} - -// If a token is not carrying any information itself, the type -// field can be used to determine how to validate the token. -// Also, different tokens can be encoded in different ways. -message Token { - string accessToken = 1; - optional string refreshToken = 2; - string accessType = 3; -} - -message AuthNReq { - string token = 1; -} - -message AuthNRes { - string id = 1; // IMPROVEMENT NOTE: change name from "id" to "subject" , sub in jwt = user id + domain id // - string user_id = 2; // user id - string domain_id = 3; // domain id -} - -message IssueReq { - string user_id = 1; - uint32 type = 2; -} - -message RefreshReq { - string refresh_token = 1; -} - -message AuthZReq { - string domain = 1; // Domain - string subject_type = 2; // Thing or User - string subject_kind = 3; // ID or Token - string subject_relation = 4; // Subject relation - string subject = 5; // Subject value (id or token, depending on kind) - string relation = 6; // Relation to filter - string permission = 7; // Action - string object = 8; // Object ID - string object_type = 9; // Thing, User, Group -} - -message AuthZRes { - bool authorized = 1; - string id = 2; -} - -message DeleteUserRes { bool deleted = 1; } - -message DeleteUserReq{ - string id = 1; -} - -message ThingsAuthzReq { - string channelID = 1; - string thingID = 2; - string thingKey = 3; - string permission = 4; -} - -message ThingsAuthzRes { - bool authorized = 1; - string id = 2; -} diff --git a/auth/api/grpc/auth/client.go b/auth/api/grpc/auth/client.go index f53f4f5734..41f4e9e66a 100644 --- a/auth/api/grpc/auth/client.go +++ b/auth/api/grpc/auth/client.go @@ -7,14 +7,14 @@ import ( "context" "time" - "github.com/absmach/magistrala" grpcapi "github.com/absmach/magistrala/auth/api/grpc" + grpcAuthV1 "github.com/absmach/magistrala/internal/grpc/auth/v1" "github.com/go-kit/kit/endpoint" kitgrpc "github.com/go-kit/kit/transport/grpc" "google.golang.org/grpc" ) -const authSvcName = "magistrala.AuthService" +const authSvcName = "auth.v1.AuthService" type authGrpcClient struct { authenticate endpoint.Endpoint @@ -22,10 +22,10 @@ type authGrpcClient struct { timeout time.Duration } -var _ magistrala.AuthServiceClient = (*authGrpcClient)(nil) +var _ grpcAuthV1.AuthServiceClient = (*authGrpcClient)(nil) // NewAuthClient returns new auth gRPC client instance. -func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.AuthServiceClient { +func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) grpcAuthV1.AuthServiceClient { return &authGrpcClient{ authenticate: kitgrpc.NewClient( conn, @@ -33,7 +33,7 @@ func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.Auth "Authenticate", encodeIdentifyRequest, decodeIdentifyResponse, - magistrala.AuthNRes{}, + grpcAuthV1.AuthNRes{}, ).Endpoint(), authorize: kitgrpc.NewClient( conn, @@ -41,35 +41,35 @@ func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.Auth "Authorize", encodeAuthorizeRequest, decodeAuthorizeResponse, - magistrala.AuthZRes{}, + grpcAuthV1.AuthZRes{}, ).Endpoint(), timeout: timeout, } } -func (client authGrpcClient) Authenticate(ctx context.Context, token *magistrala.AuthNReq, _ ...grpc.CallOption) (*magistrala.AuthNRes, error) { +func (client authGrpcClient) Authenticate(ctx context.Context, token *grpcAuthV1.AuthNReq, _ ...grpc.CallOption) (*grpcAuthV1.AuthNRes, error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() res, err := client.authenticate(ctx, authenticateReq{token: token.GetToken()}) if err != nil { - return &magistrala.AuthNRes{}, grpcapi.DecodeError(err) + return &grpcAuthV1.AuthNRes{}, grpcapi.DecodeError(err) } ir := res.(authenticateRes) - return &magistrala.AuthNRes{Id: ir.id, UserId: ir.userID, DomainId: ir.domainID}, nil + return &grpcAuthV1.AuthNRes{Id: ir.id, UserId: ir.userID, DomainId: ir.domainID}, nil } func encodeIdentifyRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { req := grpcReq.(authenticateReq) - return &magistrala.AuthNReq{Token: req.token}, nil + return &grpcAuthV1.AuthNReq{Token: req.token}, nil } func decodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { - res := grpcRes.(*magistrala.AuthNRes) + res := grpcRes.(*grpcAuthV1.AuthNRes) return authenticateRes{id: res.GetId(), userID: res.GetUserId(), domainID: res.GetDomainId()}, nil } -func (client authGrpcClient) Authorize(ctx context.Context, req *magistrala.AuthZReq, _ ...grpc.CallOption) (r *magistrala.AuthZRes, err error) { +func (client authGrpcClient) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq, _ ...grpc.CallOption) (r *grpcAuthV1.AuthZRes, err error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() @@ -84,21 +84,21 @@ func (client authGrpcClient) Authorize(ctx context.Context, req *magistrala.Auth Object: req.GetObject(), }) if err != nil { - return &magistrala.AuthZRes{}, grpcapi.DecodeError(err) + return &grpcAuthV1.AuthZRes{}, grpcapi.DecodeError(err) } ar := res.(authorizeRes) - return &magistrala.AuthZRes{Authorized: ar.authorized, Id: ar.id}, nil + return &grpcAuthV1.AuthZRes{Authorized: ar.authorized, Id: ar.id}, nil } func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { - res := grpcRes.(*magistrala.AuthZRes) + res := grpcRes.(*grpcAuthV1.AuthZRes) return authorizeRes{authorized: res.Authorized, id: res.Id}, nil } func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { req := grpcReq.(authReq) - return &magistrala.AuthZReq{ + return &grpcAuthV1.AuthZReq{ Domain: req.Domain, SubjectType: req.SubjectType, Subject: req.Subject, diff --git a/auth/api/grpc/auth/endpoint_test.go b/auth/api/grpc/auth/endpoint_test.go index 4b920617a8..5bec5d833d 100644 --- a/auth/api/grpc/auth/endpoint_test.go +++ b/auth/api/grpc/auth/endpoint_test.go @@ -10,9 +10,9 @@ import ( "testing" "time" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" grpcapi "github.com/absmach/magistrala/auth/api/grpc/auth" + grpcAuthV1 "github.com/absmach/magistrala/internal/grpc/auth/v1" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" @@ -52,7 +52,7 @@ var ( func startGRPCServer(svc auth.Service, port int) *grpc.Server { listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port)) server := grpc.NewServer() - magistrala.RegisterAuthServiceServer(server, grpcapi.NewAuthServer(svc)) + grpcAuthV1.RegisterAuthServiceServer(server, grpcapi.NewAuthServer(svc)) go func() { err := server.Serve(listener) assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err)) @@ -69,34 +69,34 @@ func TestIdentify(t *testing.T) { cases := []struct { desc string token string - idt *magistrala.AuthNRes + idt *grpcAuthV1.AuthNRes svcErr error err error }{ { desc: "authenticate user with valid user token", token: validToken, - idt: &magistrala.AuthNRes{Id: id, UserId: email, DomainId: domainID}, + idt: &grpcAuthV1.AuthNRes{Id: id, UserId: email, DomainId: domainID}, err: nil, }, { desc: "authenticate user with invalid user token", token: "invalid", - idt: &magistrala.AuthNRes{}, + idt: &grpcAuthV1.AuthNRes{}, svcErr: svcerr.ErrAuthentication, err: svcerr.ErrAuthentication, }, { desc: "authenticate user with empty token", token: "", - idt: &magistrala.AuthNRes{}, + idt: &grpcAuthV1.AuthNRes{}, err: apiutil.ErrBearerToken, }, } for _, tc := range cases { svcCall := svc.On("Identify", mock.Anything, mock.Anything, mock.Anything).Return(auth.Key{Subject: id, User: email, Domain: domainID}, tc.svcErr) - idt, err := grpcClient.Authenticate(context.Background(), &magistrala.AuthNReq{Token: tc.token}) + idt, err := grpcClient.Authenticate(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token}) if idt != nil { assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt)) } @@ -113,14 +113,14 @@ func TestAuthorize(t *testing.T) { cases := []struct { desc string token string - authRequest *magistrala.AuthZReq - authResponse *magistrala.AuthZRes + authRequest *grpcAuthV1.AuthZReq + authResponse *grpcAuthV1.AuthZRes err error }{ { desc: "authorize user with authorized token", token: validToken, - authRequest: &magistrala.AuthZReq{ + authRequest: &grpcAuthV1.AuthZReq{ Subject: id, SubjectType: usersType, Object: authoritiesObj, @@ -128,13 +128,13 @@ func TestAuthorize(t *testing.T) { Relation: memberRelation, Permission: adminpermission, }, - authResponse: &magistrala.AuthZRes{Authorized: true}, + authResponse: &grpcAuthV1.AuthZRes{Authorized: true}, err: nil, }, { desc: "authorize user with unauthorized token", token: inValidToken, - authRequest: &magistrala.AuthZReq{ + authRequest: &grpcAuthV1.AuthZReq{ Subject: id, SubjectType: usersType, Object: authoritiesObj, @@ -142,13 +142,13 @@ func TestAuthorize(t *testing.T) { Relation: memberRelation, Permission: adminpermission, }, - authResponse: &magistrala.AuthZRes{Authorized: false}, + authResponse: &grpcAuthV1.AuthZRes{Authorized: false}, err: svcerr.ErrAuthorization, }, { desc: "authorize user with empty subject", token: validToken, - authRequest: &magistrala.AuthZReq{ + authRequest: &grpcAuthV1.AuthZReq{ Subject: "", SubjectType: usersType, Object: authoritiesObj, @@ -156,13 +156,13 @@ func TestAuthorize(t *testing.T) { Relation: memberRelation, Permission: adminpermission, }, - authResponse: &magistrala.AuthZRes{Authorized: false}, + authResponse: &grpcAuthV1.AuthZRes{Authorized: false}, err: apiutil.ErrMissingPolicySub, }, { desc: "authorize user with empty subject type", token: validToken, - authRequest: &magistrala.AuthZReq{ + authRequest: &grpcAuthV1.AuthZReq{ Subject: id, SubjectType: "", Object: authoritiesObj, @@ -170,13 +170,13 @@ func TestAuthorize(t *testing.T) { Relation: memberRelation, Permission: adminpermission, }, - authResponse: &magistrala.AuthZRes{Authorized: false}, + authResponse: &grpcAuthV1.AuthZRes{Authorized: false}, err: apiutil.ErrMissingPolicySub, }, { desc: "authorize user with empty object", token: validToken, - authRequest: &magistrala.AuthZReq{ + authRequest: &grpcAuthV1.AuthZReq{ Subject: id, SubjectType: usersType, Object: "", @@ -184,13 +184,13 @@ func TestAuthorize(t *testing.T) { Relation: memberRelation, Permission: adminpermission, }, - authResponse: &magistrala.AuthZRes{Authorized: false}, + authResponse: &grpcAuthV1.AuthZRes{Authorized: false}, err: apiutil.ErrMissingPolicyObj, }, { desc: "authorize user with empty object type", token: validToken, - authRequest: &magistrala.AuthZReq{ + authRequest: &grpcAuthV1.AuthZReq{ Subject: id, SubjectType: usersType, Object: authoritiesObj, @@ -198,13 +198,13 @@ func TestAuthorize(t *testing.T) { Relation: memberRelation, Permission: adminpermission, }, - authResponse: &magistrala.AuthZRes{Authorized: false}, + authResponse: &grpcAuthV1.AuthZRes{Authorized: false}, err: apiutil.ErrMissingPolicyObj, }, { desc: "authorize user with empty permission", token: validToken, - authRequest: &magistrala.AuthZReq{ + authRequest: &grpcAuthV1.AuthZReq{ Subject: id, SubjectType: usersType, Object: authoritiesObj, @@ -212,7 +212,7 @@ func TestAuthorize(t *testing.T) { Relation: memberRelation, Permission: "", }, - authResponse: &magistrala.AuthZRes{Authorized: false}, + authResponse: &grpcAuthV1.AuthZRes{Authorized: false}, err: apiutil.ErrMalformedPolicyPer, }, } diff --git a/auth/api/grpc/auth/server.go b/auth/api/grpc/auth/server.go index 491b915db8..af6833cf9a 100644 --- a/auth/api/grpc/auth/server.go +++ b/auth/api/grpc/auth/server.go @@ -6,22 +6,22 @@ package auth import ( "context" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" grpcapi "github.com/absmach/magistrala/auth/api/grpc" + grpcAuthV1 "github.com/absmach/magistrala/internal/grpc/auth/v1" kitgrpc "github.com/go-kit/kit/transport/grpc" ) -var _ magistrala.AuthServiceServer = (*authGrpcServer)(nil) +var _ grpcAuthV1.AuthServiceServer = (*authGrpcServer)(nil) type authGrpcServer struct { - magistrala.UnimplementedAuthServiceServer + grpcAuthV1.UnimplementedAuthServiceServer authorize kitgrpc.Handler authenticate kitgrpc.Handler } // NewAuthServer returns new AuthnServiceServer instance. -func NewAuthServer(svc auth.Service) magistrala.AuthServiceServer { +func NewAuthServer(svc auth.Service) grpcAuthV1.AuthServiceServer { return &authGrpcServer{ authorize: kitgrpc.NewServer( (authorizeEndpoint(svc)), @@ -37,34 +37,34 @@ func NewAuthServer(svc auth.Service) magistrala.AuthServiceServer { } } -func (s *authGrpcServer) Authenticate(ctx context.Context, req *magistrala.AuthNReq) (*magistrala.AuthNRes, error) { +func (s *authGrpcServer) Authenticate(ctx context.Context, req *grpcAuthV1.AuthNReq) (*grpcAuthV1.AuthNRes, error) { _, res, err := s.authenticate.ServeGRPC(ctx, req) if err != nil { return nil, grpcapi.EncodeError(err) } - return res.(*magistrala.AuthNRes), nil + return res.(*grpcAuthV1.AuthNRes), nil } -func (s *authGrpcServer) Authorize(ctx context.Context, req *magistrala.AuthZReq) (*magistrala.AuthZRes, error) { +func (s *authGrpcServer) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq) (*grpcAuthV1.AuthZRes, error) { _, res, err := s.authorize.ServeGRPC(ctx, req) if err != nil { return nil, grpcapi.EncodeError(err) } - return res.(*magistrala.AuthZRes), nil + return res.(*grpcAuthV1.AuthZRes), nil } func decodeAuthenticateRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { - req := grpcReq.(*magistrala.AuthNReq) + req := grpcReq.(*grpcAuthV1.AuthNReq) return authenticateReq{token: req.GetToken()}, nil } func encodeAuthenticateResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { res := grpcRes.(authenticateRes) - return &magistrala.AuthNRes{Id: res.id, UserId: res.userID, DomainId: res.domainID}, nil + return &grpcAuthV1.AuthNRes{Id: res.id, UserId: res.userID, DomainId: res.domainID}, nil } func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { - req := grpcReq.(*magistrala.AuthZReq) + req := grpcReq.(*grpcAuthV1.AuthZReq) return authReq{ Domain: req.GetDomain(), SubjectType: req.GetSubjectType(), @@ -79,5 +79,5 @@ func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{} func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { res := grpcRes.(authorizeRes) - return &magistrala.AuthZRes{Authorized: res.authorized, Id: res.id}, nil + return &grpcAuthV1.AuthZRes{Authorized: res.authorized, Id: res.id}, nil } diff --git a/auth/api/grpc/token/client.go b/auth/api/grpc/token/client.go index ffb8247a56..63503d3bac 100644 --- a/auth/api/grpc/token/client.go +++ b/auth/api/grpc/token/client.go @@ -7,15 +7,15 @@ import ( "context" "time" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" grpcapi "github.com/absmach/magistrala/auth/api/grpc" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/go-kit/kit/endpoint" kitgrpc "github.com/go-kit/kit/transport/grpc" "google.golang.org/grpc" ) -const tokenSvcName = "magistrala.TokenService" +const tokenSvcName = "token.v1.TokenService" type tokenGrpcClient struct { issue endpoint.Endpoint @@ -23,10 +23,10 @@ type tokenGrpcClient struct { timeout time.Duration } -var _ magistrala.TokenServiceClient = (*tokenGrpcClient)(nil) +var _ grpcTokenV1.TokenServiceClient = (*tokenGrpcClient)(nil) // NewAuthClient returns new auth gRPC client instance. -func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.TokenServiceClient { +func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) grpcTokenV1.TokenServiceClient { return &tokenGrpcClient{ issue: kitgrpc.NewClient( conn, @@ -34,7 +34,7 @@ func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.Tok "Issue", encodeIssueRequest, decodeIssueResponse, - magistrala.Token{}, + grpcTokenV1.Token{}, ).Endpoint(), refresh: kitgrpc.NewClient( conn, @@ -42,13 +42,13 @@ func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.Tok "Refresh", encodeRefreshRequest, decodeRefreshResponse, - magistrala.Token{}, + grpcTokenV1.Token{}, ).Endpoint(), timeout: timeout, } } -func (client tokenGrpcClient) Issue(ctx context.Context, req *magistrala.IssueReq, _ ...grpc.CallOption) (*magistrala.Token, error) { +func (client tokenGrpcClient) Issue(ctx context.Context, req *grpcTokenV1.IssueReq, _ ...grpc.CallOption) (*grpcTokenV1.Token, error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() @@ -57,14 +57,14 @@ func (client tokenGrpcClient) Issue(ctx context.Context, req *magistrala.IssueRe keyType: auth.KeyType(req.GetType()), }) if err != nil { - return &magistrala.Token{}, grpcapi.DecodeError(err) + return &grpcTokenV1.Token{}, grpcapi.DecodeError(err) } - return res.(*magistrala.Token), nil + return res.(*grpcTokenV1.Token), nil } func encodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { req := grpcReq.(issueReq) - return &magistrala.IssueReq{ + return &grpcTokenV1.IssueReq{ UserId: req.userID, Type: uint32(req.keyType), }, nil @@ -74,20 +74,20 @@ func decodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, e return grpcRes, nil } -func (client tokenGrpcClient) Refresh(ctx context.Context, req *magistrala.RefreshReq, _ ...grpc.CallOption) (*magistrala.Token, error) { +func (client tokenGrpcClient) Refresh(ctx context.Context, req *grpcTokenV1.RefreshReq, _ ...grpc.CallOption) (*grpcTokenV1.Token, error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() res, err := client.refresh(ctx, refreshReq{refreshToken: req.GetRefreshToken()}) if err != nil { - return &magistrala.Token{}, grpcapi.DecodeError(err) + return &grpcTokenV1.Token{}, grpcapi.DecodeError(err) } - return res.(*magistrala.Token), nil + return res.(*grpcTokenV1.Token), nil } func encodeRefreshRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { req := grpcReq.(refreshReq) - return &magistrala.RefreshReq{RefreshToken: req.refreshToken}, nil + return &grpcTokenV1.RefreshReq{RefreshToken: req.refreshToken}, nil } func decodeRefreshResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { diff --git a/auth/api/grpc/token/endpoint_test.go b/auth/api/grpc/token/endpoint_test.go index 8e0b8b7a92..c18ffd744b 100644 --- a/auth/api/grpc/token/endpoint_test.go +++ b/auth/api/grpc/token/endpoint_test.go @@ -10,9 +10,9 @@ import ( "testing" "time" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" grpcapi "github.com/absmach/magistrala/auth/api/grpc/token" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" @@ -52,7 +52,7 @@ var ( func startGRPCServer(svc auth.Service, port int) *grpc.Server { listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port)) server := grpc.NewServer() - magistrala.RegisterTokenServiceServer(server, grpcapi.NewTokenServer(svc)) + grpcTokenV1.RegisterTokenServiceServer(server, grpcapi.NewTokenServer(svc)) go func() { err := server.Serve(listener) assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err)) @@ -117,12 +117,10 @@ func TestIssue(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err) - _, err := grpcClient.Issue(context.Background(), &magistrala.IssueReq{UserId: tc.userId, Type: uint32(tc.kind)}) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) + svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err) + _, err := grpcClient.Issue(context.Background(), &grpcTokenV1.IssueReq{UserId: tc.userId, Type: uint32(tc.kind)}) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() } } @@ -161,11 +159,9 @@ func TestRefresh(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err) - _, err := grpcClient.Refresh(context.Background(), &magistrala.RefreshReq{RefreshToken: tc.token}) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() - }) + svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err) + _, err := grpcClient.Refresh(context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: tc.token}) + 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/auth/api/grpc/token/server.go b/auth/api/grpc/token/server.go index a2432b323d..83244b3367 100644 --- a/auth/api/grpc/token/server.go +++ b/auth/api/grpc/token/server.go @@ -6,22 +6,22 @@ package token import ( "context" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" grpcapi "github.com/absmach/magistrala/auth/api/grpc" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" kitgrpc "github.com/go-kit/kit/transport/grpc" ) -var _ magistrala.TokenServiceServer = (*tokenGrpcServer)(nil) +var _ grpcTokenV1.TokenServiceServer = (*tokenGrpcServer)(nil) type tokenGrpcServer struct { - magistrala.UnimplementedTokenServiceServer + grpcTokenV1.UnimplementedTokenServiceServer issue kitgrpc.Handler refresh kitgrpc.Handler } // NewAuthServer returns new AuthnServiceServer instance. -func NewTokenServer(svc auth.Service) magistrala.TokenServiceServer { +func NewTokenServer(svc auth.Service) grpcTokenV1.TokenServiceServer { return &tokenGrpcServer{ issue: kitgrpc.NewServer( (issueEndpoint(svc)), @@ -36,24 +36,24 @@ func NewTokenServer(svc auth.Service) magistrala.TokenServiceServer { } } -func (s *tokenGrpcServer) Issue(ctx context.Context, req *magistrala.IssueReq) (*magistrala.Token, error) { +func (s *tokenGrpcServer) Issue(ctx context.Context, req *grpcTokenV1.IssueReq) (*grpcTokenV1.Token, error) { _, res, err := s.issue.ServeGRPC(ctx, req) if err != nil { return nil, grpcapi.EncodeError(err) } - return res.(*magistrala.Token), nil + return res.(*grpcTokenV1.Token), nil } -func (s *tokenGrpcServer) Refresh(ctx context.Context, req *magistrala.RefreshReq) (*magistrala.Token, error) { +func (s *tokenGrpcServer) Refresh(ctx context.Context, req *grpcTokenV1.RefreshReq) (*grpcTokenV1.Token, error) { _, res, err := s.refresh.ServeGRPC(ctx, req) if err != nil { return nil, grpcapi.EncodeError(err) } - return res.(*magistrala.Token), nil + return res.(*grpcTokenV1.Token), nil } func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { - req := grpcReq.(*magistrala.IssueReq) + req := grpcReq.(*grpcTokenV1.IssueReq) return issueReq{ userID: req.GetUserId(), keyType: auth.KeyType(req.GetType()), @@ -61,14 +61,14 @@ func decodeIssueRequest(_ context.Context, grpcReq interface{}) (interface{}, er } func decodeRefreshRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { - req := grpcReq.(*magistrala.RefreshReq) + req := grpcReq.(*grpcTokenV1.RefreshReq) return refreshReq{refreshToken: req.GetRefreshToken()}, nil } func encodeIssueResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { res := grpcRes.(issueRes) - return &magistrala.Token{ + return &grpcTokenV1.Token{ AccessToken: res.accessToken, RefreshToken: &res.refreshToken, AccessType: res.accessType, diff --git a/auth/api/http/domains/endpoint.go b/auth/api/http/domains/endpoint.go deleted file mode 100644 index ffb00a36e8..0000000000 --- a/auth/api/http/domains/endpoint.go +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package domains - -import ( - "context" - - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/pkg/apiutil" - "github.com/absmach/magistrala/pkg/errors" - "github.com/go-kit/kit/endpoint" -) - -func createDomainEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(createDomainReq) - if err := req.validate(); err != nil { - return nil, err - } - - d := auth.Domain{ - Name: req.Name, - Metadata: req.Metadata, - Tags: req.Tags, - Alias: req.Alias, - } - domain, err := svc.CreateDomain(ctx, req.token, d) - if err != nil { - return nil, err - } - - return createDomainRes{domain}, nil - } -} - -func retrieveDomainEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(retrieveDomainRequest) - if err := req.validate(); err != nil { - return nil, err - } - - domain, err := svc.RetrieveDomain(ctx, req.token, req.domainID) - if err != nil { - return nil, err - } - return retrieveDomainRes{domain}, nil - } -} - -func retrieveDomainPermissionsEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(retrieveDomainPermissionsRequest) - if err := req.validate(); err != nil { - return nil, err - } - - permissions, err := svc.RetrieveDomainPermissions(ctx, req.token, req.domainID) - if err != nil { - return nil, err - } - return retrieveDomainPermissionsRes{Permissions: permissions}, nil - } -} - -func updateDomainEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(updateDomainReq) - if err := req.validate(); err != nil { - return nil, err - } - - var metadata auth.Metadata - if req.Metadata != nil { - metadata = *req.Metadata - } - d := auth.DomainReq{ - Name: req.Name, - Metadata: &metadata, - Tags: req.Tags, - Alias: req.Alias, - } - domain, err := svc.UpdateDomain(ctx, req.token, req.domainID, d) - if err != nil { - return nil, err - } - - return updateDomainRes{domain}, nil - } -} - -func listDomainsEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(listDomainsReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - page := auth.Page{ - Offset: req.offset, - Limit: req.limit, - Name: req.name, - Metadata: req.metadata, - Order: req.order, - Dir: req.dir, - Tag: req.tag, - Permission: req.permission, - Status: req.status, - } - dp, err := svc.ListDomains(ctx, req.token, page) - if err != nil { - return nil, err - } - return listDomainsRes{dp}, nil - } -} - -func enableDomainEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(enableDomainReq) - if err := req.validate(); err != nil { - return nil, err - } - - enable := auth.EnabledStatus - d := auth.DomainReq{ - Status: &enable, - } - if _, err := svc.ChangeDomainStatus(ctx, req.token, req.domainID, d); err != nil { - return nil, err - } - return enableDomainRes{}, nil - } -} - -func disableDomainEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(disableDomainReq) - if err := req.validate(); err != nil { - return nil, err - } - - disable := auth.DisabledStatus - d := auth.DomainReq{ - Status: &disable, - } - if _, err := svc.ChangeDomainStatus(ctx, req.token, req.domainID, d); err != nil { - return nil, err - } - return disableDomainRes{}, nil - } -} - -func freezeDomainEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(freezeDomainReq) - if err := req.validate(); err != nil { - return nil, err - } - - freeze := auth.FreezeStatus - d := auth.DomainReq{ - Status: &freeze, - } - if _, err := svc.ChangeDomainStatus(ctx, req.token, req.domainID, d); err != nil { - return nil, err - } - return freezeDomainRes{}, nil - } -} - -func assignDomainUsersEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignUsersReq) - if err := req.validate(); err != nil { - return nil, err - } - - if err := svc.AssignUsers(ctx, req.token, req.domainID, req.UserIDs, req.Relation); err != nil { - return nil, err - } - return assignUsersRes{}, nil - } -} - -func unassignDomainUserEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(unassignUserReq) - if err := req.validate(); err != nil { - return nil, err - } - - if err := svc.UnassignUser(ctx, req.token, req.domainID, req.UserID); err != nil { - return nil, err - } - return unassignUsersRes{}, nil - } -} - -func listUserDomainsEndpoint(svc auth.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(listUserDomainsReq) - if err := req.validate(); err != nil { - return nil, err - } - - page := auth.Page{ - Offset: req.offset, - Limit: req.limit, - Name: req.name, - Metadata: req.metadata, - Order: req.order, - Dir: req.dir, - Tag: req.tag, - Permission: req.permission, - Status: req.status, - } - dp, err := svc.ListUserDomains(ctx, req.token, req.userID, page) - if err != nil { - return nil, err - } - return listUserDomainsRes{dp}, nil - } -} diff --git a/auth/api/http/keys/endpoint_test.go b/auth/api/http/keys/endpoint_test.go index 4ed62a340d..ede95053a8 100644 --- a/auth/api/http/keys/endpoint_test.go +++ b/auth/api/http/keys/endpoint_test.go @@ -69,14 +69,12 @@ func (tr testRequest) make() (*http.Response, error) { func newService() (auth.Service, *mocks.KeyRepository) { krepo := new(mocks.KeyRepository) - drepo := new(mocks.DomainsRepository) idProvider := uuid.NewMock() pService := new(policymocks.Service) pEvaluator := new(policymocks.Evaluator) - t := jwt.New([]byte(secret)) - return auth.New(krepo, drepo, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration), krepo + return auth.New(krepo, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration), krepo } func newServer(svc auth.Service) *httptest.Server { diff --git a/auth/api/http/transport.go b/auth/api/http/transport.go index 5e31ee553f..27d0214934 100644 --- a/auth/api/http/transport.go +++ b/auth/api/http/transport.go @@ -8,7 +8,6 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/auth/api/http/domains" "github.com/absmach/magistrala/auth/api/http/keys" "github.com/go-chi/chi/v5" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -19,7 +18,6 @@ func MakeHandler(svc auth.Service, logger *slog.Logger, instanceID string) http. mux := chi.NewRouter() mux = keys.MakeHandler(svc, mux, logger) - mux = domains.MakeHandler(svc, mux, logger) mux.Get("/health", magistrala.Health("auth", instanceID)) mux.Handle("/metrics", promhttp.Handler()) diff --git a/auth/api/logging.go b/auth/api/logging.go index 30182bb4c4..c53bbc4cd9 100644 --- a/auth/api/logging.go +++ b/auth/api/logging.go @@ -124,180 +124,3 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy) }(time.Now()) return lm.svc.Authorize(ctx, pr) } - -func (lm *loggingMiddleware) CreateDomain(ctx context.Context, token string, d auth.Domain) (do auth.Domain, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.Group("domain", - slog.String("id", d.ID), - slog.String("name", d.Name), - ), - } - if err != nil { - args := append(args, slog.String("error", err.Error())) - lm.logger.Warn("Create domain failed", args...) - return - } - lm.logger.Info("Create domain completed successfully", args...) - }(time.Now()) - return lm.svc.CreateDomain(ctx, token, d) -} - -func (lm *loggingMiddleware) RetrieveDomain(ctx context.Context, token, id string) (do auth.Domain, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", id), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Retrieve domain failed", args...) - return - } - lm.logger.Info("Retrieve domain completed successfully", args...) - }(time.Now()) - return lm.svc.RetrieveDomain(ctx, token, id) -} - -func (lm *loggingMiddleware) RetrieveDomainPermissions(ctx context.Context, token, id string) (permissions policies.Permissions, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", id), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Retrieve domain permissions failed", args...) - return - } - lm.logger.Info("Retrieve domain permissions completed successfully", args...) - }(time.Now()) - return lm.svc.RetrieveDomainPermissions(ctx, token, id) -} - -func (lm *loggingMiddleware) UpdateDomain(ctx context.Context, token, id string, d auth.DomainReq) (do auth.Domain, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.Group("domain", - slog.String("id", id), - slog.Any("name", d.Name), - ), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Update domain failed", args...) - return - } - lm.logger.Info("Update domain completed successfully", args...) - }(time.Now()) - return lm.svc.UpdateDomain(ctx, token, id, d) -} - -func (lm *loggingMiddleware) ChangeDomainStatus(ctx context.Context, token, id string, d auth.DomainReq) (do auth.Domain, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.Group("domain", - slog.String("id", id), - slog.String("name", do.Name), - slog.Any("status", d.Status), - ), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Change domain status failed", args...) - return - } - lm.logger.Info("Change domain status completed successfully", args...) - }(time.Now()) - return lm.svc.ChangeDomainStatus(ctx, token, id, d) -} - -func (lm *loggingMiddleware) ListDomains(ctx context.Context, token string, page auth.Page) (do auth.DomainsPage, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.Group("page", - slog.Uint64("limit", page.Limit), - slog.Uint64("offset", page.Offset), - slog.Uint64("total", page.Total), - ), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("List domains failed", args...) - return - } - lm.logger.Info("List domains completed successfully", args...) - }(time.Now()) - return lm.svc.ListDomains(ctx, token, page) -} - -func (lm *loggingMiddleware) AssignUsers(ctx context.Context, token, id string, userIds []string, relation string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", id), - slog.String("relation", relation), - slog.Any("user_ids", userIds), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Assign users to domain failed", args...) - return - } - lm.logger.Info("Assign users to domain completed successfully", args...) - }(time.Now()) - return lm.svc.AssignUsers(ctx, token, id, userIds, relation) -} - -func (lm *loggingMiddleware) UnassignUser(ctx context.Context, token, id, userID string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("domain_id", id), - slog.Any("user_id", userID), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Unassign user from domain failed", args...) - return - } - lm.logger.Info("Unassign user from domain completed successfully", args...) - }(time.Now()) - return lm.svc.UnassignUser(ctx, token, id, userID) -} - -func (lm *loggingMiddleware) ListUserDomains(ctx context.Context, token, userID string, page auth.Page) (do auth.DomainsPage, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("user_id", userID), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("List user domains failed", args...) - return - } - lm.logger.Info("List user domains completed successfully", args...) - }(time.Now()) - return lm.svc.ListUserDomains(ctx, token, userID, page) -} - -func (lm *loggingMiddleware) DeleteUserFromDomains(ctx context.Context, id string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("id", id), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Delete entity policies failed to complete successfully", args...) - return - } - lm.logger.Info("Delete entity policies completed successfully", args...) - }(time.Now()) - return lm.svc.DeleteUserFromDomains(ctx, id) -} diff --git a/auth/api/metrics.go b/auth/api/metrics.go index 1e2befa8d2..7048ee3ca5 100644 --- a/auth/api/metrics.go +++ b/auth/api/metrics.go @@ -74,83 +74,3 @@ func (ms *metricsMiddleware) Authorize(ctx context.Context, pr policies.Policy) }(time.Now()) return ms.svc.Authorize(ctx, pr) } - -func (ms *metricsMiddleware) CreateDomain(ctx context.Context, token string, d auth.Domain) (auth.Domain, error) { - defer func(begin time.Time) { - ms.counter.With("method", "create_domain").Add(1) - ms.latency.With("method", "create_domain").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.CreateDomain(ctx, token, d) -} - -func (ms *metricsMiddleware) RetrieveDomain(ctx context.Context, token, id string) (auth.Domain, error) { - defer func(begin time.Time) { - ms.counter.With("method", "retrieve_domain").Add(1) - ms.latency.With("method", "retrieve_domain").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.RetrieveDomain(ctx, token, id) -} - -func (ms *metricsMiddleware) RetrieveDomainPermissions(ctx context.Context, token, id string) (policies.Permissions, error) { - defer func(begin time.Time) { - ms.counter.With("method", "retrieve_domain_permissions").Add(1) - ms.latency.With("method", "retrieve_domain_permissions").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.RetrieveDomainPermissions(ctx, token, id) -} - -func (ms *metricsMiddleware) UpdateDomain(ctx context.Context, token, id string, d auth.DomainReq) (auth.Domain, error) { - defer func(begin time.Time) { - ms.counter.With("method", "update_domain").Add(1) - ms.latency.With("method", "update_domain").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.UpdateDomain(ctx, token, id, d) -} - -func (ms *metricsMiddleware) ChangeDomainStatus(ctx context.Context, token, id string, d auth.DomainReq) (auth.Domain, error) { - defer func(begin time.Time) { - ms.counter.With("method", "change_domain_status").Add(1) - ms.latency.With("method", "change_domain_status").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.ChangeDomainStatus(ctx, token, id, d) -} - -func (ms *metricsMiddleware) ListDomains(ctx context.Context, token string, page auth.Page) (auth.DomainsPage, error) { - defer func(begin time.Time) { - ms.counter.With("method", "list_domains").Add(1) - ms.latency.With("method", "list_domains").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.ListDomains(ctx, token, page) -} - -func (ms *metricsMiddleware) AssignUsers(ctx context.Context, token, id string, userIds []string, relation string) error { - defer func(begin time.Time) { - ms.counter.With("method", "assign_users").Add(1) - ms.latency.With("method", "assign_users").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.AssignUsers(ctx, token, id, userIds, relation) -} - -func (ms *metricsMiddleware) UnassignUser(ctx context.Context, token, id, userID string) error { - defer func(begin time.Time) { - ms.counter.With("method", "unassign_users").Add(1) - ms.latency.With("method", "unassign_users").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.UnassignUser(ctx, token, id, userID) -} - -func (ms *metricsMiddleware) ListUserDomains(ctx context.Context, token, userID string, page auth.Page) (auth.DomainsPage, error) { - defer func(begin time.Time) { - ms.counter.With("method", "list_user_domains").Add(1) - ms.latency.With("method", "list_user_domains").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.ListUserDomains(ctx, token, userID, page) -} - -func (ms *metricsMiddleware) DeleteUserFromDomains(ctx context.Context, id string) error { - defer func(begin time.Time) { - ms.counter.With("method", "delete_user_from_domains").Add(1) - ms.latency.With("method", "delete_user_from_domains").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.DeleteUserFromDomains(ctx, id) -} diff --git a/auth/events/streams.go b/auth/events/streams.go deleted file mode 100644 index 702242cfb0..0000000000 --- a/auth/events/streams.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package events - -import ( - "context" - - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/pkg/events/store" - "github.com/absmach/magistrala/pkg/policies" -) - -const streamID = "magistrala.auth" - -var _ auth.Service = (*eventStore)(nil) - -type eventStore struct { - events.Publisher - svc auth.Service -} - -// NewEventStoreMiddleware returns wrapper around auth service that sends -// events to event store. -func NewEventStoreMiddleware(ctx context.Context, svc auth.Service, url string) (auth.Service, error) { - publisher, err := store.NewPublisher(ctx, url, streamID) - if err != nil { - return nil, err - } - - return &eventStore{ - svc: svc, - Publisher: publisher, - }, nil -} - -func (es *eventStore) CreateDomain(ctx context.Context, token string, domain auth.Domain) (auth.Domain, error) { - domain, err := es.svc.CreateDomain(ctx, token, domain) - if err != nil { - return domain, err - } - - event := createDomainEvent{ - domain, - } - - if err := es.Publish(ctx, event); err != nil { - return domain, err - } - - return domain, nil -} - -func (es *eventStore) RetrieveDomain(ctx context.Context, token, id string) (auth.Domain, error) { - domain, err := es.svc.RetrieveDomain(ctx, token, id) - if err != nil { - return domain, err - } - - event := retrieveDomainEvent{ - domain, - } - - if err := es.Publish(ctx, event); err != nil { - return domain, err - } - - return domain, nil -} - -func (es *eventStore) RetrieveDomainPermissions(ctx context.Context, token, id string) (policies.Permissions, error) { - permissions, err := es.svc.RetrieveDomainPermissions(ctx, token, id) - if err != nil { - return permissions, err - } - - event := retrieveDomainPermissionsEvent{ - domainID: id, - permissions: permissions, - } - - if err := es.Publish(ctx, event); err != nil { - return permissions, err - } - - return permissions, nil -} - -func (es *eventStore) UpdateDomain(ctx context.Context, token, id string, d auth.DomainReq) (auth.Domain, error) { - domain, err := es.svc.UpdateDomain(ctx, token, id, d) - if err != nil { - return domain, err - } - - event := updateDomainEvent{ - domain, - } - - if err := es.Publish(ctx, event); err != nil { - return domain, err - } - - return domain, nil -} - -func (es *eventStore) ChangeDomainStatus(ctx context.Context, token, id string, d auth.DomainReq) (auth.Domain, error) { - domain, err := es.svc.ChangeDomainStatus(ctx, token, id, d) - if err != nil { - return domain, err - } - - event := changeDomainStatusEvent{ - domainID: id, - status: domain.Status, - updatedAt: domain.UpdatedAt, - updatedBy: domain.UpdatedBy, - } - - if err := es.Publish(ctx, event); err != nil { - return domain, err - } - - return domain, nil -} - -func (es *eventStore) ListDomains(ctx context.Context, token string, p auth.Page) (auth.DomainsPage, error) { - dp, err := es.svc.ListDomains(ctx, token, p) - if err != nil { - return dp, err - } - - event := listDomainsEvent{ - p, dp.Total, - } - - if err := es.Publish(ctx, event); err != nil { - return dp, err - } - - return dp, nil -} - -func (es *eventStore) AssignUsers(ctx context.Context, token, id string, userIds []string, relation string) error { - err := es.svc.AssignUsers(ctx, token, id, userIds, relation) - if err != nil { - return err - } - - event := assignUsersEvent{ - domainID: id, - userIDs: userIds, - relation: relation, - } - - if err := es.Publish(ctx, event); err != nil { - return err - } - - return nil -} - -func (es *eventStore) UnassignUser(ctx context.Context, token, id, userID string) error { - err := es.svc.UnassignUser(ctx, token, id, userID) - if err != nil { - return err - } - - event := unassignUsersEvent{ - domainID: id, - userID: userID, - } - - if err := es.Publish(ctx, event); err != nil { - return err - } - - return nil -} - -func (es *eventStore) ListUserDomains(ctx context.Context, token, userID string, p auth.Page) (auth.DomainsPage, error) { - dp, err := es.svc.ListUserDomains(ctx, token, userID, p) - if err != nil { - return dp, err - } - - event := listUserDomainsEvent{ - Page: p, - userID: userID, - } - - if err := es.Publish(ctx, event); err != nil { - return dp, err - } - - return dp, nil -} - -func (es *eventStore) Issue(ctx context.Context, token string, key auth.Key) (auth.Token, error) { - return es.svc.Issue(ctx, token, key) -} - -func (es *eventStore) Revoke(ctx context.Context, token, id string) error { - return es.svc.Revoke(ctx, token, id) -} - -func (es *eventStore) RetrieveKey(ctx context.Context, token, id string) (auth.Key, error) { - return es.svc.RetrieveKey(ctx, token, id) -} - -func (es *eventStore) Identify(ctx context.Context, token string) (auth.Key, error) { - return es.svc.Identify(ctx, token) -} - -func (es *eventStore) Authorize(ctx context.Context, pr policies.Policy) error { - return es.svc.Authorize(ctx, pr) -} - -func (es *eventStore) DeleteUserFromDomains(ctx context.Context, id string) error { - return es.svc.DeleteUserFromDomains(ctx, id) -} diff --git a/auth/mocks/domains.go b/auth/mocks/domains.go deleted file mode 100644 index c9bc09c95f..0000000000 --- a/auth/mocks/domains.go +++ /dev/null @@ -1,306 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -// Copyright (c) Abstract Machines - -package mocks - -import ( - context "context" - - auth "github.com/absmach/magistrala/auth" - - mock "github.com/stretchr/testify/mock" -) - -// DomainsRepository is an autogenerated mock type for the DomainsRepository type -type DomainsRepository struct { - mock.Mock -} - -// CheckPolicy provides a mock function with given fields: ctx, pc -func (_m *DomainsRepository) CheckPolicy(ctx context.Context, pc auth.Policy) error { - ret := _m.Called(ctx, pc) - - if len(ret) == 0 { - panic("no return value specified for CheckPolicy") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, auth.Policy) error); ok { - r0 = rf(ctx, pc) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Delete provides a mock function with given fields: ctx, id -func (_m *DomainsRepository) Delete(ctx context.Context, id string) error { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// DeletePolicies provides a mock function with given fields: ctx, pcs -func (_m *DomainsRepository) DeletePolicies(ctx context.Context, pcs ...auth.Policy) error { - _va := make([]interface{}, len(pcs)) - for _i := range pcs { - _va[_i] = pcs[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for DeletePolicies") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, ...auth.Policy) error); ok { - r0 = rf(ctx, pcs...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// DeleteUserPolicies provides a mock function with given fields: ctx, id -func (_m *DomainsRepository) DeleteUserPolicies(ctx context.Context, id string) error { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for DeleteUserPolicies") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ListDomains provides a mock function with given fields: ctx, pm -func (_m *DomainsRepository) ListDomains(ctx context.Context, pm auth.Page) (auth.DomainsPage, error) { - ret := _m.Called(ctx, pm) - - if len(ret) == 0 { - panic("no return value specified for ListDomains") - } - - var r0 auth.DomainsPage - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, auth.Page) (auth.DomainsPage, error)); ok { - return rf(ctx, pm) - } - if rf, ok := ret.Get(0).(func(context.Context, auth.Page) auth.DomainsPage); ok { - r0 = rf(ctx, pm) - } else { - r0 = ret.Get(0).(auth.DomainsPage) - } - - if rf, ok := ret.Get(1).(func(context.Context, auth.Page) error); ok { - r1 = rf(ctx, pm) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RetrieveAllByIDs provides a mock function with given fields: ctx, pm -func (_m *DomainsRepository) RetrieveAllByIDs(ctx context.Context, pm auth.Page) (auth.DomainsPage, error) { - ret := _m.Called(ctx, pm) - - if len(ret) == 0 { - panic("no return value specified for RetrieveAllByIDs") - } - - var r0 auth.DomainsPage - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, auth.Page) (auth.DomainsPage, error)); ok { - return rf(ctx, pm) - } - if rf, ok := ret.Get(0).(func(context.Context, auth.Page) auth.DomainsPage); ok { - r0 = rf(ctx, pm) - } else { - r0 = ret.Get(0).(auth.DomainsPage) - } - - if rf, ok := ret.Get(1).(func(context.Context, auth.Page) error); ok { - r1 = rf(ctx, pm) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RetrieveByID provides a mock function with given fields: ctx, id -func (_m *DomainsRepository) RetrieveByID(ctx context.Context, id string) (auth.Domain, error) { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for RetrieveByID") - } - - var r0 auth.Domain - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (auth.Domain, error)); ok { - return rf(ctx, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string) auth.Domain); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Get(0).(auth.Domain) - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RetrievePermissions provides a mock function with given fields: ctx, subject, id -func (_m *DomainsRepository) RetrievePermissions(ctx context.Context, subject string, id string) ([]string, error) { - ret := _m.Called(ctx, subject, id) - - if len(ret) == 0 { - panic("no return value specified for RetrievePermissions") - } - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]string, error)); ok { - return rf(ctx, subject, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []string); ok { - r0 = rf(ctx, subject, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, subject, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Save provides a mock function with given fields: ctx, d -func (_m *DomainsRepository) Save(ctx context.Context, d auth.Domain) (auth.Domain, error) { - ret := _m.Called(ctx, d) - - if len(ret) == 0 { - panic("no return value specified for Save") - } - - var r0 auth.Domain - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, auth.Domain) (auth.Domain, error)); ok { - return rf(ctx, d) - } - if rf, ok := ret.Get(0).(func(context.Context, auth.Domain) auth.Domain); ok { - r0 = rf(ctx, d) - } else { - r0 = ret.Get(0).(auth.Domain) - } - - if rf, ok := ret.Get(1).(func(context.Context, auth.Domain) error); ok { - r1 = rf(ctx, d) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SavePolicies provides a mock function with given fields: ctx, pcs -func (_m *DomainsRepository) SavePolicies(ctx context.Context, pcs ...auth.Policy) error { - _va := make([]interface{}, len(pcs)) - for _i := range pcs { - _va[_i] = pcs[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for SavePolicies") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, ...auth.Policy) error); ok { - r0 = rf(ctx, pcs...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Update provides a mock function with given fields: ctx, id, userID, d -func (_m *DomainsRepository) Update(ctx context.Context, id string, userID string, d auth.DomainReq) (auth.Domain, error) { - ret := _m.Called(ctx, id, userID, d) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 auth.Domain - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.DomainReq) (auth.Domain, error)); ok { - return rf(ctx, id, userID, d) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.DomainReq) auth.Domain); ok { - r0 = rf(ctx, id, userID, d) - } else { - r0 = ret.Get(0).(auth.Domain) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, auth.DomainReq) error); ok { - r1 = rf(ctx, id, userID, d) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewDomainsRepository creates a new instance of DomainsRepository. 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 NewDomainsRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *DomainsRepository { - mock := &DomainsRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/auth/mocks/service.go b/auth/mocks/service.go index 80ec2714fb..fae27a98be 100644 --- a/auth/mocks/service.go +++ b/auth/mocks/service.go @@ -19,24 +19,6 @@ type Service struct { mock.Mock } -// AssignUsers provides a mock function with given fields: ctx, token, id, userIds, relation -func (_m *Service) AssignUsers(ctx context.Context, token string, id string, userIds []string, relation string) error { - ret := _m.Called(ctx, token, id, userIds, relation) - - if len(ret) == 0 { - panic("no return value specified for AssignUsers") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, []string, string) error); ok { - r0 = rf(ctx, token, id, userIds, relation) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Authorize provides a mock function with given fields: ctx, pr func (_m *Service) Authorize(ctx context.Context, pr policies.Policy) error { ret := _m.Called(ctx, pr) @@ -55,80 +37,6 @@ func (_m *Service) Authorize(ctx context.Context, pr policies.Policy) error { return r0 } -// ChangeDomainStatus provides a mock function with given fields: ctx, token, id, d -func (_m *Service) ChangeDomainStatus(ctx context.Context, token string, id string, d auth.DomainReq) (auth.Domain, error) { - ret := _m.Called(ctx, token, id, d) - - if len(ret) == 0 { - panic("no return value specified for ChangeDomainStatus") - } - - var r0 auth.Domain - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.DomainReq) (auth.Domain, error)); ok { - return rf(ctx, token, id, d) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.DomainReq) auth.Domain); ok { - r0 = rf(ctx, token, id, d) - } else { - r0 = ret.Get(0).(auth.Domain) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, auth.DomainReq) error); ok { - r1 = rf(ctx, token, id, d) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CreateDomain provides a mock function with given fields: ctx, token, d -func (_m *Service) CreateDomain(ctx context.Context, token string, d auth.Domain) (auth.Domain, error) { - ret := _m.Called(ctx, token, d) - - if len(ret) == 0 { - panic("no return value specified for CreateDomain") - } - - var r0 auth.Domain - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, auth.Domain) (auth.Domain, error)); ok { - return rf(ctx, token, d) - } - if rf, ok := ret.Get(0).(func(context.Context, string, auth.Domain) auth.Domain); ok { - r0 = rf(ctx, token, d) - } else { - r0 = ret.Get(0).(auth.Domain) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, auth.Domain) error); ok { - r1 = rf(ctx, token, d) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// DeleteUserFromDomains provides a mock function with given fields: ctx, id -func (_m *Service) DeleteUserFromDomains(ctx context.Context, id string) error { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for DeleteUserFromDomains") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Identify provides a mock function with given fields: ctx, token func (_m *Service) Identify(ctx context.Context, token string) (auth.Key, error) { ret := _m.Called(ctx, token) @@ -185,120 +93,6 @@ func (_m *Service) Issue(ctx context.Context, token string, key auth.Key) (auth. return r0, r1 } -// ListDomains provides a mock function with given fields: ctx, token, page -func (_m *Service) ListDomains(ctx context.Context, token string, page auth.Page) (auth.DomainsPage, error) { - ret := _m.Called(ctx, token, page) - - if len(ret) == 0 { - panic("no return value specified for ListDomains") - } - - var r0 auth.DomainsPage - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, auth.Page) (auth.DomainsPage, error)); ok { - return rf(ctx, token, page) - } - if rf, ok := ret.Get(0).(func(context.Context, string, auth.Page) auth.DomainsPage); ok { - r0 = rf(ctx, token, page) - } else { - r0 = ret.Get(0).(auth.DomainsPage) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, auth.Page) error); ok { - r1 = rf(ctx, token, page) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ListUserDomains provides a mock function with given fields: ctx, token, userID, page -func (_m *Service) ListUserDomains(ctx context.Context, token string, userID string, page auth.Page) (auth.DomainsPage, error) { - ret := _m.Called(ctx, token, userID, page) - - if len(ret) == 0 { - panic("no return value specified for ListUserDomains") - } - - var r0 auth.DomainsPage - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.Page) (auth.DomainsPage, error)); ok { - return rf(ctx, token, userID, page) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.Page) auth.DomainsPage); ok { - r0 = rf(ctx, token, userID, page) - } else { - r0 = ret.Get(0).(auth.DomainsPage) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, auth.Page) error); ok { - r1 = rf(ctx, token, userID, page) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RetrieveDomain provides a mock function with given fields: ctx, token, id -func (_m *Service) RetrieveDomain(ctx context.Context, token string, id string) (auth.Domain, error) { - ret := _m.Called(ctx, token, id) - - if len(ret) == 0 { - panic("no return value specified for RetrieveDomain") - } - - var r0 auth.Domain - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (auth.Domain, error)); ok { - return rf(ctx, token, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) auth.Domain); ok { - r0 = rf(ctx, token, id) - } else { - r0 = ret.Get(0).(auth.Domain) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, token, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RetrieveDomainPermissions provides a mock function with given fields: ctx, token, id -func (_m *Service) RetrieveDomainPermissions(ctx context.Context, token string, id string) (policies.Permissions, error) { - ret := _m.Called(ctx, token, id) - - if len(ret) == 0 { - panic("no return value specified for RetrieveDomainPermissions") - } - - var r0 policies.Permissions - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (policies.Permissions, error)); ok { - return rf(ctx, token, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) policies.Permissions); ok { - r0 = rf(ctx, token, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(policies.Permissions) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, token, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // RetrieveKey provides a mock function with given fields: ctx, token, id func (_m *Service) RetrieveKey(ctx context.Context, token string, id string) (auth.Key, error) { ret := _m.Called(ctx, token, id) @@ -345,52 +139,6 @@ func (_m *Service) Revoke(ctx context.Context, token string, id string) error { return r0 } -// UnassignUser provides a mock function with given fields: ctx, token, id, userID -func (_m *Service) UnassignUser(ctx context.Context, token string, id string, userID string) error { - ret := _m.Called(ctx, token, id, userID) - - if len(ret) == 0 { - panic("no return value specified for UnassignUser") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { - r0 = rf(ctx, token, id, userID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UpdateDomain provides a mock function with given fields: ctx, token, id, d -func (_m *Service) UpdateDomain(ctx context.Context, token string, id string, d auth.DomainReq) (auth.Domain, error) { - ret := _m.Called(ctx, token, id, d) - - if len(ret) == 0 { - panic("no return value specified for UpdateDomain") - } - - var r0 auth.Domain - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.DomainReq) (auth.Domain, error)); ok { - return rf(ctx, token, id, d) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, auth.DomainReq) auth.Domain); ok { - r0 = rf(ctx, token, id, d) - } else { - r0 = ret.Get(0).(auth.Domain) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, auth.DomainReq) error); ok { - r1 = rf(ctx, token, id, d) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // NewService creates a new instance of Service. 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 NewService(t interface { diff --git a/auth/mocks/token_client.go b/auth/mocks/token_client.go index ae2e03e7fb..dce85cf1b4 100644 --- a/auth/mocks/token_client.go +++ b/auth/mocks/token_client.go @@ -11,9 +11,9 @@ import ( grpc "google.golang.org/grpc" - magistrala "github.com/absmach/magistrala" - mock "github.com/stretchr/testify/mock" + + v1 "github.com/absmach/magistrala/internal/grpc/token/v1" ) // TokenServiceClient is an autogenerated mock type for the TokenServiceClient type @@ -30,7 +30,7 @@ func (_m *TokenServiceClient) EXPECT() *TokenServiceClient_Expecter { } // Issue provides a mock function with given fields: ctx, in, opts -func (_m *TokenServiceClient) Issue(ctx context.Context, in *magistrala.IssueReq, opts ...grpc.CallOption) (*magistrala.Token, error) { +func (_m *TokenServiceClient) Issue(ctx context.Context, in *v1.IssueReq, opts ...grpc.CallOption) (*v1.Token, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] @@ -44,20 +44,20 @@ func (_m *TokenServiceClient) Issue(ctx context.Context, in *magistrala.IssueReq panic("no return value specified for Issue") } - var r0 *magistrala.Token + var r0 *v1.Token var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.IssueReq, ...grpc.CallOption) (*magistrala.Token, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.IssueReq, ...grpc.CallOption) (*v1.Token, error)); ok { return rf(ctx, in, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.IssueReq, ...grpc.CallOption) *magistrala.Token); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.IssueReq, ...grpc.CallOption) *v1.Token); ok { r0 = rf(ctx, in, opts...) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*magistrala.Token) + r0 = ret.Get(0).(*v1.Token) } } - if rf, ok := ret.Get(1).(func(context.Context, *magistrala.IssueReq, ...grpc.CallOption) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *v1.IssueReq, ...grpc.CallOption) error); ok { r1 = rf(ctx, in, opts...) } else { r1 = ret.Error(1) @@ -73,14 +73,14 @@ type TokenServiceClient_Issue_Call struct { // Issue is a helper method to define mock.On call // - ctx context.Context -// - in *magistrala.IssueReq +// - in *v1.IssueReq // - opts ...grpc.CallOption func (_e *TokenServiceClient_Expecter) Issue(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_Issue_Call { return &TokenServiceClient_Issue_Call{Call: _e.mock.On("Issue", append([]interface{}{ctx, in}, opts...)...)} } -func (_c *TokenServiceClient_Issue_Call) Run(run func(ctx context.Context, in *magistrala.IssueReq, opts ...grpc.CallOption)) *TokenServiceClient_Issue_Call { +func (_c *TokenServiceClient_Issue_Call) Run(run func(ctx context.Context, in *v1.IssueReq, opts ...grpc.CallOption)) *TokenServiceClient_Issue_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]grpc.CallOption, len(args)-2) for i, a := range args[2:] { @@ -88,23 +88,23 @@ func (_c *TokenServiceClient_Issue_Call) Run(run func(ctx context.Context, in *m variadicArgs[i] = a.(grpc.CallOption) } } - run(args[0].(context.Context), args[1].(*magistrala.IssueReq), variadicArgs...) + run(args[0].(context.Context), args[1].(*v1.IssueReq), variadicArgs...) }) return _c } -func (_c *TokenServiceClient_Issue_Call) Return(_a0 *magistrala.Token, _a1 error) *TokenServiceClient_Issue_Call { +func (_c *TokenServiceClient_Issue_Call) Return(_a0 *v1.Token, _a1 error) *TokenServiceClient_Issue_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *TokenServiceClient_Issue_Call) RunAndReturn(run func(context.Context, *magistrala.IssueReq, ...grpc.CallOption) (*magistrala.Token, error)) *TokenServiceClient_Issue_Call { +func (_c *TokenServiceClient_Issue_Call) RunAndReturn(run func(context.Context, *v1.IssueReq, ...grpc.CallOption) (*v1.Token, error)) *TokenServiceClient_Issue_Call { _c.Call.Return(run) return _c } // Refresh provides a mock function with given fields: ctx, in, opts -func (_m *TokenServiceClient) Refresh(ctx context.Context, in *magistrala.RefreshReq, opts ...grpc.CallOption) (*magistrala.Token, error) { +func (_m *TokenServiceClient) Refresh(ctx context.Context, in *v1.RefreshReq, opts ...grpc.CallOption) (*v1.Token, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] @@ -118,20 +118,20 @@ func (_m *TokenServiceClient) Refresh(ctx context.Context, in *magistrala.Refres panic("no return value specified for Refresh") } - var r0 *magistrala.Token + var r0 *v1.Token var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.RefreshReq, ...grpc.CallOption) (*magistrala.Token, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.RefreshReq, ...grpc.CallOption) (*v1.Token, error)); ok { return rf(ctx, in, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.RefreshReq, ...grpc.CallOption) *magistrala.Token); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.RefreshReq, ...grpc.CallOption) *v1.Token); ok { r0 = rf(ctx, in, opts...) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*magistrala.Token) + r0 = ret.Get(0).(*v1.Token) } } - if rf, ok := ret.Get(1).(func(context.Context, *magistrala.RefreshReq, ...grpc.CallOption) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *v1.RefreshReq, ...grpc.CallOption) error); ok { r1 = rf(ctx, in, opts...) } else { r1 = ret.Error(1) @@ -147,14 +147,14 @@ type TokenServiceClient_Refresh_Call struct { // Refresh is a helper method to define mock.On call // - ctx context.Context -// - in *magistrala.RefreshReq +// - in *v1.RefreshReq // - opts ...grpc.CallOption func (_e *TokenServiceClient_Expecter) Refresh(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_Refresh_Call { return &TokenServiceClient_Refresh_Call{Call: _e.mock.On("Refresh", append([]interface{}{ctx, in}, opts...)...)} } -func (_c *TokenServiceClient_Refresh_Call) Run(run func(ctx context.Context, in *magistrala.RefreshReq, opts ...grpc.CallOption)) *TokenServiceClient_Refresh_Call { +func (_c *TokenServiceClient_Refresh_Call) Run(run func(ctx context.Context, in *v1.RefreshReq, opts ...grpc.CallOption)) *TokenServiceClient_Refresh_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]grpc.CallOption, len(args)-2) for i, a := range args[2:] { @@ -162,17 +162,17 @@ func (_c *TokenServiceClient_Refresh_Call) Run(run func(ctx context.Context, in variadicArgs[i] = a.(grpc.CallOption) } } - run(args[0].(context.Context), args[1].(*magistrala.RefreshReq), variadicArgs...) + run(args[0].(context.Context), args[1].(*v1.RefreshReq), variadicArgs...) }) return _c } -func (_c *TokenServiceClient_Refresh_Call) Return(_a0 *magistrala.Token, _a1 error) *TokenServiceClient_Refresh_Call { +func (_c *TokenServiceClient_Refresh_Call) Return(_a0 *v1.Token, _a1 error) *TokenServiceClient_Refresh_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *TokenServiceClient_Refresh_Call) RunAndReturn(run func(context.Context, *magistrala.RefreshReq, ...grpc.CallOption) (*magistrala.Token, error)) *TokenServiceClient_Refresh_Call { +func (_c *TokenServiceClient_Refresh_Call) RunAndReturn(run func(context.Context, *v1.RefreshReq, ...grpc.CallOption) (*v1.Token, error)) *TokenServiceClient_Refresh_Call { _c.Call.Return(run) return _c } diff --git a/auth/postgres/domains_test.go b/auth/postgres/domains_test.go deleted file mode 100644 index 1e1997a90e..0000000000 --- a/auth/postgres/domains_test.go +++ /dev/null @@ -1,1148 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/auth/postgres" - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/policies" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - inValid = "invalid" -) - -var ( - domainID = testsutil.GenerateUUID(&testing.T{}) - userID = testsutil.GenerateUUID(&testing.T{}) -) - -func TestAddPolicyCopy(t *testing.T) { - repo := postgres.NewDomainRepository(database) - cases := []struct { - desc string - pc auth.Policy - err error - }{ - { - desc: "add a policy copy", - pc: auth.Policy{ - SubjectType: "unknown", - SubjectID: "unknown", - Relation: "unknown", - ObjectType: "unknown", - ObjectID: "unknown", - }, - err: nil, - }, - { - desc: "add again same policy copy", - pc: auth.Policy{ - SubjectType: "unknown", - SubjectID: "unknown", - Relation: "unknown", - ObjectType: "unknown", - ObjectID: "unknown", - }, - err: repoerr.ErrConflict, - }, - } - - for _, tc := range cases { - err := repo.SavePolicies(context.Background(), tc.pc) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} - -func TestDeletePolicyCopy(t *testing.T) { - repo := postgres.NewDomainRepository(database) - cases := []struct { - desc string - pc auth.Policy - err error - }{ - { - desc: "delete a policy copy", - pc: auth.Policy{ - SubjectType: "unknown", - SubjectID: "unknown", - Relation: "unknown", - ObjectType: "unknown", - ObjectID: "unknown", - }, - err: nil, - }, - { - desc: "delete a policy with empty relation", - pc: auth.Policy{ - SubjectType: "unknown", - SubjectID: "unknown", - Relation: "", - ObjectType: "unknown", - ObjectID: "unknown", - }, - err: nil, - }, - } - - for _, tc := range cases { - err := repo.DeletePolicies(context.Background(), tc.pc) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} - -func TestSave(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM domains") - require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) - }) - - repo := postgres.NewDomainRepository(database) - - cases := []struct { - desc string - domain auth.Domain - err error - }{ - { - desc: "add new domain with all fields successfully", - domain: auth.Domain{ - ID: domainID, - Name: "test", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - }, - err: nil, - }, - { - desc: "add the same domain again", - domain: auth.Domain{ - ID: domainID, - Name: "test", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - }, - err: repoerr.ErrConflict, - }, - { - desc: "add domain with empty ID", - domain: auth.Domain{ - ID: "", - Name: "test1", - Alias: "test1", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - }, - err: nil, - }, - { - desc: "add domain with empty alias", - domain: auth.Domain{ - ID: testsutil.GenerateUUID(&testing.T{}), - Name: "test1", - Alias: "", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - }, - err: repoerr.ErrCreateEntity, - }, - { - desc: "add domain with malformed metadata", - domain: auth.Domain{ - ID: domainID, - Name: "test1", - Alias: "test1", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "key": make(chan int), - }, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - }, - err: repoerr.ErrCreateEntity, - }, - } - - for _, tc := range cases { - _, err := repo.Save(context.Background(), tc.domain) - { - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } - } -} - -func TestRetrieveByID(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM domains") - require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) - }) - - repo := postgres.NewDomainRepository(database) - - domain := auth.Domain{ - ID: domainID, - Name: "test", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - } - - _, err := repo.Save(context.Background(), domain) - require.Nil(t, err, fmt.Sprintf("failed to save client %s", domain.ID)) - - cases := []struct { - desc string - domainID string - response auth.Domain - err error - }{ - { - desc: "retrieve existing client", - domainID: domain.ID, - response: domain, - err: nil, - }, - { - desc: "retrieve non-existing client", - domainID: inValid, - response: auth.Domain{}, - err: repoerr.ErrNotFound, - }, - { - desc: "retrieve with empty client id", - domainID: "", - response: auth.Domain{}, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - d, err := repo.RetrieveByID(context.Background(), tc.domainID) - assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} - -func TestRetreivePermissions(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM domains") - require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) - _, err = db.Exec("DELETE FROM policies") - require.Nil(t, err, fmt.Sprintf("clean policies unexpected error: %s", err)) - }) - - repo := postgres.NewDomainRepository(database) - - domain := auth.Domain{ - ID: domainID, - Name: "test", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - Permission: "admin", - } - - policy := auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: "admin", - Relation: "admin", - ObjectType: policies.DomainType, - ObjectID: domainID, - } - - _, err := repo.Save(context.Background(), domain) - require.Nil(t, err, fmt.Sprintf("failed to save domain %s", domain.ID)) - - err = repo.SavePolicies(context.Background(), policy) - require.Nil(t, err, fmt.Sprintf("failed to save policy %s", policy.SubjectID)) - - cases := []struct { - desc string - domainID string - policySubject string - response []string - err error - }{ - { - desc: "retrieve existing permissions with valid domaiinID and policySubject", - domainID: domain.ID, - policySubject: userID, - response: []string{"admin"}, - err: nil, - }, - { - desc: "retreieve permissions with invalid domainID", - domainID: inValid, - policySubject: userID, - response: []string{}, - err: nil, - }, - { - desc: "retreieve permissions with invalid policySubject", - domainID: domain.ID, - policySubject: inValid, - response: []string{}, - err: nil, - }, - } - - for _, tc := range cases { - d, err := repo.RetrievePermissions(context.Background(), tc.policySubject, tc.domainID) - assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} - -func TestRetrieveAllByIDs(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM domains") - require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) - }) - - repo := postgres.NewDomainRepository(database) - - items := []auth.Domain{} - for i := 0; i < 10; i++ { - domain := auth.Domain{ - ID: testsutil.GenerateUUID(t), - Name: fmt.Sprintf(`"test%d"`, i), - Alias: fmt.Sprintf(`"test%d"`, i), - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - } - if i%5 == 0 { - domain.Status = auth.DisabledStatus - domain.Tags = []string{"test", "admin"} - domain.Metadata = map[string]interface{}{ - "test1": "test1", - } - } - _, err := repo.Save(context.Background(), domain) - require.Nil(t, err, fmt.Sprintf("save domain unexpected error: %s", err)) - items = append(items, domain) - } - - cases := []struct { - desc string - pm auth.Page - response auth.DomainsPage - err error - }{ - { - desc: "retrieve by ids successfully", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{items[1].ID, items[2].ID}, - }, - response: auth.DomainsPage{ - Total: 2, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[1], items[2]}, - }, - err: nil, - }, - { - desc: "retrieve by ids with empty ids", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{}, - }, - response: auth.DomainsPage{ - Total: 0, - Offset: 0, - Limit: 0, - }, - err: nil, - }, - { - desc: "retrieve by ids with invalid ids", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{inValid}, - }, - response: auth.DomainsPage{ - Total: 0, - Offset: 0, - Limit: 10, - }, - err: nil, - }, - { - desc: "retrieve by ids and status", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{items[0].ID, items[1].ID}, - Status: auth.DisabledStatus, - }, - response: auth.DomainsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[0]}, - }, - }, - { - desc: "retrieve by ids and status with invalid status", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{items[0].ID, items[1].ID}, - Status: 5, - }, - response: auth.DomainsPage{ - Total: 2, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[0], items[1]}, - }, - }, - { - desc: "retrieve by ids and tags", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{items[0].ID, items[1].ID}, - Tag: "test", - }, - response: auth.DomainsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[1]}, - }, - }, - { - desc: " retrieve by ids and metadata", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{items[1].ID, items[2].ID}, - Metadata: map[string]interface{}{ - "test": "test", - }, - Status: auth.EnabledStatus, - }, - response: auth.DomainsPage{ - Total: 2, - Offset: 0, - Limit: 10, - Domains: items[1:3], - }, - }, - { - desc: "retrieve by ids and metadata with invalid metadata", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{items[1].ID, items[2].ID}, - Metadata: map[string]interface{}{ - "test1": "test1", - }, - Status: auth.EnabledStatus, - }, - response: auth.DomainsPage{ - Total: 0, - Offset: 0, - Limit: 10, - }, - }, - { - desc: "retrieve by ids and malfomed metadata", - pm: auth.Page{ - Offset: 0, - Limit: 10, - IDs: []string{items[1].ID, items[2].ID}, - Metadata: map[string]interface{}{ - "key": make(chan int), - }, - Status: auth.EnabledStatus, - }, - response: auth.DomainsPage{}, - err: repoerr.ErrViewEntity, - }, - { - desc: "retrieve all by ids and id", - pm: auth.Page{ - Offset: 0, - Limit: 10, - ID: items[1].ID, - IDs: []string{items[1].ID, items[2].ID}, - }, - response: auth.DomainsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[1]}, - }, - }, - { - desc: "retrieve all by ids and id with invalid id", - pm: auth.Page{ - Offset: 0, - Limit: 10, - ID: inValid, - IDs: []string{items[1].ID, items[2].ID}, - }, - response: auth.DomainsPage{ - Total: 0, - Offset: 0, - Limit: 10, - }, - }, - { - desc: "retrieve all by ids and name", - pm: auth.Page{ - Offset: 0, - Limit: 10, - Name: items[1].Name, - IDs: []string{items[1].ID, items[2].ID}, - }, - response: auth.DomainsPage{ - Total: 1, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[1]}, - }, - }, - { - desc: "retrieve all by ids with empty page", - pm: auth.Page{}, - response: auth.DomainsPage{}, - }, - } - - for _, tc := range cases { - d, err := repo.RetrieveAllByIDs(context.Background(), tc.pm) - assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} - -func TestListDomains(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM domains") - require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) - }) - - repo := postgres.NewDomainRepository(database) - - items := []auth.Domain{} - rDomains := []auth.Domain{} - policyList := []auth.Policy{} - for i := 0; i < 10; i++ { - domain := auth.Domain{ - ID: testsutil.GenerateUUID(t), - Name: fmt.Sprintf(`"test%d"`, i), - Alias: fmt.Sprintf(`"test%d"`, i), - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - } - if i%5 == 0 { - domain.Status = auth.DisabledStatus - domain.Tags = []string{"test", "admin"} - domain.Metadata = map[string]interface{}{ - "test1": "test1", - } - } - policy := auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: policies.AdministratorRelation, - Relation: policies.DomainRelation, - ObjectType: policies.DomainType, - ObjectID: domain.ID, - } - _, err := repo.Save(context.Background(), domain) - require.Nil(t, err, fmt.Sprintf("save domain unexpected error: %s", err)) - items = append(items, domain) - policyList = append(policyList, policy) - rDomain := domain - rDomain.Permission = "domain" - rDomains = append(rDomains, rDomain) - } - - err := repo.SavePolicies(context.Background(), policyList...) - require.Nil(t, err, fmt.Sprintf("failed to save policies %s", policyList)) - - cases := []struct { - desc string - pm auth.Page - response auth.DomainsPage - err error - }{ - { - desc: "list all domains successfully", - pm: auth.Page{ - Offset: 0, - Limit: 10, - Status: auth.AllStatus, - }, - response: auth.DomainsPage{ - Total: 10, - Offset: 0, - Limit: 10, - Domains: items, - }, - err: nil, - }, - { - desc: "list domains with empty page", - pm: auth.Page{ - Offset: 0, - Limit: 0, - }, - response: auth.DomainsPage{ - Total: 8, - Offset: 0, - Limit: 0, - }, - err: nil, - }, - { - desc: "list domains with enabled status", - pm: auth.Page{ - Offset: 0, - Limit: 10, - Status: auth.EnabledStatus, - }, - response: auth.DomainsPage{ - Total: 8, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[1], items[2], items[3], items[4], items[6], items[7], items[8], items[9]}, - }, - err: nil, - }, - { - desc: "list domains with disabled status", - pm: auth.Page{ - Offset: 0, - Limit: 10, - Status: auth.DisabledStatus, - }, - response: auth.DomainsPage{ - Total: 2, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{items[0], items[5]}, - }, - err: nil, - }, - { - desc: "list domains with subject ID", - pm: auth.Page{ - Offset: 0, - Limit: 10, - SubjectID: userID, - Status: auth.AllStatus, - }, - response: auth.DomainsPage{ - Total: 10, - Offset: 0, - Limit: 10, - Domains: rDomains, - }, - err: nil, - }, - { - desc: "list domains with subject ID and status", - pm: auth.Page{ - Offset: 0, - Limit: 10, - SubjectID: userID, - Status: auth.EnabledStatus, - }, - response: auth.DomainsPage{ - Total: 8, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{rDomains[1], rDomains[2], rDomains[3], rDomains[4], rDomains[6], rDomains[7], rDomains[8], rDomains[9]}, - }, - err: nil, - }, - { - desc: "list domains with subject Id and permission", - pm: auth.Page{ - Offset: 0, - Limit: 10, - SubjectID: userID, - Permission: "domain", - Status: auth.AllStatus, - }, - response: auth.DomainsPage{ - Total: 10, - Offset: 0, - Limit: 10, - Domains: rDomains, - }, - err: nil, - }, - { - desc: "list domains with subject id and tags", - pm: auth.Page{ - Offset: 0, - Limit: 10, - SubjectID: userID, - Tag: "test", - Status: auth.AllStatus, - }, - response: auth.DomainsPage{ - Total: 10, - Offset: 0, - Limit: 10, - Domains: rDomains, - }, - err: nil, - }, - { - desc: "list domains with subject id and metadata", - pm: auth.Page{ - Offset: 0, - Limit: 10, - SubjectID: userID, - Metadata: map[string]interface{}{ - "test": "test", - }, - Status: auth.AllStatus, - }, - response: auth.DomainsPage{ - Total: 8, - Offset: 0, - Limit: 10, - Domains: []auth.Domain{rDomains[1], rDomains[2], rDomains[3], rDomains[4], rDomains[6], rDomains[7], rDomains[8], rDomains[9]}, - }, - }, - { - desc: "list domains with subject id and metadata with malforned metadata", - pm: auth.Page{ - Offset: 0, - Limit: 10, - SubjectID: userID, - Metadata: map[string]interface{}{ - "key": make(chan int), - }, - Status: auth.AllStatus, - }, - response: auth.DomainsPage{}, - err: repoerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - d, err := repo.ListDomains(context.Background(), tc.pm) - assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} - -func TestUpdate(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM domains") - require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) - }) - - updatedName := "test1" - updatedMetadata := auth.Metadata{ - "test1": "test1", - } - updatedTags := []string{"test1"} - updatedStatus := auth.DisabledStatus - updatedAlias := "test1" - - repo := postgres.NewDomainRepository(database) - - domain := auth.Domain{ - ID: domainID, - Name: "test", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - } - - _, err := repo.Save(context.Background(), domain) - require.Nil(t, err, fmt.Sprintf("failed to save client %s", domain.ID)) - - cases := []struct { - desc string - domainID string - d auth.DomainReq - response auth.Domain - err error - }{ - { - desc: "update existing domain name and metadata", - domainID: domain.ID, - d: auth.DomainReq{ - Name: &updatedName, - Metadata: &updatedMetadata, - }, - response: auth.Domain{ - ID: domainID, - Name: "test1", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test1": "test1", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - UpdatedAt: time.Now(), - }, - err: nil, - }, - { - desc: "update existing domain name, metadata, tags, status and alias", - domainID: domain.ID, - d: auth.DomainReq{ - Name: &updatedName, - Metadata: &updatedMetadata, - Tags: &updatedTags, - Status: &updatedStatus, - Alias: &updatedAlias, - }, - response: auth.Domain{ - ID: domainID, - Name: "test1", - Alias: "test1", - Tags: []string{"test1"}, - Metadata: map[string]interface{}{ - "test1": "test1", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.DisabledStatus, - UpdatedAt: time.Now(), - }, - err: nil, - }, - { - desc: "update non-existing domain", - domainID: inValid, - d: auth.DomainReq{ - Name: &updatedName, - Metadata: &updatedMetadata, - }, - response: auth.Domain{}, - err: repoerr.ErrFailedOpDB, - }, - { - desc: "update domain with empty ID", - domainID: "", - d: auth.DomainReq{ - Name: &updatedName, - Metadata: &updatedMetadata, - }, - response: auth.Domain{}, - err: repoerr.ErrFailedOpDB, - }, - { - desc: "update domain with malformed metadata", - domainID: domainID, - d: auth.DomainReq{ - Name: &updatedName, - Metadata: &auth.Metadata{"key": make(chan int)}, - }, - response: auth.Domain{}, - err: repoerr.ErrUpdateEntity, - }, - } - - for _, tc := range cases { - d, err := repo.Update(context.Background(), tc.domainID, userID, tc.d) - d.UpdatedAt = tc.response.UpdatedAt - assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestDelete(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM domains") - require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) - }) - - repo := postgres.NewDomainRepository(database) - - domain := auth.Domain{ - ID: domainID, - Name: "test", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - } - - _, err := repo.Save(context.Background(), domain) - require.Nil(t, err, fmt.Sprintf("failed to save client %s", domain.ID)) - - cases := []struct { - desc string - domainID string - err error - }{ - { - desc: "delete existing domain", - domainID: domain.ID, - err: nil, - }, - { - desc: "delete non-existing domain", - domainID: inValid, - err: repoerr.ErrNotFound, - }, - { - desc: "delete domain with empty ID", - domainID: "", - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - err := repo.Delete(context.Background(), tc.domainID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestCheckPolicy(t *testing.T) { - t.Cleanup(func() { - _, err := db.Exec("DELETE FROM policies") - require.Nil(t, err, fmt.Sprintf("clean policies unexpected error: %s", err)) - }) - - repo := postgres.NewDomainRepository(database) - - policy := auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: policies.AdministratorRelation, - Relation: policies.DomainRelation, - ObjectType: policies.DomainType, - ObjectID: domainID, - } - - err := repo.SavePolicies(context.Background(), policy) - require.Nil(t, err, fmt.Sprintf("failed to save policy %s", policy.SubjectID)) - - cases := []struct { - desc string - policy auth.Policy - err error - }{ - { - desc: "check valid policy", - policy: policy, - err: nil, - }, - { - desc: "check policy with invalid subject type", - policy: auth.Policy{ - SubjectType: inValid, - SubjectID: userID, - SubjectRelation: policies.AdministratorRelation, - Relation: policies.DomainRelation, - ObjectType: policies.DomainType, - ObjectID: domainID, - }, - err: repoerr.ErrNotFound, - }, - { - desc: "check policy with invalid subject id", - policy: auth.Policy{ - SubjectType: policies.UserType, - SubjectID: inValid, - SubjectRelation: policies.AdministratorRelation, - Relation: policies.DomainRelation, - ObjectType: policies.DomainType, - ObjectID: domainID, - }, - err: repoerr.ErrNotFound, - }, - { - desc: "check policy with invalid subject relation", - policy: auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: inValid, - Relation: policies.DomainRelation, - ObjectType: policies.DomainType, - ObjectID: domainID, - }, - err: repoerr.ErrNotFound, - }, - { - desc: "check policy with invalid relation", - policy: auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: policies.AdministratorRelation, - Relation: inValid, - ObjectType: policies.DomainType, - ObjectID: domainID, - }, - err: repoerr.ErrNotFound, - }, - { - desc: "check policy with invalid object type", - policy: auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: policies.AdministratorRelation, - Relation: policies.DomainRelation, - ObjectType: inValid, - ObjectID: domainID, - }, - err: repoerr.ErrNotFound, - }, - { - desc: "check policy with invalid object id", - policy: auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: policies.AdministratorRelation, - Relation: policies.DomainRelation, - ObjectType: policies.DomainType, - ObjectID: inValid, - }, - err: repoerr.ErrNotFound, - }, - } - for _, tc := range cases { - err := repo.CheckPolicy(context.Background(), tc.policy) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} - -func TestDeleteUserPolicies(t *testing.T) { - repo := postgres.NewDomainRepository(database) - - domain := auth.Domain{ - ID: domainID, - Name: "test", - Alias: "test", - Tags: []string{"test"}, - Metadata: map[string]interface{}{ - "test": "test", - }, - CreatedBy: userID, - UpdatedBy: userID, - Status: auth.EnabledStatus, - Permission: "admin", - } - - policy := auth.Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - SubjectRelation: "admin", - Relation: "admin", - ObjectType: policies.DomainType, - ObjectID: domainID, - } - - _, err := repo.Save(context.Background(), domain) - require.Nil(t, err, fmt.Sprintf("failed to save domain %s", domain.ID)) - - err = repo.SavePolicies(context.Background(), policy) - require.Nil(t, err, fmt.Sprintf("failed to save policy %s", policy.SubjectID)) - - cases := []struct { - desc string - id string - err error - }{ - { - desc: "delete valid user policy", - id: userID, - err: nil, - }, - { - desc: "delete invalid user policy", - id: inValid, - err: nil, - }, - } - - for _, tc := range cases { - err := repo.DeleteUserPolicies(context.Background(), tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) - } -} diff --git a/auth/postgres/init.go b/auth/postgres/init.go index ae69c3a0ca..32e0ab002d 100644 --- a/auth/postgres/init.go +++ b/auth/postgres/init.go @@ -57,6 +57,14 @@ func Migration() *migrate.MemoryMigrationSource { `ALTER TABLE domains ALTER COLUMN alias SET NOT NULL`, }, }, + { + Id: "auth_3", + Up: []string{ + `DROP TABLE IF EXISTS policies; + DROP TABLE IF EXISTS domains; + `, + }, + }, }, } } diff --git a/auth/service.go b/auth/service.go index c336d87e25..4b52e99f6b 100644 --- a/auth/service.go +++ b/auth/service.go @@ -5,7 +5,6 @@ package auth import ( "context" - "fmt" "strings" "time" @@ -82,14 +81,12 @@ type Authn interface { type Service interface { Authn Authz - Domains } var _ Service = (*service)(nil) type service struct { keys KeyRepository - domains DomainsRepository idProvider magistrala.IDProvider evaluator policies.Evaluator policysvc policies.Service @@ -100,10 +97,9 @@ type service struct { } // New instantiates the auth service implementation. -func New(keys KeyRepository, domains DomainsRepository, idp magistrala.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service { +func New(keys KeyRepository, idp magistrala.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service { return &service{ tokenizer: tokenizer, - domains: domains, keys: keys, idProvider: idp, evaluator: policyEvaluator, @@ -233,36 +229,12 @@ func (svc service) checkDomain(ctx context.Context, subjectType, subject, domain return svcerr.ErrDomainAuthorization } - d, err := svc.domains.RetrieveByID(ctx, domainID) - if err != nil { - return errors.Wrap(svcerr.ErrViewEntity, err) - } + // ToDo: Add domain status in spiceDB like with new relation called status - switch d.Status { - case EnabledStatus: - case DisabledStatus: - if err := svc.evaluator.CheckPolicy(ctx, policies.Policy{ - Subject: subject, - SubjectType: subjectType, - Permission: policies.AdminPermission, - Object: domainID, - ObjectType: policies.DomainType, - }); err != nil { - return svcerr.ErrDomainAuthorization - } - case FreezeStatus: - if err := svc.evaluator.CheckPolicy(ctx, policies.Policy{ - Subject: subject, - SubjectType: subjectType, - Permission: policies.AdminPermission, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - }); err != nil { - return svcerr.ErrDomainAuthorization - } - default: - return svcerr.ErrDomainAuthorization - } + // d, err := svc.domains.RetrieveByID(ctx, domainID) + // if err != nil { + // return errors.Wrap(svcerr.ErrViewEntity, err) + // } return nil } @@ -451,401 +423,6 @@ func SwitchToPermission(relation string) string { } } -func (svc service) CreateDomain(ctx context.Context, token string, d Domain) (do Domain, err error) { - key, err := svc.Identify(ctx, token) - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrAuthentication, err) - } - d.CreatedBy = key.User - - domainID, err := svc.idProvider.ID() - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrCreateEntity, err) - } - d.ID = domainID - - if d.Status != DisabledStatus && d.Status != EnabledStatus { - return Domain{}, svcerr.ErrInvalidStatus - } - - d.CreatedAt = time.Now() - - if err := svc.createDomainPolicy(ctx, key.User, domainID, policies.AdministratorRelation); err != nil { - return Domain{}, errors.Wrap(errCreateDomainPolicy, err) - } - defer func() { - if err != nil { - if errRollBack := svc.createDomainPolicyRollback(ctx, key.User, domainID, policies.AdministratorRelation); errRollBack != nil { - err = errors.Wrap(err, errors.Wrap(errRollbackPolicy, errRollBack)) - } - } - }() - dom, err := svc.domains.Save(ctx, d) - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - return dom, nil -} - -func (svc service) RetrieveDomain(ctx context.Context, token, id string) (Domain, error) { - res, err := svc.Identify(ctx, token) - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrAuthentication, err) - } - domain, err := svc.domains.RetrieveByID(ctx, id) - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - if err = svc.Authorize(ctx, policies.Policy{ - Subject: res.Subject, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: id, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return Domain{ID: domain.ID, Name: domain.Name, Alias: domain.Alias}, nil - } - return domain, nil -} - -func (svc service) RetrieveDomainPermissions(ctx context.Context, token, id string) (policies.Permissions, error) { - res, err := svc.Identify(ctx, token) - if err != nil { - return []string{}, err - } - - if err := svc.Authorize(ctx, policies.Policy{ - Subject: res.User, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: id, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }); err != nil { - return []string{}, err - } - - lp, err := svc.policysvc.ListPermissions(ctx, policies.Policy{ - SubjectType: policies.UserType, - Subject: res.User, - Object: id, - ObjectType: policies.DomainType, - }, []string{policies.AdminPermission, policies.EditPermission, policies.ViewPermission, policies.MembershipPermission, policies.CreatePermission}) - if err != nil { - return []string{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - return lp, nil -} - -func (svc service) UpdateDomain(ctx context.Context, token, id string, d DomainReq) (Domain, error) { - key, err := svc.Identify(ctx, token) - if err != nil { - return Domain{}, err - } - if err := svc.Authorize(ctx, policies.Policy{ - Subject: key.User, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: id, - ObjectType: policies.DomainType, - Permission: policies.EditPermission, - }); err != nil { - return Domain{}, err - } - - dom, err := svc.domains.Update(ctx, id, key.User, d) - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - return dom, nil -} - -func (svc service) ChangeDomainStatus(ctx context.Context, token, id string, d DomainReq) (Domain, error) { - key, err := svc.Identify(ctx, token) - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrAuthentication, err) - } - if err := svc.Authorize(ctx, policies.Policy{ - Subject: key.User, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: id, - ObjectType: policies.DomainType, - Permission: policies.AdminPermission, - }); err != nil { - return Domain{}, err - } - - dom, err := svc.domains.Update(ctx, id, key.User, d) - if err != nil { - return Domain{}, errors.Wrap(svcerr.ErrUpdateEntity, err) - } - return dom, nil -} - -func (svc service) ListDomains(ctx context.Context, token string, p Page) (DomainsPage, error) { - key, err := svc.Identify(ctx, token) - if err != nil { - return DomainsPage{}, errors.Wrap(svcerr.ErrAuthentication, err) - } - p.SubjectID = key.User - if err := svc.Authorize(ctx, policies.Policy{ - Subject: key.User, - SubjectType: policies.UserType, - Permission: policies.AdminPermission, - ObjectType: policies.PlatformType, - Object: policies.MagistralaObject, - }); err == nil { - p.SubjectID = "" - } - dp, err := svc.domains.ListDomains(ctx, p) - if err != nil { - return DomainsPage{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - if p.SubjectID == "" { - for i := range dp.Domains { - dp.Domains[i].Permission = policies.AdministratorRelation - } - } - return dp, nil -} - -func (svc service) AssignUsers(ctx context.Context, token, id string, userIds []string, relation string) error { - res, err := svc.Identify(ctx, token) - if err != nil { - return errors.Wrap(svcerr.ErrAuthentication, err) - } - - if err := svc.Authorize(ctx, policies.Policy{ - Subject: res.User, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: id, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }); err != nil { - return err - } - - if err := svc.Authorize(ctx, policies.Policy{ - Subject: res.User, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: id, - ObjectType: policies.DomainType, - Permission: SwitchToPermission(relation), - }); err != nil { - return err - } - - for _, userID := range userIds { - if err := svc.Authorize(ctx, policies.Policy{ - Subject: userID, - SubjectType: policies.UserType, - Permission: policies.MembershipPermission, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - }); err != nil { - return errors.Wrap(svcerr.ErrMalformedEntity, fmt.Errorf("invalid user id : %s ", userID)) - } - } - - return svc.addDomainPolicies(ctx, id, relation, userIds...) -} - -func (svc service) UnassignUser(ctx context.Context, token, id, userID string) error { - res, err := svc.Identify(ctx, token) - if err != nil { - return errors.Wrap(svcerr.ErrAuthentication, err) - } - - pr := policies.Policy{ - Subject: res.User, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: id, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - } - if err := svc.Authorize(ctx, pr); err != nil { - return err - } - - pr.Permission = policies.AdminPermission - if err := svc.Authorize(ctx, pr); err != nil { - pr.SubjectKind = policies.UsersKind - // User is not admin. - pr.Subject = userID - if err := svc.Authorize(ctx, pr); err == nil { - // Non admin attempts to remove admin. - return errors.Wrap(svcerr.ErrAuthorization, err) - } - } - - if err := svc.policysvc.DeletePolicyFilter(ctx, policies.Policy{ - Subject: EncodeDomainUserID(id, userID), - SubjectType: policies.UserType, - }); err != nil { - return errors.Wrap(errRemovePolicies, err) - } - - pc := Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - ObjectType: policies.DomainType, - ObjectID: id, - } - - if err := svc.domains.DeletePolicies(ctx, pc); err != nil { - return errors.Wrap(errRemovePolicies, err) - } - - return nil -} - -// IMPROVEMENT NOTE: Take decision: Only Patform admin or both Patform and domain admins can see others users domain. -func (svc service) ListUserDomains(ctx context.Context, token, userID string, p Page) (DomainsPage, error) { - res, err := svc.Identify(ctx, token) - if err != nil { - return DomainsPage{}, errors.Wrap(svcerr.ErrAuthentication, err) - } - if err := svc.Authorize(ctx, policies.Policy{ - Subject: res.User, - SubjectType: policies.UserType, - Permission: policies.AdminPermission, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - }); err != nil { - return DomainsPage{}, errors.Wrap(svcerr.ErrAuthorization, err) - } - if userID != "" && res.User != userID { - p.SubjectID = userID - } else { - p.SubjectID = res.User - } - dp, err := svc.domains.ListDomains(ctx, p) - if err != nil { - return DomainsPage{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - return dp, nil -} - -func (svc service) addDomainPolicies(ctx context.Context, domainID, relation string, userIDs ...string) (err error) { - var prs []policies.Policy - var pcs []Policy - - for _, userID := range userIDs { - prs = append(prs, policies.Policy{ - Subject: EncodeDomainUserID(domainID, userID), - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Relation: relation, - Object: domainID, - ObjectType: policies.DomainType, - }) - pcs = append(pcs, Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - Relation: relation, - ObjectType: policies.DomainType, - ObjectID: domainID, - }) - } - if err := svc.policysvc.AddPolicies(ctx, prs); err != nil { - return errors.Wrap(errAddPolicies, err) - } - defer func() { - if err != nil { - if errDel := svc.policysvc.DeletePolicies(ctx, prs); errDel != nil { - err = errors.Wrap(err, errors.Wrap(errRollbackPolicy, errDel)) - } - } - }() - - if err = svc.domains.SavePolicies(ctx, pcs...); err != nil { - return errors.Wrap(errAddPolicies, err) - } - return nil -} - -func (svc service) createDomainPolicy(ctx context.Context, userID, domainID, relation string) (err error) { - prs := []policies.Policy{ - { - Subject: EncodeDomainUserID(domainID, userID), - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Relation: relation, - Object: domainID, - ObjectType: policies.DomainType, - }, - { - Subject: policies.MagistralaObject, - SubjectType: policies.PlatformType, - Relation: policies.PlatformRelation, - Object: domainID, - ObjectType: policies.DomainType, - }, - } - if err := svc.policysvc.AddPolicies(ctx, prs); err != nil { - return err - } - defer func() { - if err != nil { - if errDel := svc.policysvc.DeletePolicies(ctx, prs); errDel != nil { - err = errors.Wrap(err, errors.Wrap(errRollbackPolicy, errDel)) - } - } - }() - err = svc.domains.SavePolicies(ctx, Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - Relation: relation, - ObjectType: policies.DomainType, - ObjectID: domainID, - }) - if err != nil { - return errors.Wrap(errCreateDomainPolicy, err) - } - return err -} - -func (svc service) createDomainPolicyRollback(ctx context.Context, userID, domainID, relation string) error { - var err error - prs := []policies.Policy{ - { - Subject: EncodeDomainUserID(domainID, userID), - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Relation: relation, - Object: domainID, - ObjectType: policies.DomainType, - }, - { - Subject: policies.MagistralaObject, - SubjectType: policies.PlatformType, - Relation: policies.PlatformRelation, - Object: domainID, - ObjectType: policies.DomainType, - }, - } - if errPolicy := svc.policysvc.DeletePolicies(ctx, prs); errPolicy != nil { - err = errors.Wrap(errRemovePolicyEngine, errPolicy) - } - errPolicyCopy := svc.domains.DeletePolicies(ctx, Policy{ - SubjectType: policies.UserType, - SubjectID: userID, - Relation: relation, - ObjectType: policies.DomainType, - ObjectID: domainID, - }) - if errPolicyCopy != nil { - err = errors.Wrap(err, errors.Wrap(errRemoveLocalPolicy, errPolicyCopy)) - } - return err -} - func EncodeDomainUserID(domainID, userID string) string { if domainID == "" || userID == "" { return "" @@ -870,37 +447,3 @@ func DecodeDomainUserID(domainUserID string) (string, string) { return "", "" } } - -func (svc service) DeleteUserFromDomains(ctx context.Context, id string) (err error) { - domainsPage, err := svc.domains.ListDomains(ctx, Page{SubjectID: id, Limit: defLimit}) - if err != nil { - return err - } - - if domainsPage.Total > defLimit { - for i := defLimit; i < int(domainsPage.Total); i += defLimit { - page := Page{SubjectID: id, Offset: uint64(i), Limit: defLimit} - dp, err := svc.domains.ListDomains(ctx, page) - if err != nil { - return err - } - domainsPage.Domains = append(domainsPage.Domains, dp.Domains...) - } - } - - for _, domain := range domainsPage.Domains { - req := policies.Policy{ - Subject: EncodeDomainUserID(domain.ID, id), - SubjectType: policies.UserType, - } - if err := svc.policysvc.DeletePolicyFilter(ctx, req); err != nil { - return err - } - } - - if err := svc.domains.DeleteUserPolicies(ctx, id); err != nil { - return err - } - - return nil -} diff --git a/auth/service_test.go b/auth/service_test.go index 77baefce32..698f739fd9 100644 --- a/auth/service_test.go +++ b/auth/service_test.go @@ -47,27 +47,16 @@ var ( inValidToken = "invalid" inValid = "invalid" valid = "valid" - domain = auth.Domain{ - ID: validID, - Name: groupName, - Tags: []string{"tag1", "tag2"}, - Alias: "test", - Permission: policies.AdminPermission, - CreatedBy: validID, - UpdatedBy: validID, - } ) var ( krepo *mocks.KeyRepository - drepo *mocks.DomainsRepository pService *policymocks.Service pEvaluator *policymocks.Evaluator ) func newService() (auth.Service, string) { krepo = new(mocks.KeyRepository) - drepo = new(mocks.DomainsRepository) pService = new(policymocks.Service) pEvaluator = new(policymocks.Evaluator) idProvider := uuid.NewMock() @@ -83,7 +72,7 @@ func newService() (auth.Service, string) { } token, _ := t.Issue(key) - return auth.New(krepo, drepo, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration), token + return auth.New(krepo, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration), token } func TestIssue(t *testing.T) { @@ -139,7 +128,6 @@ func TestIssue(t *testing.T) { desc string key auth.Key saveResponse auth.Key - retrieveByIDResponse auth.Domain token string saveErr error checkPolicyRequest policies.Policy @@ -211,10 +199,9 @@ func TestIssue(t *testing.T) { Permission: policies.MembershipPermission, Object: groupName, }, - checkPolicyErr: repoerr.ErrNotFound, - retrieveByIDResponse: auth.Domain{}, - retreiveByIDErr: repoerr.ErrNotFound, - err: repoerr.ErrNotFound, + checkPolicyErr: repoerr.ErrNotFound, + retreiveByIDErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, }, { desc: "issue login key with failed check on platform admin with enabled status", @@ -241,10 +228,9 @@ func TestIssue(t *testing.T) { ObjectType: policies.DomainType, Permission: policies.MembershipPermission, }, - checkPolicyErr: svcerr.ErrAuthorization, - checkPolicyErr1: svcerr.ErrAuthorization, - retrieveByIDResponse: auth.Domain{Status: auth.EnabledStatus}, - err: svcerr.ErrAuthorization, + checkPolicyErr: svcerr.ErrAuthorization, + checkPolicyErr1: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, }, { desc: "issue login key with membership permission", @@ -271,10 +257,9 @@ func TestIssue(t *testing.T) { ObjectType: policies.DomainType, Permission: policies.MembershipPermission, }, - checkPolicyErr: svcerr.ErrAuthorization, - checkPolicyErr1: svcerr.ErrAuthorization, - retrieveByIDResponse: auth.Domain{Status: auth.EnabledStatus}, - err: svcerr.ErrAuthorization, + checkPolicyErr: svcerr.ErrAuthorization, + checkPolicyErr1: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, }, { desc: "issue login key with membership permission with failed to authorize", @@ -301,27 +286,22 @@ func TestIssue(t *testing.T) { ObjectType: policies.DomainType, Permission: policies.MembershipPermission, }, - checkPolicyErr: svcerr.ErrAuthorization, - checkPolicyErr1: svcerr.ErrAuthorization, - retrieveByIDResponse: auth.Domain{Status: auth.EnabledStatus}, - err: svcerr.ErrAuthorization, + checkPolicyErr: svcerr.ErrAuthorization, + checkPolicyErr1: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, }, } for _, tc := range cases2 { - t.Run(tc.desc, func(t *testing.T) { - repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, tc.saveErr) - repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyRequest).Return(tc.checkPolicyErr) - repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPlatformPolicyReq).Return(tc.checkPolicyErr1) - repoCall3 := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(tc.retrieveByIDResponse, tc.retreiveByIDErr) - repoCall4 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr) - _, err := svc.Issue(context.Background(), tc.token, 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() - repoCall3.Unset() - repoCall4.Unset() - }) + repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, tc.saveErr) + repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyRequest).Return(tc.checkPolicyErr) + repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPlatformPolicyReq).Return(tc.checkPolicyErr1) + repoCall4 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr) + _, err := svc.Issue(context.Background(), tc.token, 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() + repoCall4.Unset() } cases3 := []struct { @@ -407,7 +387,6 @@ func TestIssue(t *testing.T) { key: auth.Key{ Type: auth.RefreshKey, IssuedAt: time.Now(), - Domain: groupName, }, checkPolicyRequest: policies.Policy{ Subject: email, @@ -501,12 +480,10 @@ func TestIssue(t *testing.T) { } for _, tc := range cases4 { repoCall := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyRequest).Return(tc.checkPolicyErr) - repoCall1 := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(auth.Domain{}, tc.retrieveByIDErr) repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDOmainPolicyReq).Return(tc.checkPolicyErr) _, err := svc.Issue(context.Background(), tc.token, 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() } } @@ -557,12 +534,10 @@ func TestRevoke(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repocall := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(tc.err) - err := svc.Revoke(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() - }) + repocall := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(tc.err) + err := svc.Revoke(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() } } @@ -629,12 +604,10 @@ func TestRetrieve(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repocall := krepo.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(auth.Key{}, tc.err) - _, err := svc.RetrieveKey(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() - }) + repocall := krepo.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(auth.Key{}, tc.err) + _, err := svc.RetrieveKey(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() } } @@ -733,15 +706,13 @@ func TestIdentify(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repocall := krepo.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(auth.Key{}, tc.err) - repocall1 := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(tc.err) - idt, 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)) - assert.Equal(t, tc.idt, idt.Subject, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.idt, idt)) - repocall.Unset() - repocall1.Unset() - }) + repocall := krepo.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(auth.Key{}, tc.err) + repocall1 := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(tc.err) + idt, 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)) + assert.Equal(t, tc.idt, idt.Subject, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.idt, idt)) + repocall.Unset() + repocall1.Unset() } } @@ -780,7 +751,6 @@ func TestAuthorize(t *testing.T) { cases := []struct { desc string policyReq policies.Policy - retrieveDomainRes auth.Domain checkPolicyReq3 policies.Policy checkAdminPolicyReq policies.Policy checkDomainPolicyReq policies.Policy @@ -800,7 +770,7 @@ func TestAuthorize(t *testing.T) { Permission: policies.AdminPermission, }, checkPolicyReq3: policies.Policy{ - Domain: "", + Domain: groupName, Subject: id, SubjectType: policies.UserType, SubjectKind: policies.TokenKind, @@ -876,11 +846,6 @@ func TestAuthorize(t *testing.T) { Permission: policies.AdminPermission, }, - retrieveDomainRes: auth.Domain{ - ID: validID, - Name: groupName, - Status: auth.DisabledStatus, - }, err: nil, }, { @@ -915,11 +880,6 @@ func TestAuthorize(t *testing.T) { Permission: policies.MembershipPermission, }, - retrieveDomainRes: auth.Domain{ - ID: validID, - Name: groupName, - Status: auth.DisabledStatus, - }, checkPolicyErr1: svcerr.ErrDomainAuthorization, err: svcerr.ErrDomainAuthorization, }, @@ -956,11 +916,6 @@ func TestAuthorize(t *testing.T) { Permission: policies.MembershipPermission, }, - retrieveDomainRes: auth.Domain{ - ID: validID, - Name: groupName, - Status: auth.FreezeStatus, - }, err: nil, }, { @@ -996,11 +951,6 @@ func TestAuthorize(t *testing.T) { Permission: policies.MembershipPermission, }, - retrieveDomainRes: auth.Domain{ - ID: validID, - Name: groupName, - Status: auth.FreezeStatus, - }, checkPolicyErr1: svcerr.ErrDomainAuthorization, err: svcerr.ErrDomainAuthorization, }, @@ -1037,11 +987,6 @@ func TestAuthorize(t *testing.T) { Permission: policies.MembershipPermission, }, - retrieveDomainRes: auth.Domain{ - ID: validID, - Name: groupName, - Status: auth.AllStatus, - }, err: svcerr.ErrDomainAuthorization, }, @@ -1173,20 +1118,16 @@ func TestAuthorize(t *testing.T) { }, } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyReq3).Return(tc.checkPolicyErr) - repoCall1 := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(tc.retrieveDomainRes, nil) - repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkAdminPolicyReq).Return(tc.checkPolicyErr1) - repoCall3 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr1) - repoCall4 := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(nil) - err := svc.Authorize(context.Background(), tc.policyReq) - 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() - }) + repoCall := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyReq3).Return(tc.checkPolicyErr) + repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkAdminPolicyReq).Return(tc.checkPolicyErr1) + repoCall3 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr1) + repoCall4 := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(nil) + err := svc.Authorize(context.Background(), tc.policyReq) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + repoCall2.Unset() + repoCall3.Unset() + repoCall4.Unset() } cases2 := []struct { desc string @@ -1206,10 +1147,8 @@ func TestAuthorize(t *testing.T) { }, } for _, tc := range cases2 { - t.Run(tc.desc, func(t *testing.T) { - err := svc.Authorize(context.Background(), tc.policyReq) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) - }) + err := svc.Authorize(context.Background(), tc.policyReq) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) } } @@ -1251,1095 +1190,8 @@ func TestSwitchToPermission(t *testing.T) { }, } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - result := auth.SwitchToPermission(tc.relation) - assert.Equal(t, tc.result, result, fmt.Sprintf("switching to permission expected to succeed: %s", result)) - }) - } -} - -func TestCreateDomain(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - d auth.Domain - token string - userID string - addPolicyErr error - savePolicyErr error - saveDomainErr error - deleteDomainErr error - deletePoliciesErr error - err error - }{ - { - desc: "create domain successfully", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - err: nil, - }, - { - desc: "create domain with invalid token", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: inValidToken, - err: svcerr.ErrAuthentication, - }, - { - desc: "create domain with invalid status", - d: auth.Domain{ - Status: auth.AllStatus, - }, - token: accessToken, - err: svcerr.ErrInvalidStatus, - }, - { - desc: "create domain with failed policy request", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - addPolicyErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - { - desc: "create domain with failed save policyrequest", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - savePolicyErr: errors.ErrMalformedEntity, - err: errCreateDomainPolicy, - }, - { - desc: "create domain with failed save domain request", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - saveDomainErr: errors.ErrMalformedEntity, - err: svcerr.ErrCreateEntity, - }, - { - desc: "create domain with rollback error", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - savePolicyErr: errors.ErrMalformedEntity, - deleteDomainErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - { - desc: "create domain with rollback error and failed to delete policies", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - savePolicyErr: errors.ErrMalformedEntity, - deleteDomainErr: errors.ErrMalformedEntity, - deletePoliciesErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - { - desc: "create domain with failed to create and failed rollback", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - saveDomainErr: errors.ErrMalformedEntity, - deletePoliciesErr: errors.ErrMalformedEntity, - err: errRollbackPolicy, - }, - { - desc: "create domain with failed to create and failed rollback", - d: auth.Domain{ - Status: auth.EnabledStatus, - }, - token: accessToken, - saveDomainErr: errors.ErrMalformedEntity, - deleteDomainErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := pService.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPolicyErr) - repoCall1 := drepo.On("SavePolicies", mock.Anything, mock.Anything).Return(tc.savePolicyErr) - repoCall2 := pService.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesErr) - repoCall3 := drepo.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deleteDomainErr) - repoCall4 := drepo.On("Save", mock.Anything, mock.Anything).Return(auth.Domain{}, tc.saveDomainErr) - _, err := svc.CreateDomain(context.Background(), tc.token, tc.d) - 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() - }) - } -} - -func TestRetrieveDomain(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - token string - domainID string - domainRepoErr error - domainRepoErr1 error - checkPolicyErr error - err error - }{ - { - desc: "retrieve domain successfully", - token: accessToken, - domainID: validID, - err: nil, - }, - { - desc: "retrieve domain with invalid token", - token: inValidToken, - domainID: validID, - err: svcerr.ErrAuthentication, - }, - { - desc: "retrieve domain with empty domain id", - token: accessToken, - domainID: "", - err: svcerr.ErrViewEntity, - domainRepoErr1: repoerr.ErrNotFound, - }, - { - desc: "retrieve non-existing domain", - token: accessToken, - domainID: inValid, - domainRepoErr: repoerr.ErrNotFound, - err: svcerr.ErrViewEntity, - domainRepoErr1: repoerr.ErrNotFound, - }, - { - desc: "retrieve domain with failed to retrieve by id", - token: accessToken, - domainID: validID, - domainRepoErr1: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := drepo.On("RetrieveByID", mock.Anything, groupName).Return(auth.Domain{}, tc.domainRepoErr) - repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(tc.checkPolicyErr) - repoCall2 := drepo.On("RetrieveByID", mock.Anything, tc.domainID).Return(auth.Domain{}, tc.domainRepoErr1) - _, err := svc.RetrieveDomain(context.Background(), tc.token, tc.domainID) - 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() - }) - } -} - -func TestRetrieveDomainPermissions(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - token string - domainID string - retreivePermissionsErr error - retreiveByIDErr error - checkPolicyErr error - err error - }{ - { - desc: "retrieve domain permissions successfully", - token: accessToken, - domainID: validID, - err: nil, - }, - { - desc: "retrieve domain permissions with invalid token", - token: inValidToken, - domainID: validID, - err: svcerr.ErrAuthentication, - }, - { - desc: "retrieve domain permissions with empty domainID", - token: accessToken, - domainID: "", - checkPolicyErr: svcerr.ErrAuthorization, - err: svcerr.ErrDomainAuthorization, - }, - { - desc: "retrieve domain permissions with failed to retrieve permissions", - token: accessToken, - domainID: validID, - retreivePermissionsErr: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - { - desc: "retrieve domain permissions with failed to retrieve by id", - token: accessToken, - domainID: validID, - retreiveByIDErr: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := pService.On("ListPermissions", mock.Anything, mock.Anything, mock.Anything).Return(policies.Permissions{}, tc.retreivePermissionsErr) - repoCall1 := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(auth.Domain{}, tc.retreiveByIDErr) - repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(tc.checkPolicyErr) - _, err := svc.RetrieveDomainPermissions(context.Background(), tc.token, tc.domainID) - 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() - }) - } -} - -func TestUpdateDomain(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - token string - domainID string - domReq auth.DomainReq - checkPolicyErr error - retrieveByIDErr error - updateErr error - err error - }{ - { - desc: "update domain successfully", - token: accessToken, - domainID: validID, - domReq: auth.DomainReq{ - Name: &valid, - Alias: &valid, - }, - err: nil, - }, - { - desc: "update domain with invalid token", - token: inValidToken, - domainID: validID, - domReq: auth.DomainReq{ - Name: &valid, - Alias: &valid, - }, - err: svcerr.ErrAuthentication, - }, - { - desc: "update domain with empty domainID", - token: accessToken, - domainID: "", - domReq: auth.DomainReq{ - Name: &valid, - Alias: &valid, - }, - checkPolicyErr: svcerr.ErrAuthorization, - err: svcerr.ErrDomainAuthorization, - }, - { - desc: "update domain with failed to retrieve by id", - token: accessToken, - domainID: validID, - domReq: auth.DomainReq{ - Name: &valid, - Alias: &valid, - }, - retrieveByIDErr: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - { - desc: "update domain with failed to update", - token: accessToken, - domainID: validID, - domReq: auth.DomainReq{ - Name: &valid, - Alias: &valid, - }, - updateErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(tc.checkPolicyErr) - repoCall1 := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(auth.Domain{}, tc.retrieveByIDErr) - repoCall2 := drepo.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(auth.Domain{}, tc.updateErr) - _, err := svc.UpdateDomain(context.Background(), tc.token, tc.domainID, tc.domReq) - 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() - }) - } -} - -func TestChangeDomainStatus(t *testing.T) { - svc, accessToken := newService() - - disabledStatus := auth.DisabledStatus - - cases := []struct { - desc string - token string - domainID string - domainReq auth.DomainReq - retreieveByIDErr error - checkPolicyErr error - updateErr error - err error - }{ - { - desc: "change domain status successfully", - token: accessToken, - domainID: validID, - domainReq: auth.DomainReq{ - Status: &disabledStatus, - }, - err: nil, - }, - { - desc: "change domain status with invalid token", - token: inValidToken, - domainID: validID, - domainReq: auth.DomainReq{ - Status: &disabledStatus, - }, - err: svcerr.ErrAuthentication, - }, - { - desc: "change domain status with empty domainID", - token: accessToken, - domainID: "", - domainReq: auth.DomainReq{ - Status: &disabledStatus, - }, - retreieveByIDErr: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - { - desc: "change domain status with unauthorized domain ID", - token: accessToken, - domainID: validID, - domainReq: auth.DomainReq{ - Status: &disabledStatus, - }, - checkPolicyErr: svcerr.ErrAuthorization, - err: svcerr.ErrDomainAuthorization, - }, - { - desc: "change domain status with repository error on update", - token: accessToken, - domainID: validID, - domainReq: auth.DomainReq{ - Status: &disabledStatus, - }, - updateErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(auth.Domain{}, tc.retreieveByIDErr) - repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(tc.checkPolicyErr) - repoCall2 := drepo.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(auth.Domain{}, tc.updateErr) - _, err := svc.ChangeDomainStatus(context.Background(), tc.token, tc.domainID, tc.domainReq) - 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() - }) - } -} - -func TestListDomains(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - token string - domainID string - authReq auth.Page - listDomainsRes auth.DomainsPage - retreiveByIDErr error - checkPolicyErr error - listDomainErr error - err error - }{ - { - desc: "list domains successfully", - token: accessToken, - domainID: validID, - authReq: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - Status: auth.EnabledStatus, - }, - listDomainsRes: auth.DomainsPage{ - Domains: []auth.Domain{domain}, - }, - err: nil, - }, - { - desc: "list domains with invalid token", - token: inValidToken, - domainID: validID, - authReq: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - Status: auth.EnabledStatus, - }, - err: svcerr.ErrAuthentication, - }, - { - desc: "list domains with repository error on list domains", - token: accessToken, - domainID: validID, - authReq: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - Status: auth.EnabledStatus, - }, - listDomainErr: errors.ErrMalformedEntity, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(tc.checkPolicyErr) - repoCall1 := drepo.On("ListDomains", mock.Anything, mock.Anything).Return(tc.listDomainsRes, tc.listDomainErr) - _, err := svc.ListDomains(context.Background(), tc.token, auth.Page{}) - 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() - }) - } -} - -func TestAssignUsers(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - token string - domainID string - userIDs []string - relation string - checkPolicyReq3 policies.Policy - checkAdminPolicyReq policies.Policy - checkDomainPolicyReq policies.Policy - checkPolicyReq33 policies.Policy - checkpolicyErr error - checkPolicyErr1 error - checkPolicyErr2 error - addPoliciesErr error - savePoliciesErr error - deletePoliciesErr error - err error - }{ - { - desc: "assign users successfully", - token: accessToken, - domainID: validID, - userIDs: []string{validID}, - relation: policies.ContributorRelation, - checkPolicyReq3: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.ViewPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: validID, - SubjectType: policies.UserType, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - Permission: policies.MembershipPermission, - }, - checkPolicyReq33: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - err: nil, - }, - { - desc: "assign users with invalid token", - token: inValidToken, - domainID: validID, - userIDs: []string{validID}, - relation: policies.ContributorRelation, - checkPolicyReq3: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Domain: groupName, - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.ViewPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: validID, - SubjectType: policies.UserType, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - Permission: policies.MembershipPermission, - }, - err: svcerr.ErrAuthentication, - }, - { - desc: "assign users with invalid domainID", - token: accessToken, - domainID: inValid, - relation: policies.ContributorRelation, - checkPolicyReq3: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: inValid, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: inValid, - ObjectType: policies.DomainType, - Permission: policies.ViewPermission, - }, - checkPolicyReq33: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: inValid, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - checkPolicyErr1: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "assign users with invalid userIDs", - token: accessToken, - userIDs: []string{inValid}, - domainID: validID, - relation: policies.ContributorRelation, - checkPolicyReq3: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.ViewPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: inValid, - SubjectType: policies.UserType, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - Permission: policies.MembershipPermission, - }, - checkPolicyReq33: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - checkPolicyErr2: svcerr.ErrMalformedEntity, - err: svcerr.ErrDomainAuthorization, - }, - { - desc: "assign users with failed to add policies to agent", - token: accessToken, - domainID: validID, - userIDs: []string{validID}, - relation: policies.ContributorRelation, - checkPolicyReq3: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.ViewPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: validID, - SubjectType: policies.UserType, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - Permission: policies.MembershipPermission, - }, - checkPolicyReq33: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - addPoliciesErr: svcerr.ErrAuthorization, - err: errAddPolicies, - }, - { - desc: "assign users with failed to save policies to domain", - token: accessToken, - domainID: validID, - userIDs: []string{validID}, - relation: policies.ContributorRelation, - checkPolicyReq3: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.ViewPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: validID, - SubjectType: policies.UserType, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - Permission: policies.MembershipPermission, - }, - checkPolicyReq33: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - savePoliciesErr: repoerr.ErrCreateEntity, - err: errAddPolicies, - }, - { - desc: "assign users with failed to save policies to domain and failed to delete", - token: accessToken, - domainID: validID, - userIDs: []string{validID}, - relation: policies.ContributorRelation, - checkPolicyReq3: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.ViewPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: validID, - SubjectType: policies.UserType, - Object: policies.MagistralaObject, - ObjectType: policies.PlatformType, - Permission: policies.MembershipPermission, - }, - checkPolicyReq33: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - savePoliciesErr: repoerr.ErrCreateEntity, - deletePoliciesErr: svcerr.ErrDomainAuthorization, - err: errAddPolicies, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(auth.Domain{}, nil) - repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyReq3).Return(tc.checkpolicyErr) - repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkAdminPolicyReq).Return(tc.checkPolicyErr1) - repoCall3 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr2) - repoCall4 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyReq33).Return(tc.checkPolicyErr2) - repoCall5 := pService.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPoliciesErr) - repoCall6 := drepo.On("SavePolicies", mock.Anything, mock.Anything, mock.Anything).Return(tc.savePoliciesErr) - repoCall7 := pService.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesErr) - err := svc.AssignUsers(context.Background(), tc.token, tc.domainID, tc.userIDs, tc.relation) - 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() - repoCall5.Unset() - repoCall6.Unset() - repoCall7.Unset() - }) - } -} - -func TestUnassignUser(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - token string - domainID string - userID string - checkPolicyReq policies.Policy - checkAdminPolicyReq policies.Policy - checkDomainPolicyReq policies.Policy - checkPolicyErr error - checkPolicyErr1 error - deletePolicyFilterErr error - deletePoliciesErr error - err error - }{ - { - desc: "unassign user successfully", - token: accessToken, - domainID: validID, - userID: validID, - checkPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.AdminPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - err: nil, - }, - { - desc: "unassign users with invalid token", - token: inValidToken, - domainID: validID, - userID: validID, - checkPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.AdminPermission, - }, - err: svcerr.ErrAuthentication, - }, - { - desc: "unassign users with invalid domainID", - token: accessToken, - domainID: inValid, - userID: validID, - checkPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: inValid, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: inValid, - ObjectType: policies.DomainType, - Permission: policies.AdminPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: inValid, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - checkPolicyErr1: svcerr.ErrAuthorization, - err: svcerr.ErrDomainAuthorization, - }, - { - desc: "unassign users with failed to delete policies from agent", - token: accessToken, - domainID: validID, - userID: validID, - checkPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.AdminPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - deletePolicyFilterErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - { - desc: "unassign users with failed to delete policies from domain", - token: accessToken, - domainID: validID, - userID: validID, - checkPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.AdminPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - deletePoliciesErr: errors.ErrMalformedEntity, - deletePolicyFilterErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - { - desc: "unassign user with failed to delete pService from domain", - token: accessToken, - domainID: validID, - userID: validID, - checkPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.MembershipPermission, - }, - checkAdminPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.AdminPermission, - }, - checkDomainPolicyReq: policies.Policy{ - Subject: email, - SubjectType: policies.UserType, - SubjectKind: policies.UsersKind, - Object: validID, - ObjectType: policies.DomainType, - Permission: policies.SharePermission, - }, - deletePoliciesErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(auth.Domain{}, nil) - repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyReq).Return(tc.checkPolicyErr) - repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkAdminPolicyReq).Return(tc.checkPolicyErr1) - repoCall3 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr1) - repoCall4 := pService.On("DeletePolicyFilter", mock.Anything, mock.Anything).Return(tc.deletePolicyFilterErr) - repoCall5 := drepo.On("DeletePolicies", mock.Anything, mock.Anything, mock.Anything).Return(tc.deletePoliciesErr) - err := svc.UnassignUser(context.Background(), tc.token, tc.domainID, 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() - repoCall3.Unset() - repoCall4.Unset() - repoCall5.Unset() - }) - } -} - -func TestListUsersDomains(t *testing.T) { - svc, accessToken := newService() - - cases := []struct { - desc string - token string - userID string - page auth.Page - retreiveByIDErr error - checkPolicyErr error - listDomainErr error - err error - }{ - { - desc: "list users domains successfully", - token: accessToken, - userID: validID, - page: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - }, - err: nil, - }, - { - desc: "list users domains successfully was admin", - token: accessToken, - userID: email, - page: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - }, - err: nil, - }, - { - desc: "list users domains with invalid token", - token: inValidToken, - userID: validID, - page: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - }, - err: svcerr.ErrAuthentication, - }, - { - desc: "list users domains with invalid domainID", - token: accessToken, - userID: inValid, - page: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - }, - checkPolicyErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "list users domains with repository error on list domains", - token: accessToken, - userID: validID, - page: auth.Page{ - Offset: 0, - Limit: 10, - Permission: policies.AdminPermission, - }, - listDomainErr: repoerr.ErrNotFound, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(tc.checkPolicyErr) - repoCall1 := drepo.On("ListDomains", mock.Anything, mock.Anything).Return(auth.DomainsPage{}, tc.listDomainErr) - _, err := svc.ListUserDomains(context.Background(), tc.token, tc.userID, tc.page) - 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() - }) + result := auth.SwitchToPermission(tc.relation) + assert.Equal(t, tc.result, result, fmt.Sprintf("switching to permission expected to succeed: %s", result)) } } @@ -2377,10 +1229,8 @@ func TestEncodeDomainUserID(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ar := auth.EncodeDomainUserID(tc.domainID, tc.userID) - assert.Equal(t, tc.response, ar, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.response, ar)) - }) + ar := auth.EncodeDomainUserID(tc.domainID, tc.userID) + assert.Equal(t, tc.response, ar, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.response, ar)) } } @@ -2418,10 +1268,8 @@ func TestDecodeDomainUserID(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ar, er := auth.DecodeDomainUserID(tc.domainUserID) - assert.Equal(t, tc.respUserID, er, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.respUserID, er)) - assert.Equal(t, tc.respDomainID, ar, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.respDomainID, ar)) - }) + ar, er := auth.DecodeDomainUserID(tc.domainUserID) + assert.Equal(t, tc.respUserID, er, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.respUserID, er)) + assert.Equal(t, tc.respDomainID, ar, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.respDomainID, ar)) } } diff --git a/auth/tracing/tracing.go b/auth/tracing/tracing.go index 97b5f1790f..6869398708 100644 --- a/auth/tracing/tracing.go +++ b/auth/tracing/tracing.go @@ -74,84 +74,3 @@ func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy) return tm.svc.Authorize(ctx, pr) } - -func (tm *tracingMiddleware) CreateDomain(ctx context.Context, token string, d auth.Domain) (auth.Domain, error) { - ctx, span := tm.tracer.Start(ctx, "create_domain", trace.WithAttributes( - attribute.String("name", d.Name), - )) - defer span.End() - return tm.svc.CreateDomain(ctx, token, d) -} - -func (tm *tracingMiddleware) RetrieveDomain(ctx context.Context, token, id string) (auth.Domain, error) { - ctx, span := tm.tracer.Start(ctx, "view_domain", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - return tm.svc.RetrieveDomain(ctx, token, id) -} - -func (tm *tracingMiddleware) RetrieveDomainPermissions(ctx context.Context, token, id string) (policies.Permissions, error) { - ctx, span := tm.tracer.Start(ctx, "view_domain_permissions", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - return tm.svc.RetrieveDomainPermissions(ctx, token, id) -} - -func (tm *tracingMiddleware) UpdateDomain(ctx context.Context, token, id string, d auth.DomainReq) (auth.Domain, error) { - ctx, span := tm.tracer.Start(ctx, "update_domain", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - return tm.svc.UpdateDomain(ctx, token, id, d) -} - -func (tm *tracingMiddleware) ChangeDomainStatus(ctx context.Context, token, id string, d auth.DomainReq) (auth.Domain, error) { - ctx, span := tm.tracer.Start(ctx, "change_domain_status", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - return tm.svc.ChangeDomainStatus(ctx, token, id, d) -} - -func (tm *tracingMiddleware) ListDomains(ctx context.Context, token string, p auth.Page) (auth.DomainsPage, error) { - ctx, span := tm.tracer.Start(ctx, "list_domains") - defer span.End() - return tm.svc.ListDomains(ctx, token, p) -} - -func (tm *tracingMiddleware) AssignUsers(ctx context.Context, token, id string, userIds []string, relation string) error { - ctx, span := tm.tracer.Start(ctx, "assign_users", trace.WithAttributes( - attribute.String("id", id), - attribute.StringSlice("user_ids", userIds), - attribute.String("relation", relation), - )) - defer span.End() - return tm.svc.AssignUsers(ctx, token, id, userIds, relation) -} - -func (tm *tracingMiddleware) UnassignUser(ctx context.Context, token, id, userID string) error { - ctx, span := tm.tracer.Start(ctx, "unassign_user", trace.WithAttributes( - attribute.String("id", id), - attribute.String("user_id", userID), - )) - defer span.End() - return tm.svc.UnassignUser(ctx, token, id, userID) -} - -func (tm *tracingMiddleware) ListUserDomains(ctx context.Context, token, userID string, p auth.Page) (auth.DomainsPage, error) { - ctx, span := tm.tracer.Start(ctx, "list_user_domains", trace.WithAttributes( - attribute.String("user_id", userID), - )) - defer span.End() - return tm.svc.ListUserDomains(ctx, token, userID, p) -} - -func (tm *tracingMiddleware) DeleteUserFromDomains(ctx context.Context, id string) error { - ctx, span := tm.tracer.Start(ctx, "delete_user_from_domains", trace.WithAttributes( - attribute.String("id", id), - )) - defer span.End() - return tm.svc.DeleteUserFromDomains(ctx, id) -} diff --git a/auth_grpc.pb.go b/auth_grpc.pb.go deleted file mode 100644 index a9bb42ddb2..0000000000 --- a/auth_grpc.pb.go +++ /dev/null @@ -1,484 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.4.0 -// - protoc v5.27.1 -// source: auth.proto - -package magistrala - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.62.0 or later. -const _ = grpc.SupportPackageIsVersion8 - -const ( - ThingsService_Authorize_FullMethodName = "/magistrala.ThingsService/Authorize" -) - -// ThingsServiceClient is the client API for ThingsService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// ThingsService is a service that provides things authorization functionalities -// for magistrala services. -type ThingsServiceClient interface { - // Authorize checks if the thing is authorized to perform - // the action on the channel. - Authorize(ctx context.Context, in *ThingsAuthzReq, opts ...grpc.CallOption) (*ThingsAuthzRes, error) -} - -type thingsServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewThingsServiceClient(cc grpc.ClientConnInterface) ThingsServiceClient { - return &thingsServiceClient{cc} -} - -func (c *thingsServiceClient) Authorize(ctx context.Context, in *ThingsAuthzReq, opts ...grpc.CallOption) (*ThingsAuthzRes, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ThingsAuthzRes) - err := c.cc.Invoke(ctx, ThingsService_Authorize_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// ThingsServiceServer is the server API for ThingsService service. -// All implementations must embed UnimplementedThingsServiceServer -// for forward compatibility -// -// ThingsService is a service that provides things authorization functionalities -// for magistrala services. -type ThingsServiceServer interface { - // Authorize checks if the thing is authorized to perform - // the action on the channel. - Authorize(context.Context, *ThingsAuthzReq) (*ThingsAuthzRes, error) - mustEmbedUnimplementedThingsServiceServer() -} - -// UnimplementedThingsServiceServer must be embedded to have forward compatible implementations. -type UnimplementedThingsServiceServer struct { -} - -func (UnimplementedThingsServiceServer) Authorize(context.Context, *ThingsAuthzReq) (*ThingsAuthzRes, error) { - return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented") -} -func (UnimplementedThingsServiceServer) mustEmbedUnimplementedThingsServiceServer() {} - -// UnsafeThingsServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to ThingsServiceServer will -// result in compilation errors. -type UnsafeThingsServiceServer interface { - mustEmbedUnimplementedThingsServiceServer() -} - -func RegisterThingsServiceServer(s grpc.ServiceRegistrar, srv ThingsServiceServer) { - s.RegisterService(&ThingsService_ServiceDesc, srv) -} - -func _ThingsService_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ThingsAuthzReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ThingsServiceServer).Authorize(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: ThingsService_Authorize_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ThingsServiceServer).Authorize(ctx, req.(*ThingsAuthzReq)) - } - return interceptor(ctx, in, info, handler) -} - -// ThingsService_ServiceDesc is the grpc.ServiceDesc for ThingsService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var ThingsService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "magistrala.ThingsService", - HandlerType: (*ThingsServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Authorize", - Handler: _ThingsService_Authorize_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "auth.proto", -} - -const ( - TokenService_Issue_FullMethodName = "/magistrala.TokenService/Issue" - TokenService_Refresh_FullMethodName = "/magistrala.TokenService/Refresh" -) - -// TokenServiceClient is the client API for TokenService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type TokenServiceClient interface { - Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error) - Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*Token, error) -} - -type tokenServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewTokenServiceClient(cc grpc.ClientConnInterface) TokenServiceClient { - return &tokenServiceClient{cc} -} - -func (c *tokenServiceClient) Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Token) - err := c.cc.Invoke(ctx, TokenService_Issue_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *tokenServiceClient) Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*Token, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Token) - err := c.cc.Invoke(ctx, TokenService_Refresh_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// TokenServiceServer is the server API for TokenService service. -// All implementations must embed UnimplementedTokenServiceServer -// for forward compatibility -type TokenServiceServer interface { - Issue(context.Context, *IssueReq) (*Token, error) - Refresh(context.Context, *RefreshReq) (*Token, error) - mustEmbedUnimplementedTokenServiceServer() -} - -// UnimplementedTokenServiceServer must be embedded to have forward compatible implementations. -type UnimplementedTokenServiceServer struct { -} - -func (UnimplementedTokenServiceServer) Issue(context.Context, *IssueReq) (*Token, error) { - return nil, status.Errorf(codes.Unimplemented, "method Issue not implemented") -} -func (UnimplementedTokenServiceServer) Refresh(context.Context, *RefreshReq) (*Token, error) { - return nil, status.Errorf(codes.Unimplemented, "method Refresh not implemented") -} -func (UnimplementedTokenServiceServer) mustEmbedUnimplementedTokenServiceServer() {} - -// UnsafeTokenServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to TokenServiceServer will -// result in compilation errors. -type UnsafeTokenServiceServer interface { - mustEmbedUnimplementedTokenServiceServer() -} - -func RegisterTokenServiceServer(s grpc.ServiceRegistrar, srv TokenServiceServer) { - s.RegisterService(&TokenService_ServiceDesc, srv) -} - -func _TokenService_Issue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(IssueReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(TokenServiceServer).Issue(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: TokenService_Issue_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(TokenServiceServer).Issue(ctx, req.(*IssueReq)) - } - return interceptor(ctx, in, info, handler) -} - -func _TokenService_Refresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RefreshReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(TokenServiceServer).Refresh(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: TokenService_Refresh_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(TokenServiceServer).Refresh(ctx, req.(*RefreshReq)) - } - return interceptor(ctx, in, info, handler) -} - -// TokenService_ServiceDesc is the grpc.ServiceDesc for TokenService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var TokenService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "magistrala.TokenService", - HandlerType: (*TokenServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Issue", - Handler: _TokenService_Issue_Handler, - }, - { - MethodName: "Refresh", - Handler: _TokenService_Refresh_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "auth.proto", -} - -const ( - AuthService_Authorize_FullMethodName = "/magistrala.AuthService/Authorize" - AuthService_Authenticate_FullMethodName = "/magistrala.AuthService/Authenticate" -) - -// AuthServiceClient is the client API for AuthService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// AuthService is a service that provides authentication and authorization -// functionalities for magistrala services. -type AuthServiceClient interface { - Authorize(ctx context.Context, in *AuthZReq, opts ...grpc.CallOption) (*AuthZRes, error) - Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) -} - -type authServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { - return &authServiceClient{cc} -} - -func (c *authServiceClient) Authorize(ctx context.Context, in *AuthZReq, opts ...grpc.CallOption) (*AuthZRes, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(AuthZRes) - err := c.cc.Invoke(ctx, AuthService_Authorize_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *authServiceClient) Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(AuthNRes) - err := c.cc.Invoke(ctx, AuthService_Authenticate_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// AuthServiceServer is the server API for AuthService service. -// All implementations must embed UnimplementedAuthServiceServer -// for forward compatibility -// -// AuthService is a service that provides authentication and authorization -// functionalities for magistrala services. -type AuthServiceServer interface { - Authorize(context.Context, *AuthZReq) (*AuthZRes, error) - Authenticate(context.Context, *AuthNReq) (*AuthNRes, error) - mustEmbedUnimplementedAuthServiceServer() -} - -// UnimplementedAuthServiceServer must be embedded to have forward compatible implementations. -type UnimplementedAuthServiceServer struct { -} - -func (UnimplementedAuthServiceServer) Authorize(context.Context, *AuthZReq) (*AuthZRes, error) { - return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented") -} -func (UnimplementedAuthServiceServer) Authenticate(context.Context, *AuthNReq) (*AuthNRes, error) { - return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented") -} -func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} - -// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to AuthServiceServer will -// result in compilation errors. -type UnsafeAuthServiceServer interface { - mustEmbedUnimplementedAuthServiceServer() -} - -func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { - s.RegisterService(&AuthService_ServiceDesc, srv) -} - -func _AuthService_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(AuthZReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(AuthServiceServer).Authorize(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: AuthService_Authorize_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AuthServiceServer).Authorize(ctx, req.(*AuthZReq)) - } - return interceptor(ctx, in, info, handler) -} - -func _AuthService_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(AuthNReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(AuthServiceServer).Authenticate(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: AuthService_Authenticate_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AuthServiceServer).Authenticate(ctx, req.(*AuthNReq)) - } - return interceptor(ctx, in, info, handler) -} - -// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var AuthService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "magistrala.AuthService", - HandlerType: (*AuthServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Authorize", - Handler: _AuthService_Authorize_Handler, - }, - { - MethodName: "Authenticate", - Handler: _AuthService_Authenticate_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "auth.proto", -} - -const ( - DomainsService_DeleteUserFromDomains_FullMethodName = "/magistrala.DomainsService/DeleteUserFromDomains" -) - -// DomainsServiceClient is the client API for DomainsService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -// -// DomainsService is a service that provides access to domains -// functionalities for magistrala services. -type DomainsServiceClient interface { - DeleteUserFromDomains(ctx context.Context, in *DeleteUserReq, opts ...grpc.CallOption) (*DeleteUserRes, error) -} - -type domainsServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewDomainsServiceClient(cc grpc.ClientConnInterface) DomainsServiceClient { - return &domainsServiceClient{cc} -} - -func (c *domainsServiceClient) DeleteUserFromDomains(ctx context.Context, in *DeleteUserReq, opts ...grpc.CallOption) (*DeleteUserRes, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(DeleteUserRes) - err := c.cc.Invoke(ctx, DomainsService_DeleteUserFromDomains_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// DomainsServiceServer is the server API for DomainsService service. -// All implementations must embed UnimplementedDomainsServiceServer -// for forward compatibility -// -// DomainsService is a service that provides access to domains -// functionalities for magistrala services. -type DomainsServiceServer interface { - DeleteUserFromDomains(context.Context, *DeleteUserReq) (*DeleteUserRes, error) - mustEmbedUnimplementedDomainsServiceServer() -} - -// UnimplementedDomainsServiceServer must be embedded to have forward compatible implementations. -type UnimplementedDomainsServiceServer struct { -} - -func (UnimplementedDomainsServiceServer) DeleteUserFromDomains(context.Context, *DeleteUserReq) (*DeleteUserRes, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeleteUserFromDomains not implemented") -} -func (UnimplementedDomainsServiceServer) mustEmbedUnimplementedDomainsServiceServer() {} - -// UnsafeDomainsServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to DomainsServiceServer will -// result in compilation errors. -type UnsafeDomainsServiceServer interface { - mustEmbedUnimplementedDomainsServiceServer() -} - -func RegisterDomainsServiceServer(s grpc.ServiceRegistrar, srv DomainsServiceServer) { - s.RegisterService(&DomainsService_ServiceDesc, srv) -} - -func _DomainsService_DeleteUserFromDomains_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeleteUserReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DomainsServiceServer).DeleteUserFromDomains(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: DomainsService_DeleteUserFromDomains_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DomainsServiceServer).DeleteUserFromDomains(ctx, req.(*DeleteUserReq)) - } - return interceptor(ctx, in, info, handler) -} - -// DomainsService_ServiceDesc is the grpc.ServiceDesc for DomainsService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var DomainsService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "magistrala.DomainsService", - HandlerType: (*DomainsServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "DeleteUserFromDomains", - Handler: _DomainsService_DeleteUserFromDomains_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "auth.proto", -} diff --git a/bootstrap/events/producer/streams_test.go b/bootstrap/events/producer/streams_test.go index aa5f1de866..ca3b542704 100644 --- a/bootstrap/events/producer/streams_test.go +++ b/bootstrap/events/producer/streams_test.go @@ -11,11 +11,11 @@ import ( "testing" "time" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/bootstrap" "github.com/absmach/magistrala/bootstrap/events/producer" "github.com/absmach/magistrala/bootstrap/mocks" "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/authn" mgauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" @@ -990,7 +990,7 @@ func TestChangeState(t *testing.T) { token string session mgauthn.Session state bootstrap.State - authResponse *magistrala.AuthZRes + authResponse authn.Session authorizeErr error connectErr error retrieveErr error @@ -1006,7 +1006,7 @@ func TestChangeState(t *testing.T) { userID: validID, domainID: domainID, state: bootstrap.Active, - authResponse: &magistrala.AuthZRes{Authorized: true}, + authResponse: authn.Session{}, err: nil, event: map[string]interface{}{ "thing_id": config.ThingID, diff --git a/channels/api/grpc/client.go b/channels/api/grpc/client.go new file mode 100644 index 0000000000..69977aad7e --- /dev/null +++ b/channels/api/grpc/client.go @@ -0,0 +1,164 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + "fmt" + "time" + + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/go-kit/kit/endpoint" + kitgrpc "github.com/go-kit/kit/transport/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const svcName = "channels.v1.ChannelsService" + +var _ grpcChannelsV1.ChannelsServiceClient = (*grpcClient)(nil) + +type grpcClient struct { + timeout time.Duration + authorize endpoint.Endpoint + removeThingConnections endpoint.Endpoint + unsetParentGroupFromChannels endpoint.Endpoint +} + +// NewClient returns new gRPC client instance. +func NewClient(conn *grpc.ClientConn, timeout time.Duration) grpcChannelsV1.ChannelsServiceClient { + return &grpcClient{ + authorize: kitgrpc.NewClient( + conn, + svcName, + "Authorize", + encodeAuthorizeRequest, + decodeAuthorizeResponse, + grpcChannelsV1.AuthzRes{}, + ).Endpoint(), + removeThingConnections: kitgrpc.NewClient( + conn, + svcName, + "RemoveThingConnections", + encodeRemoveThingConnectionsRequest, + decodeRemoveThingConnectionsResponse, + grpcChannelsV1.RemoveThingConnectionsRes{}, + ).Endpoint(), + unsetParentGroupFromChannels: kitgrpc.NewClient( + conn, + svcName, + "UnsetParentGroupFromChannels", + encodeUnsetParentGroupFromChannelsRequest, + decodeUnsetParentGroupFromChannelsResponse, + grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, + ).Endpoint(), + timeout: timeout, + } +} + +func (client grpcClient) Authorize(ctx context.Context, req *grpcChannelsV1.AuthzReq, _ ...grpc.CallOption) (r *grpcChannelsV1.AuthzRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + res, err := client.authorize(ctx, authorizeReq{ + domainID: req.GetDomainId(), + clientID: req.GetClientId(), + clientType: req.GetClientType(), + channelID: req.GetChannelId(), + permission: req.GetPermission(), + }) + if err != nil { + return &grpcChannelsV1.AuthzRes{}, decodeError(err) + } + + ar := res.(authorizeRes) + + return &grpcChannelsV1.AuthzRes{Authorized: ar.authorized}, nil +} + +func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(authorizeReq) + + return &grpcChannelsV1.AuthzReq{ + DomainId: req.domainID, + ClientId: req.clientID, + ClientType: req.clientType, + ChannelId: req.channelID, + Permission: req.permission, + }, nil +} + +func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(*grpcChannelsV1.AuthzRes) + + return authorizeRes{authorized: res.GetAuthorized()}, nil +} + +func (client grpcClient) RemoveThingConnections(ctx context.Context, req *grpcChannelsV1.RemoveThingConnectionsReq, _ ...grpc.CallOption) (r *grpcChannelsV1.RemoveThingConnectionsRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + if _, err := client.removeThingConnections(ctx, req); err != nil { + return &grpcChannelsV1.RemoveThingConnectionsRes{}, decodeError(err) + } + + return &grpcChannelsV1.RemoveThingConnectionsRes{}, nil +} + +func encodeRemoveThingConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + return grpcReq.(*grpcChannelsV1.RemoveThingConnectionsReq), nil +} + +func decodeRemoveThingConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + return grpcRes.(*grpcChannelsV1.RemoveThingConnectionsRes), nil +} + +func (client grpcClient) UnsetParentGroupFromChannels(ctx context.Context, req *grpcChannelsV1.UnsetParentGroupFromChannelsReq, _ ...grpc.CallOption) (r *grpcChannelsV1.UnsetParentGroupFromChannelsRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + if _, err := client.unsetParentGroupFromChannels(ctx, req); err != nil { + return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, decodeError(err) + } + + return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, nil +} + +func encodeUnsetParentGroupFromChannelsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + return grpcReq.(*grpcChannelsV1.UnsetParentGroupFromChannelsReq), nil +} + +func decodeUnsetParentGroupFromChannelsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + return grpcRes.(*grpcChannelsV1.UnsetParentGroupFromChannelsRes), nil +} + +func decodeError(err error) error { + if st, ok := status.FromError(err); ok { + switch st.Code() { + case codes.Unauthenticated: + return errors.Wrap(svcerr.ErrAuthentication, errors.New(st.Message())) + case codes.PermissionDenied: + return errors.Wrap(svcerr.ErrAuthorization, errors.New(st.Message())) + case codes.InvalidArgument: + return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message())) + case codes.FailedPrecondition: + return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message())) + case codes.NotFound: + return errors.Wrap(svcerr.ErrNotFound, errors.New(st.Message())) + case codes.AlreadyExists: + return errors.Wrap(svcerr.ErrConflict, errors.New(st.Message())) + case codes.OK: + if msg := st.Message(); msg != "" { + return errors.Wrap(errors.ErrUnidentified, errors.New(msg)) + } + return nil + default: + return errors.Wrap(fmt.Errorf("unexpected gRPC status: %s (status code:%v)", st.Code().String(), st.Code()), errors.New(st.Message())) + } + } + return err +} diff --git a/channels/api/grpc/doc.go b/channels/api/grpc/doc.go new file mode 100644 index 0000000000..20956ee50b --- /dev/null +++ b/channels/api/grpc/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package grpc contains implementation of Auth service gRPC API. +package grpc diff --git a/channels/api/grpc/endpoint.go b/channels/api/grpc/endpoint.go new file mode 100644 index 0000000000..64bf0766d8 --- /dev/null +++ b/channels/api/grpc/endpoint.go @@ -0,0 +1,54 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + + ch "github.com/absmach/magistrala/channels" + channels "github.com/absmach/magistrala/channels/private" + "github.com/go-kit/kit/endpoint" +) + +func authorizeEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(authorizeReq) + + if err := svc.Authorize(ctx, ch.AuthzReq{ + DomainID: req.domainID, + ClientID: req.clientID, + ClientType: req.clientType, + ChannelID: req.channelID, + Permission: req.permission, + }); err != nil { + return authorizeRes{}, err + } + + return authorizeRes{authorized: true}, nil + } +} + +func removeThingConnectionsEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(removeThingConnectionsReq) + + if err := svc.RemoveThingConnections(ctx, req.thingID); err != nil { + return removeThingConnectionsRes{}, err + } + + return removeThingConnectionsRes{}, nil + } +} + +func unsetParentGroupFromChannelsEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(unsetParentGroupFromChannelsReq) + + if err := svc.UnsetParentGroupFromChannels(ctx, req.parentGroupID); err != nil { + return unsetParentGroupFromChannelsRes{}, err + } + + return unsetParentGroupFromChannelsRes{}, nil + } +} diff --git a/channels/api/grpc/endpoint_test.go b/channels/api/grpc/endpoint_test.go new file mode 100644 index 0000000000..37be2940b7 --- /dev/null +++ b/channels/api/grpc/endpoint_test.go @@ -0,0 +1,37 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc_test + +import ( + "fmt" + "net" + + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + grpcapi "github.com/absmach/magistrala/things/api/grpc" + "github.com/absmach/magistrala/things/private/mocks" + "google.golang.org/grpc" +) + +const port = 7000 + +var ( + thingID = "testID" + thingKey = "testKey" + channelID = "testID" + invalid = "invalid" +) + +func startGRPCServer(svc *mocks.Service, port int) { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + panic(fmt.Sprintf("failed to obtain port: %s", err)) + } + server := grpc.NewServer() + grpcThingsV1.RegisterThingsServiceServer(server, grpcapi.NewServer(svc)) + go func() { + if err := server.Serve(listener); err != nil { + panic(fmt.Sprintf("failed to serve: %s", err)) + } + }() +} diff --git a/channels/api/grpc/request.go b/channels/api/grpc/request.go new file mode 100644 index 0000000000..f49c3c6983 --- /dev/null +++ b/channels/api/grpc/request.go @@ -0,0 +1,19 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +type authorizeReq struct { + domainID string + channelID string + clientID string + clientType string + permission string +} +type removeThingConnectionsReq struct { + thingID string +} + +type unsetParentGroupFromChannelsReq struct { + parentGroupID string +} diff --git a/channels/api/grpc/responses.go b/channels/api/grpc/responses.go new file mode 100644 index 0000000000..8f31b36e6c --- /dev/null +++ b/channels/api/grpc/responses.go @@ -0,0 +1,12 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +type authorizeRes struct { + authorized bool +} + +type removeThingConnectionsRes struct{} + +type unsetParentGroupFromChannelsRes struct{} diff --git a/channels/api/grpc/server.go b/channels/api/grpc/server.go new file mode 100644 index 0000000000..4fad3056a8 --- /dev/null +++ b/channels/api/grpc/server.go @@ -0,0 +1,140 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + + mgauth "github.com/absmach/magistrala/auth" + channels "github.com/absmach/magistrala/channels/private" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + kitgrpc "github.com/go-kit/kit/transport/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var _ grpcChannelsV1.ChannelsServiceServer = (*grpcServer)(nil) + +type grpcServer struct { + grpcChannelsV1.UnimplementedChannelsServiceServer + + authorize kitgrpc.Handler + removeThingConnections kitgrpc.Handler + unsetParentGroupFromChannels kitgrpc.Handler +} + +// NewServer returns new AuthServiceServer instance. +func NewServer(svc channels.Service) grpcChannelsV1.ChannelsServiceServer { + return &grpcServer{ + authorize: kitgrpc.NewServer( + authorizeEndpoint(svc), + decodeAuthorizeRequest, + encodeAuthorizeResponse, + ), + removeThingConnections: kitgrpc.NewServer( + removeThingConnectionsEndpoint(svc), + decodeRemoveThingConnectionsRequest, + encodeRemoveThingConnectionsResponse, + ), + unsetParentGroupFromChannels: kitgrpc.NewServer( + unsetParentGroupFromChannelsEndpoint(svc), + decodeUnsetParentGroupFromChannelsRequest, + encodeUnsetParentGroupFromChannelsResponse, + ), + } +} + +func (s *grpcServer) Authorize(ctx context.Context, req *grpcChannelsV1.AuthzReq) (*grpcChannelsV1.AuthzRes, error) { + _, res, err := s.authorize.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcChannelsV1.AuthzRes), nil +} + +func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcChannelsV1.AuthzReq) + + return authorizeReq{ + domainID: req.GetDomainId(), + clientID: req.GetClientId(), + clientType: req.GetClientType(), + channelID: req.GetChannelId(), + permission: req.GetPermission(), + }, nil +} + +func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(authorizeRes) + return &grpcChannelsV1.AuthzRes{Authorized: res.authorized}, nil +} + +func (s *grpcServer) RemoveThingConnections(ctx context.Context, req *grpcChannelsV1.RemoveThingConnectionsReq) (*grpcChannelsV1.RemoveThingConnectionsRes, error) { + _, res, err := s.removeThingConnections.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcChannelsV1.RemoveThingConnectionsRes), nil +} + +func decodeRemoveThingConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcChannelsV1.RemoveThingConnectionsReq) + + return removeThingConnectionsReq{ + thingID: req.GetThingId(), + }, nil +} + +func encodeRemoveThingConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + _ = grpcRes.(removeThingConnectionsRes) + return &grpcChannelsV1.RemoveThingConnectionsRes{}, nil +} + +func (s *grpcServer) UnsetParentGroupFromChannels(ctx context.Context, req *grpcChannelsV1.UnsetParentGroupFromChannelsReq) (*grpcChannelsV1.UnsetParentGroupFromChannelsRes, error) { + _, res, err := s.unsetParentGroupFromChannels.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcChannelsV1.UnsetParentGroupFromChannelsRes), nil +} + +func decodeUnsetParentGroupFromChannelsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcChannelsV1.UnsetParentGroupFromChannelsReq) + + return unsetParentGroupFromChannelsReq{ + parentGroupID: req.GetParentGroupId(), + }, nil +} + +func encodeUnsetParentGroupFromChannelsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + _ = grpcRes.(unsetParentGroupFromChannelsRes) + return &grpcChannelsV1.UnsetParentGroupFromChannelsRes{}, nil +} + +func encodeError(err error) error { + switch { + case errors.Contains(err, nil): + return nil + case errors.Contains(err, errors.ErrMalformedEntity), + err == apiutil.ErrInvalidAuthKey, + err == apiutil.ErrMissingID, + err == apiutil.ErrMissingMemberType, + err == apiutil.ErrMissingPolicySub, + err == apiutil.ErrMissingPolicyObj, + err == apiutil.ErrMalformedPolicyAct: + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Contains(err, svcerr.ErrAuthentication), + errors.Contains(err, mgauth.ErrKeyExpired), + err == apiutil.ErrMissingEmail, + err == apiutil.ErrBearerToken: + return status.Error(codes.Unauthenticated, err.Error()) + case errors.Contains(err, svcerr.ErrAuthorization): + return status.Error(codes.PermissionDenied, err.Error()) + default: + return status.Error(codes.Internal, err.Error()) + } +} diff --git a/channels/api/http/decode.go b/channels/api/http/decode.go new file mode 100644 index 0000000000..40cf13ea04 --- /dev/null +++ b/channels/api/http/decode.go @@ -0,0 +1,229 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + "encoding/json" + "net/http" + "strings" + + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/errors" + mgclients "github.com/absmach/magistrala/things" + "github.com/go-chi/chi/v5" +) + +func decodeViewChannel(_ context.Context, r *http.Request) (interface{}, error) { + req := viewChannelReq{ + id: chi.URLParam(r, "channelID"), + } + + return req, nil +} + +func decodeCreateChannelReq(_ 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 := createChannelReq{} + if err := json.NewDecoder(r.Body).Decode(&req.Channel); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) + } + + return req, nil +} + +func decodeCreateChannelsReq(_ 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 := createChannelsReq{} + if err := json.NewDecoder(r.Body).Decode(&req.Channels); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) + } + + return req, nil +} + +func decodeListChannels(_ context.Context, r *http.Request) (interface{}, error) { + s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefClientStatus) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + m, err := apiutil.ReadMetadataQuery(r, api.MetadataKey, nil) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + n, err := apiutil.ReadStringQuery(r, api.NameKey, "") + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + t, err := apiutil.ReadStringQuery(r, api.TagKey, "") + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + id, err := apiutil.ReadStringQuery(r, api.IDOrder, "") + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + p, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + lp, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + st, err := mgclients.ToStatus(s) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + req := listChannelsReq{ + status: st, + offset: o, + limit: l, + metadata: m, + name: n, + tag: t, + permission: p, + listPerms: lp, + userID: chi.URLParam(r, "userID"), + id: id, + } + return req, nil +} + +func decodeUpdateChannel(_ 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 := updateChannelReq{ + id: chi.URLParam(r, "channelID"), + } + 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 decodeUpdateChannelTags(_ 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 := updateChannelTagsReq{ + id: chi.URLParam(r, "channelID"), + } + 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 decodeSetChannelParentGroupStatus(_ 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 := setChannelParentGroupReq{ + id: chi.URLParam(r, "channelID"), + } + 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 decodeRemoveChannelParentGroupStatus(_ context.Context, r *http.Request) (interface{}, error) { + req := removeChannelParentGroupReq{ + id: chi.URLParam(r, "channelID"), + } + + return req, nil +} + +func decodeChangeChannelStatus(_ context.Context, r *http.Request) (interface{}, error) { + req := changeChannelStatusReq{ + id: chi.URLParam(r, "channelID"), + } + + return req, nil +} + +func decodeDeleteChannelReq(_ context.Context, r *http.Request) (interface{}, error) { + req := deleteChannelReq{ + id: chi.URLParam(r, "channelID"), + } + return req, nil +} + +func decodeConnectChannelThingsRequest(_ 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 := connectChannelThingsRequest{ + channelID: chi.URLParam(r, "channelID"), + } + 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 decodeDisconnectChannelThingsRequest(_ 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 := disconnectChannelThingsRequest{ + channelID: chi.URLParam(r, "channelID"), + } + 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 decodeConnectRequest(_ 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 := connectRequest{} + 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 decodeDisconnectRequest(_ 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 := disconnectRequest{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) + } + + return req, nil +} diff --git a/channels/api/http/endpoints.go b/channels/api/http/endpoints.go new file mode 100644 index 0000000000..6517be2588 --- /dev/null +++ b/channels/api/http/endpoints.go @@ -0,0 +1,369 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/go-kit/kit/endpoint" +) + +func createChannelEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(createChannelReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + channels, err := svc.CreateChannels(ctx, session, req.Channel) + if err != nil { + return nil, err + } + + return createChannelRes{ + Channel: channels[0], + created: true, + }, nil + } +} + +func createChannelsEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(createChannelsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + channels, err := svc.CreateChannels(ctx, session, req.Channels...) + if err != nil { + return nil, err + } + + res := channelsPageRes{ + pageRes: pageRes{ + Total: uint64(len(channels)), + }, + Channels: []viewChannelRes{}, + } + for _, c := range channels { + res.Channels = append(res.Channels, viewChannelRes{Channel: c}) + } + + return res, nil + } +} + +func viewChannelEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(viewChannelReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + c, err := svc.ViewChannel(ctx, session, req.id) + if err != nil { + return nil, err + } + + return viewChannelRes{Channel: c}, nil + } +} + +func listChannelsEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listChannelsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + pm := channels.PageMetadata{ + Status: req.status, + Offset: req.offset, + Limit: req.limit, + Name: req.name, + Tag: req.tag, + Permission: req.permission, + Metadata: req.metadata, + ListPerms: req.listPerms, + Id: req.id, + } + page, err := svc.ListChannels(ctx, session, pm) + if err != nil { + return nil, err + } + + res := channelsPageRes{ + pageRes: pageRes{ + Total: page.Total, + Offset: page.Offset, + Limit: page.Limit, + }, + Channels: []viewChannelRes{}, + } + for _, c := range page.Channels { + res.Channels = append(res.Channels, viewChannelRes{Channel: c}) + } + + return res, nil + } +} + +func updateChannelEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(updateChannelReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ch := channels.Channel{ + ID: req.id, + Name: req.Name, + Metadata: req.Metadata, + } + ch, err := svc.UpdateChannel(ctx, session, ch) + if err != nil { + return nil, err + } + + return updateChannelRes{Channel: ch}, nil + } +} + +func updateChannelTagsEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(updateChannelTagsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ch := channels.Channel{ + ID: req.id, + Tags: req.Tags, + } + ch, err := svc.UpdateChannelTags(ctx, session, ch) + if err != nil { + return nil, err + } + + return updateChannelRes{Channel: ch}, nil + } +} + +func setChannelParentGroupEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(setChannelParentGroupReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.SetParentGroup(ctx, session, req.ParentGroupID, req.id); err != nil { + return nil, err + } + + return setChannelParentGroupRes{}, nil + } +} + +func removeChannelParentGroupEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(removeChannelParentGroupReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.RemoveParentGroup(ctx, session, req.id); err != nil { + return nil, err + } + + return removeChannelParentGroupRes{}, nil + } +} + +func enableChannelEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(changeChannelStatusReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ch, err := svc.EnableChannel(ctx, session, req.id) + if err != nil { + return nil, err + } + + return changeChannelStatusRes{Channel: ch}, nil + } +} + +func disableChannelEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(changeChannelStatusReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ch, err := svc.DisableChannel(ctx, session, req.id) + if err != nil { + return nil, err + } + + return changeChannelStatusRes{Channel: ch}, nil + } +} + +func connectChannelThingsEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(connectChannelThingsRequest) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.Connect(ctx, session, []string{req.channelID}, req.ThingIds); err != nil { + return nil, err + } + + return connectChannelThingsRes{}, nil + } +} + +func disconnectChannelThingsEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(disconnectChannelThingsRequest) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.Disconnect(ctx, session, []string{req.channelID}, req.ThingIds); err != nil { + return nil, err + } + + return disconnectChannelThingsRes{}, nil + } +} + +func connectEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(connectRequest) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.Connect(ctx, session, req.ChannelIds, req.ThingIds); err != nil { + return nil, err + } + + return connectRes{}, nil + } +} + +func disconnectEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(disconnectRequest) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.Disconnect(ctx, session, req.ChannelIds, req.ThingIds); err != nil { + return nil, err + } + + return disconnectRes{}, nil + } +} + +func deleteChannelEndpoint(svc channels.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteChannelReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.RemoveChannel(ctx, session, req.id); err != nil { + return nil, err + } + + return deleteChannelRes{}, nil + } +} diff --git a/channels/api/http/requests.go b/channels/api/http/requests.go new file mode 100644 index 0000000000..a5ca153bd3 --- /dev/null +++ b/channels/api/http/requests.go @@ -0,0 +1,280 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + mgclients "github.com/absmach/magistrala/things" +) + +type createChannelReq struct { + Channel channels.Channel +} + +func (req createChannelReq) validate() error { + if len(req.Channel.Name) > api.MaxNameSize { + return apiutil.ErrNameSize + } + if req.Channel.ID != "" { + return api.ValidateUUID(req.Channel.ID) + } + + return nil +} + +type createChannelsReq struct { + Channels []channels.Channel +} + +func (req createChannelsReq) validate() error { + if len(req.Channels) == 0 { + return apiutil.ErrEmptyList + } + for _, channel := range req.Channels { + if channel.ID != "" { + if err := api.ValidateUUID(channel.ID); err != nil { + return err + } + } + if len(channel.Name) > api.MaxNameSize { + return apiutil.ErrNameSize + } + } + + return nil +} + +type viewChannelReq struct { + id string +} + +func (req viewChannelReq) validate() error { + + if req.id == "" { + return apiutil.ErrMissingID + } + return nil +} + +type listChannelsReq struct { + status mgclients.Status + offset uint64 + limit uint64 + name string + tag string + permission string + visibility string + userID string + listPerms bool + metadata mgclients.Metadata + id string +} + +func (req listChannelsReq) validate() error { + if req.limit > api.MaxLimitSize || req.limit < 1 { + return apiutil.ErrLimitSize + } + if req.visibility != "" && + req.visibility != api.AllVisibility && + req.visibility != api.MyVisibility && + req.visibility != api.SharedVisibility { + return apiutil.ErrInvalidVisibilityType + } + if len(req.name) > api.MaxNameSize { + return apiutil.ErrNameSize + } + + return nil +} + +type updateChannelReq struct { + id string + Name string `json:"name,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +func (req updateChannelReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + if len(req.Name) > api.MaxNameSize { + return apiutil.ErrNameSize + } + + return nil +} + +type updateChannelTagsReq struct { + id string + Tags []string `json:"tags,omitempty"` +} + +func (req updateChannelTagsReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + + return nil +} + +type setChannelParentGroupReq struct { + id string + ParentGroupID string `json:"parent_group_id"` +} + +func (req setChannelParentGroupReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + if req.ParentGroupID == "" { + return apiutil.ErrMissingParentGroupID + } + + return nil +} + +type removeChannelParentGroupReq struct { + id string +} + +func (req removeChannelParentGroupReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + + return nil +} + +type changeChannelStatusReq struct { + id string +} + +func (req changeChannelStatusReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + + return nil +} + +type connectChannelThingsRequest struct { + channelID string + ThingIds []string `json:"thing_ids,omitempty"` +} + +func (req *connectChannelThingsRequest) validate() error { + + if req.channelID == "" { + return apiutil.ErrMissingID + } + + if err := api.ValidateUUID(req.channelID); err != nil { + return err + } + + if len(req.ThingIds) == 0 { + return apiutil.ErrMissingID + } + + for _, tid := range req.ThingIds { + if err := api.ValidateUUID(tid); err != nil { + return err + } + } + return nil +} + +type disconnectChannelThingsRequest struct { + channelID string + ThingIds []string `json:"thing_ids,omitempty"` +} + +func (req *disconnectChannelThingsRequest) validate() error { + if req.channelID == "" { + return apiutil.ErrMissingID + } + + if err := api.ValidateUUID(req.channelID); err != nil { + return err + } + + if len(req.ThingIds) == 0 { + return apiutil.ErrMissingID + } + + for _, tid := range req.ThingIds { + if err := api.ValidateUUID(tid); err != nil { + return err + } + } + return nil +} + +type connectRequest struct { + ChannelIds []string `json:"channel_ids,omitempty"` + ThingIds []string `json:"thing_ids,omitempty"` +} + +func (req *connectRequest) validate() error { + if len(req.ChannelIds) == 0 { + return apiutil.ErrMissingID + } + for _, cid := range req.ChannelIds { + if err := api.ValidateUUID(cid); err != nil { + return err + } + } + + if len(req.ThingIds) == 0 { + return apiutil.ErrMissingID + } + + for _, tid := range req.ThingIds { + if err := api.ValidateUUID(tid); err != nil { + return err + } + } + return nil +} + +type disconnectRequest struct { + ChannelIds []string `json:"channel_ids,omitempty"` + ThingIds []string `json:"thing_ids,omitempty"` +} + +func (req *disconnectRequest) validate() error { + if len(req.ChannelIds) == 0 { + return apiutil.ErrMissingID + } + for _, cid := range req.ChannelIds { + if err := api.ValidateUUID(cid); err != nil { + return err + } + } + + if len(req.ThingIds) == 0 { + return apiutil.ErrMissingID + } + + for _, tid := range req.ThingIds { + if err := api.ValidateUUID(tid); err != nil { + return err + } + } + return nil +} + +type deleteChannelReq struct { + id string +} + +func (req deleteChannelReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + return nil +} diff --git a/channels/api/http/responses.go b/channels/api/http/responses.go new file mode 100644 index 0000000000..1f8c25a7ad --- /dev/null +++ b/channels/api/http/responses.go @@ -0,0 +1,221 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "fmt" + "net/http" + + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/channels" +) + +var ( + _ magistrala.Response = (*createChannelRes)(nil) + _ magistrala.Response = (*viewChannelRes)(nil) + _ magistrala.Response = (*channelsPageRes)(nil) + _ magistrala.Response = (*updateChannelRes)(nil) + _ magistrala.Response = (*deleteChannelRes)(nil) + _ magistrala.Response = (*connectChannelThingsRes)(nil) + _ magistrala.Response = (*disconnectChannelThingsRes)(nil) + _ magistrala.Response = (*connectRes)(nil) + _ magistrala.Response = (*disconnectRes)(nil) + _ magistrala.Response = (*changeChannelStatusRes)(nil) +) + +type pageRes struct { + Limit uint64 `json:"limit,omitempty"` + Offset uint64 `json:"offset"` + Total uint64 `json:"total"` +} + +type createChannelRes struct { + channels.Channel + created bool +} + +func (res createChannelRes) Code() int { + if res.created { + return http.StatusCreated + } + + return http.StatusOK +} + +func (res createChannelRes) Headers() map[string]string { + if res.created { + return map[string]string{ + "Location": fmt.Sprintf("/channels/%s", res.ID), + } + } + + return map[string]string{} +} + +func (res createChannelRes) Empty() bool { + return false +} + +type viewChannelRes struct { + channels.Channel +} + +func (res viewChannelRes) Code() int { + return http.StatusOK +} + +func (res viewChannelRes) Headers() map[string]string { + return map[string]string{} +} + +func (res viewChannelRes) Empty() bool { + return false +} + +type channelsPageRes struct { + pageRes + Channels []viewChannelRes `json:"channels"` +} + +func (res channelsPageRes) Code() int { + return http.StatusOK +} + +func (res channelsPageRes) Headers() map[string]string { + return map[string]string{} +} + +func (res channelsPageRes) Empty() bool { + return false +} + +type changeChannelStatusRes struct { + channels.Channel +} + +func (res changeChannelStatusRes) Code() int { + return http.StatusOK +} + +func (res changeChannelStatusRes) Headers() map[string]string { + return map[string]string{} +} + +func (res changeChannelStatusRes) Empty() bool { + return false +} + +type updateChannelRes struct { + channels.Channel +} + +func (res updateChannelRes) Code() int { + return http.StatusOK +} + +func (res updateChannelRes) Headers() map[string]string { + return map[string]string{} +} + +func (res updateChannelRes) Empty() bool { + return false +} + +type setChannelParentGroupRes struct{} + +func (res setChannelParentGroupRes) Code() int { + return http.StatusAccepted +} + +func (res setChannelParentGroupRes) Headers() map[string]string { + return map[string]string{} +} + +func (res setChannelParentGroupRes) Empty() bool { + return true +} + +type removeChannelParentGroupRes struct{} + +func (res removeChannelParentGroupRes) Code() int { + return http.StatusNoContent +} + +func (res removeChannelParentGroupRes) Headers() map[string]string { + return map[string]string{} +} + +func (res removeChannelParentGroupRes) Empty() bool { + return true +} + +type deleteChannelRes struct{} + +func (res deleteChannelRes) Code() int { + return http.StatusNoContent +} + +func (res deleteChannelRes) Headers() map[string]string { + return map[string]string{} +} + +func (res deleteChannelRes) Empty() bool { + return true +} + +type connectChannelThingsRes struct{} + +func (res connectChannelThingsRes) Code() int { + return http.StatusCreated +} + +func (res connectChannelThingsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res connectChannelThingsRes) Empty() bool { + return true +} + +type disconnectChannelThingsRes struct{} + +func (res disconnectChannelThingsRes) Code() int { + return http.StatusNoContent +} + +func (res disconnectChannelThingsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res disconnectChannelThingsRes) Empty() bool { + return true +} + +type connectRes struct{} + +func (res connectRes) Code() int { + return http.StatusCreated +} + +func (res connectRes) Headers() map[string]string { + return map[string]string{} +} + +func (res connectRes) Empty() bool { + return true +} + +type disconnectRes struct{} + +func (res disconnectRes) Code() int { + return http.StatusNoContent +} + +func (res disconnectRes) Headers() map[string]string { + return map[string]string{} +} + +func (res disconnectRes) Empty() bool { + return true +} diff --git a/channels/api/http/transport.go b/channels/api/http/transport.go new file mode 100644 index 0000000000..923cb9e54b --- /dev/null +++ b/channels/api/http/transport.go @@ -0,0 +1,143 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "log/slog" + + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + mgauthn "github.com/absmach/magistrala/pkg/authn" + "github.com/go-chi/chi/v5" + kithttp "github.com/go-kit/kit/transport/http" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +// MakeHandler returns a HTTP handler for Channels API endpoints. +func MakeHandler(svc channels.Service, authn mgauthn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string) *chi.Mux { + + opts := []kithttp.ServerOption{ + kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), + } + mux.Route("/{domainID}/channels", func(r chi.Router) { + r.Use(api.AuthenticateMiddleware(authn, true)) + + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + createChannelEndpoint(svc), + decodeCreateChannelReq, + api.EncodeResponse, + opts..., + ), "create_channel").ServeHTTP) + + r.Post("/bulk", otelhttp.NewHandler(kithttp.NewServer( + createChannelsEndpoint(svc), + decodeCreateChannelsReq, + api.EncodeResponse, + opts..., + ), "create_channels").ServeHTTP) + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + listChannelsEndpoint(svc), + decodeListChannels, + api.EncodeResponse, + opts..., + ), "list_channels").ServeHTTP) + + r.Post("/connect", otelhttp.NewHandler(kithttp.NewServer( + connectEndpoint(svc), + decodeConnectRequest, + api.EncodeResponse, + opts..., + ), "connect").ServeHTTP) + + r.Post("/disconnect", otelhttp.NewHandler(kithttp.NewServer( + disconnectEndpoint(svc), + decodeDisconnectRequest, + api.EncodeResponse, + opts..., + ), "disconnect").ServeHTTP) + + r.Route("/{channelID}", func(r chi.Router) { + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + viewChannelEndpoint(svc), + decodeViewChannel, + api.EncodeResponse, + opts..., + ), "view_channel").ServeHTTP) + + r.Patch("/", otelhttp.NewHandler(kithttp.NewServer( + updateChannelEndpoint(svc), + decodeUpdateChannel, + api.EncodeResponse, + opts..., + ), "update_channel_name_and_metadata").ServeHTTP) + + r.Patch("/tags", otelhttp.NewHandler(kithttp.NewServer( + updateChannelTagsEndpoint(svc), + decodeUpdateChannelTags, + api.EncodeResponse, + opts..., + ), "update_channel_tag").ServeHTTP) + + r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( + deleteChannelEndpoint(svc), + decodeDeleteChannelReq, + api.EncodeResponse, + opts..., + ), "delete_channel").ServeHTTP) + + r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer( + enableChannelEndpoint(svc), + decodeChangeChannelStatus, + api.EncodeResponse, + opts..., + ), "enable_channel").ServeHTTP) + + r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer( + disableChannelEndpoint(svc), + decodeChangeChannelStatus, + api.EncodeResponse, + opts..., + ), "disable_channel").ServeHTTP) + + r.Post("/parent", otelhttp.NewHandler(kithttp.NewServer( + setChannelParentGroupEndpoint(svc), + decodeSetChannelParentGroupStatus, + api.EncodeResponse, + opts..., + ), "set_channel_parent_group").ServeHTTP) + + r.Delete("/parent", otelhttp.NewHandler(kithttp.NewServer( + removeChannelParentGroupEndpoint(svc), + decodeRemoveChannelParentGroupStatus, + api.EncodeResponse, + opts..., + ), "remove_channel_parent_group").ServeHTTP) + + r.Post("/connect", otelhttp.NewHandler(kithttp.NewServer( + connectChannelThingsEndpoint(svc), + decodeConnectChannelThingsRequest, + api.EncodeResponse, + opts..., + ), "connect_channel_thing").ServeHTTP) + + r.Post("/disconnect", otelhttp.NewHandler(kithttp.NewServer( + disconnectChannelThingsEndpoint(svc), + decodeDisconnectChannelThingsRequest, + api.EncodeResponse, + opts..., + ), "disconnect_channel_thing").ServeHTTP) + }) + + }) + + mux.Get("/health", magistrala.Health("channels", instanceID)) + mux.Handle("/metrics", promhttp.Handler()) + + return mux +} diff --git a/channels/channels.go b/channels/channels.go new file mode 100644 index 0000000000..dc3e676400 --- /dev/null +++ b/channels/channels.go @@ -0,0 +1,168 @@ +// Copyright (c) Mainflux +// SPDX-License-Identifier: Apache-2.0 + +package channels + +import ( + "context" + "time" + + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/roles" + clients "github.com/absmach/magistrala/things" +) + +// Channel represents a Mainflux "communication group". This group contains the +// things that can exchange messages between each other. +type Channel struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Tags []string `json:"tags,omitempty"` + ParentGroup string `json:"parent_group_id,omitempty"` + Domain string `json:"domain_id,omitempty"` + Metadata clients.Metadata `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + UpdatedBy string `json:"updated_by,omitempty"` + Status clients.Status `json:"status,omitempty"` // 1 for enabled, 0 for disabled + Permissions []string `json:"permissions,omitempty"` // 1 for enabled, 0 for disabled +} + +type PageMetadata struct { + Total uint64 `json:"total"` + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Name string `json:"name,omitempty"` + Id string `json:"id,omitempty"` + Order string `json:"order,omitempty"` + Dir string `json:"dir,omitempty"` + Metadata clients.Metadata `json:"metadata,omitempty"` + Domain string `json:"domain,omitempty"` + Tag string `json:"tag,omitempty"` + Permission string `json:"permission,omitempty"` + Status clients.Status `json:"status,omitempty"` + IDs []string `json:"ids,omitempty"` + ListPerms bool `json:"-"` + ThingID string `json:"-"` +} + +// ChannelsPage contains page related metadata as well as list of channels that +// belong to this page. +type Page struct { + PageMetadata + Channels []Channel +} + +type Connection struct { + ThingID string + ChannelID string + DomainID string +} + +type AuthzReq struct { + DomainID string + ChannelID string + ClientID string + ClientType string + Permission string +} + +//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines" +type Service interface { + // CreateChannels adds channels to the user identified by the provided key. + CreateChannels(ctx context.Context, session authn.Session, channels ...Channel) ([]Channel, error) + + // ViewChannel retrieves data about the channel identified by the provided + // ID, that belongs to the user identified by the provided key. + ViewChannel(ctx context.Context, session authn.Session, id string) (Channel, error) + + // UpdateChannel updates the channel identified by the provided ID, that + // belongs to the user identified by the provided key. + UpdateChannel(ctx context.Context, session authn.Session, channel Channel) (Channel, error) + + // UpdateChannelTags updates the channel's tags. + UpdateChannelTags(ctx context.Context, session authn.Session, channel Channel) (Channel, error) + + EnableChannel(ctx context.Context, session authn.Session, id string) (Channel, error) + + DisableChannel(ctx context.Context, session authn.Session, id string) (Channel, error) + + // ListChannels retrieves data about subset of channels that belongs to the + // user identified by the provided key. + ListChannels(ctx context.Context, session authn.Session, pm PageMetadata) (Page, error) + + // ListChannelsByThing retrieves data about subset of channels that have + // specified thing connected or not connected to them and belong to the user identified by + // the provided key. + ListChannelsByThing(ctx context.Context, session authn.Session, thID string, pm PageMetadata) (Page, error) + + // RemoveChannel removes the thing identified by the provided ID, that + // belongs to the user identified by the provided key. + RemoveChannel(ctx context.Context, session authn.Session, id string) error + + // Connect adds things to the channels list of connected things. + Connect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error + + // Disconnect removes things from the channels list of connected things. + Disconnect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error + + SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error + + RemoveParentGroup(ctx context.Context, session authn.Session, id string) error + + roles.RoleManager +} + +// ChannelRepository specifies a channel persistence API. +// +//go:generate mockery --name Repository --output=./mocks --filename repository.go --quiet --note "Copyright (c) Abstract Machines" +type Repository interface { + // Save persists multiple channels. Channels are saved using a transaction. If one channel + // fails then none will be saved. Successful operation is indicated by non-nil + // error response. + Save(ctx context.Context, chs ...Channel) ([]Channel, error) + + // Update performs an update to the existing channel. + Update(ctx context.Context, c Channel) (Channel, error) + + UpdateTags(ctx context.Context, ch Channel) (Channel, error) + + ChangeStatus(ctx context.Context, channel Channel) (Channel, error) + + // RetrieveByID retrieves the channel having the provided identifier + RetrieveByID(ctx context.Context, id string) (Channel, error) + + // RetrieveAll retrieves the subset of channels. + RetrieveAll(ctx context.Context, pm PageMetadata) (Page, error) + + // Remove removes the channel having the provided identifier + Remove(ctx context.Context, ids ...string) error + + // SetParentGroup set parent group id to a given channel id + SetParentGroup(ctx context.Context, ch Channel) error + + // RemoveParentGroup remove parent group id fr given chanel id + RemoveParentGroup(ctx context.Context, ch Channel) error + + AddConnections(ctx context.Context, conns []Connection) error + + RemoveConnections(ctx context.Context, conns []Connection) error + + CheckConnection(ctx context.Context, conn Connection) error + + ThingAuthorize(ctx context.Context, conn Connection) error + + ChannelConnectionsCount(ctx context.Context, id string) (uint64, error) + + DoesChannelHaveConnections(ctx context.Context, id string) (bool, error) + + RemoveThingConnections(ctx context.Context, thingID string) error + + RemoveChannelConnections(ctx context.Context, channelID string) error + + RetrieveParentGroupChannels(ctx context.Context, parentGroupID string) ([]Channel, error) + + UnsetParentGroupFromChannels(ctx context.Context, parentGroupID string) error + + roles.Repository +} diff --git a/channels/events/doc.go b/channels/events/doc.go new file mode 100644 index 0000000000..cb8cccbf3a --- /dev/null +++ b/channels/events/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package events provides the domain concept definitions needed to support +// things clients events functionality. +package events diff --git a/channels/events/events.go b/channels/events/events.go new file mode 100644 index 0000000000..f56ba5df52 --- /dev/null +++ b/channels/events/events.go @@ -0,0 +1,308 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "time" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/pkg/events" +) + +const ( + channelPrefix = "channels." + channelCreate = channelPrefix + "create" + channelUpdate = channelPrefix + "update" + channelChangeStatus = channelPrefix + "change_status" + channelRemove = channelPrefix + "remove" + channelView = channelPrefix + "view" + channelList = channelPrefix + "list" + channelConnect = channelPrefix + "connect" + channelDisconnect = channelPrefix + "disconnect" + channelSetParent = channelPrefix + "set_parent" + channelRemoveParent = channelPrefix + "remove_parent" +) + +var ( + _ events.Event = (*createChannelEvent)(nil) + _ events.Event = (*updateChannelEvent)(nil) + _ events.Event = (*changeStatusChannelEvent)(nil) + _ events.Event = (*viewChannelEvent)(nil) + _ events.Event = (*listChannelEvent)(nil) + _ events.Event = (*removeChannelEvent)(nil) + _ events.Event = (*connectEvent)(nil) + _ events.Event = (*disconnectEvent)(nil) +) + +type createChannelEvent struct { + channels.Channel +} + +func (cce createChannelEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": channelCreate, + "id": cce.ID, + "status": cce.Status.String(), + "created_at": cce.CreatedAt, + } + + if cce.Name != "" { + val["name"] = cce.Name + } + if len(cce.Tags) > 0 { + val["tags"] = cce.Tags + } + if cce.Domain != "" { + val["domain"] = cce.Domain + } + if cce.Metadata != nil { + val["metadata"] = cce.Metadata + } + + return val, nil +} + +type updateChannelEvent struct { + channels.Channel + operation string +} + +func (uce updateChannelEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": channelUpdate, + "updated_at": uce.UpdatedAt, + "updated_by": uce.UpdatedBy, + } + if uce.operation != "" { + val["operation"] = channelUpdate + "_" + uce.operation + } + + if uce.ID != "" { + val["id"] = uce.ID + } + if uce.Name != "" { + val["name"] = uce.Name + } + if len(uce.Tags) > 0 { + val["tags"] = uce.Tags + } + if uce.Domain != "" { + val["domain"] = uce.Domain + } + if uce.Metadata != nil { + val["metadata"] = uce.Metadata + } + if !uce.CreatedAt.IsZero() { + val["created_at"] = uce.CreatedAt + } + if uce.Status.String() != "" { + val["status"] = uce.Status.String() + } + + return val, nil +} + +type changeStatusChannelEvent struct { + id string + status string + updatedAt time.Time + updatedBy string +} + +func (rce changeStatusChannelEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": channelChangeStatus, + "id": rce.id, + "status": rce.status, + "updated_at": rce.updatedAt, + "updated_by": rce.updatedBy, + }, nil +} + +type viewChannelEvent struct { + channels.Channel +} + +func (vce viewChannelEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": channelView, + "id": vce.ID, + } + + if vce.Name != "" { + val["name"] = vce.Name + } + if len(vce.Tags) > 0 { + val["tags"] = vce.Tags + } + if vce.Domain != "" { + val["domain"] = vce.Domain + } + if vce.Metadata != nil { + val["metadata"] = vce.Metadata + } + if !vce.CreatedAt.IsZero() { + val["created_at"] = vce.CreatedAt + } + if !vce.UpdatedAt.IsZero() { + val["updated_at"] = vce.UpdatedAt + } + if vce.UpdatedBy != "" { + val["updated_by"] = vce.UpdatedBy + } + if vce.Status.String() != "" { + val["status"] = vce.Status.String() + } + + return val, nil +} + +type listChannelEvent struct { + channels.PageMetadata +} + +func (lce listChannelEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": channelList, + "total": lce.Total, + "offset": lce.Offset, + "limit": lce.Limit, + } + + if lce.Name != "" { + val["name"] = lce.Name + } + if lce.Order != "" { + val["order"] = lce.Order + } + if lce.Dir != "" { + val["dir"] = lce.Dir + } + if lce.Metadata != nil { + val["metadata"] = lce.Metadata + } + if lce.Domain != "" { + val["domain"] = lce.Domain + } + if lce.Tag != "" { + val["tag"] = lce.Tag + } + if lce.Permission != "" { + val["permission"] = lce.Permission + } + if lce.Status.String() != "" { + val["status"] = lce.Status.String() + } + if len(lce.IDs) > 0 { + val["ids"] = lce.IDs + } + + return val, nil +} + +type listChannelByThingEvent struct { + thingID string + channels.PageMetadata +} + +func (lcte listChannelByThingEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": channelList, + "thing_id": lcte.thingID, + "total": lcte.Total, + "offset": lcte.Offset, + "limit": lcte.Limit, + } + + if lcte.Name != "" { + val["name"] = lcte.Name + } + if lcte.Order != "" { + val["order"] = lcte.Order + } + if lcte.Dir != "" { + val["dir"] = lcte.Dir + } + if lcte.Metadata != nil { + val["metadata"] = lcte.Metadata + } + if lcte.Domain != "" { + val["domain"] = lcte.Domain + } + if lcte.Tag != "" { + val["tag"] = lcte.Tag + } + if lcte.Permission != "" { + val["permission"] = lcte.Permission + } + if lcte.Status.String() != "" { + val["status"] = lcte.Status.String() + } + if len(lcte.IDs) > 0 { + val["ids"] = lcte.IDs + } + + return val, nil +} + +type removeChannelEvent struct { + id string +} + +func (dce removeChannelEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": channelRemove, + "id": dce.id, + }, nil +} + +type connectEvent struct { + chIDs []string + thIDs []string +} + +func (ce connectEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": channelConnect, + "thing_ids": ce.thIDs, + "channel_ids": ce.chIDs, + }, nil +} + +type disconnectEvent struct { + chIDs []string + thIDs []string +} + +func (de disconnectEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": channelDisconnect, + "thing_ids": de.thIDs, + "channel_ids": de.chIDs, + }, nil +} + +type setParentGroupEvent struct { + id string + parentGroupID string +} + +func (spge setParentGroupEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": channelSetParent, + "id": spge.id, + "parent_group_id": spge.parentGroupID, + }, nil +} + +type removeParentGroupEvent struct { + id string +} + +func (rpge removeParentGroupEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": channelRemoveParent, + "id": rpge.id, + }, nil +} diff --git a/channels/events/streams.go b/channels/events/streams.go new file mode 100644 index 0000000000..e76911ffa7 --- /dev/null +++ b/channels/events/streams.go @@ -0,0 +1,235 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/events" + "github.com/absmach/magistrala/pkg/events/store" + rmEvents "github.com/absmach/magistrala/pkg/roles/rolemanager/events" +) + +const streamID = "magistrala.things" + +var _ channels.Service = (*eventStore)(nil) + +type eventStore struct { + events.Publisher + svc channels.Service + rmEvents.RoleManagerEventStore +} + +// NewEventStoreMiddleware returns wrapper around things service that sends +// events to event store. +func NewEventStoreMiddleware(ctx context.Context, svc channels.Service, url string) (channels.Service, error) { + publisher, err := store.NewPublisher(ctx, url, streamID) + if err != nil { + return nil, err + } + + rolesSvcEventStoreMiddleware := rmEvents.NewRoleManagerEventStore("channels", svc, publisher) + return &eventStore{ + svc: svc, + Publisher: publisher, + RoleManagerEventStore: rolesSvcEventStoreMiddleware, + }, nil +} + +func (es *eventStore) CreateChannels(ctx context.Context, session authn.Session, chs ...channels.Channel) ([]channels.Channel, error) { + chs, err := es.svc.CreateChannels(ctx, session, chs...) + if err != nil { + return chs, err + } + + for _, ch := range chs { + event := createChannelEvent{ + ch, + } + if err := es.Publish(ctx, event); err != nil { + return chs, err + } + } + + return chs, nil +} + +func (es *eventStore) UpdateChannel(ctx context.Context, session authn.Session, ch channels.Channel) (channels.Channel, error) { + chann, err := es.svc.UpdateChannel(ctx, session, ch) + if err != nil { + return chann, err + } + + return es.update(ctx, "", chann) +} + +func (es *eventStore) UpdateChannelTags(ctx context.Context, session authn.Session, ch channels.Channel) (channels.Channel, error) { + chann, err := es.svc.UpdateChannelTags(ctx, session, ch) + if err != nil { + return chann, err + } + + return es.update(ctx, "tags", chann) +} + +func (es *eventStore) update(ctx context.Context, operation string, ch channels.Channel) (channels.Channel, error) { + event := updateChannelEvent{ + ch, operation, + } + + if err := es.Publish(ctx, event); err != nil { + return ch, err + } + + return ch, nil +} + +func (es *eventStore) ViewChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + chann, err := es.svc.ViewChannel(ctx, session, id) + if err != nil { + return chann, err + } + + event := viewChannelEvent{ + chann, + } + if err := es.Publish(ctx, event); err != nil { + return chann, err + } + + return chann, nil +} + +func (es *eventStore) ListChannels(ctx context.Context, session authn.Session, pm channels.PageMetadata) (channels.Page, error) { + cp, err := es.svc.ListChannels(ctx, session, pm) + if err != nil { + return cp, err + } + event := listChannelEvent{ + pm, + } + if err := es.Publish(ctx, event); err != nil { + return cp, err + } + + return cp, nil +} +func (es *eventStore) ListChannelsByThing(ctx context.Context, session authn.Session, thID string, pm channels.PageMetadata) (channels.Page, error) { + cp, err := es.svc.ListChannelsByThing(ctx, session, thID, pm) + if err != nil { + return cp, err + } + event := listChannelByThingEvent{ + thID, + pm, + } + if err := es.Publish(ctx, event); err != nil { + return cp, err + } + + return cp, nil +} +func (es *eventStore) EnableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + cli, err := es.svc.EnableChannel(ctx, session, id) + if err != nil { + return cli, err + } + + return es.changeStatus(ctx, cli) +} + +func (es *eventStore) DisableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + cli, err := es.svc.DisableChannel(ctx, session, id) + if err != nil { + return cli, err + } + + return es.changeStatus(ctx, cli) +} + +func (es *eventStore) changeStatus(ctx context.Context, ch channels.Channel) (channels.Channel, error) { + event := changeStatusChannelEvent{ + id: ch.ID, + updatedAt: ch.UpdatedAt, + updatedBy: ch.UpdatedBy, + status: ch.Status.String(), + } + if err := es.Publish(ctx, event); err != nil { + return ch, err + } + + return ch, nil +} + +func (es *eventStore) RemoveChannel(ctx context.Context, session authn.Session, id string) error { + if err := es.svc.RemoveChannel(ctx, session, id); err != nil { + return err + } + + event := removeChannelEvent{id} + + if err := es.Publish(ctx, event); err != nil { + return err + } + + return nil +} + +func (es *eventStore) Connect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + if err := es.svc.Connect(ctx, session, chIDs, thIDs); err != nil { + return err + } + + event := connectEvent{chIDs, thIDs} + + if err := es.Publish(ctx, event); err != nil { + return err + } + + return nil +} + +func (es *eventStore) Disconnect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + if err := es.svc.Disconnect(ctx, session, chIDs, thIDs); err != nil { + return err + } + + event := disconnectEvent{chIDs, thIDs} + + if err := es.Publish(ctx, event); err != nil { + return err + } + + return nil +} + +func (es *eventStore) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (err error) { + if err := es.svc.SetParentGroup(ctx, session, parentGroupID, id); err != nil { + return err + } + + event := setParentGroupEvent{parentGroupID: parentGroupID, id: id} + + if err := es.Publish(ctx, event); err != nil { + return err + } + + return nil +} + +func (es *eventStore) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (err error) { + if err := es.svc.RemoveParentGroup(ctx, session, id); err != nil { + return err + } + + event := removeParentGroupEvent{id: id} + + if err := es.Publish(ctx, event); err != nil { + return err + } + + return nil +} diff --git a/channels/middleware/authorization.go b/channels/middleware/authorization.go new file mode 100644 index 0000000000..7c1718ffc1 --- /dev/null +++ b/channels/middleware/authorization.go @@ -0,0 +1,348 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + "fmt" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/authz" + mgauthz "github.com/absmach/magistrala/pkg/authz" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" + "github.com/absmach/magistrala/pkg/svcutil" +) + +var ( + errView = errors.New("not authorized to view channel") + errUpdate = errors.New("not authorized to update channel") + errUpdateTags = errors.New("not authorized to update channel tags") + errEnable = errors.New("not authorized to enable channel") + errDisable = errors.New("not authorized to disable channel") + errDelete = errors.New("not authorized to delete channel") + errConnect = errors.New("not authorized to connect to channel") + errDisconnect = errors.New("not authorized to disconnect from channel") + errSetParentGroup = errors.New("not authorized to set parent group to channel") + errRemoveParentGroup = errors.New("not authorized to remove parent group from channel") + errDomainCreateChannels = errors.New("not authorized to create channel in domain") + errGroupSetChildChannels = errors.New("not authorized to set child channel for group") + errGroupRemoveChildChannels = errors.New("not authorized to remove child channel for group") + errThingDisConnectChannels = errors.New("not authorized to disconnect channel for thing") + errThingConnectChannels = errors.New("not authorized to connect channel for thing") +) + +var _ channels.Service = (*authorizationMiddleware)(nil) + +type authorizationMiddleware struct { + svc channels.Service + repo channels.Repository + authz mgauthz.Authorization + opp svcutil.OperationPerm + extOpp svcutil.ExternalOperationPerm + rmMW.RoleManagerAuthorizationMiddleware +} + +// AuthorizationMiddleware adds authorization to the channels service. +func AuthorizationMiddleware(svc channels.Service, repo channels.Repository, authz mgauthz.Authorization, channelsOpPerm, rolesOpPerm map[svcutil.Operation]svcutil.Permission, extOpPerm map[svcutil.ExternalOperation]svcutil.Permission) (channels.Service, error) { + opp := channels.NewOperationPerm() + if err := opp.AddOperationPermissionMap(channelsOpPerm); err != nil { + return nil, err + } + if err := opp.Validate(); err != nil { + return nil, err + } + ram, err := rmMW.NewRoleManagerAuthorizationMiddleware(policies.ChannelType, svc, authz, rolesOpPerm) + if err != nil { + return nil, err + } + extOpp := channels.NewExternalOperationPerm() + if err := extOpp.AddOperationPermissionMap(extOpPerm); err != nil { + return nil, err + } + if err := extOpp.Validate(); err != nil { + return nil, err + } + return &authorizationMiddleware{ + svc: svc, + repo: repo, + authz: authz, + RoleManagerAuthorizationMiddleware: ram, + opp: opp, + extOpp: extOpp, + }, nil +} + +func (am *authorizationMiddleware) CreateChannels(ctx context.Context, session authn.Session, chs ...channels.Channel) ([]channels.Channel, error) { + // If domain is disabled , then this authorization will fail for all non-admin domain users + if err := am.extAuthorize(ctx, channels.DomainOpCreateChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.DomainType, + Object: session.DomainID, + }); err != nil { + return []channels.Channel{}, errors.Wrap(err, errDomainCreateChannels) + } + + for _, ch := range chs { + if ch.ParentGroup != "" { + if err := am.extAuthorize(ctx, channels.GroupOpSetChildChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.GroupType, + Object: ch.ParentGroup, + }); err != nil { + return []channels.Channel{}, errors.Wrap(err, errors.Wrap(errGroupSetChildChannels, fmt.Errorf("channel name %s parent group id %s", ch.Name, ch.ParentGroup))) + } + } + + } + return am.svc.CreateChannels(ctx, session, chs...) +} + +func (am *authorizationMiddleware) ViewChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + if err := am.authorize(ctx, channels.OpViewChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: id, + }); err != nil { + return channels.Channel{}, errors.Wrap(err, errView) + } + return am.svc.ViewChannel(ctx, session, id) +} + +func (am *authorizationMiddleware) ListChannels(ctx context.Context, session authn.Session, pm channels.PageMetadata) (channels.Page, error) { + if err := am.checkSuperAdmin(ctx, session.UserID); err != nil { + session.SuperAdmin = true + } + return am.svc.ListChannels(ctx, session, pm) +} + +func (am *authorizationMiddleware) ListChannelsByThing(ctx context.Context, session authn.Session, thingID string, pm channels.PageMetadata) (channels.Page, error) { + + return am.svc.ListChannelsByThing(ctx, session, thingID, pm) +} + +func (am *authorizationMiddleware) UpdateChannel(ctx context.Context, session authn.Session, channel channels.Channel) (channels.Channel, error) { + if err := am.authorize(ctx, channels.OpUpdateChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: channel.ID, + }); err != nil { + return channels.Channel{}, errors.Wrap(err, errUpdate) + } + return am.svc.UpdateChannel(ctx, session, channel) +} + +func (am *authorizationMiddleware) UpdateChannelTags(ctx context.Context, session authn.Session, channel channels.Channel) (channels.Channel, error) { + if err := am.authorize(ctx, channels.OpUpdateChannelTags, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: channel.ID, + }); err != nil { + return channels.Channel{}, errors.Wrap(err, errUpdateTags) + } + return am.svc.UpdateChannelTags(ctx, session, channel) +} + +func (am *authorizationMiddleware) EnableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + if err := am.authorize(ctx, channels.OpEnableChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: id, + }); err != nil { + return channels.Channel{}, errors.Wrap(err, errEnable) + } + return am.svc.EnableChannel(ctx, session, id) +} + +func (am *authorizationMiddleware) DisableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + if err := am.authorize(ctx, channels.OpDisableChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: id, + }); err != nil { + return channels.Channel{}, errors.Wrap(err, errDisable) + } + return am.svc.DisableChannel(ctx, session, id) +} + +func (am *authorizationMiddleware) RemoveChannel(ctx context.Context, session authn.Session, id string) error { + if err := am.authorize(ctx, channels.OpDeleteChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: id, + }); err != nil { + return errors.Wrap(err, errDelete) + } + return am.svc.RemoveChannel(ctx, session, id) +} + +func (am *authorizationMiddleware) Connect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + //ToDo: This authorization will be changed with Bulk Authorization. For this we need to add bulk authorization API in policies. + for _, chID := range chIDs { + if err := am.authorize(ctx, channels.OpConnectThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: chID, + }); err != nil { + return errors.Wrap(err, errConnect) + } + } + + for _, thID := range thIDs { + if err := am.extAuthorize(ctx, channels.ThingsOpConnectChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: thID, + }); err != nil { + return errors.Wrap(err, errThingConnectChannels) + } + } + return am.svc.Connect(ctx, session, chIDs, thIDs) +} +func (am *authorizationMiddleware) Disconnect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + //ToDo: This authorization will be changed with Bulk Authorization. For this we need to add bulk authorization API in policies. + for _, chID := range chIDs { + if err := am.authorize(ctx, channels.OpDisconnectThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: chID, + }); err != nil { + return errors.Wrap(err, errDisconnect) + } + } + + for _, thID := range thIDs { + if err := am.extAuthorize(ctx, channels.ThingsOpDisconnectChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: thID, + }); err != nil { + return errors.Wrap(err, errThingDisConnectChannels) + } + } + return am.svc.Disconnect(ctx, session, chIDs, thIDs) +} + +func (am *authorizationMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error { + if err := am.authorize(ctx, channels.OpSetParentGroup, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: id, + }); err != nil { + return errors.Wrap(err, errSetParentGroup) + } + + if err := am.extAuthorize(ctx, channels.GroupOpSetChildChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.GroupType, + Object: parentGroupID, + }); err != nil { + return errors.Wrap(err, errGroupSetChildChannels) + } + return am.svc.SetParentGroup(ctx, session, parentGroupID, id) +} + +func (am *authorizationMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + if err := am.authorize(ctx, channels.OpSetParentGroup, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ChannelType, + Object: id, + }); err != nil { + return errors.Wrap(err, errRemoveParentGroup) + } + + ch, err := am.repo.RetrieveByID(ctx, id) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + if ch.ParentGroup != "" { + if err := am.extAuthorize(ctx, channels.GroupOpSetChildChannel, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.GroupType, + Object: ch.ParentGroup, + }); err != nil { + return errors.Wrap(err, errGroupRemoveChildChannels) + } + return am.svc.RemoveParentGroup(ctx, session, id) + } + return nil +} + +func (am *authorizationMiddleware) authorize(ctx context.Context, op svcutil.Operation, req authz.PolicyReq) error { + perm, err := am.opp.GetPermission(op) + if err != nil { + return err + } + + req.Permission = perm.String() + + if err := am.authz.Authorize(ctx, req); err != nil { + return err + } + + return nil +} + +func (am *authorizationMiddleware) extAuthorize(ctx context.Context, extOp svcutil.ExternalOperation, req authz.PolicyReq) error { + perm, err := am.extOpp.GetPermission(extOp) + if err != nil { + return err + } + + req.Permission = perm.String() + + if err := am.authz.Authorize(ctx, req); err != nil { + return err + } + + return nil +} + +func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, userID string) error { + if err := am.authz.Authorize(ctx, mgauthz.PolicyReq{ + SubjectType: policies.UserType, + Subject: userID, + Permission: policies.AdminPermission, + ObjectType: policies.PlatformType, + Object: policies.MagistralaObject, + }); err != nil { + return err + } + return nil +} diff --git a/channels/middleware/logging.go b/channels/middleware/logging.go new file mode 100644 index 0000000000..7cd17b8f96 --- /dev/null +++ b/channels/middleware/logging.go @@ -0,0 +1,263 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/pkg/authn" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" +) + +var _ channels.Service = (*loggingMiddleware)(nil) + +type loggingMiddleware struct { + logger *slog.Logger + svc channels.Service + rmMW.RoleManagerLoggingMiddleware +} + +func LoggingMiddleware(svc channels.Service, logger *slog.Logger) channels.Service { + return &loggingMiddleware{logger, svc, rmMW.NewRoleManagerLoggingMiddleware("channels", svc, logger)} +} + +func (lm *loggingMiddleware) CreateChannels(ctx context.Context, session authn.Session, clients ...channels.Channel) (cs []channels.Channel, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(fmt.Sprintf("Create %d channels failed", len(clients)), args...) + return + } + lm.logger.Info(fmt.Sprintf("Create %d channel completed successfully", len(clients)), args...) + }(time.Now()) + return lm.svc.CreateChannels(ctx, session, clients...) +} + +func (lm *loggingMiddleware) ViewChannel(ctx context.Context, session authn.Session, id string) (c channels.Channel, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("channel", + slog.String("id", c.ID), + slog.String("name", c.Name), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("View channel failed", args...) + return + } + lm.logger.Info("View channel completed successfully", args...) + }(time.Now()) + return lm.svc.ViewChannel(ctx, session, id) +} + +func (lm *loggingMiddleware) ListChannels(ctx context.Context, session authn.Session, pm channels.PageMetadata) (cp channels.Page, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("page", + slog.Uint64("limit", pm.Limit), + slog.Uint64("offset", pm.Offset), + slog.Uint64("total", cp.Total), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("List channels failed", args...) + return + } + lm.logger.Info("List channels completed successfully", args...) + }(time.Now()) + return lm.svc.ListChannels(ctx, session, pm) +} + +func (lm *loggingMiddleware) ListChannelsByThing(ctx context.Context, session authn.Session, thingID string, pm channels.PageMetadata) (cp channels.Page, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("thing_id", thingID), + slog.Group("page", + slog.Uint64("limit", pm.Limit), + slog.Uint64("offset", pm.Offset), + slog.Uint64("total", cp.Total), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("List channels by thing failed", args...) + return + } + lm.logger.Info("List channels by thing completed successfully", args...) + }(time.Now()) + return lm.svc.ListChannelsByThing(ctx, session, thingID, pm) +} + +func (lm *loggingMiddleware) UpdateChannel(ctx context.Context, session authn.Session, client channels.Channel) (c channels.Channel, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("channel", + slog.String("id", client.ID), + slog.String("name", client.Name), + slog.Any("metadata", client.Metadata), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Update channel failed", args...) + return + } + lm.logger.Info("Update channel completed successfully", args...) + }(time.Now()) + return lm.svc.UpdateChannel(ctx, session, client) +} + +func (lm *loggingMiddleware) UpdateChannelTags(ctx context.Context, session authn.Session, client channels.Channel) (c channels.Channel, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("channel", + slog.String("id", c.ID), + slog.String("name", c.Name), + slog.Any("tags", c.Tags), + ), + } + if err != nil { + args := append(args, slog.String("error", err.Error())) + lm.logger.Warn("Update channel tags failed", args...) + return + } + lm.logger.Info("Update channel tags completed successfully", args...) + }(time.Now()) + return lm.svc.UpdateChannelTags(ctx, session, client) +} + +func (lm *loggingMiddleware) EnableChannel(ctx context.Context, session authn.Session, id string) (c channels.Channel, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("channel", + slog.String("id", id), + slog.String("name", c.Name), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Enable channel failed", args...) + return + } + lm.logger.Info("Enable channel completed successfully", args...) + }(time.Now()) + return lm.svc.EnableChannel(ctx, session, id) +} + +func (lm *loggingMiddleware) DisableChannel(ctx context.Context, session authn.Session, id string) (c channels.Channel, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("channel", + slog.String("id", id), + slog.String("name", c.Name), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Disable channel failed", args...) + return + } + lm.logger.Info("Disable channel completed successfully", args...) + }(time.Now()) + return lm.svc.DisableChannel(ctx, session, id) +} + +func (lm *loggingMiddleware) RemoveChannel(ctx context.Context, session authn.Session, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("channel_id", id), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Delete channel failed", args...) + return + } + lm.logger.Info("Delete channel completed successfully", args...) + }(time.Now()) + return lm.svc.RemoveChannel(ctx, session, id) +} + +func (lm *loggingMiddleware) Connect(ctx context.Context, session authn.Session, chIDs, thIDs []string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Any("channel_ids", chIDs), + slog.Any("thing_ids", thIDs), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Connect channels and things failed", args...) + return + } + lm.logger.Info("Connect channels and things completed successfully", args...) + }(time.Now()) + return lm.svc.Connect(ctx, session, chIDs, thIDs) +} + +func (lm *loggingMiddleware) Disconnect(ctx context.Context, session authn.Session, chIDs, thIDs []string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Any("channel_ids", chIDs), + slog.Any("thing_ids", thIDs), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Disconnect channels and things failed", args...) + return + } + lm.logger.Info("Disconnect channels and things completed successfully", args...) + }(time.Now()) + return lm.svc.Disconnect(ctx, session, chIDs, thIDs) +} + +func (lm *loggingMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("parent_group_id", parentGroupID), + slog.String("channel_id", id), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Set parent group to channel failed", args...) + return + } + lm.logger.Info("Set parent group to channel completed successfully", args...) + }(time.Now()) + return lm.svc.SetParentGroup(ctx, session, parentGroupID, id) +} + +func (lm *loggingMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("channel_id", id), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Remove parent group from channel failed", args...) + return + } + lm.logger.Info("Remove parent group from channel completed successfully", args...) + }(time.Now()) + return lm.svc.RemoveParentGroup(ctx, session, id) +} diff --git a/channels/middleware/metrics.go b/channels/middleware/metrics.go new file mode 100644 index 0000000000..bab2ca03dd --- /dev/null +++ b/channels/middleware/metrics.go @@ -0,0 +1,136 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + "time" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/pkg/authn" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" + "github.com/go-kit/kit/metrics" +) + +var _ channels.Service = (*metricsMiddleware)(nil) + +type metricsMiddleware struct { + counter metrics.Counter + latency metrics.Histogram + svc channels.Service + rmMW.RoleManagerMetricsMiddleware +} + +// MetricsMiddleware returns a new metrics middleware wrapper. +func MetricsMiddleware(svc channels.Service, counter metrics.Counter, latency metrics.Histogram) channels.Service { + return &metricsMiddleware{ + counter: counter, + latency: latency, + svc: svc, + RoleManagerMetricsMiddleware: rmMW.NewRoleManagerMetricsMiddleware("channels", svc, counter, latency), + } +} + +func (ms *metricsMiddleware) CreateChannels(ctx context.Context, session authn.Session, chs ...channels.Channel) ([]channels.Channel, error) { + defer func(begin time.Time) { + ms.counter.With("method", "register_channels").Add(1) + ms.latency.With("method", "register_channels").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.CreateChannels(ctx, session, chs...) +} + +func (ms *metricsMiddleware) ViewChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + defer func(begin time.Time) { + ms.counter.With("method", "view_channel").Add(1) + ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.ViewChannel(ctx, session, id) +} + +func (ms *metricsMiddleware) ListChannels(ctx context.Context, session authn.Session, pm channels.PageMetadata) (channels.Page, error) { + defer func(begin time.Time) { + ms.counter.With("method", "list_channels").Add(1) + ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.ListChannels(ctx, session, pm) +} + +func (ms *metricsMiddleware) ListChannelsByThing(ctx context.Context, session authn.Session, thingID string, pm channels.PageMetadata) (channels.Page, error) { + defer func(begin time.Time) { + ms.counter.With("method", "list_channels_by_thing").Add(1) + ms.latency.With("method", "list_channels_by_thing").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.ListChannelsByThing(ctx, session, thingID, pm) +} + +func (ms *metricsMiddleware) UpdateChannel(ctx context.Context, session authn.Session, channel channels.Channel) (channels.Channel, error) { + defer func(begin time.Time) { + ms.counter.With("method", "update_channel").Add(1) + ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.UpdateChannel(ctx, session, channel) +} + +func (ms *metricsMiddleware) UpdateChannelTags(ctx context.Context, session authn.Session, channel channels.Channel) (channels.Channel, error) { + defer func(begin time.Time) { + ms.counter.With("method", "update_channel_tags").Add(1) + ms.latency.With("method", "update_channel_tags").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.UpdateChannelTags(ctx, session, channel) +} + +func (ms *metricsMiddleware) EnableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + defer func(begin time.Time) { + ms.counter.With("method", "enable_channel").Add(1) + ms.latency.With("method", "enable_channel").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.EnableChannel(ctx, session, id) +} + +func (ms *metricsMiddleware) DisableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + defer func(begin time.Time) { + ms.counter.With("method", "disable_channel").Add(1) + ms.latency.With("method", "disable_channel").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.DisableChannel(ctx, session, id) +} + +func (ms *metricsMiddleware) RemoveChannel(ctx context.Context, session authn.Session, id string) error { + defer func(begin time.Time) { + ms.counter.With("method", "delete_channel").Add(1) + ms.latency.With("method", "delete_channel").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.RemoveChannel(ctx, session, id) +} + +func (ms *metricsMiddleware) Connect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + defer func(begin time.Time) { + ms.counter.With("method", "connect").Add(1) + ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.Connect(ctx, session, chIDs, thIDs) +} +func (ms *metricsMiddleware) Disconnect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + defer func(begin time.Time) { + ms.counter.With("method", "disconnect").Add(1) + ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.Disconnect(ctx, session, chIDs, thIDs) +} + +func (ms *metricsMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (err error) { + defer func(begin time.Time) { + ms.counter.With("method", "set_parent_group").Add(1) + ms.latency.With("method", "set_parent_group").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.SetParentGroup(ctx, session, parentGroupID, id) +} + +func (ms *metricsMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (err error) { + defer func(begin time.Time) { + ms.counter.With("method", "remove_parent_group").Add(1) + ms.latency.With("method", "remove_parent_group").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.RemoveParentGroup(ctx, session, id) +} diff --git a/channels/mocks/channels_client.go b/channels/mocks/channels_client.go new file mode 100644 index 0000000000..e5b5a54275 --- /dev/null +++ b/channels/mocks/channels_client.go @@ -0,0 +1,266 @@ +// 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" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + v1 "github.com/absmach/magistrala/internal/grpc/channels/v1" +) + +// ChannelsServiceClient is an autogenerated mock type for the ChannelsServiceClient type +type ChannelsServiceClient struct { + mock.Mock +} + +type ChannelsServiceClient_Expecter struct { + mock *mock.Mock +} + +func (_m *ChannelsServiceClient) EXPECT() *ChannelsServiceClient_Expecter { + return &ChannelsServiceClient_Expecter{mock: &_m.Mock} +} + +// Authorize provides a mock function with given fields: ctx, in, opts +func (_m *ChannelsServiceClient) Authorize(ctx context.Context, in *v1.AuthzReq, opts ...grpc.CallOption) (*v1.AuthzRes, 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") + } + + var r0 *v1.AuthzRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.AuthzReq, ...grpc.CallOption) (*v1.AuthzRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.AuthzReq, ...grpc.CallOption) *v1.AuthzRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.AuthzRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.AuthzReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChannelsServiceClient_Authorize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authorize' +type ChannelsServiceClient_Authorize_Call struct { + *mock.Call +} + +// Authorize is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.AuthzReq +// - opts ...grpc.CallOption +func (_e *ChannelsServiceClient_Expecter) Authorize(ctx interface{}, in interface{}, opts ...interface{}) *ChannelsServiceClient_Authorize_Call { + return &ChannelsServiceClient_Authorize_Call{Call: _e.mock.On("Authorize", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ChannelsServiceClient_Authorize_Call) Run(run func(ctx context.Context, in *v1.AuthzReq, opts ...grpc.CallOption)) *ChannelsServiceClient_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].(*v1.AuthzReq), variadicArgs...) + }) + return _c +} + +func (_c *ChannelsServiceClient_Authorize_Call) Return(_a0 *v1.AuthzRes, _a1 error) *ChannelsServiceClient_Authorize_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ChannelsServiceClient_Authorize_Call) RunAndReturn(run func(context.Context, *v1.AuthzReq, ...grpc.CallOption) (*v1.AuthzRes, error)) *ChannelsServiceClient_Authorize_Call { + _c.Call.Return(run) + return _c +} + +// RemoveThingConnections provides a mock function with given fields: ctx, in, opts +func (_m *ChannelsServiceClient) RemoveThingConnections(ctx context.Context, in *v1.RemoveThingConnectionsReq, opts ...grpc.CallOption) (*v1.RemoveThingConnectionsRes, 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 RemoveThingConnections") + } + + var r0 *v1.RemoveThingConnectionsRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RemoveThingConnectionsReq, ...grpc.CallOption) (*v1.RemoveThingConnectionsRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.RemoveThingConnectionsReq, ...grpc.CallOption) *v1.RemoveThingConnectionsRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RemoveThingConnectionsRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.RemoveThingConnectionsReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChannelsServiceClient_RemoveThingConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveThingConnections' +type ChannelsServiceClient_RemoveThingConnections_Call struct { + *mock.Call +} + +// RemoveThingConnections is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.RemoveThingConnectionsReq +// - opts ...grpc.CallOption +func (_e *ChannelsServiceClient_Expecter) RemoveThingConnections(ctx interface{}, in interface{}, opts ...interface{}) *ChannelsServiceClient_RemoveThingConnections_Call { + return &ChannelsServiceClient_RemoveThingConnections_Call{Call: _e.mock.On("RemoveThingConnections", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ChannelsServiceClient_RemoveThingConnections_Call) Run(run func(ctx context.Context, in *v1.RemoveThingConnectionsReq, opts ...grpc.CallOption)) *ChannelsServiceClient_RemoveThingConnections_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].(*v1.RemoveThingConnectionsReq), variadicArgs...) + }) + return _c +} + +func (_c *ChannelsServiceClient_RemoveThingConnections_Call) Return(_a0 *v1.RemoveThingConnectionsRes, _a1 error) *ChannelsServiceClient_RemoveThingConnections_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ChannelsServiceClient_RemoveThingConnections_Call) RunAndReturn(run func(context.Context, *v1.RemoveThingConnectionsReq, ...grpc.CallOption) (*v1.RemoveThingConnectionsRes, error)) *ChannelsServiceClient_RemoveThingConnections_Call { + _c.Call.Return(run) + return _c +} + +// UnsetParentGroupFromChannels provides a mock function with given fields: ctx, in, opts +func (_m *ChannelsServiceClient) UnsetParentGroupFromChannels(ctx context.Context, in *v1.UnsetParentGroupFromChannelsReq, opts ...grpc.CallOption) (*v1.UnsetParentGroupFromChannelsRes, 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 UnsetParentGroupFromChannels") + } + + var r0 *v1.UnsetParentGroupFromChannelsRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.UnsetParentGroupFromChannelsReq, ...grpc.CallOption) (*v1.UnsetParentGroupFromChannelsRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.UnsetParentGroupFromChannelsReq, ...grpc.CallOption) *v1.UnsetParentGroupFromChannelsRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.UnsetParentGroupFromChannelsRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.UnsetParentGroupFromChannelsReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChannelsServiceClient_UnsetParentGroupFromChannels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UnsetParentGroupFromChannels' +type ChannelsServiceClient_UnsetParentGroupFromChannels_Call struct { + *mock.Call +} + +// UnsetParentGroupFromChannels is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.UnsetParentGroupFromChannelsReq +// - opts ...grpc.CallOption +func (_e *ChannelsServiceClient_Expecter) UnsetParentGroupFromChannels(ctx interface{}, in interface{}, opts ...interface{}) *ChannelsServiceClient_UnsetParentGroupFromChannels_Call { + return &ChannelsServiceClient_UnsetParentGroupFromChannels_Call{Call: _e.mock.On("UnsetParentGroupFromChannels", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ChannelsServiceClient_UnsetParentGroupFromChannels_Call) Run(run func(ctx context.Context, in *v1.UnsetParentGroupFromChannelsReq, opts ...grpc.CallOption)) *ChannelsServiceClient_UnsetParentGroupFromChannels_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].(*v1.UnsetParentGroupFromChannelsReq), variadicArgs...) + }) + return _c +} + +func (_c *ChannelsServiceClient_UnsetParentGroupFromChannels_Call) Return(_a0 *v1.UnsetParentGroupFromChannelsRes, _a1 error) *ChannelsServiceClient_UnsetParentGroupFromChannels_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ChannelsServiceClient_UnsetParentGroupFromChannels_Call) RunAndReturn(run func(context.Context, *v1.UnsetParentGroupFromChannelsReq, ...grpc.CallOption) (*v1.UnsetParentGroupFromChannelsRes, error)) *ChannelsServiceClient_UnsetParentGroupFromChannels_Call { + _c.Call.Return(run) + return _c +} + +// NewChannelsServiceClient creates a new instance of ChannelsServiceClient. 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 NewChannelsServiceClient(t interface { + mock.TestingT + Cleanup(func()) +}) *ChannelsServiceClient { + mock := &ChannelsServiceClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/channels/mocks/repository.go b/channels/mocks/repository.go new file mode 100644 index 0000000000..a515e4438a --- /dev/null +++ b/channels/mocks/repository.go @@ -0,0 +1,947 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + channels "github.com/absmach/magistrala/channels" + + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// Repository is an autogenerated mock type for the Repository type +type Repository struct { + mock.Mock +} + +// AddConnections provides a mock function with given fields: ctx, conns +func (_m *Repository) AddConnections(ctx context.Context, conns []channels.Connection) error { + ret := _m.Called(ctx, conns) + + if len(ret) == 0 { + panic("no return value specified for AddConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []channels.Connection) error); ok { + r0 = rf(ctx, conns) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AddRoles provides a mock function with given fields: ctx, rps +func (_m *Repository) AddRoles(ctx context.Context, rps []roles.RoleProvision) ([]roles.Role, error) { + ret := _m.Called(ctx, rps) + + if len(ret) == 0 { + panic("no return value specified for AddRoles") + } + + var r0 []roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) ([]roles.Role, error)); ok { + return rf(ctx, rps) + } + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) []roles.Role); ok { + r0 = rf(ctx, rps) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []roles.RoleProvision) error); ok { + r1 = rf(ctx, rps) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChangeStatus provides a mock function with given fields: ctx, channel +func (_m *Repository) ChangeStatus(ctx context.Context, channel channels.Channel) (channels.Channel, error) { + ret := _m.Called(ctx, channel) + + if len(ret) == 0 { + panic("no return value specified for ChangeStatus") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) (channels.Channel, error)); ok { + return rf(ctx, channel) + } + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) channels.Channel); ok { + r0 = rf(ctx, channel) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, channels.Channel) error); ok { + r1 = rf(ctx, channel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChannelConnectionsCount provides a mock function with given fields: ctx, id +func (_m *Repository) ChannelConnectionsCount(ctx context.Context, id string) (uint64, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for ChannelConnectionsCount") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (uint64, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) uint64); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckConnection provides a mock function with given fields: ctx, conn +func (_m *Repository) CheckConnection(ctx context.Context, conn channels.Connection) error { + ret := _m.Called(ctx, conn) + + if len(ret) == 0 { + panic("no return value specified for CheckConnection") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, channels.Connection) error); ok { + r0 = rf(ctx, conn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DoesChannelHaveConnections provides a mock function with given fields: ctx, id +func (_m *Repository) DoesChannelHaveConnections(ctx context.Context, id string) (bool, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DoesChannelHaveConnections") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Remove provides a mock function with given fields: ctx, ids +func (_m *Repository) Remove(ctx context.Context, ids ...string) error { + _va := make([]interface{}, len(ids)) + for _i := range ids { + _va[_i] = ids[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Remove") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, ...string) error); ok { + r0 = rf(ctx, ids...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveChannelConnections provides a mock function with given fields: ctx, channelID +func (_m *Repository) RemoveChannelConnections(ctx context.Context, channelID string) error { + ret := _m.Called(ctx, channelID) + + if len(ret) == 0 { + panic("no return value specified for RemoveChannelConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, channelID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveConnections provides a mock function with given fields: ctx, conns +func (_m *Repository) RemoveConnections(ctx context.Context, conns []channels.Connection) error { + ret := _m.Called(ctx, conns) + + if len(ret) == 0 { + panic("no return value specified for RemoveConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []channels.Connection) error); ok { + r0 = rf(ctx, conns) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, members +func (_m *Repository) RemoveMemberFromAllRoles(ctx context.Context, members string) error { + ret := _m.Called(ctx, members) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveParentGroup provides a mock function with given fields: ctx, ch +func (_m *Repository) RemoveParentGroup(ctx context.Context, ch channels.Channel) error { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for RemoveParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) error); ok { + r0 = rf(ctx, ch) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRoles provides a mock function with given fields: ctx, roleIDs +func (_m *Repository) RemoveRoles(ctx context.Context, roleIDs []string) error { + ret := _m.Called(ctx, roleIDs) + + if len(ret) == 0 { + panic("no return value specified for RemoveRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok { + r0 = rf(ctx, roleIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveThingConnections provides a mock function with given fields: ctx, thingID +func (_m *Repository) RemoveThingConnections(ctx context.Context, thingID string) error { + ret := _m.Called(ctx, thingID) + + if len(ret) == 0 { + panic("no return value specified for RemoveThingConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, thingID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAll provides a mock function with given fields: ctx, pm +func (_m *Repository) RetrieveAll(ctx context.Context, pm channels.PageMetadata) (channels.Page, error) { + ret := _m.Called(ctx, pm) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAll") + } + + var r0 channels.Page + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, channels.PageMetadata) (channels.Page, error)); ok { + return rf(ctx, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, channels.PageMetadata) channels.Page); ok { + r0 = rf(ctx, pm) + } else { + r0 = ret.Get(0).(channels.Page) + } + + if rf, ok := ret.Get(1).(func(context.Context, channels.PageMetadata) error); ok { + r1 = rf(ctx, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, entityID, limit, offset +func (_m *Repository) RetrieveAllRoles(ctx context.Context, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveByID provides a mock function with given fields: ctx, id +func (_m *Repository) RetrieveByID(ctx context.Context, id string) (channels.Channel, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for RetrieveByID") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (channels.Channel, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) channels.Channel); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveEntitiesRolesActionsMembers provides a mock function with given fields: ctx, entityIDs +func (_m *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) { + ret := _m.Called(ctx, entityIDs) + + if len(ret) == 0 { + panic("no return value specified for RetrieveEntitiesRolesActionsMembers") + } + + var r0 []roles.EntityActionRole + var r1 []roles.EntityMemberRole + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error)); ok { + return rf(ctx, entityIDs) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []roles.EntityActionRole); ok { + r0 = rf(ctx, entityIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.EntityActionRole) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) []roles.EntityMemberRole); ok { + r1 = rf(ctx, entityIDs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]roles.EntityMemberRole) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, []string) error); ok { + r2 = rf(ctx, entityIDs) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RetrieveParentGroupChannels provides a mock function with given fields: ctx, parentGroupID +func (_m *Repository) RetrieveParentGroupChannels(ctx context.Context, parentGroupID string) ([]channels.Channel, error) { + ret := _m.Called(ctx, parentGroupID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveParentGroupChannels") + } + + var r0 []channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]channels.Channel, error)); ok { + return rf(ctx, parentGroupID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []channels.Channel); ok { + r0 = rf(ctx, parentGroupID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]channels.Channel) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, parentGroupID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRole provides a mock function with given fields: ctx, roleID +func (_m *Repository) RetrieveRole(ctx context.Context, roleID string) (roles.Role, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (roles.Role, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) roles.Role); ok { + r0 = rf(ctx, roleID) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRoleByEntityIDAndName provides a mock function with given fields: ctx, entityID, roleName +func (_m *Repository) RetrieveRoleByEntityIDAndName(ctx context.Context, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRoleByEntityIDAndName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (roles.Role, error)); ok { + return rf(ctx, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) roles.Role); ok { + r0 = rf(ctx, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleAddActions(ctx context.Context, role roles.Role, actions []string) ([]string, error) { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleAddMembers(ctx context.Context, role roles.Role, members []string) ([]string, error) { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, members) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, roleID, actions +func (_m *Repository) RoleCheckActionsExists(ctx context.Context, roleID string, actions []string) (bool, error) { + ret := _m.Called(ctx, roleID, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, roleID, members +func (_m *Repository) RoleCheckMembersExists(ctx context.Context, roleID string, members []string) (bool, error) { + ret := _m.Called(ctx, roleID, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, members) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, roleID +func (_m *Repository) RoleListActions(ctx context.Context, roleID string) ([]string, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, roleID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, roleID, limit, offset +func (_m *Repository) RoleListMembers(ctx context.Context, roleID string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, roleID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, roleID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, roleID, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, roleID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleRemoveActions(ctx context.Context, role roles.Role, actions []string) error { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllActions(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllMembers(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleRemoveMembers(ctx context.Context, role roles.Role, members []string) error { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Save provides a mock function with given fields: ctx, chs +func (_m *Repository) Save(ctx context.Context, chs ...channels.Channel) ([]channels.Channel, error) { + _va := make([]interface{}, len(chs)) + for _i := range chs { + _va[_i] = chs[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 []channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ...channels.Channel) ([]channels.Channel, error)); ok { + return rf(ctx, chs...) + } + if rf, ok := ret.Get(0).(func(context.Context, ...channels.Channel) []channels.Channel); ok { + r0 = rf(ctx, chs...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]channels.Channel) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ...channels.Channel) error); ok { + r1 = rf(ctx, chs...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetParentGroup provides a mock function with given fields: ctx, ch +func (_m *Repository) SetParentGroup(ctx context.Context, ch channels.Channel) error { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for SetParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) error); ok { + r0 = rf(ctx, ch) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ThingAuthorize provides a mock function with given fields: ctx, conn +func (_m *Repository) ThingAuthorize(ctx context.Context, conn channels.Connection) error { + ret := _m.Called(ctx, conn) + + if len(ret) == 0 { + panic("no return value specified for ThingAuthorize") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, channels.Connection) error); ok { + r0 = rf(ctx, conn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnsetParentGroupFromChannels provides a mock function with given fields: ctx, parentGroupID +func (_m *Repository) UnsetParentGroupFromChannels(ctx context.Context, parentGroupID string) error { + ret := _m.Called(ctx, parentGroupID) + + if len(ret) == 0 { + panic("no return value specified for UnsetParentGroupFromChannels") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, parentGroupID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, c +func (_m *Repository) Update(ctx context.Context, c channels.Channel) (channels.Channel, error) { + ret := _m.Called(ctx, c) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) (channels.Channel, error)); ok { + return rf(ctx, c) + } + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) channels.Channel); ok { + r0 = rf(ctx, c) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, channels.Channel) error); ok { + r1 = rf(ctx, c) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRole provides a mock function with given fields: ctx, ro +func (_m *Repository) UpdateRole(ctx context.Context, ro roles.Role) (roles.Role, error) { + ret := _m.Called(ctx, ro) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) (roles.Role, error)); ok { + return rf(ctx, ro) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) roles.Role); ok { + r0 = rf(ctx, ro) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role) error); ok { + r1 = rf(ctx, ro) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateTags provides a mock function with given fields: ctx, ch +func (_m *Repository) UpdateTags(ctx context.Context, ch channels.Channel) (channels.Channel, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for UpdateTags") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) (channels.Channel, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, channels.Channel) channels.Channel); ok { + r0 = rf(ctx, ch) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, channels.Channel) error); ok { + r1 = rf(ctx, ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRepository creates a new instance of Repository. 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 NewRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *Repository { + mock := &Repository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/channels/mocks/service.go b/channels/mocks/service.go new file mode 100644 index 0000000000..25785d0648 --- /dev/null +++ b/channels/mocks/service.go @@ -0,0 +1,782 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + channels "github.com/absmach/magistrala/channels" + authn "github.com/absmach/magistrala/pkg/authn" + + context "context" + + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +// AddRole provides a mock function with given fields: ctx, session, entityID, roleName, optionalActions, optionalMembers +func (_m *Service) AddRole(ctx context.Context, session authn.Session, entityID string, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName, optionalActions, optionalMembers) + + if len(ret) == 0 { + panic("no return value specified for AddRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Connect provides a mock function with given fields: ctx, session, chIDs, thIDs +func (_m *Service) Connect(ctx context.Context, session authn.Session, chIDs []string, thIDs []string) error { + ret := _m.Called(ctx, session, chIDs, thIDs) + + if len(ret) == 0 { + panic("no return value specified for Connect") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, []string, []string) error); ok { + r0 = rf(ctx, session, chIDs, thIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreateChannels provides a mock function with given fields: ctx, session, _a2 +func (_m *Service) CreateChannels(ctx context.Context, session authn.Session, _a2 ...channels.Channel) ([]channels.Channel, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, session) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateChannels") + } + + var r0 []channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, ...channels.Channel) ([]channels.Channel, error)); ok { + return rf(ctx, session, _a2...) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, ...channels.Channel) []channels.Channel); ok { + r0 = rf(ctx, session, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]channels.Channel) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, ...channels.Channel) error); ok { + r1 = rf(ctx, session, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DisableChannel provides a mock function with given fields: ctx, session, id +func (_m *Service) DisableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for DisableChannel") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (channels.Channel, error)); ok { + return rf(ctx, session, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) channels.Channel); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, session, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Disconnect provides a mock function with given fields: ctx, session, chIDs, thIDs +func (_m *Service) Disconnect(ctx context.Context, session authn.Session, chIDs []string, thIDs []string) error { + ret := _m.Called(ctx, session, chIDs, thIDs) + + if len(ret) == 0 { + panic("no return value specified for Disconnect") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, []string, []string) error); ok { + r0 = rf(ctx, session, chIDs, thIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// EnableChannel provides a mock function with given fields: ctx, session, id +func (_m *Service) EnableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for EnableChannel") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (channels.Channel, error)); ok { + return rf(ctx, session, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) channels.Channel); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, session, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListAvailableActions provides a mock function with given fields: ctx, session +func (_m *Service) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + ret := _m.Called(ctx, session) + + if len(ret) == 0 { + panic("no return value specified for ListAvailableActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) ([]string, error)); ok { + return rf(ctx, session) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) []string); ok { + r0 = rf(ctx, session) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session) error); ok { + r1 = rf(ctx, session) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListChannels provides a mock function with given fields: ctx, session, pm +func (_m *Service) ListChannels(ctx context.Context, session authn.Session, pm channels.PageMetadata) (channels.Page, error) { + ret := _m.Called(ctx, session, pm) + + if len(ret) == 0 { + panic("no return value specified for ListChannels") + } + + var r0 channels.Page + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, channels.PageMetadata) (channels.Page, error)); ok { + return rf(ctx, session, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, channels.PageMetadata) channels.Page); ok { + r0 = rf(ctx, session, pm) + } else { + r0 = ret.Get(0).(channels.Page) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, channels.PageMetadata) error); ok { + r1 = rf(ctx, session, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListChannelsByThing provides a mock function with given fields: ctx, session, thID, pm +func (_m *Service) ListChannelsByThing(ctx context.Context, session authn.Session, thID string, pm channels.PageMetadata) (channels.Page, error) { + ret := _m.Called(ctx, session, thID, pm) + + if len(ret) == 0 { + panic("no return value specified for ListChannelsByThing") + } + + var r0 channels.Page + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, channels.PageMetadata) (channels.Page, error)); ok { + return rf(ctx, session, thID, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, channels.PageMetadata) channels.Page); ok { + r0 = rf(ctx, session, thID, pm) + } else { + r0 = ret.Get(0).(channels.Page) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, channels.PageMetadata) error); ok { + r1 = rf(ctx, session, thID, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveChannel provides a mock function with given fields: ctx, session, id +func (_m *Service) RemoveChannel(ctx context.Context, session authn.Session, id string) error { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for RemoveChannel") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, session, memberID +func (_m *Service) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) error { + ret := _m.Called(ctx, session, memberID) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, memberID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveParentGroup provides a mock function with given fields: ctx, session, id +func (_m *Service) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for RemoveParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RemoveRole(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RemoveRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, session, entityID, limit, offset +func (_m *Service) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, session, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, session, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, session, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RetrieveRole(ctx context.Context, session authn.Session, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleAddActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleAddMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleListActions(ctx context.Context, session authn.Session, entityID string, roleName string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) []string); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, session, entityID, roleName, limit, offset +func (_m *Service) RoleListMembers(ctx context.Context, session authn.Session, entityID string, roleName string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, session, entityID, roleName, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, session, entityID, roleName, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleRemoveActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) error { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) error { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetParentGroup provides a mock function with given fields: ctx, session, parentGroupID, id +func (_m *Service) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error { + ret := _m.Called(ctx, session, parentGroupID, id) + + if len(ret) == 0 { + panic("no return value specified for SetParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, parentGroupID, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateChannel provides a mock function with given fields: ctx, session, channel +func (_m *Service) UpdateChannel(ctx context.Context, session authn.Session, channel channels.Channel) (channels.Channel, error) { + ret := _m.Called(ctx, session, channel) + + if len(ret) == 0 { + panic("no return value specified for UpdateChannel") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, channels.Channel) (channels.Channel, error)); ok { + return rf(ctx, session, channel) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, channels.Channel) channels.Channel); ok { + r0 = rf(ctx, session, channel) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, channels.Channel) error); ok { + r1 = rf(ctx, session, channel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateChannelTags provides a mock function with given fields: ctx, session, channel +func (_m *Service) UpdateChannelTags(ctx context.Context, session authn.Session, channel channels.Channel) (channels.Channel, error) { + ret := _m.Called(ctx, session, channel) + + if len(ret) == 0 { + panic("no return value specified for UpdateChannelTags") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, channels.Channel) (channels.Channel, error)); ok { + return rf(ctx, session, channel) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, channels.Channel) channels.Channel); ok { + r0 = rf(ctx, session, channel) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, channels.Channel) error); ok { + r1 = rf(ctx, session, channel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRoleName provides a mock function with given fields: ctx, session, entityID, oldRoleName, newRoleName +func (_m *Service) UpdateRoleName(ctx context.Context, session authn.Session, entityID string, oldRoleName string, newRoleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, oldRoleName, newRoleName) + + if len(ret) == 0 { + panic("no return value specified for UpdateRoleName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, oldRoleName, newRoleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string) error); ok { + r1 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ViewChannel provides a mock function with given fields: ctx, session, id +func (_m *Service) ViewChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for ViewChannel") + } + + var r0 channels.Channel + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (channels.Channel, error)); ok { + return rf(ctx, session, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) channels.Channel); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Get(0).(channels.Channel) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, session, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewService creates a new instance of Service. 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 NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/channels/postgres/channels.go b/channels/postgres/channels.go new file mode 100644 index 0000000000..157ec03c4e --- /dev/null +++ b/channels/postgres/channels.go @@ -0,0 +1,632 @@ +// Copyright (c) Mainflux +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + "github.com/absmach/magistrala/pkg/postgres" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" + clients "github.com/absmach/magistrala/things" + "github.com/jackc/pgtype" +) + +const ( + rolesTableNamePrefix = "channels" + entityTableName = "channels" + entityIDColumnName = "id" +) + +var _ channels.Repository = (*channelRepository)(nil) + +type channelRepository struct { + db postgres.Database + rolesPostgres.Repository +} + +// NewChannelRepository instantiates a PostgreSQL implementation of channel +// repository. +func NewRepository(db postgres.Database) channels.Repository { + + rolesRepo := rolesPostgres.NewRepository(db, rolesTableNamePrefix, entityTableName, entityIDColumnName) + return &channelRepository{ + db: db, + Repository: rolesRepo, + } +} + +func (cr *channelRepository) Save(ctx context.Context, chs ...channels.Channel) ([]channels.Channel, error) { + + var dbchs []dbChannel + for _, ch := range chs { + dbch, err := toDBChannel(ch) + if err != nil { + return []channels.Channel{}, errors.Wrap(repoerr.ErrCreateEntity, err) + } + dbchs = append(dbchs, dbch) + } + + q := `INSERT INTO channels (id, name, tags, domain_id, parent_group_id, metadata, created_at, updated_at, updated_by, status) + VALUES (:id, :name, :tags, :domain_id, :parent_group_id, :metadata, :created_at, :updated_at, :updated_by, :status) + RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by` + + row, err := cr.db.NamedQueryContext(ctx, q, dbchs) + if err != nil { + return []channels.Channel{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + defer row.Close() + + var reChs []channels.Channel + + for row.Next() { + dbch := dbChannel{} + if err := row.StructScan(&dbch); err != nil { + return []channels.Channel{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + } + + ch, err := toChannel(dbch) + if err != nil { + return []channels.Channel{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + } + reChs = append(reChs, ch) + } + return reChs, nil +} + +func (cr *channelRepository) Update(ctx context.Context, channel channels.Channel) (channels.Channel, error) { + var query []string + var upq string + if channel.Name != "" { + query = append(query, "name = :name,") + } + if channel.Metadata != nil { + query = append(query, "metadata = :metadata,") + } + if len(query) > 0 { + upq = strings.Join(query, " ") + } + q := fmt.Sprintf(`UPDATE channels SET %s updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id AND status = :status + RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`, + upq) + channel.Status = clients.EnabledStatus + return cr.update(ctx, channel, q) +} + +func (cr *channelRepository) UpdateTags(ctx context.Context, channel channels.Channel) (channels.Channel, error) { + q := `UPDATE channels SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id AND status = :status + RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by` + channel.Status = clients.EnabledStatus + return cr.update(ctx, channel, q) +} + +func (cr *channelRepository) ChangeStatus(ctx context.Context, channel channels.Channel) (channels.Channel, error) { + q := `UPDATE channels SET status = :status, updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id + RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by` + + return cr.update(ctx, channel, q) +} + +func (cr *channelRepository) RetrieveByID(ctx context.Context, id string) (channels.Channel, error) { + q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, metadata, created_at, updated_at, updated_by, status FROM channels WHERE id = :id` + + dbch := dbChannel{ + ID: id, + } + + row, err := cr.db.NamedQueryContext(ctx, q, dbch) + if err != nil { + return channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + defer row.Close() + + dbch = dbChannel{} + if row.Next() { + if err := row.StructScan(&dbch); err != nil { + return channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + return toChannel(dbch) + } + + return channels.Channel{}, repoerr.ErrNotFound +} + +func (cr *channelRepository) RetrieveAll(ctx context.Context, pm channels.PageMetadata) (channels.Page, error) { + query, err := PageQuery(pm) + if err != nil { + return channels.Page{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + query = applyOrdering(query, pm) + + q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.metadata, COALESCE(c.domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, c.status, + c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM channels c %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, query) + + dbPage, err := toDBChannelsPage(pm) + if err != nil { + return channels.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + } + rows, err := cr.db.NamedQueryContext(ctx, q, dbPage) + if err != nil { + return channels.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + } + defer rows.Close() + + var items []channels.Channel + for rows.Next() { + dbch := dbChannel{} + if err := rows.StructScan(&dbch); err != nil { + return channels.Page{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + ch, err := toChannel(dbch) + if err != nil { + return channels.Page{}, err + } + + items = append(items, ch) + } + cq := fmt.Sprintf(`SELECT COUNT(*) FROM channels c %s;`, query) + + total, err := postgres.Total(ctx, cr.db, cq, dbPage) + if err != nil { + return channels.Page{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + page := channels.Page{ + Channels: items, + PageMetadata: channels.PageMetadata{ + Total: total, + Offset: pm.Offset, + Limit: pm.Limit, + }, + } + return page, nil +} + +func (cr *channelRepository) Remove(ctx context.Context, ids ...string) error { + q := "DELETE FROM channels AS c WHERE c.id = ANY(:channel_ids) ;" + params := map[string]interface{}{ + "channel_ids": ids, + } + result, err := cr.db.NamedExecContext(ctx, q, params) + if err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + if rows, _ := result.RowsAffected(); rows == 0 { + return repoerr.ErrNotFound + } + return nil +} + +func (cr *channelRepository) SetParentGroup(ctx context.Context, ch channels.Channel) error { + q := "UPDATE channels SET parent_group_id = :parent_group_id, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id" + dbCh, err := toDBChannel(ch) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + result, err := cr.db.NamedExecContext(ctx, q, dbCh) + if err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + if rows, _ := result.RowsAffected(); rows == 0 { + return repoerr.ErrNotFound + } + return nil +} + +func (cr *channelRepository) RemoveParentGroup(ctx context.Context, ch channels.Channel) error { + q := "UPDATE channels SET parent_group_id = NULL, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id" + dbCh, err := toDBChannel(ch) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + result, err := cr.db.NamedExecContext(ctx, q, dbCh) + if err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + if rows, _ := result.RowsAffected(); rows == 0 { + return repoerr.ErrNotFound + } + return nil +} + +func (cr *channelRepository) AddConnections(ctx context.Context, conns []channels.Connection) error { + + dbConns := toDBConnections(conns) + + q := `INSERT INTO connections (channel_id, domain_id, thing_id) + VALUES (:channel_id, :domain_id, :thing_id);` + + if _, err := cr.db.NamedExecContext(ctx, q, dbConns); err != nil { + return postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + return nil + +} + +func (cr *channelRepository) RemoveConnections(ctx context.Context, conns []channels.Connection) (retErr error) { + tx, err := cr.db.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + defer func() { + if retErr != nil { + if errRollBack := tx.Rollback(); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollBack)) + } + } + }() + + query := `DELETE FROM connections WHERE channel_id = :channel_id AND domain_id = :domain_id AND thing_id = :thing_id` + + for _, conn := range conns { + dbConn := toDBConnection(conn) + if _, err := tx.NamedExec(query, dbConn); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, errors.Wrap(fmt.Errorf("failed to delete connection for channel_id: %s, domain_id: %s thing_id %s", conn.ChannelID, conn.DomainID, conn.ThingID), err)) + } + } + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (cr *channelRepository) CheckConnection(ctx context.Context, conn channels.Connection) error { + query := `SELECT 1 FROM connections WHERE channel_id = :channel_id AND domain_id = :domain_id AND thing_id = :thing_id LIMIT 1` + dbConn := toDBConnection(conn) + rows, err := cr.db.NamedQueryContext(ctx, query, dbConn) + if err != nil { + return postgres.HandleError(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + if !rows.Next() { + return repoerr.ErrNotFound + } + return nil +} + +func (cr *channelRepository) ThingAuthorize(ctx context.Context, conn channels.Connection) error { + query := `SELECT 1 FROM connections WHERE channel_id = :channel_id AND thing_id = :thing_id LIMIT 1` + dbConn := toDBConnection(conn) + rows, err := cr.db.NamedQueryContext(ctx, query, dbConn) + if err != nil { + return postgres.HandleError(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + if !rows.Next() { + return repoerr.ErrNotFound + } + return nil +} + +func (cr *channelRepository) ChannelConnectionsCount(ctx context.Context, id string) (uint64, error) { + query := `SELECT COUNT(*) FROM connections WHERE channel_id = :channel_id` + dbConn := dbConnection{ChannelID: id} + + total, err := postgres.Total(ctx, cr.db, query, dbConn) + if err != nil { + return 0, postgres.HandleError(repoerr.ErrViewEntity, err) + } + return total, nil +} + +func (cr *channelRepository) DoesChannelHaveConnections(ctx context.Context, id string) (bool, error) { + query := `SELECT 1 FROM connections WHERE channel_id = :channel_id` + dbConn := dbConnection{ChannelID: id} + + rows, err := cr.db.NamedQueryContext(ctx, query, dbConn) + if err != nil { + return false, postgres.HandleError(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + return rows.Next(), nil +} + +func (cr *channelRepository) RemoveThingConnections(ctx context.Context, thingID string) error { + query := `DELETE FROM connections WHERE thing_id = :thing_id` + + dbConn := dbConnection{ThingID: thingID} + if _, err := cr.db.NamedExecContext(ctx, query, dbConn); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (cr *channelRepository) RemoveChannelConnections(ctx context.Context, channelID string) error { + query := `DELETE FROM connections WHERE channel_id = :channel_id` + + dbConn := dbConnection{ChannelID: channelID} + if _, err := cr.db.NamedExecContext(ctx, query, dbConn); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (cr *channelRepository) RetrieveParentGroupChannels(ctx context.Context, parentGroupID string) ([]channels.Channel, error) { + query := `SELECT c.id, c.name, c.tags, c.metadata, COALESCE(c.domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, c.status, + c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM channels c WHERE c.parent_group_id = :parent_group_id ;` + + rows, err := cr.db.NamedQueryContext(ctx, query, dbChannel{ParentGroup: nullString(parentGroupID)}) + if err != nil { + return []channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + var chs []channels.Channel + for rows.Next() { + dbch := dbChannel{} + if err := rows.StructScan(&dbch); err != nil { + return []channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + ch, err := toChannel(dbch) + if err != nil { + return []channels.Channel{}, err + } + + chs = append(chs, ch) + } + return chs, nil +} + +func (cr *channelRepository) UnsetParentGroupFromChannels(ctx context.Context, parentGroupID string) error { + query := "UPDATE channels SET parent_group_id = NULL WHERE parent_group_id = :parent_group_id" + + dbCh := dbChannel{ParentGroup: nullString(parentGroupID)} + if _, err := cr.db.NamedExecContext(ctx, query, dbCh); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (cr *channelRepository) update(ctx context.Context, ch channels.Channel, query string) (channels.Channel, error) { + dbch, err := toDBChannel(ch) + if err != nil { + return channels.Channel{}, errors.Wrap(repoerr.ErrUpdateEntity, err) + } + + row, err := cr.db.NamedQueryContext(ctx, query, dbch) + if err != nil { + return channels.Channel{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) + } + defer row.Close() + + dbch = dbChannel{} + if row.Next() { + if err := row.StructScan(&dbch); err != nil { + return channels.Channel{}, errors.Wrap(repoerr.ErrUpdateEntity, err) + } + + return toChannel(dbch) + } + + return channels.Channel{}, repoerr.ErrNotFound +} + +type dbChannel struct { + ID string `db:"id"` + Name string `db:"name,omitempty"` + ParentGroup sql.NullString `db:"parent_group_id,omitempty"` + Tags pgtype.TextArray `db:"tags,omitempty"` + Domain string `db:"domain_id"` + Metadata []byte `db:"metadata,omitempty"` + CreatedAt time.Time `db:"created_at,omitempty"` + UpdatedAt sql.NullTime `db:"updated_at,omitempty"` + UpdatedBy *string `db:"updated_by,omitempty"` + Status clients.Status `db:"status,omitempty"` + Role *clients.Role `db:"role,omitempty"` +} + +func toDBChannel(ch channels.Channel) (dbChannel, error) { + data := []byte("{}") + if len(ch.Metadata) > 0 { + b, err := json.Marshal(ch.Metadata) + if err != nil { + return dbChannel{}, errors.Wrap(repoerr.ErrMalformedEntity, err) + } + data = b + } + var tags pgtype.TextArray + if err := tags.Set(ch.Tags); err != nil { + return dbChannel{}, err + } + var updatedBy *string + if ch.UpdatedBy != "" { + updatedBy = &ch.UpdatedBy + } + var updatedAt sql.NullTime + if ch.UpdatedAt != (time.Time{}) { + updatedAt = sql.NullTime{Time: ch.UpdatedAt, Valid: true} + } + return dbChannel{ + ID: ch.ID, + Name: ch.Name, + ParentGroup: nullString(ch.ParentGroup), + Domain: ch.Domain, + Tags: tags, + Metadata: data, + CreatedAt: ch.CreatedAt, + UpdatedAt: updatedAt, + UpdatedBy: updatedBy, + Status: ch.Status, + }, nil +} + +func nullString(s string) sql.NullString { + if s == "" { + return sql.NullString{} + } + + return sql.NullString{ + String: s, + Valid: true, + } +} + +func toChannel(ch dbChannel) (channels.Channel, error) { + var metadata clients.Metadata + if ch.Metadata != nil { + if err := json.Unmarshal([]byte(ch.Metadata), &metadata); err != nil { + return channels.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err) + } + } + var tags []string + for _, e := range ch.Tags.Elements { + tags = append(tags, e.String) + } + var updatedBy string + if ch.UpdatedBy != nil { + updatedBy = *ch.UpdatedBy + } + var updatedAt time.Time + if ch.UpdatedAt.Valid { + updatedAt = ch.UpdatedAt.Time + } + + parentGroup := "" + if ch.ParentGroup.Valid { + parentGroup = ch.ParentGroup.String + } + newCh := channels.Channel{ + ID: ch.ID, + Name: ch.Name, + Tags: tags, + Domain: ch.Domain, + ParentGroup: parentGroup, + Metadata: metadata, + CreatedAt: ch.CreatedAt, + UpdatedAt: updatedAt, + UpdatedBy: updatedBy, + Status: ch.Status, + } + + return newCh, nil +} + +func PageQuery(pm channels.PageMetadata) (string, error) { + mq, _, err := postgres.CreateMetadataQuery("", pm.Metadata) + if err != nil { + return "", errors.Wrap(errors.ErrMalformedEntity, err) + } + + var query []string + if pm.Name != "" { + query = append(query, "c.name ILIKE '%' || :name || '%'") + } + + if pm.ThingID != "" { + query = append(query, "conn.thing_id = :thing_id") + } + if pm.Id != "" { + query = append(query, "c.id ILIKE '%' || :id || '%'") + } + if pm.Tag != "" { + query = append(query, "EXISTS (SELECT 1 FROM unnest(tags) AS tag WHERE tag ILIKE '%' || :tag || '%')") + } + + // If there are search params presents, use search and ignore other options. + // Always combine role with search params, so len(query) > 1. + if len(query) > 1 { + return fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")), nil + } + + if mq != "" { + query = append(query, mq) + } + + if len(pm.IDs) != 0 { + query = append(query, fmt.Sprintf("id IN ('%s')", strings.Join(pm.IDs, "','"))) + } + if pm.Status != clients.AllStatus { + query = append(query, "c.status = :status") + } + if pm.Domain != "" { + query = append(query, "c.domain_id = :domain_id") + } + var emq string + if len(query) > 0 { + emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) + } + return emq, nil +} + +func applyOrdering(emq string, pm channels.PageMetadata) string { + switch pm.Order { + case "name", "created_at", "updated_at": + emq = fmt.Sprintf("%s ORDER BY %s", emq, pm.Order) + if pm.Dir == api.AscDir || pm.Dir == api.DescDir { + emq = fmt.Sprintf("%s %s", emq, pm.Dir) + } + } + return emq +} + +func toDBChannelsPage(pm channels.PageMetadata) (dbChannelsPage, error) { + _, data, err := postgres.CreateMetadataQuery("", pm.Metadata) + if err != nil { + return dbChannelsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + return dbChannelsPage{ + Name: pm.Name, + Id: pm.Id, + Metadata: data, + Domain: pm.Domain, + Total: pm.Total, + Offset: pm.Offset, + Limit: pm.Limit, + Status: pm.Status, + Tag: pm.Tag, + }, nil +} + +type dbChannelsPage struct { + Total uint64 `db:"total"` + Limit uint64 `db:"limit"` + Offset uint64 `db:"offset"` + Name string `db:"name"` + Id string `db:"id"` + Domain string `db:"domain_id"` + Metadata []byte `db:"metadata"` + Tag string `db:"tag"` + Status clients.Status `db:"status"` +} + +type dbConnection struct { + ChannelID string `db:"channel_id"` + DomainID string `db:"domain_id"` + ThingID string `db:"thing_id"` +} + +func toDBConnections(conns []channels.Connection) []dbConnection { + var dbconns []dbConnection + for _, conn := range conns { + dbconns = append(dbconns, toDBConnection(conn)) + } + return dbconns +} + +func toDBConnection(conn channels.Connection) dbConnection { + return dbConnection{ + ThingID: conn.ThingID, + ChannelID: conn.ChannelID, + DomainID: conn.DomainID, + } +} diff --git a/channels/postgres/init.go b/channels/postgres/init.go new file mode 100644 index 0000000000..9244fa6f0f --- /dev/null +++ b/channels/postgres/init.go @@ -0,0 +1,59 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" + _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access + migrate "github.com/rubenv/sql-migrate" +) + +func Migration() (*migrate.MemoryMigrationSource, error) { + rolesMigration, err := rolesPostgres.Migration(rolesTableNamePrefix, entityTableName, entityIDColumnName) + if err != nil { + return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err) + } + channelsMigration := &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: "channels_01", + // VARCHAR(36) for colums with IDs as UUIDS have a maximum of 36 characters + // STATUS 0 to imply enabled and 1 to imply disabled + Up: []string{ + `CREATE TABLE IF NOT EXISTS channels ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(1024), + domain_id VARCHAR(36) NOT NULL, + parent_group_id VARCHAR(36) DEFAULT NULL, + tags TEXT[], + metadata JSONB, + created_by VARCHAR(254), + created_at TIMESTAMP, + updated_at TIMESTAMP, + updated_by VARCHAR(254), + status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), + UNIQUE (id, domain_id), + UNIQUE (domain_id, name) + )`, + `CREATE TABLE IF NOT EXISTS connections ( + channel_id VARCHAR(36), + domain_id VARCHAR(36), + thing_id VARCHAR(36), + FOREIGN KEY (channel_id, domain_id) REFERENCES channels (id, domain_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (channel_id, domain_id, thing_id), + UNIQUE (channel_id, thing_id) + )`, + }, + Down: []string{ + `DROP TABLE IF EXISTS channels`, + `DROP TABLE IF EXISTS connections`, + }, + }, + }, + } + channelsMigration.Migrations = append(channelsMigration.Migrations, rolesMigration.Migrations...) + return channelsMigration, nil +} diff --git a/channels/private/service.go b/channels/private/service.go new file mode 100644 index 0000000000..7b193b8d0e --- /dev/null +++ b/channels/private/service.go @@ -0,0 +1,97 @@ +package private + +import ( + "context" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" +) + +type Service interface { + Authorize(ctx context.Context, req channels.AuthzReq) error + UnsetParentGroupFromChannels(ctx context.Context, parentGroupID string) error + RemoveThingConnections(ctx context.Context, thingID string) error +} + +type service struct { + repo channels.Repository + evaluator policies.Evaluator + policy policies.Service +} + +var _ Service = (*service)(nil) + +func New(repo channels.Repository, evaluator policies.Evaluator, policy policies.Service) Service { + return service{repo, evaluator, policy} +} + +func (svc service) Authorize(ctx context.Context, req channels.AuthzReq) error { + + switch req.ClientType { + case policies.UserType: + pr := policies.Policy{ + Subject: req.ClientID, + SubjectType: policies.UserType, + Object: req.ChannelID, + ObjectType: policies.ChannelType, + } + if err := svc.evaluator.CheckPolicy(ctx, pr); err != nil { + return errors.Wrap(svcerr.ErrAuthorization, err) + } + return nil + case policies.ThingType: + //Optimization: Add cache + if err := svc.repo.ThingAuthorize(ctx, channels.Connection{ + ChannelID: req.ChannelID, + ThingID: req.ClientID, + }); err != nil { + return errors.Wrap(svcerr.ErrAuthorization, err) + } + return nil + default: + return svcerr.ErrAuthentication + } + +} + +func (svc service) RemoveThingConnections(ctx context.Context, thingID string) error { + return svc.repo.RemoveThingConnections(ctx, thingID) +} + +func (svc service) UnsetParentGroupFromChannels(ctx context.Context, parentGroupID string) (retErr error) { + chs, err := svc.repo.RetrieveParentGroupChannels(ctx, parentGroupID) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + + if len(chs) > 0 { + prs := []policies.Policy{} + for _, ch := range chs { + prs = append(prs, policies.Policy{ + SubjectType: policies.GroupType, + Subject: ch.ParentGroup, + Relation: policies.ParentGroupRelation, + ObjectType: policies.ChannelType, + Object: ch.ID, + }) + } + + if err := svc.policy.DeletePolicies(ctx, prs); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.AddPolicies(ctx, prs); err != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errors.ErrRollbackTx, errRollback)) + } + } + }() + + if err := svc.repo.UnsetParentGroupFromChannels(ctx, parentGroupID); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + } + return nil +} diff --git a/channels/roleactions.go b/channels/roleactions.go new file mode 100644 index 0000000000..e6b8983381 --- /dev/null +++ b/channels/roleactions.go @@ -0,0 +1,43 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 +package channels + +import "github.com/absmach/magistrala/pkg/roles" + +// Below codes should moved out of service, may be can be kept in `cmd//main.go` + +const ( + ChannelUpdate roles.Action = "update" + ChannelRead roles.Action = "read" + ChannelDelete roles.Action = "delete" + ChannelSetParentGroup roles.Action = "set_parent_group" + ChannelConnectToChannel roles.Action = "connect_to_thing" + ChannelManageRole roles.Action = "manage_role" + ChannelAddRoleUsers roles.Action = "add_role_users" + ChannelRemoveRoleUsers roles.Action = "remove_role_users" + ChannelViewRoleUsers roles.Action = "view_role_users" +) + +const ( + BuiltInRoleAdmin = "admin" +) + +func AvailableActions() []roles.Action { + return []roles.Action{ + ChannelUpdate, + ChannelRead, + ChannelDelete, + ChannelSetParentGroup, + ChannelConnectToChannel, + ChannelManageRole, + ChannelAddRoleUsers, + ChannelRemoveRoleUsers, + ChannelViewRoleUsers, + } +} + +func BuiltInRoles() map[roles.BuiltInRoleName][]roles.Action { + return map[roles.BuiltInRoleName][]roles.Action{ + BuiltInRoleAdmin: AvailableActions(), + } +} diff --git a/channels/roleoperations.go b/channels/roleoperations.go new file mode 100644 index 0000000000..413ce581d1 --- /dev/null +++ b/channels/roleoperations.go @@ -0,0 +1,159 @@ +package channels + +import ( + "github.com/absmach/magistrala/pkg/roles" + "github.com/absmach/magistrala/pkg/svcutil" +) + +// Internal Operations + +const ( + OpViewChannel svcutil.Operation = iota + OpUpdateChannel + OpUpdateChannelTags + OpEnableChannel + OpDisableChannel + OpDeleteChannel + OpSetParentGroup + OpRemoveParentGroup + OpConnectThing + OpDisconnectThing +) + +var expectedOperations = []svcutil.Operation{ + OpViewChannel, + OpUpdateChannel, + OpUpdateChannelTags, + OpEnableChannel, + OpDisableChannel, + OpDeleteChannel, + OpSetParentGroup, + OpRemoveParentGroup, + OpConnectThing, + OpDisconnectThing, +} + +var operationNames = []string{ + "OpViewChannel", + "OpUpdateChannel", + "OpUpdateChannelTags", + "OpEnableChannel", + "OpDisableChannel", + "OpDeleteChannel", + "OpSetParentGroup", + "OpRemoveParentGroup", + "OpConnectThing", + "OpDisconnectThing", +} + +func NewOperationPerm() svcutil.OperationPerm { + return svcutil.NewOperationPerm(expectedOperations, operationNames) +} + +// External Operations +const ( + DomainOpCreateChannel svcutil.ExternalOperation = iota + DomainOpListChannel + GroupOpSetChildChannel + GroupsOpRemoveChildChannel + ThingsOpConnectChannel + ThingsOpDisconnectChannel +) + +var expectedExternalOperations = []svcutil.ExternalOperation{ + DomainOpCreateChannel, + DomainOpListChannel, + GroupOpSetChildChannel, + GroupsOpRemoveChildChannel, + ThingsOpConnectChannel, + ThingsOpDisconnectChannel, +} +var externalOperationNames = []string{ + "DomainOpCreateChannel", + "DomainOpListChannel", + "GroupOpSetChildChannel", + "GroupsOpRemoveChildChannel", + "ThingsOpConnectChannel", + "ThingsOpDisconnectChannel", +} + +func NewExternalOperationPerm() svcutil.ExternalOperationPerm { + return svcutil.NewExternalOperationPerm(expectedExternalOperations, externalOperationNames) +} + +// Below codes should moved out of service, may be can be kept in `cmd//main.go` + +const ( + updatePermission = "update_permission" + readPermission = "read_permission" + deletePermission = "delete_permission" + setParentGroupPermission = "set_parent_group_permission" + connectToThingPermission = "connect_to_thing_permission" + + manageRolePermission = "manage_role_permission" + addRoleUsersPermission = "add_role_users_permission" + removeRoleUsersPermission = "remove_role_users_permission" + viewRoleUsersPermission = "view_role_users_permission" +) + +func NewOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + OpViewChannel: readPermission, + OpUpdateChannel: updatePermission, + OpUpdateChannelTags: updatePermission, + OpEnableChannel: updatePermission, + OpDisableChannel: updatePermission, + OpDeleteChannel: deletePermission, + OpSetParentGroup: setParentGroupPermission, + OpRemoveParentGroup: setParentGroupPermission, + OpConnectThing: connectToThingPermission, + OpDisconnectThing: connectToThingPermission, + } + return opPerm +} + +func NewRolesOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + roles.OpAddRole: manageRolePermission, + roles.OpRemoveRole: manageRolePermission, + roles.OpUpdateRoleName: manageRolePermission, + roles.OpRetrieveRole: manageRolePermission, + roles.OpRetrieveAllRoles: manageRolePermission, + roles.OpRoleAddActions: manageRolePermission, + roles.OpRoleListActions: manageRolePermission, + roles.OpRoleCheckActionsExists: manageRolePermission, + roles.OpRoleRemoveActions: manageRolePermission, + roles.OpRoleRemoveAllActions: manageRolePermission, + roles.OpRoleAddMembers: addRoleUsersPermission, + roles.OpRoleListMembers: viewRoleUsersPermission, + roles.OpRoleCheckMembersExists: viewRoleUsersPermission, + roles.OpRoleRemoveMembers: removeRoleUsersPermission, + roles.OpRoleRemoveAllMembers: manageRolePermission, + } + return opPerm +} + +const ( + // External Permission + // Domains + domainCreateChannelPermission = "channel_create_permission" + domainListChanelPermission = "list_channels_permission" + // Groups + groupSetChildChannelPermission = "channel_create_permission" + groupRemoveChildChannelPermission = "channel_create_permission" + // Thing + thingsConnectChannelPermission = "connect_to_channel_permission" + thingsDisconnectChannelPermission = "connect_to_channel_permission" +) + +func NewExternalOperationPermissionMap() map[svcutil.ExternalOperation]svcutil.Permission { + extOpPerm := map[svcutil.ExternalOperation]svcutil.Permission{ + DomainOpCreateChannel: domainCreateChannelPermission, + DomainOpListChannel: domainListChanelPermission, + GroupOpSetChildChannel: groupSetChildChannelPermission, + GroupsOpRemoveChildChannel: groupRemoveChildChannelPermission, + ThingsOpConnectChannel: thingsConnectChannelPermission, + ThingsOpDisconnectChannel: thingsDisconnectChannelPermission, + } + return extOpPerm +} diff --git a/channels/service.go b/channels/service.go new file mode 100644 index 0000000000..67c8a703f1 --- /dev/null +++ b/channels/service.go @@ -0,0 +1,542 @@ +package channels + +import ( + "context" + "fmt" + "time" + + "github.com/absmach/magistrala" + grpcCommonV1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/roles" + mgclients "github.com/absmach/magistrala/things" + "golang.org/x/sync/errgroup" +) + +var ( + errCreateChannelsPolicies = errors.New("failed to create channels policies") + errRollbackRepo = errors.New("failed to rollback repo") + errAddConnectionsThings = errors.New("failed to add connections in things service") + errRemoveConnectionsThings = errors.New("failed to remove connections from things service") + errSetParentGroup = errors.New("channel already have parent") +) + +type service struct { + repo Repository + policy policies.Service + idProvider magistrala.IDProvider + things grpcThingsV1.ThingsServiceClient + groups grpcGroupsV1.GroupsServiceClient + roles.ProvisionManageService +} + +var _ Service = (*service)(nil) + +func New(repo Repository, policy policies.Service, idProvider magistrala.IDProvider, things grpcThingsV1.ThingsServiceClient, groups grpcGroupsV1.GroupsServiceClient, sidProvider magistrala.IDProvider) (Service, error) { + rpms, err := roles.NewProvisionManageService(policies.ChannelType, repo, policy, sidProvider, AvailableActions(), BuiltInRoles()) + if err != nil { + return nil, err + } + + return service{ + repo: repo, + policy: policy, + idProvider: idProvider, + things: things, + groups: groups, + ProvisionManageService: rpms, + }, nil +} + +func (svc service) CreateChannels(ctx context.Context, session authn.Session, chs ...Channel) (retChs []Channel, retErr error) { + var reChs []Channel + for _, c := range chs { + if c.ID == "" { + clientID, err := svc.idProvider.ID() + if err != nil { + return []Channel{}, err + } + c.ID = clientID + } + + if c.Status != mgclients.DisabledStatus && c.Status != mgclients.EnabledStatus { + return []Channel{}, svcerr.ErrInvalidStatus + } + c.Domain = session.DomainID + c.CreatedAt = time.Now() + reChs = append(reChs, c) + } + + savedChs, err := svc.repo.Save(ctx, reChs...) + if err != nil { + return nil, errors.Wrap(svcerr.ErrCreateEntity, err) + } + chIDs := []string{} + for _, c := range savedChs { + chIDs = append(chIDs, c.ID) + } + + defer func() { + if retErr != nil { + if errRollBack := svc.repo.Remove(ctx, chIDs...); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errRollbackRepo, errRollBack)) + } + } + }() + + newBuiltInRoleMembers := map[roles.BuiltInRoleName][]roles.Member{ + BuiltInRoleAdmin: {roles.Member(session.UserID)}, + } + + optionalPolicies := []policies.Policy{} + + for _, chID := range chIDs { + optionalPolicies = append(optionalPolicies, + policies.Policy{ + SubjectType: policies.DomainType, + Subject: session.DomainID, + Relation: policies.DomainRelation, + ObjectType: policies.ChannelType, + Object: chID, + }, + ) + } + if _, err := svc.AddNewEntitiesRoles(ctx, session.DomainID, session.UserID, chIDs, optionalPolicies, newBuiltInRoleMembers); err != nil { + return []Channel{}, errors.Wrap(errCreateChannelsPolicies, err) + } + return savedChs, nil +} + +func (svc service) UpdateChannel(ctx context.Context, session authn.Session, ch Channel) (Channel, error) { + channel := Channel{ + ID: ch.ID, + Name: ch.Name, + Metadata: ch.Metadata, + UpdatedAt: time.Now(), + UpdatedBy: session.UserID, + } + channel, err := svc.repo.Update(ctx, channel) + if err != nil { + return Channel{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return channel, nil +} + +func (svc service) UpdateChannelTags(ctx context.Context, session authn.Session, ch Channel) (Channel, error) { + + channel := Channel{ + ID: ch.ID, + Tags: ch.Tags, + UpdatedAt: time.Now(), + UpdatedBy: session.UserID, + } + channel, err := svc.repo.UpdateTags(ctx, channel) + if err != nil { + return Channel{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return channel, nil +} + +func (svc service) EnableChannel(ctx context.Context, session authn.Session, id string) (Channel, error) { + channel := Channel{ + ID: id, + Status: mgclients.EnabledStatus, + UpdatedAt: time.Now(), + } + ch, err := svc.changeChannelStatus(ctx, session.UserID, channel) + if err != nil { + return Channel{}, errors.Wrap(mgclients.ErrEnableClient, err) + } + + return ch, nil +} + +func (svc service) DisableChannel(ctx context.Context, session authn.Session, id string) (Channel, error) { + channel := Channel{ + ID: id, + Status: mgclients.DisabledStatus, + UpdatedAt: time.Now(), + } + ch, err := svc.changeChannelStatus(ctx, session.UserID, channel) + if err != nil { + return Channel{}, errors.Wrap(mgclients.ErrDisableClient, err) + } + + return ch, nil +} + +func (svc service) ViewChannel(ctx context.Context, session authn.Session, id string) (Channel, error) { + channel, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { + return Channel{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return channel, nil +} + +func (svc service) ListChannels(ctx context.Context, session authn.Session, pm PageMetadata) (Page, error) { + var ids []string + var err error + + switch session.SuperAdmin { + case true: + pm.Domain = session.DomainID + default: + ids, err = svc.listChannelIDs(ctx, session.DomainUserID, pm.Permission) + if err != nil { + return Page{}, errors.Wrap(svcerr.ErrNotFound, err) + } + } + if len(ids) == 0 && pm.Domain == "" { + return Page{}, nil + } + pm.IDs = ids + + cp, err := svc.repo.RetrieveAll(ctx, pm) + if err != nil { + return Page{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + + if pm.ListPerms && len(cp.Channels) > 0 { + g, ctx := errgroup.WithContext(ctx) + + for i := range cp.Channels { + // Copying loop variable "i" to avoid "loop variable captured by func literal" + iter := i + g.Go(func() error { + return svc.retrievePermissions(ctx, session.DomainUserID, &cp.Channels[iter]) + }) + } + + if err := g.Wait(); err != nil { + return Page{}, err + } + } + return cp, nil +} + +func (svc service) ListChannelsByThing(ctx context.Context, session authn.Session, thID string, pm PageMetadata) (Page, error) { + + return Page{}, nil +} + +func (svc service) RemoveChannel(ctx context.Context, session authn.Session, id string) error { + ok, err := svc.repo.DoesChannelHaveConnections(ctx, id) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + if ok { + if _, err := svc.things.RemoveChannelConnections(ctx, &grpcThingsV1.RemoveChannelConnectionsReq{ChannelId: id}); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + } + ch, err := svc.repo.ChangeStatus(ctx, Channel{ID: id, Status: mgclients.DeletedStatus}) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + deletePolicies := []policies.Policy{ + { + SubjectType: policies.DomainType, + Subject: session.DomainID, + Relation: policies.DomainRelation, + ObjectType: policies.ChannelType, + Object: id, + }, + } + + if ch.ParentGroup != "" { + deletePolicies = append(deletePolicies, policies.Policy{ + SubjectType: policies.GroupType, + Subject: ch.ParentGroup, + Relation: policies.ParentGroupRelation, + ObjectType: policies.ChannelType, + Object: id, + }) + } + + filterDeletePolicies := []policies.Policy{ + { + SubjectType: policies.ChannelType, + Subject: id, + }, + { + ObjectType: policies.ChannelType, + Object: id, + }, + } + + if err := svc.RemoveEntitiesRoles(ctx, session.DomainID, session.DomainUserID, []string{id}, filterDeletePolicies, deletePolicies); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + if err := svc.repo.Remove(ctx, id); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + return nil +} + +func (svc service) Connect(ctx context.Context, session authn.Session, chIDs, thIDs []string) (retErr error) { + + for _, chID := range chIDs { + c, err := svc.repo.RetrieveByID(ctx, chID) + if err != nil { + return errors.Wrap(svcerr.ErrCreateEntity, err) + } + if c.Status != mgclients.EnabledStatus { + return errors.Wrap(svcerr.ErrCreateEntity, fmt.Errorf("channel id %s is not in enabled state", chID)) + } + if c.Domain != session.DomainID { + return errors.Wrap(svcerr.ErrCreateEntity, fmt.Errorf("channel id %s has invalid domain id", chID)) + } + } + + for _, thID := range thIDs { + resp, err := svc.things.RetrieveEntity(ctx, &grpcCommonV1.RetrieveEntityReq{Id: thID}) + if err != nil { + return errors.Wrap(svcerr.ErrCreateEntity, err) + } + if resp.GetEntity().GetStatus() != uint32(mgclients.EnabledStatus) { + return errors.Wrap(svcerr.ErrCreateEntity, fmt.Errorf("thing id %s is not in enabled state", thID)) + } + if resp.GetEntity().GetDomainId() != session.DomainID { + return errors.Wrap(svcerr.ErrCreateEntity, fmt.Errorf("thing id %s has invalid domain id", thID)) + } + } + + conns := []Connection{} + thConns := []*grpcCommonV1.Connection{} + for _, chID := range chIDs { + for _, thID := range thIDs { + conns = append(conns, Connection{ + ThingID: thID, + ChannelID: chID, + DomainID: session.DomainID, + }) + thConns = append(thConns, &grpcCommonV1.Connection{ + ThingId: thID, + ChannelId: chID, + DomainId: session.DomainID, + }) + } + } + + for _, conn := range conns { + err := svc.repo.CheckConnection(ctx, conn) + + switch { + case err == nil: + return errors.Wrap(svcerr.ErrConflict, fmt.Errorf("channel %s and thing %s are already connected in domain %s", conn.ChannelID, conn.ThingID, conn.DomainID)) + case err != repoerr.ErrNotFound: + return errors.Wrap(svcerr.ErrCreateEntity, err) + } + } + if _, err := svc.things.AddConnections(ctx, &grpcCommonV1.AddConnectionsReq{Connections: thConns}); err != nil { + return errors.Wrap(svcerr.ErrCreateEntity, errors.Wrap(errAddConnectionsThings, err)) + } + + if err := svc.repo.AddConnections(ctx, conns); err != nil { + return errors.Wrap(svcerr.ErrCreateEntity, err) + } + + return nil +} + +func (svc service) Disconnect(ctx context.Context, session authn.Session, chIDs, thIDs []string) (retErr error) { + + for _, chID := range chIDs { + c, err := svc.repo.RetrieveByID(ctx, chID) + if err != nil { + return errors.Wrap(svcerr.ErrCreateEntity, err) + } + if c.Domain != session.DomainID { + return errors.Wrap(svcerr.ErrCreateEntity, fmt.Errorf("channel id %s has invalid domain id", chID)) + } + } + + for _, thID := range thIDs { + resp, err := svc.things.RetrieveEntity(ctx, &grpcCommonV1.RetrieveEntityReq{Id: thID}) + if err != nil { + return errors.Wrap(svcerr.ErrCreateEntity, err) + } + + if resp.GetEntity().GetDomainId() != session.DomainID { + return errors.Wrap(svcerr.ErrCreateEntity, fmt.Errorf("thing id %s has invalid domain id", thID)) + } + } + + conns := []Connection{} + thConns := []*grpcCommonV1.Connection{} + for _, chID := range chIDs { + for _, thID := range thIDs { + conns = append(conns, Connection{ + ThingID: thID, + ChannelID: chID, + DomainID: session.DomainID, + }) + thConns = append(thConns, &grpcCommonV1.Connection{ + ThingId: thID, + ChannelId: chID, + DomainId: session.DomainID, + }) + } + } + + if _, err := svc.things.RemoveConnections(ctx, &grpcCommonV1.RemoveConnectionsReq{Connections: thConns}); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, errors.Wrap(errRemoveConnectionsThings, err)) + } + + if err := svc.repo.RemoveConnections(ctx, conns); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + return nil +} + +func (svc service) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (retErr error) { + ch, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { + return errors.Wrap(svcerr.ErrUpdateEntity, err) + } + + resp, err := svc.groups.RetrieveEntity(ctx, &grpcCommonV1.RetrieveEntityReq{Id: parentGroupID}) + if err != nil { + return errors.Wrap(svcerr.ErrUpdateEntity, err) + } + if resp.GetEntity().GetDomainId() != session.DomainID { + return errors.Wrap(svcerr.ErrUpdateEntity, fmt.Errorf("parent group id %s has invalid domain id", parentGroupID)) + } + if resp.GetEntity().GetStatus() != uint32(mgclients.EnabledStatus) { + return errors.Wrap(svcerr.ErrUpdateEntity, fmt.Errorf("parent group id %s is not in enabled state", parentGroupID)) + } + + var pols []policies.Policy + if ch.ParentGroup != "" { + return errors.Wrap(svcerr.ErrConflict, errSetParentGroup) + } + pols = append(pols, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: parentGroupID, + Relation: policies.ParentGroupRelation, + ObjectType: policies.ChannelType, + Object: id, + }) + + if err := svc.policy.AddPolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrAddPolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.DeletePolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + ch = Channel{ID: id, ParentGroup: parentGroupID, UpdatedBy: session.UserID, UpdatedAt: time.Now()} + + if err := svc.repo.SetParentGroup(ctx, ch); err != nil { + return errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return nil +} + +func (svc service) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (retErr error) { + ch, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + + if ch.ParentGroup != "" { + var pols []policies.Policy + pols = append(pols, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: ch.ParentGroup, + Relation: policies.ParentGroupRelation, + ObjectType: policies.ChannelType, + Object: id, + }) + + if err := svc.policy.DeletePolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrAddPolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.AddPolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + + ch := Channel{ID: id, UpdatedBy: session.UserID, UpdatedAt: time.Now()} + + if err := svc.repo.RemoveParentGroup(ctx, ch); err != nil { + return err + } + } + + return nil +} + +func (svc service) listChannelIDs(ctx context.Context, userID, permission string) ([]string, error) { + tids, err := svc.policy.ListAllObjects(ctx, policies.Policy{ + SubjectType: policies.UserType, + Subject: userID, + Permission: permission, + ObjectType: policies.ChannelType, + }) + if err != nil { + return nil, errors.Wrap(svcerr.ErrNotFound, err) + } + return tids.Policies, nil +} + +func (svc service) retrievePermissions(ctx context.Context, userID string, channel *Channel) error { + permissions, err := svc.listUserThingPermission(ctx, userID, channel.ID) + if err != nil { + return err + } + channel.Permissions = permissions + return nil +} + +func (svc service) listUserThingPermission(ctx context.Context, userID, thingID string) ([]string, error) { + lp, err := svc.policy.ListPermissions(ctx, policies.Policy{ + SubjectType: policies.UserType, + Subject: userID, + Object: thingID, + ObjectType: policies.ChannelType, + }, []string{}) + if err != nil { + return []string{}, errors.Wrap(svcerr.ErrAuthorization, err) + } + return lp, nil +} + +func (svc service) changeChannelStatus(ctx context.Context, userID string, channel Channel) (Channel, error) { + + dbchannel, err := svc.repo.RetrieveByID(ctx, channel.ID) + if err != nil { + return Channel{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + if dbchannel.Status == channel.Status { + return Channel{}, errors.ErrStatusAlreadyAssigned + } + + channel.UpdatedBy = userID + + channel, err = svc.repo.ChangeStatus(ctx, channel) + if err != nil { + return Channel{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return channel, nil +} diff --git a/channels/tracing/doc.go b/channels/tracing/doc.go new file mode 100644 index 0000000000..0820cc9289 --- /dev/null +++ b/channels/tracing/doc.go @@ -0,0 +1,12 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package tracing provides tracing instrumentation for Magistrala channels service. +// +// This package provides tracing middleware for Magistrala channels service. +// It can be used to trace incoming requests and add tracing capabilities to +// Magistrala channels service. +// +// For more details about tracing instrumentation for Magistrala messaging refer +// to the documentation at https://docs.magistrala.abstractmachines.fr/tracing/. +package tracing diff --git a/channels/tracing/tracing.go b/channels/tracing/tracing.go new file mode 100644 index 0000000000..ec444dc10a --- /dev/null +++ b/channels/tracing/tracing.go @@ -0,0 +1,132 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package tracing + +import ( + "context" + + "github.com/absmach/magistrala/channels" + "github.com/absmach/magistrala/pkg/authn" + rmTrace "github.com/absmach/magistrala/pkg/roles/rolemanager/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var _ channels.Service = (*tracingMiddleware)(nil) + +type tracingMiddleware struct { + tracer trace.Tracer + svc channels.Service + rmTrace.RoleManagerTracing +} + +// New returns a new group service with tracing capabilities. +func New(svc channels.Service, tracer trace.Tracer) channels.Service { + return &tracingMiddleware{tracer, svc, rmTrace.NewRoleManagerTracing("channels", svc, tracer)} +} + +// CreateChannels traces the "CreateChannels" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) CreateChannels(ctx context.Context, session authn.Session, chs ...channels.Channel) ([]channels.Channel, error) { + ctx, span := tm.tracer.Start(ctx, "svc_create_channel") + defer span.End() + + return tm.svc.CreateChannels(ctx, session, chs...) +} + +// ViewChannel traces the "ViewChannel" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) ViewChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + ctx, span := tm.tracer.Start(ctx, "svc_view_channel", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + return tm.svc.ViewChannel(ctx, session, id) +} + +// ListChannels traces the "ListChannels" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) ListChannels(ctx context.Context, session authn.Session, pm channels.PageMetadata) (channels.Page, error) { + ctx, span := tm.tracer.Start(ctx, "svc_list_channels") + defer span.End() + return tm.svc.ListChannels(ctx, session, pm) +} + +func (tm *tracingMiddleware) ListChannelsByThing(ctx context.Context, session authn.Session, thingID string, pm channels.PageMetadata) (channels.Page, error) { + ctx, span := tm.tracer.Start(ctx, "svc_list_channels") + defer span.End() + return tm.svc.ListChannelsByThing(ctx, session, thingID, pm) +} + +// UpdateChannel traces the "UpdateChannel" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) UpdateChannel(ctx context.Context, session authn.Session, cli channels.Channel) (channels.Channel, error) { + ctx, span := tm.tracer.Start(ctx, "svc_update_channel", trace.WithAttributes(attribute.String("id", cli.ID))) + defer span.End() + + return tm.svc.UpdateChannel(ctx, session, cli) +} + +// UpdateChannelTags traces the "UpdateChannelTags" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) UpdateChannelTags(ctx context.Context, session authn.Session, cli channels.Channel) (channels.Channel, error) { + ctx, span := tm.tracer.Start(ctx, "svc_update_channel_tags", trace.WithAttributes( + attribute.String("id", cli.ID), + attribute.StringSlice("tags", cli.Tags), + )) + defer span.End() + + return tm.svc.UpdateChannelTags(ctx, session, cli) +} + +// EnableChannel traces the "EnableChannel" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) EnableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + ctx, span := tm.tracer.Start(ctx, "svc_enable_channel", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + + return tm.svc.EnableChannel(ctx, session, id) +} + +// DisableChannel traces the "DisableChannel" operation of the wrapped policies.Service. +func (tm *tracingMiddleware) DisableChannel(ctx context.Context, session authn.Session, id string) (channels.Channel, error) { + ctx, span := tm.tracer.Start(ctx, "svc_disable_channel", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + + return tm.svc.DisableChannel(ctx, session, id) +} + +// DeleteChannel traces the "DeleteChannel" operation of the wrapped channels.Service. +func (tm *tracingMiddleware) RemoveChannel(ctx context.Context, session authn.Session, id string) error { + ctx, span := tm.tracer.Start(ctx, "delete_channel", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + return tm.svc.RemoveChannel(ctx, session, id) +} + +func (tm *tracingMiddleware) Connect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + ctx, span := tm.tracer.Start(ctx, "connect", trace.WithAttributes( + attribute.StringSlice("channel_ids", chIDs), + attribute.StringSlice("thing_ids", thIDs), + )) + defer span.End() + return tm.svc.Connect(ctx, session, chIDs, thIDs) +} + +func (tm *tracingMiddleware) Disconnect(ctx context.Context, session authn.Session, chIDs, thIDs []string) error { + ctx, span := tm.tracer.Start(ctx, "disconnect", trace.WithAttributes( + attribute.StringSlice("channel_ids", chIDs), + attribute.StringSlice("thing_ids", thIDs), + )) + defer span.End() + return tm.svc.Disconnect(ctx, session, chIDs, thIDs) +} + +func (tm *tracingMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error { + ctx, span := tm.tracer.Start(ctx, "set_parent_group", trace.WithAttributes( + attribute.String("parent_group_id", parentGroupID), + attribute.String("id", id), + )) + defer span.End() + return tm.svc.SetParentGroup(ctx, session, parentGroupID, id) +} + +func (tm *tracingMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + ctx, span := tm.tracer.Start(ctx, "remove_parent_group", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + return tm.svc.RemoveParentGroup(ctx, session, id) +} diff --git a/cli/groups.go b/cli/groups.go index 867d1ec664..2a26bec9e0 100644 --- a/cli/groups.go +++ b/cli/groups.go @@ -6,7 +6,7 @@ package cli import ( "encoding/json" - "github.com/absmach/magistrala/internal/groups" + "github.com/absmach/magistrala/groups" mgxsdk "github.com/absmach/magistrala/pkg/sdk/go" "github.com/spf13/cobra" ) diff --git a/cmd/auth/main.go b/cmd/auth/main.go index a29477830c..f75691b2b9 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -17,13 +17,13 @@ import ( "github.com/absmach/magistrala/auth" api "github.com/absmach/magistrala/auth/api" authgrpcapi "github.com/absmach/magistrala/auth/api/grpc/auth" - domainsgrpcapi "github.com/absmach/magistrala/auth/api/grpc/domains" tokengrpcapi "github.com/absmach/magistrala/auth/api/grpc/token" httpapi "github.com/absmach/magistrala/auth/api/http" - "github.com/absmach/magistrala/auth/events" "github.com/absmach/magistrala/auth/jwt" apostgres "github.com/absmach/magistrala/auth/postgres" "github.com/absmach/magistrala/auth/tracing" + grpcAuthV1 "github.com/absmach/magistrala/internal/grpc/auth/v1" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/jaeger" "github.com/absmach/magistrala/pkg/policies/spicedb" @@ -103,7 +103,8 @@ func main() { logger.Error(err.Error()) } - db, err := pgclient.Setup(dbConfig, *apostgres.Migration()) + am := apostgres.Migration() + db, err := pgclient.Setup(dbConfig, *am) if err != nil { logger.Error(err.Error()) exitCode = 1 @@ -130,17 +131,8 @@ func main() { exitCode = 1 return } - svc := newService(ctx, db, tracer, cfg, dbConfig, logger, spicedbclient) - httpServerConfig := server.Config{Port: defSvcHTTPPort} - if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err.Error())) - exitCode = 1 - return - } - hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, logger, cfg.InstanceID), logger) - grpcServerConfig := server.Config{Port: defSvcGRPCPort} if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGrpc}); err != nil { logger.Error(fmt.Sprintf("failed to load %s gRPC server configuration : %s", svcName, err.Error())) @@ -149,9 +141,8 @@ func main() { } registerAuthServiceServer := func(srv *grpc.Server) { reflection.Register(srv) - magistrala.RegisterTokenServiceServer(srv, tokengrpcapi.NewTokenServer(svc)) - magistrala.RegisterDomainsServiceServer(srv, domainsgrpcapi.NewDomainsServer(svc)) - magistrala.RegisterAuthServiceServer(srv, authgrpcapi.NewAuthServer(svc)) + grpcTokenV1.RegisterTokenServiceServer(srv, tokengrpcapi.NewTokenServer(svc)) + grpcAuthV1.RegisterAuthServiceServer(srv, authgrpcapi.NewAuthServer(svc)) } gs := grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerAuthServiceServer, logger) @@ -160,12 +151,25 @@ func main() { chc := chclient.New(svcName, magistrala.Version, logger, cancel) go chc.CallHome(ctx) } - g.Go(func() error { - return hs.Start() + return gs.Start() }) + + httpServerConfig := server.Config{Port: defSvcHTTPPort} + if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err.Error())) + exitCode = 1 + return + } + hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, logger, cfg.InstanceID), logger) + + if cfg.SendTelemetry { + chc := chclient.New(svcName, magistrala.Version, logger, cancel) + go chc.CallHome(ctx) + } + g.Go(func() error { - return gs.Start() + return hs.Start() }) g.Go(func() error { @@ -207,10 +211,9 @@ func initSchema(ctx context.Context, client *authzed.ClientWithExperimental, sch return nil } -func newService(ctx context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental) auth.Service { +func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental) auth.Service { database := postgres.NewDatabase(db, dbConfig, tracer) keysRepo := apostgres.New(database) - domainsRepo := apostgres.NewDomainRepository(database) idProvider := uuid.New() pEvaluator := spicedb.NewPolicyEvaluator(spicedbClient, logger) @@ -218,14 +221,9 @@ func newService(ctx context.Context, db *sqlx.DB, tracer trace.Tracer, cfg confi t := jwt.New([]byte(cfg.SecretKey)) - svc := auth.New(keysRepo, domainsRepo, idProvider, t, pEvaluator, pService, cfg.AccessDuration, cfg.RefreshDuration, cfg.InvitationDuration) - svc, err := events.NewEventStoreMiddleware(ctx, svc, cfg.ESURL) - if err != nil { - logger.Error(fmt.Sprintf("failed to init event store middleware : %s", err)) - return nil - } + svc := auth.New(keysRepo, idProvider, t, pEvaluator, pService, cfg.AccessDuration, cfg.RefreshDuration, cfg.InvitationDuration) svc = api.LoggingMiddleware(svc, logger) - counter, latency := prometheus.MakeMetrics("groups", "api") + counter, latency := prometheus.MakeMetrics("auth", "api") svc = api.MetricsMiddleware(svc, counter, latency) svc = tracing.New(svc, tracer) diff --git a/cmd/channels/main.go b/cmd/channels/main.go new file mode 100644 index 0000000000..1da3837ad2 --- /dev/null +++ b/cmd/channels/main.go @@ -0,0 +1,304 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package main contains things main function to start the things service. +package main + +import ( + "context" + "fmt" + "log" + "log/slog" + "net/url" + "os" + + chclient "github.com/absmach/callhome/pkg/client" + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/channels" + grpcapi "github.com/absmach/magistrala/channels/api/grpc" + httpapi "github.com/absmach/magistrala/channels/api/http" + "github.com/absmach/magistrala/channels/events" + "github.com/absmach/magistrala/channels/middleware" + "github.com/absmach/magistrala/channels/postgres" + pChannels "github.com/absmach/magistrala/channels/private" + "github.com/absmach/magistrala/channels/tracing" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + mglog "github.com/absmach/magistrala/logger" + authsvcAuthn "github.com/absmach/magistrala/pkg/authn/authsvc" + mgauthz "github.com/absmach/magistrala/pkg/authz" + authsvcAuthz "github.com/absmach/magistrala/pkg/authz/authsvc" + "github.com/absmach/magistrala/pkg/grpcclient" + jaegerclient "github.com/absmach/magistrala/pkg/jaeger" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/policies/spicedb" + pg "github.com/absmach/magistrala/pkg/postgres" + pgclient "github.com/absmach/magistrala/pkg/postgres" + "github.com/absmach/magistrala/pkg/prometheus" + "github.com/absmach/magistrala/pkg/server" + grpcserver "github.com/absmach/magistrala/pkg/server/grpc" + httpserver "github.com/absmach/magistrala/pkg/server/http" + "github.com/absmach/magistrala/pkg/sid" + "github.com/absmach/magistrala/pkg/uuid" + "github.com/authzed/authzed-go/v1" + "github.com/authzed/grpcutil" + "github.com/caarlos0/env/v11" + "github.com/go-chi/chi/v5" + "github.com/jmoiron/sqlx" + "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/reflection" +) + +const ( + svcName = "channels" + envPrefixDB = "MG_CHANNELS_DB_" + envPrefixHTTP = "MG_CHANNELS_HTTP_" + envPrefixGRPC = "MG_CHANNELS_GRPC_" + envPrefixAuth = "MG_AUTH_GRPC_" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + envPrefixGroups = "MG_GROUPS_GRPC_" + defDB = "channels" + defSvcHTTPPort = "9005" + defSvcGRPCPort = "7005" +) + +type config struct { + LogLevel string `env:"MG_CHANNELS_LOG_LEVEL" envDefault:"info"` + InstanceID string `env:"MG_CHANNELS_INSTANCE_ID" envDefault:""` + JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` + SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` + ESURL string `env:"MG_ES_URL" envDefault:"nats://localhost:4222"` + TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` + SpicedbHost string `env:"MG_SPICEDB_HOST" envDefault:"localhost"` + SpicedbPort string `env:"MG_SPICEDB_PORT" envDefault:"50051"` + SpicedbPreSharedKey string `env:"MG_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + + // Create new things configuration + cfg := config{} + if err := env.Parse(&cfg); err != nil { + log.Fatalf("failed to load %s configuration : %s", svcName, err) + } + + var logger *slog.Logger + logger, err := mglog.New(os.Stdout, cfg.LogLevel) + if err != nil { + log.Fatalf("failed to init logger: %s", err.Error()) + } + + var exitCode int + defer mglog.ExitWithError(&exitCode) + + if cfg.InstanceID == "" { + if cfg.InstanceID, err = uuid.New().ID(); err != nil { + logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err)) + exitCode = 1 + return + } + } + + // Create new database for things + dbConfig := pgclient.Config{Name: defDB} + if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + migrations, err := postgres.Migration() + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + db, err := pgclient.Setup(dbConfig, *migrations) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer db.Close() + + tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) + if err != nil { + logger.Error(fmt.Sprintf("Failed to init Jaeger: %s", err)) + exitCode = 1 + return + } + defer func() { + if err := tp.Shutdown(ctx); err != nil { + logger.Error(fmt.Sprintf("Error shutting down tracer provider: %v", err)) + } + }() + tracer := tp.Tracer(svcName) + + policyEvaluator, policyService, err := newSpiceDBPolicyServiceEvaluator(cfg, logger) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + logger.Info("Policy service are successfully connected to SpiceDB gRPC server") + + grpcCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&grpcCfg, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + exitCode = 1 + return + } + authn, authnClient, err := authsvcAuthn.NewAuthentication(ctx, grpcCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer authnClient.Close() + logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure()) + + authz, authzClient, err := authsvcAuthz.NewAuthorization(ctx, grpcCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer authzClient.Close() + logger.Info("AuthZ successfully connected to auth gRPC server " + authzClient.Secure()) + + thgrpcCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&thgrpcCfg, env.Options{Prefix: envPrefixThings}); err != nil { + logger.Error(fmt.Sprintf("failed to load things gRPC client configuration : %s", err)) + exitCode = 1 + return + } + thingsClient, thingsHandler, err := grpcclient.SetupThingsClient(ctx, thgrpcCfg) + if err != nil { + logger.Error(fmt.Sprintf("failed to connect to things gRPC server: %s", err)) + exitCode = 1 + return + } + defer thingsHandler.Close() + logger.Info("Things gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + + groupsgRPCCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&groupsgRPCCfg, env.Options{Prefix: envPrefixGroups}); err != nil { + logger.Error(fmt.Sprintf("failed to load groups gRPC client configuration : %s", err)) + exitCode = 1 + return + } + groupsClient, groupsHandler, err := grpcclient.SetupGroupsClient(ctx, groupsgRPCCfg) + if err != nil { + logger.Error(fmt.Sprintf("failed to connect to groups gRPC server: %s", err)) + exitCode = 1 + return + } + defer groupsHandler.Close() + logger.Info("Groups gRPC client successfully connected to groups gRPC server " + groupsHandler.Secure()) + + svc, psvc, err := newService(ctx, db, dbConfig, authz, policyEvaluator, policyService, cfg.ESURL, tracer, thingsClient, groupsClient, logger) + if err != nil { + logger.Error(fmt.Sprintf("failed to create services: %s", err)) + exitCode = 1 + return + } + + grpcServerConfig := server.Config{Port: defSvcGRPCPort} + if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGRPC}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s gRPC server configuration : %s", svcName, err)) + exitCode = 1 + return + } + registerChannelsServer := func(srv *grpc.Server) { + reflection.Register(srv) + grpcChannelsV1.RegisterChannelsServiceServer(srv, grpcapi.NewServer(psvc)) + } + + gs := grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerChannelsServer, logger) + + httpServerConfig := server.Config{Port: defSvcHTTPPort} + if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err)) + exitCode = 1 + return + } + mux := chi.NewRouter() + httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, mux, logger, cfg.InstanceID), logger) + + if cfg.SendTelemetry { + chc := chclient.New(svcName, magistrala.Version, logger, cancel) + go chc.CallHome(ctx) + } + + // Start all servers + g.Go(func() error { + return httpSvc.Start() + }) + + g.Go(func() error { + return gs.Start() + }) + + g.Go(func() error { + return server.StopSignalHandler(ctx, cancel, logger, svcName, httpSvc) + }) + + if err := g.Wait(); err != nil { + logger.Error(fmt.Sprintf("%s service terminated: %s", svcName, err)) + } +} + +func newService(ctx context.Context, db *sqlx.DB, dbConfig pgclient.Config, authz mgauthz.Authorization, pe policies.Evaluator, ps policies.Service, esURL string, tracer trace.Tracer, thingsClient grpcThingsV1.ThingsServiceClient, groupsClient grpcGroupsV1.GroupsServiceClient, logger *slog.Logger) (channels.Service, pChannels.Service, error) { + database := pg.NewDatabase(db, dbConfig, tracer) + repo := postgres.NewRepository(database) + + idp := uuid.New() + sidp, err := sid.New() + if err != nil { + return nil, nil, err + } + + svc, err := channels.New(repo, ps, idp, thingsClient, groupsClient, sidp) + if err != nil { + return nil, nil, err + } + + svc, err = events.NewEventStoreMiddleware(ctx, svc, esURL) + if err != nil { + return nil, nil, err + } + + svc = tracing.New(svc, tracer) + + counter, latency := prometheus.MakeMetrics("channels", "api") + svc = middleware.MetricsMiddleware(svc, counter, latency) + + svc, err = middleware.AuthorizationMiddleware(svc, repo, authz, channels.NewOperationPermissionMap(), channels.NewRolesOperationPermissionMap(), channels.NewExternalOperationPermissionMap()) + if err != nil { + return nil, nil, err + } + svc = middleware.LoggingMiddleware(svc, logger) + + psvc := pChannels.New(repo, pe, ps) + return svc, psvc, err +} + +func newSpiceDBPolicyServiceEvaluator(cfg config, logger *slog.Logger) (policies.Evaluator, policies.Service, error) { + client, err := authzed.NewClientWithExperimentalAPIs( + fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey), + ) + if err != nil { + return nil, nil, err + } + ps := spicedb.NewPolicyService(client, logger) + + pe := spicedb.NewPolicyEvaluator(client, logger) + return pe, ps, nil +} diff --git a/cmd/coap/main.go b/cmd/coap/main.go index ad16e992c1..7b12913e92 100644 --- a/cmd/coap/main.go +++ b/cmd/coap/main.go @@ -31,12 +31,13 @@ import ( ) const ( - svcName = "coap_adapter" - envPrefix = "MG_COAP_ADAPTER_" - envPrefixHTTP = "MG_COAP_ADAPTER_HTTP_" - envPrefixThings = "MG_THINGS_AUTH_GRPC_" - defSvcHTTPPort = "5683" - defSvcCoAPPort = "5683" + svcName = "coap_adapter" + envPrefix = "MG_COAP_ADAPTER_" + envPrefixHTTP = "MG_COAP_ADAPTER_HTTP_" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + defSvcHTTPPort = "5683" + defSvcCoAPPort = "5683" ) type config struct { @@ -104,6 +105,22 @@ func main() { logger.Info("Things service gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + channelsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&channelsClientCfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + channelsClient, channelsHandler, err := grpcclient.SetupChannelsClient(ctx, channelsClientCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer channelsHandler.Close() + logger.Info("Channels service gRPC client successfully connected to channels gRPC server " + channelsHandler.Secure()) + tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) if err != nil { logger.Error(fmt.Sprintf("Failed to init Jaeger: %s", err)) @@ -126,7 +143,7 @@ func main() { defer nps.Close() nps = brokerstracing.NewPubSub(coapServerConfig, tracer, nps) - svc := coap.New(thingsClient, nps) + svc := coap.New(thingsClient, channelsClient, nps) svc = tracing.New(tracer, svc) diff --git a/cmd/domains/main.go b/cmd/domains/main.go new file mode 100644 index 0000000000..450e5a9a90 --- /dev/null +++ b/cmd/domains/main.go @@ -0,0 +1,263 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "log" + "log/slog" + "net/url" + "os" + "time" + + chclient "github.com/absmach/callhome/pkg/client" + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/domains" + domainsSvc "github.com/absmach/magistrala/domains" + domainsgrpcapi "github.com/absmach/magistrala/domains/api/grpc" + httpapi "github.com/absmach/magistrala/domains/api/http" + "github.com/absmach/magistrala/domains/events" + dmw "github.com/absmach/magistrala/domains/middleware" + dpostgres "github.com/absmach/magistrala/domains/postgres" + dtracing "github.com/absmach/magistrala/domains/tracing" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" + mglog "github.com/absmach/magistrala/logger" + authsvcAuthn "github.com/absmach/magistrala/pkg/authn/authsvc" + "github.com/absmach/magistrala/pkg/authz" + authsvcAuthz "github.com/absmach/magistrala/pkg/authz/authsvc" + "github.com/absmach/magistrala/pkg/grpcclient" + "github.com/absmach/magistrala/pkg/jaeger" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/policies/spicedb" + "github.com/absmach/magistrala/pkg/postgres" + pgclient "github.com/absmach/magistrala/pkg/postgres" + "github.com/absmach/magistrala/pkg/prometheus" + "github.com/absmach/magistrala/pkg/server" + grpcserver "github.com/absmach/magistrala/pkg/server/grpc" + httpserver "github.com/absmach/magistrala/pkg/server/http" + "github.com/absmach/magistrala/pkg/sid" + "github.com/absmach/magistrala/pkg/uuid" + "github.com/authzed/authzed-go/v1" + "github.com/authzed/grpcutil" + "github.com/caarlos0/env/v11" + "github.com/go-chi/chi/v5" + "github.com/jmoiron/sqlx" + "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/reflection" +) + +const ( + svcName = "domains" + envPrefixHTTP = "MG_DOMAINS_HTTP_" + envPrefixGrpc = "MG_DOMAINS_GRPC_" + envPrefixDB = "MG_DOMAINS_DB_" + envPrefixAuth = "MG_AUTH_GRPC_" + defDB = "domains" + defSvcHTTPPort = "9004" + defSvcGRPCPort = "7004" +) + +type config struct { + LogLevel string `env:"MG_DOMAINS_LOG_LEVEL" envDefault:"info"` + JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` + SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` + InstanceID string `env:"MG_DOMAINS_INSTANCE_ID" envDefault:""` + SpicedbHost string `env:"MG_SPICEDB_HOST" envDefault:"localhost"` + SpicedbPort string `env:"MG_SPICEDB_PORT" envDefault:"50051"` + SpicedbPreSharedKey string `env:"MG_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` + TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` + ESURL string `env:"MG_ES_URL" envDefault:"nats://localhost:4222"` +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + + cfg := config{} + if err := env.Parse(&cfg); err != nil { + log.Fatalf("failed to load %s configuration : %s", svcName, err.Error()) + } + + logger, err := mglog.New(os.Stdout, cfg.LogLevel) + if err != nil { + log.Fatalf("failed to init logger: %s", err.Error()) + } + + var exitCode int + defer mglog.ExitWithError(&exitCode) + + if cfg.InstanceID == "" { + if cfg.InstanceID, err = uuid.New().ID(); err != nil { + logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err)) + exitCode = 1 + return + } + } + + dbConfig := pgclient.Config{Name: defDB} + if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { + logger.Error(err.Error()) + } + + dm, err := dpostgres.Migration() + if err != nil { + logger.Error(fmt.Sprintf("failed create migrations for domain: %s", err.Error())) + exitCode = 1 + return + } + + db, err := pgclient.Setup(dbConfig, *dm) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer db.Close() + + 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)) + exitCode = 1 + return + } + defer func() { + if err := tp.Shutdown(ctx); err != nil { + logger.Error(fmt.Sprintf("error shutting down tracer provider: %v", err)) + } + }() + tracer := tp.Tracer(svcName) + + time.Sleep(1 * time.Second) + + clientConfig := grpcclient.Config{} + if err := env.ParseWithOptions(&clientConfig, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC server configuration : %s", err)) + exitCode = 1 + return + } + + authn, authnHandler, err := authsvcAuthn.NewAuthentication(ctx, clientConfig) + if err != nil { + logger.Error(fmt.Sprintf("authn failed to connect to auth gRPC server : %s", err.Error())) + exitCode = 1 + return + } + defer authnHandler.Close() + logger.Info("Authn successfully connected to auth gRPC server " + authnHandler.Secure()) + + authz, authzHandler, err := authsvcAuthz.NewAuthorization(ctx, clientConfig) + if err != nil { + logger.Error(fmt.Sprintf("authz failed to connect to auth gRPC server : %s", err.Error())) + exitCode = 1 + return + } + defer authzHandler.Close() + logger.Info("Authz successfully connected to auth gRPC server " + authzHandler.Secure()) + + policyService, err := newPolicyService(cfg, logger) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + logger.Info("Policy client successfully connected to spicedb gRPC server") + + svc, err := newDomainService(ctx, db, tracer, cfg, dbConfig, authz, policyService, logger) + if err != nil { + logger.Error(fmt.Sprintf("failed to create %s service: %s", svcName, err.Error())) + exitCode = 1 + return + } + + grpcServerConfig := server.Config{Port: defSvcGRPCPort} + if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGrpc}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s gRPC server configuration : %s", svcName, err.Error())) + exitCode = 1 + return + } + registerDomainsServiceServer := func(srv *grpc.Server) { + reflection.Register(srv) + grpcDomainsV1.RegisterDomainsServiceServer(srv, domainsgrpcapi.NewDomainsServer(svc)) + } + + gs := grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerDomainsServiceServer, logger) + + g.Go(func() error { + return gs.Start() + }) + + httpServerConfig := server.Config{Port: defSvcHTTPPort} + if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err.Error())) + exitCode = 1 + return + } + mux := chi.NewMux() + hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, mux, logger, cfg.InstanceID), logger) + + g.Go(func() error { + return hs.Start() + }) + + if cfg.SendTelemetry { + chc := chclient.New(svcName, magistrala.Version, logger, cancel) + go chc.CallHome(ctx) + } + + g.Go(func() error { + return server.StopSignalHandler(ctx, cancel, logger, svcName, hs, gs) + }) + + if err := g.Wait(); err != nil { + logger.Error(fmt.Sprintf("domains service terminated: %s", err)) + } +} + +func newDomainService(ctx context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, authz authz.Authorization, policiessvc policies.Service, logger *slog.Logger) (domains.Service, error) { + database := postgres.NewDatabase(db, dbConfig, tracer) + domainsRepo := dpostgres.New(database) + + idProvider := uuid.New() + sidProvider, err := sid.New() + if err != nil { + return nil, fmt.Errorf("failed to init short id provider : %w", err) + } + svc, err := domainsSvc.New(domainsRepo, policiessvc, idProvider, sidProvider) + if err != nil { + return nil, fmt.Errorf("failed to init domain service: %w", err) + } + svc, err = events.NewEventStoreMiddleware(ctx, svc, cfg.ESURL) + if err != nil { + return nil, fmt.Errorf("failed to init domain event store middleware: %w", err) + } + svc = dmw.LoggingMiddleware(svc, logger) + counter, latency := prometheus.MakeMetrics("domains", "api") + svc = dmw.MetricsMiddleware(svc, counter, latency) + + svc, err = dmw.AuthorizationMiddleware(policies.DomainType, svc, authz, domains.NewOperationPermissionMap(), domains.NewRolesOperationPermissionMap()) + if err != nil { + return nil, err + } + + svc = dtracing.New(svc, tracer) + return svc, nil +} + +func newPolicyService(cfg config, logger *slog.Logger) (policies.Service, error) { + client, err := authzed.NewClientWithExperimentalAPIs( + fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey), + ) + if err != nil { + return nil, err + } + policySvc := spicedb.NewPolicyService(client, logger) + + return policySvc, nil +} diff --git a/cmd/groups/main.go b/cmd/groups/main.go new file mode 100644 index 0000000000..c33fffe140 --- /dev/null +++ b/cmd/groups/main.go @@ -0,0 +1,301 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package main contains groups main function to start the groups service. +package main + +import ( + "context" + "fmt" + "log" + "log/slog" + "net/url" + "os" + + chclient "github.com/absmach/callhome/pkg/client" + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/groups" + gpsvc "github.com/absmach/magistrala/groups" + grpcapi "github.com/absmach/magistrala/groups/api/grpc" + httpapi "github.com/absmach/magistrala/groups/api/http" + "github.com/absmach/magistrala/groups/events" + "github.com/absmach/magistrala/groups/middleware" + "github.com/absmach/magistrala/groups/postgres" + pgroups "github.com/absmach/magistrala/groups/private" + "github.com/absmach/magistrala/groups/tracing" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + mglog "github.com/absmach/magistrala/logger" + authsvcAuthn "github.com/absmach/magistrala/pkg/authn/authsvc" + mgauthz "github.com/absmach/magistrala/pkg/authz" + authsvcAuthz "github.com/absmach/magistrala/pkg/authz/authsvc" + "github.com/absmach/magistrala/pkg/grpcclient" + jaegerclient "github.com/absmach/magistrala/pkg/jaeger" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/policies/spicedb" + pg "github.com/absmach/magistrala/pkg/postgres" + pgclient "github.com/absmach/magistrala/pkg/postgres" + "github.com/absmach/magistrala/pkg/prometheus" + "github.com/absmach/magistrala/pkg/server" + grpcserver "github.com/absmach/magistrala/pkg/server/grpc" + httpserver "github.com/absmach/magistrala/pkg/server/http" + "github.com/absmach/magistrala/pkg/sid" + "github.com/absmach/magistrala/pkg/uuid" + "github.com/authzed/authzed-go/v1" + "github.com/authzed/grpcutil" + "github.com/caarlos0/env/v11" + "github.com/go-chi/chi/v5" + "github.com/jmoiron/sqlx" + "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/reflection" +) + +const ( + svcName = "groups" + envPrefixDB = "MG_GROUPS_DB_" + envPrefixHTTP = "MG_GROUPS_HTTP_" + envPrefixgRPC = "MG_GROUPS_GRPC_" + envPrefixAuth = "MG_AUTH_GRPC_" + envPrefixDomains = "MG_DOMAINS_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + defDB = "groups" + defSvcHTTPPort = "9004" + defSvcgRPCPort = "7004" +) + +type config struct { + LogLevel string `env:"MG_GROUPS_LOG_LEVEL" envDefault:"info"` + InstanceID string `env:"MG_GROUPS_INSTANCE_ID" envDefault:""` + JaegerURL url.URL `env:"MG_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` + SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` + ESURL string `env:"MG_ES_URL" envDefault:"nats://localhost:4222"` + TraceRatio float64 `env:"MG_JAEGER_TRACE_RATIO" envDefault:"1.0"` + SpicedbHost string `env:"MG_SPICEDB_HOST" envDefault:"localhost"` + SpicedbPort string `env:"MG_SPICEDB_PORT" envDefault:"50051"` + SpicedbPreSharedKey string `env:"MG_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + + cfg := config{} + if err := env.Parse(&cfg); err != nil { + log.Fatalf("failed to load %s configuration : %s", svcName, err.Error()) + } + + logger, err := mglog.New(os.Stdout, cfg.LogLevel) + if err != nil { + log.Fatalf("failed to init logger: %s", err.Error()) + } + + var exitCode int + defer mglog.ExitWithError(&exitCode) + + if cfg.InstanceID == "" { + if cfg.InstanceID, err = uuid.New().ID(); err != nil { + logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err)) + exitCode = 1 + return + } + } + + dbConfig := pgclient.Config{Name: defDB} + if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + gm, err := postgres.Migration() + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + db, err := pgclient.Setup(dbConfig, *gm) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer db.Close() + + tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) + if err != nil { + logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err)) + exitCode = 1 + return + } + defer func() { + if err := tp.Shutdown(ctx); err != nil { + logger.Error(fmt.Sprintf("error shutting down tracer provider: %v", err)) + } + }() + tracer := tp.Tracer(svcName) + + authClientConfig := grpcclient.Config{} + if err := env.ParseWithOptions(&authClientConfig, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) + exitCode = 1 + return + } + + authn, authnHandler, err := authsvcAuthn.NewAuthentication(ctx, authClientConfig) + if err != nil { + logger.Error("failed to create authn " + err.Error()) + exitCode = 1 + return + } + defer authnHandler.Close() + logger.Info("Authn successfully connected to auth gRPC server " + authnHandler.Secure()) + + authz, authzHandler, err := authsvcAuthz.NewAuthorization(ctx, authClientConfig) + if err != nil { + logger.Error("failed to create authz " + err.Error()) + exitCode = 1 + return + } + defer authzHandler.Close() + logger.Info("Authz successfully connected to auth gRPC server " + authzHandler.Secure()) + + policyService, err := newPolicyService(cfg, logger) + if err != nil { + logger.Error("failed to create new policies service " + err.Error()) + exitCode = 1 + return + } + logger.Info("Policy client successfully connected to spicedb gRPC server") + + chgrpcCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&chgrpcCfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) + exitCode = 1 + return + } + channelsClient, channelsHandler, err := grpcclient.SetupChannelsClient(ctx, chgrpcCfg) + if err != nil { + logger.Error(fmt.Sprintf("failed to connect to channels gRPC server: %s", err)) + exitCode = 1 + return + } + defer channelsHandler.Close() + logger.Info("Groups gRPC client successfully connected to channels gRPC server " + channelsHandler.Secure()) + + thgrpcCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&thgrpcCfg, env.Options{Prefix: envPrefixThings}); err != nil { + logger.Error(fmt.Sprintf("failed to load things gRPC client configuration : %s", err)) + exitCode = 1 + return + } + thingsClient, thingsHandler, err := grpcclient.SetupThingsClient(ctx, thgrpcCfg) + if err != nil { + logger.Error(fmt.Sprintf("failed to connect to things gRPC server: %s", err)) + exitCode = 1 + return + } + defer thingsHandler.Close() + logger.Info("Things gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + + svc, psvc, err := newService(ctx, authz, policyService, db, dbConfig, channelsClient, thingsClient, tracer, logger, cfg) + if err != nil { + logger.Error(fmt.Sprintf("failed to setup service: %s", err)) + exitCode = 1 + return + } + + httpServerConfig := server.Config{Port: defSvcHTTPPort} + if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err.Error())) + exitCode = 1 + return + } + + mux := chi.NewRouter() + httpSrv := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, mux, logger, cfg.InstanceID), logger) + + grpcServerConfig := server.Config{} + if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixgRPC}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s gRPC server configuration : %s", svcName, err)) + exitCode = 1 + return + } + + registerGroupsServer := func(srv *grpc.Server) { + reflection.Register(srv) + grpcGroupsV1.RegisterGroupsServiceServer(srv, grpcapi.NewServer(psvc)) + } + gs := grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerGroupsServer, logger) + + if cfg.SendTelemetry { + chc := chclient.New(svcName, magistrala.Version, logger, cancel) + go chc.CallHome(ctx) + } + + g.Go(func() error { + return gs.Start() + }) + + g.Go(func() error { + return httpSrv.Start() + }) + + g.Go(func() error { + return server.StopSignalHandler(ctx, cancel, logger, svcName, httpSrv) + }) + + if err := g.Wait(); err != nil { + logger.Error(fmt.Sprintf("groups service terminated: %s", err)) + } +} + +func newService(ctx context.Context, authz mgauthz.Authorization, policy policies.Service, db *sqlx.DB, dbConfig pgclient.Config, channels grpcChannelsV1.ChannelsServiceClient, things grpcThingsV1.ThingsServiceClient, tracer trace.Tracer, logger *slog.Logger, c config) (groups.Service, pgroups.Service, error) { + database := pg.NewDatabase(db, dbConfig, tracer) + idp := uuid.New() + sid, err := sid.New() + if err != nil { + return nil, nil, err + } + + // Creating groups service + repo := postgres.New(database) + svc, err := gpsvc.NewService(repo, policy, idp, channels, things, sid) + if err != nil { + return nil, nil, err + } + svc, err = events.New(ctx, svc, c.ESURL) + if err != nil { + return nil, nil, err + } + + svc, err = middleware.AuthorizationMiddleware(policies.GroupType, svc, authz, groups.NewOperationPermissionMap(), groups.NewRolesOperationPermissionMap()) + if err != nil { + return nil, nil, err + } + + svc = tracing.New(svc, tracer) + svc = middleware.LoggingMiddleware(svc, logger) + counter, latency := prometheus.MakeMetrics("groups", "api") + svc = middleware.MetricsMiddleware(svc, counter, latency) + + psvc := pgroups.New(repo) + return svc, psvc, err +} + +func newPolicyService(cfg config, logger *slog.Logger) (policies.Service, error) { + client, err := authzed.NewClientWithExperimentalAPIs( + fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey), + ) + if err != nil { + return nil, err + } + policySvc := spicedb.NewPolicyService(client, logger) + + return policySvc, nil +} diff --git a/cmd/http/main.go b/cmd/http/main.go index 4bf25efa9a..cec1f15a27 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -18,7 +18,11 @@ import ( "github.com/absmach/magistrala" adapter "github.com/absmach/magistrala/http" "github.com/absmach/magistrala/http/api" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" mglog "github.com/absmach/magistrala/logger" + mgauthn "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/authn/authsvc" "github.com/absmach/magistrala/pkg/grpcclient" jaegerclient "github.com/absmach/magistrala/pkg/jaeger" "github.com/absmach/magistrala/pkg/messaging" @@ -38,12 +42,14 @@ import ( ) const ( - svcName = "http_adapter" - envPrefix = "MG_HTTP_ADAPTER_" - envPrefixThings = "MG_THINGS_AUTH_GRPC_" - defSvcHTTPPort = "80" - targetHTTPPort = "81" - targetHTTPHost = "http://localhost" + svcName = "http_adapter" + envPrefix = "MG_HTTP_ADAPTER_" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + envPrefixAuth = "MG_AUTH_GRPC_" + defSvcHTTPPort = "80" + targetHTTPPort = "81" + targetHTTPHost = "http://localhost" ) type config struct { @@ -89,7 +95,7 @@ func main() { thingsClientCfg := grpcclient.Config{} if err := env.ParseWithOptions(&thingsClientCfg, env.Options{Prefix: envPrefixThings}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) + logger.Error(fmt.Sprintf("failed to load things gRPC client configuration : %s", err)) exitCode = 1 return } @@ -101,9 +107,40 @@ func main() { return } defer thingsHandler.Close() - logger.Info("Things service gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + channelsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&channelsClientCfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + channelsClient, channelsHandler, err := grpcclient.SetupChannelsClient(ctx, channelsClientCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer channelsHandler.Close() + logger.Info("Channels service gRPC client successfully connected to channels gRPC server " + channelsHandler.Secure()) + + authnCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&authnCfg, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + authn, authnHandler, err := authsvc.NewAuthentication(ctx, authnCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer authnHandler.Close() + logger.Info("authn successfully connected to auth gRPC server " + authnHandler.Secure()) + tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) if err != nil { logger.Error(fmt.Sprintf("Failed to init Jaeger: %s", err)) @@ -126,7 +163,7 @@ func main() { defer pub.Close() pub = brokerstracing.NewPublisher(httpServerConfig, tracer, pub) - svc := newService(pub, thingsClient, logger, tracer) + svc := newService(pub, authn, thingsClient, channelsClient, logger, tracer) targetServerCfg := server.Config{Port: targetHTTPPort} hs := httpserver.NewServer(ctx, cancel, svcName, targetServerCfg, api.MakeHandler(logger, cfg.InstanceID), logger) @@ -153,8 +190,8 @@ func main() { } } -func newService(pub messaging.Publisher, tc magistrala.ThingsServiceClient, logger *slog.Logger, tracer trace.Tracer) session.Handler { - svc := adapter.NewHandler(pub, logger, tc) +func newService(pub messaging.Publisher, authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient, logger *slog.Logger, tracer trace.Tracer) session.Handler { + svc := adapter.NewHandler(pub, authn, things, channels, logger) svc = handler.NewTracing(tracer, svc) svc = handler.LoggingMiddleware(svc, logger) counter, latency := prometheus.MakeMetrics(svcName, "api") diff --git a/cmd/invitations/main.go b/cmd/invitations/main.go index 8f79da3915..2e7b79df16 100644 --- a/cmd/invitations/main.go +++ b/cmd/invitations/main.go @@ -14,6 +14,7 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/invitations" "github.com/absmach/magistrala/invitations/api" "github.com/absmach/magistrala/invitations/middleware" @@ -175,7 +176,7 @@ func main() { } } -func newService(db *sqlx.DB, dbConfig clientspg.Config, authz mgauthz.Authorization, token magistrala.TokenServiceClient, tracer trace.Tracer, conf config, logger *slog.Logger) (invitations.Service, error) { +func newService(db *sqlx.DB, dbConfig clientspg.Config, authz mgauthz.Authorization, token grpcTokenV1.TokenServiceClient, tracer trace.Tracer, conf config, logger *slog.Logger) (invitations.Service, error) { database := postgres.NewDatabase(db, dbConfig, tracer) repo := invitationspg.NewRepository(database) diff --git a/cmd/mqtt/main.go b/cmd/mqtt/main.go index 1d226543d4..257e305e41 100644 --- a/cmd/mqtt/main.go +++ b/cmd/mqtt/main.go @@ -42,9 +42,10 @@ import ( ) const ( - svcName = "mqtt" - envPrefixThings = "MG_THINGS_AUTH_GRPC_" - wsPathPrefix = "/mqtt" + svcName = "mqtt" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + wsPathPrefix = "/mqtt" ) type config struct { @@ -179,10 +180,25 @@ func main() { return } defer thingsHandler.Close() - logger.Info("Things service gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) - h := mqtt.NewHandler(np, es, logger, thingsClient) + channelsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&channelsClientCfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + channelsClient, channelsHandler, err := grpcclient.SetupChannelsClient(ctx, channelsClientCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer channelsHandler.Close() + logger.Info("Channels service gRPC client successfully connected to channels gRPC server " + channelsHandler.Secure()) + + h := mqtt.NewHandler(np, es, logger, thingsClient, channelsClient) h = handler.NewTracing(tracer, h) if cfg.SendTelemetry { diff --git a/cmd/postgres-reader/main.go b/cmd/postgres-reader/main.go index d07f40a2af..02913357b2 100644 --- a/cmd/postgres-reader/main.go +++ b/cmd/postgres-reader/main.go @@ -14,7 +14,7 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" mglog "github.com/absmach/magistrala/logger" - "github.com/absmach/magistrala/pkg/authz/authsvc" + "github.com/absmach/magistrala/pkg/authn/authsvc" "github.com/absmach/magistrala/pkg/grpcclient" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" @@ -30,13 +30,14 @@ import ( ) const ( - svcName = "postgres-reader" - envPrefixDB = "MG_POSTGRES_" - envPrefixHTTP = "MG_POSTGRES_READER_HTTP_" - envPrefixAuth = "MG_AUTH_GRPC_" - envPrefixThings = "MG_THINGS_AUTH_GRPC_" - defDB = "magistrala" - defSvcHTTPPort = "9009" + svcName = "postgres-reader" + envPrefixDB = "MG_POSTGRES_" + envPrefixHTTP = "MG_POSTGRES_READER_HTTP_" + envPrefixAuth = "MG_AUTH_GRPC_" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + defDB = "magistrala" + defSvcHTTPPort = "9009" ) type config struct { @@ -84,38 +85,53 @@ func main() { } defer db.Close() - authzCfg := grpcclient.Config{} - if err := env.ParseWithOptions(&authzCfg, env.Options{Prefix: envPrefixAuth}); err != nil { - logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + thingsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&thingsClientCfg, env.Options{Prefix: envPrefixThings}); err != nil { + logger.Error(fmt.Sprintf("failed to load things gRPC client configuration : %s", err)) exitCode = 1 return } - authz, authzHandler, err := authsvc.NewAuthorization(ctx, authzCfg) + thingsClient, thingsHandler, err := grpcclient.SetupThingsClient(ctx, thingsClientCfg) if err != nil { logger.Error(err.Error()) exitCode = 1 return } - defer authzHandler.Close() - logger.Info("Authz successfully connected to auth gRPC server " + authzHandler.Secure()) + defer thingsHandler.Close() + logger.Info("Things service gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) - thingsClientCfg := grpcclient.Config{} - if err := env.ParseWithOptions(&thingsClientCfg, env.Options{Prefix: envPrefixThings}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) + channelsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&channelsClientCfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) exitCode = 1 return } - thingsClient, thingsHandler, err := grpcclient.SetupThingsClient(ctx, thingsClientCfg) + channelsClient, channelsHandler, err := grpcclient.SetupChannelsClient(ctx, channelsClientCfg) if err != nil { logger.Error(err.Error()) exitCode = 1 return } - defer thingsHandler.Close() + defer channelsHandler.Close() + logger.Info("Channels service gRPC client successfully connected to channels gRPC server " + channelsHandler.Secure()) - logger.Info("Things service gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + authnCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&authnCfg, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + authn, authnHandler, err := authsvc.NewAuthentication(ctx, authnCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer authnHandler.Close() + logger.Info("authn successfully connected to auth gRPC server " + authnHandler.Secure()) repo := newService(db, logger) @@ -125,7 +141,7 @@ func main() { exitCode = 1 return } - hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, api.MakeHandler(repo, authz, thingsClient, svcName, cfg.InstanceID), logger) + hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, api.MakeHandler(repo, authn, thingsClient, channelsClient, svcName, cfg.InstanceID), logger) if cfg.SendTelemetry { chc := chclient.New(svcName, magistrala.Version, logger, cancel) diff --git a/cmd/provision/main.go b/cmd/provision/main.go index 986f7acf74..ab727393b0 100644 --- a/cmd/provision/main.go +++ b/cmd/provision/main.go @@ -14,9 +14,9 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" + "github.com/absmach/magistrala/channels" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/errors" - mggroups "github.com/absmach/magistrala/pkg/groups" mgsdk "github.com/absmach/magistrala/pkg/sdk/go" "github.com/absmach/magistrala/pkg/server" httpserver "github.com/absmach/magistrala/pkg/server/http" @@ -138,7 +138,7 @@ func loadConfig() (provision.Config, error) { cfg.Bootstrap.Content = content // This is default conf for provision if there is no config file - cfg.Channels = []mggroups.Group{ + cfg.Channels = []channels.Channel{ { Name: "control-channel", Metadata: map[string]interface{}{"type": "control"}, diff --git a/cmd/things/main.go b/cmd/things/main.go index f29f05c474..9c8eda8776 100644 --- a/cmd/things/main.go +++ b/cmd/things/main.go @@ -16,35 +16,34 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" redisclient "github.com/absmach/magistrala/internal/clients/redis" - mggroups "github.com/absmach/magistrala/internal/groups" - gevents "github.com/absmach/magistrala/internal/groups/events" - gmiddleware "github.com/absmach/magistrala/internal/groups/middleware" - gpostgres "github.com/absmach/magistrala/internal/groups/postgres" - gtracing "github.com/absmach/magistrala/internal/groups/tracing" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" mglog "github.com/absmach/magistrala/logger" authsvcAuthn "github.com/absmach/magistrala/pkg/authn/authsvc" mgauthz "github.com/absmach/magistrala/pkg/authz" authsvcAuthz "github.com/absmach/magistrala/pkg/authz/authsvc" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/pkg/grpcclient" jaegerclient "github.com/absmach/magistrala/pkg/jaeger" "github.com/absmach/magistrala/pkg/policies" "github.com/absmach/magistrala/pkg/policies/spicedb" - "github.com/absmach/magistrala/pkg/postgres" + pg "github.com/absmach/magistrala/pkg/postgres" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" "github.com/absmach/magistrala/pkg/server" grpcserver "github.com/absmach/magistrala/pkg/server/grpc" httpserver "github.com/absmach/magistrala/pkg/server/http" + "github.com/absmach/magistrala/pkg/sid" "github.com/absmach/magistrala/pkg/uuid" "github.com/absmach/magistrala/things" grpcapi "github.com/absmach/magistrala/things/api/grpc" httpapi "github.com/absmach/magistrala/things/api/http" - thcache "github.com/absmach/magistrala/things/cache" - thevents "github.com/absmach/magistrala/things/events" - tmiddleware "github.com/absmach/magistrala/things/middleware" - thingspg "github.com/absmach/magistrala/things/postgres" - ctracing "github.com/absmach/magistrala/things/tracing" + "github.com/absmach/magistrala/things/cache" + "github.com/absmach/magistrala/things/events" + "github.com/absmach/magistrala/things/middleware" + "github.com/absmach/magistrala/things/postgres" + pThings "github.com/absmach/magistrala/things/private" + "github.com/absmach/magistrala/things/tracing" "github.com/authzed/authzed-go/v1" "github.com/authzed/grpcutil" "github.com/caarlos0/env/v11" @@ -64,11 +63,11 @@ const ( envPrefixHTTP = "MG_THINGS_HTTP_" envPrefixGRPC = "MG_THINGS_AUTH_GRPC_" envPrefixAuth = "MG_AUTH_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + envPrefixGroups = "MG_GROUPS_GRPC_" defDB = "things" defSvcHTTPPort = "9000" defSvcAuthGRPCPort = "7000" - - streamID = "magistrala.things" ) type config struct { @@ -121,9 +120,12 @@ func main() { exitCode = 1 return } - tm := thingspg.Migration() - gm := gpostgres.Migration() - tm.Migrations = append(tm.Migrations, gm.Migrations...) + tm, err := postgres.Migration() + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } db, err := pgclient.Setup(dbConfig, *tm) if err != nil { logger.Error(err.Error()) @@ -186,7 +188,37 @@ func main() { defer authzClient.Close() logger.Info("AuthZ successfully connected to auth gRPC server " + authnClient.Secure()) - csvc, gsvc, err := newService(ctx, db, dbConfig, authz, policyEvaluator, policyService, cacheclient, cfg.CacheKeyDuration, cfg.ESURL, tracer, logger) + chgrpccfg := grpcclient.Config{} + if err := env.ParseWithOptions(&chgrpccfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) + exitCode = 1 + return + } + channelsgRPC, channelsClient, err := grpcclient.SetupChannelsClient(ctx, chgrpccfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + logger.Info("Channels gRPC client successfully connected to channels gRPC server " + channelsClient.Secure()) + defer channelsClient.Close() + + groupsgRPCCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&groupsgRPCCfg, env.Options{Prefix: envPrefixGroups}); err != nil { + logger.Error(fmt.Sprintf("failed to load groups gRPC client configuration : %s", err)) + exitCode = 1 + return + } + groupsClient, groupsHandler, err := grpcclient.SetupGroupsClient(ctx, groupsgRPCCfg) + if err != nil { + logger.Error(fmt.Sprintf("failed to connect to groups gRPC server: %s", err)) + exitCode = 1 + return + } + defer groupsHandler.Close() + logger.Info("Groups gRPC client successfully connected to groups gRPC server " + groupsHandler.Secure()) + + svc, psvc, err := newService(ctx, db, dbConfig, authz, policyEvaluator, policyService, cacheclient, cfg.CacheKeyDuration, cfg.ESURL, channelsgRPC, groupsClient, tracer, logger) if err != nil { logger.Error(fmt.Sprintf("failed to create services: %s", err)) exitCode = 1 @@ -200,7 +232,7 @@ func main() { return } mux := chi.NewRouter() - httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(csvc, gsvc, authn, mux, logger, cfg.InstanceID), logger) + httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, mux, logger, cfg.InstanceID), logger) grpcServerConfig := server.Config{Port: defSvcAuthGRPCPort} if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGRPC}); err != nil { @@ -208,9 +240,10 @@ func main() { exitCode = 1 return } + registerThingsServer := func(srv *grpc.Server) { reflection.Register(srv) - magistrala.RegisterThingsServiceServer(srv, grpcapi.NewServer(csvc)) + grpcThingsV1.RegisterThingsServiceServer(srv, grpcapi.NewServer(psvc)) } gs := grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerThingsServer, logger) @@ -237,42 +270,44 @@ func main() { } } -func newService(ctx context.Context, db *sqlx.DB, dbConfig pgclient.Config, authz mgauthz.Authorization, pe policies.Evaluator, ps policies.Service, cacheClient *redis.Client, keyDuration time.Duration, esURL string, tracer trace.Tracer, logger *slog.Logger) (things.Service, groups.Service, error) { - database := postgres.NewDatabase(db, dbConfig, tracer) - cRepo := thingspg.NewRepository(database) - gRepo := gpostgres.New(database) +func newService(ctx context.Context, db *sqlx.DB, dbConfig pgclient.Config, authz mgauthz.Authorization, pe policies.Evaluator, ps policies.Service, cacheClient *redis.Client, keyDuration time.Duration, esURL string, channels grpcChannelsV1.ChannelsServiceClient, groups grpcGroupsV1.GroupsServiceClient, tracer trace.Tracer, logger *slog.Logger) (things.Service, pThings.Service, error) { + database := pg.NewDatabase(db, dbConfig, tracer) + repo := postgres.NewRepository(database) idp := uuid.New() + sidp, err := sid.New() + if err != nil { + return nil, nil, err + } - thingCache := thcache.NewCache(cacheClient, keyDuration) - - csvc := things.NewService(pe, ps, cRepo, thingCache, idp) - gsvc := mggroups.NewService(gRepo, idp, ps) + // Things service + cache := cache.NewCache(cacheClient, keyDuration) - csvc, err := thevents.NewEventStoreMiddleware(ctx, csvc, esURL) + tsvc, err := things.NewService(repo, ps, cache, channels, groups, idp, sidp) if err != nil { return nil, nil, err } - gsvc, err = gevents.NewEventStoreMiddleware(ctx, gsvc, esURL, streamID) + tsvc, err = events.NewEventStoreMiddleware(ctx, tsvc, esURL) if err != nil { return nil, nil, err } - csvc = tmiddleware.AuthorizationMiddleware(csvc, authz) - gsvc = gmiddleware.AuthorizationMiddleware(gsvc, authz) + tsvc = tracing.New(tsvc, tracer) - csvc = ctracing.New(csvc, tracer) - csvc = tmiddleware.LoggingMiddleware(csvc, logger) counter, latency := prometheus.MakeMetrics(svcName, "api") - csvc = tmiddleware.MetricsMiddleware(csvc, counter, latency) + tsvc = middleware.MetricsMiddleware(tsvc, counter, latency) + tsvc = middleware.MetricsMiddleware(tsvc, counter, latency) + + tsvc, err = middleware.AuthorizationMiddleware(policies.ThingType, tsvc, authz, repo, things.NewOperationPermissionMap(), things.NewRolesOperationPermissionMap(), things.NewExternalOperationPermissionMap()) + if err != nil { + return nil, nil, err + } + tsvc = middleware.LoggingMiddleware(tsvc, logger) - gsvc = gtracing.New(gsvc, tracer) - gsvc = gmiddleware.LoggingMiddleware(gsvc, logger) - counter, latency = prometheus.MakeMetrics(fmt.Sprintf("%s_groups", svcName), "api") - gsvc = gmiddleware.MetricsMiddleware(gsvc, counter, latency) + isvc := pThings.New(repo, cache, pe, ps) - return csvc, gsvc, err + return tsvc, isvc, err } func newSpiceDBPolicyServiceEvaluator(cfg config, logger *slog.Logger) (policies.Evaluator, policies.Service, error) { diff --git a/cmd/timescale-reader/main.go b/cmd/timescale-reader/main.go index ba6dde416a..b01a11d4b2 100644 --- a/cmd/timescale-reader/main.go +++ b/cmd/timescale-reader/main.go @@ -14,7 +14,7 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" mglog "github.com/absmach/magistrala/logger" - "github.com/absmach/magistrala/pkg/authz/authsvc" + "github.com/absmach/magistrala/pkg/authn/authsvc" "github.com/absmach/magistrala/pkg/grpcclient" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" @@ -30,13 +30,14 @@ import ( ) const ( - svcName = "timescaledb-reader" - envPrefixDB = "MG_TIMESCALE_" - envPrefixHTTP = "MG_TIMESCALE_READER_HTTP_" - envPrefixAuth = "MG_AUTH_GRPC_" - envPrefixThings = "MG_THINGS_AUTH_GRPC_" - defDB = "messages" - defSvcHTTPPort = "9011" + svcName = "timescaledb-reader" + envPrefixDB = "MG_TIMESCALE_" + envPrefixHTTP = "MG_TIMESCALE_READER_HTTP_" + envPrefixAuth = "MG_AUTH_GRPC_" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + defDB = "messages" + defSvcHTTPPort = "9011" ) type config struct { @@ -84,38 +85,54 @@ func main() { repo := newService(db, logger) - authzCfg := grpcclient.Config{} - if err := env.ParseWithOptions(&authzCfg, env.Options{Prefix: envPrefixAuth}); err != nil { - logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + thingsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&thingsClientCfg, env.Options{Prefix: envPrefixThings}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) exitCode = 1 return } - authz, authzHandler, err := authsvc.NewAuthorization(ctx, authzCfg) + thingsClient, thingsHandler, err := grpcclient.SetupThingsClient(ctx, thingsClientCfg) if err != nil { logger.Error(err.Error()) exitCode = 1 return } - defer authzHandler.Close() - logger.Info("Authz successfully connected to auth gRPC server " + authzHandler.Secure()) + defer thingsHandler.Close() - thingsClientCfg := grpcclient.Config{} - if err := env.ParseWithOptions(&thingsClientCfg, env.Options{Prefix: envPrefixThings}); err != nil { - logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) + logger.Info("ThingsService gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + + channelsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&channelsClientCfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) exitCode = 1 return } - thingsClient, thingsHandler, err := grpcclient.SetupThingsClient(ctx, thingsClientCfg) + channelsClient, channelsHandler, err := grpcclient.SetupChannelsClient(ctx, channelsClientCfg) if err != nil { logger.Error(err.Error()) exitCode = 1 return } - defer thingsHandler.Close() + defer channelsHandler.Close() + logger.Info("Channels service gRPC client successfully connected to channels gRPC server " + channelsHandler.Secure()) - logger.Info("ThingsService gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + authnCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&authnCfg, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + authn, authnHandler, err := authsvc.NewAuthentication(ctx, authnCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer authnHandler.Close() + logger.Info("authn successfully connected to auth gRPC server " + authnHandler.Secure()) httpServerConfig := server.Config{Port: defSvcHTTPPort} if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil { @@ -123,7 +140,7 @@ func main() { exitCode = 1 return } - hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, api.MakeHandler(repo, authz, thingsClient, svcName, cfg.InstanceID), logger) + hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, api.MakeHandler(repo, authn, thingsClient, channelsClient, svcName, cfg.InstanceID), logger) if cfg.SendTelemetry { chc := chclient.New(svcName, magistrala.Version, logger, cancel) diff --git a/cmd/users/main.go b/cmd/users/main.go index a7e432127e..337e3bc451 100644 --- a/cmd/users/main.go +++ b/cmd/users/main.go @@ -17,36 +17,32 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" "github.com/absmach/magistrala/internal/email" - mggroups "github.com/absmach/magistrala/internal/groups" - gevents "github.com/absmach/magistrala/internal/groups/events" - gmiddleware "github.com/absmach/magistrala/internal/groups/middleware" - gpostgres "github.com/absmach/magistrala/internal/groups/postgres" - gtracing "github.com/absmach/magistrala/internal/groups/tracing" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" mglog "github.com/absmach/magistrala/logger" authsvcAuthn "github.com/absmach/magistrala/pkg/authn/authsvc" mgauthz "github.com/absmach/magistrala/pkg/authz" authsvcAuthz "github.com/absmach/magistrala/pkg/authz/authsvc" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/pkg/grpcclient" jaegerclient "github.com/absmach/magistrala/pkg/jaeger" "github.com/absmach/magistrala/pkg/oauth2" googleoauth "github.com/absmach/magistrala/pkg/oauth2/google" "github.com/absmach/magistrala/pkg/policies" "github.com/absmach/magistrala/pkg/policies/spicedb" - "github.com/absmach/magistrala/pkg/postgres" + pg "github.com/absmach/magistrala/pkg/postgres" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/pkg/prometheus" "github.com/absmach/magistrala/pkg/server" httpserver "github.com/absmach/magistrala/pkg/server/http" "github.com/absmach/magistrala/pkg/uuid" "github.com/absmach/magistrala/users" - capi "github.com/absmach/magistrala/users/api" + "github.com/absmach/magistrala/users/api" "github.com/absmach/magistrala/users/emailer" - uevents "github.com/absmach/magistrala/users/events" + "github.com/absmach/magistrala/users/events" "github.com/absmach/magistrala/users/hasher" - cmiddleware "github.com/absmach/magistrala/users/middleware" - clientspg "github.com/absmach/magistrala/users/postgres" - ctracing "github.com/absmach/magistrala/users/tracing" + "github.com/absmach/magistrala/users/middleware" + "github.com/absmach/magistrala/users/postgres" + "github.com/absmach/magistrala/users/tracing" "github.com/authzed/authzed-go/v1" "github.com/authzed/grpcutil" "github.com/caarlos0/env/v11" @@ -59,15 +55,14 @@ import ( ) const ( - svcName = "users" - envPrefixDB = "MG_USERS_DB_" - envPrefixHTTP = "MG_USERS_HTTP_" - envPrefixAuth = "MG_AUTH_GRPC_" - envPrefixGoogle = "MG_GOOGLE_" - defDB = "users" - defSvcHTTPPort = "9002" - - streamID = "magistrala.users" + svcName = "users" + envPrefixDB = "MG_USERS_DB_" + envPrefixHTTP = "MG_USERS_HTTP_" + envPrefixAuth = "MG_AUTH_GRPC_" + envPrefixDomains = "MG_DOMAINS_GRPC_" + envPrefixGoogle = "MG_GOOGLE_" + defDB = "users" + defSvcHTTPPort = "9002" ) type config struct { @@ -138,10 +133,9 @@ func main() { exitCode = 1 return } - cm := clientspg.Migration() - gm := gpostgres.Migration() - cm.Migrations = append(cm.Migrations, gm.Migrations...) - db, err := pgclient.Setup(dbConfig, *cm) + + migration := postgres.Migration() + db, err := pgclient.Setup(dbConfig, *migration) if err != nil { logger.Error(err.Error()) exitCode = 1 @@ -162,58 +156,65 @@ func main() { }() tracer := tp.Tracer(svcName) - clientConfig := grpcclient.Config{} - if err := env.ParseWithOptions(&clientConfig, env.Options{Prefix: envPrefixAuth}); err != nil { + authClientConfig := grpcclient.Config{} + if err := env.ParseWithOptions(&authClientConfig, env.Options{Prefix: envPrefixAuth}); err != nil { logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) exitCode = 1 return } - tokenClient, tokenHandler, err := grpcclient.SetupTokenClient(ctx, clientConfig) + tokenClient, tokenHandler, err := grpcclient.SetupTokenClient(ctx, authClientConfig) if err != nil { - logger.Error(err.Error()) + logger.Error("failed to create token gRPC client " + err.Error()) exitCode = 1 return } defer tokenHandler.Close() logger.Info("Token service client successfully connected to auth gRPC server " + tokenHandler.Secure()) - authn, authnHandler, err := authsvcAuthn.NewAuthentication(ctx, clientConfig) + authn, authnHandler, err := authsvcAuthn.NewAuthentication(ctx, authClientConfig) if err != nil { - logger.Error(err.Error()) + logger.Error("failed to create authn " + err.Error()) exitCode = 1 return } defer authnHandler.Close() logger.Info("Authn successfully connected to auth gRPC server " + authnHandler.Secure()) - authz, authzHandler, err := authsvcAuthz.NewAuthorization(ctx, clientConfig) + authz, authzHandler, err := authsvcAuthz.NewAuthorization(ctx, authClientConfig) if err != nil { - logger.Error(err.Error()) + logger.Error("failed to create authz " + err.Error()) exitCode = 1 return } defer authzHandler.Close() logger.Info("Authz successfully connected to auth gRPC server " + authzHandler.Secure()) - domainsClient, domainsHandler, err := grpcclient.SetupDomainsClient(ctx, clientConfig) + domainsClientConfig := grpcclient.Config{} + if err := env.ParseWithOptions(&domainsClientConfig, env.Options{Prefix: envPrefixDomains}); err != nil { + logger.Error(fmt.Sprintf("failed to load %s auth configuration : %s", svcName, err)) + exitCode = 1 + return + } + + domainsClient, domainsHandler, err := grpcclient.SetupDomainsClient(ctx, domainsClientConfig) if err != nil { - logger.Error(err.Error()) + logger.Error("failed to setup domain gRPC clients " + err.Error()) exitCode = 1 return } defer domainsHandler.Close() - logger.Info("DomainsService gRPC client successfully connected to auth gRPC server " + domainsHandler.Secure()) + logger.Info("DomainsService gRPC client successfully connected to domains gRPC server " + domainsHandler.Secure()) policyService, err := newPolicyService(cfg, logger) if err != nil { - logger.Error(err.Error()) + logger.Error("failed to create new policies service " + err.Error()) exitCode = 1 return } logger.Info("Policy client successfully connected to spicedb gRPC server") - csvc, gsvc, err := newService(ctx, authz, tokenClient, policyService, domainsClient, db, dbConfig, tracer, cfg, ec, logger) + csvc, err := newService(ctx, authz, tokenClient, policyService, domainsClient, db, dbConfig, tracer, cfg, ec, logger) if err != nil { logger.Error(fmt.Sprintf("failed to setup service: %s", err)) exitCode = 1 @@ -236,7 +237,7 @@ func main() { oauthProvider := googleoauth.NewProvider(oauthConfig, cfg.OAuthUIRedirectURL, cfg.OAuthUIErrorURL) mux := chi.NewRouter() - httpSrv := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, capi.MakeHandler(csvc, authn, tokenClient, cfg.SelfRegister, gsvc, mux, logger, cfg.InstanceID, cfg.PassRegex, oauthProvider), logger) + httpSrv := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, api.MakeHandler(csvc, authn, tokenClient, cfg.SelfRegister, mux, logger, cfg.InstanceID, cfg.PassRegex, oauthProvider), logger) if cfg.SendTelemetry { chc := chclient.New(svcName, magistrala.Version, logger, cancel) @@ -256,59 +257,46 @@ func main() { } } -func newService(ctx context.Context, authz mgauthz.Authorization, token magistrala.TokenServiceClient, policyService policies.Service, domainsClient magistrala.DomainsServiceClient, db *sqlx.DB, dbConfig pgclient.Config, tracer trace.Tracer, c config, ec email.Config, logger *slog.Logger) (users.Service, groups.Service, error) { - database := postgres.NewDatabase(db, dbConfig, tracer) - - cRepo := clientspg.NewRepository(database) - gRepo := gpostgres.New(database) +func newService(ctx context.Context, authz mgauthz.Authorization, token grpcTokenV1.TokenServiceClient, policyService policies.Service, domainsClient grpcDomainsV1.DomainsServiceClient, db *sqlx.DB, dbConfig pgclient.Config, tracer trace.Tracer, c config, ec email.Config, logger *slog.Logger) (users.Service, error) { + database := pg.NewDatabase(db, dbConfig, tracer) idp := uuid.New() hsr := hasher.New() + // Creating users service + repo := postgres.NewRepository(database) emailerClient, err := emailer.New(c.ResetURL, &ec) if err != nil { logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error())) } - csvc := users.NewService(token, cRepo, policyService, emailerClient, hsr, idp) - gsvc := mggroups.NewService(gRepo, idp, policyService) + svc := users.NewService(token, repo, policyService, emailerClient, hsr, idp) - csvc, err = uevents.NewEventStoreMiddleware(ctx, csvc, c.ESURL) - if err != nil { - return nil, nil, err - } - gsvc, err = gevents.NewEventStoreMiddleware(ctx, gsvc, c.ESURL, streamID) + svc, err = events.NewEventStoreMiddleware(ctx, svc, c.ESURL) if err != nil { - return nil, nil, err + return nil, err } + svc = middleware.AuthorizationMiddleware(svc, authz, c.SelfRegister) - csvc = cmiddleware.AuthorizationMiddleware(csvc, authz, c.SelfRegister) - gsvc = gmiddleware.AuthorizationMiddleware(gsvc, authz) - - csvc = ctracing.New(csvc, tracer) - csvc = cmiddleware.LoggingMiddleware(csvc, logger) + svc = tracing.New(svc, tracer) + svc = middleware.LoggingMiddleware(svc, logger) counter, latency := prometheus.MakeMetrics(svcName, "api") - csvc = cmiddleware.MetricsMiddleware(csvc, counter, latency) + svc = middleware.MetricsMiddleware(svc, counter, latency) - gsvc = gtracing.New(gsvc, tracer) - gsvc = gmiddleware.LoggingMiddleware(gsvc, logger) - counter, latency = prometheus.MakeMetrics("groups", "api") - gsvc = gmiddleware.MetricsMiddleware(gsvc, counter, latency) - - userID, err := createAdmin(ctx, c, cRepo, hsr, csvc) + userID, err := createAdmin(ctx, c, repo, hsr, svc) if err != nil { logger.Error(fmt.Sprintf("failed to create admin client: %s", err)) } if err := createAdminPolicy(ctx, userID, authz, policyService); err != nil { - return nil, nil, err + return nil, err } - users.NewDeleteHandler(ctx, cRepo, policyService, domainsClient, c.DeleteInterval, c.DeleteAfter, logger) + users.NewDeleteHandler(ctx, repo, policyService, domainsClient, c.DeleteInterval, c.DeleteAfter, logger) - return csvc, gsvc, err + return svc, err } -func createAdmin(ctx context.Context, c config, urepo users.Repository, hsr users.Hasher, svc users.Service) (string, error) { +func createAdmin(ctx context.Context, c config, repo users.Repository, hsr users.Hasher, svc users.Service) (string, error) { id, err := uuid.New().ID() if err != nil { return "", err @@ -336,12 +324,12 @@ func createAdmin(ctx context.Context, c config, urepo users.Repository, hsr user Status: users.EnabledStatus, } - if u, err := urepo.RetrieveByEmail(ctx, user.Email); err == nil { + if u, err := repo.RetrieveByEmail(ctx, user.Email); err == nil { return u.ID, nil } // Create an admin - if _, err = urepo.Save(ctx, user); err != nil { + if _, err = repo.Save(ctx, user); err != nil { return "", err } if _, err = svc.IssueToken(ctx, c.AdminUsername, c.AdminPassword); err != nil { diff --git a/cmd/ws/main.go b/cmd/ws/main.go index a2f1e57d59..248fd522ce 100644 --- a/cmd/ws/main.go +++ b/cmd/ws/main.go @@ -14,7 +14,10 @@ import ( chclient "github.com/absmach/callhome/pkg/client" "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" mglog "github.com/absmach/magistrala/logger" + "github.com/absmach/magistrala/pkg/authn/authsvc" "github.com/absmach/magistrala/pkg/grpcclient" jaegerclient "github.com/absmach/magistrala/pkg/jaeger" "github.com/absmach/magistrala/pkg/messaging" @@ -35,12 +38,14 @@ import ( ) const ( - svcName = "ws-adapter" - envPrefixHTTP = "MG_WS_ADAPTER_HTTP_" - envPrefixThings = "MG_THINGS_AUTH_GRPC_" - defSvcHTTPPort = "8190" - targetWSPort = "8191" - targetWSHost = "localhost" + svcName = "ws-adapter" + envPrefixHTTP = "MG_WS_ADAPTER_HTTP_" + envPrefixThings = "MG_THINGS_AUTH_GRPC_" + envPrefixChannels = "MG_CHANNELS_GRPC_" + envPrefixAuth = "MG_AUTH_GRPC_" + defSvcHTTPPort = "8190" + targetWSPort = "8191" + targetWSHost = "localhost" ) type config struct { @@ -106,6 +111,38 @@ func main() { logger.Info("Things service gRPC client successfully connected to things gRPC server " + thingsHandler.Secure()) + channelsClientCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&channelsClientCfg, env.Options{Prefix: envPrefixChannels}); err != nil { + logger.Error(fmt.Sprintf("failed to load channels gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + channelsClient, channelsHandler, err := grpcclient.SetupChannelsClient(ctx, channelsClientCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer channelsHandler.Close() + logger.Info("Channels service gRPC client successfully connected to channels gRPC server " + channelsHandler.Secure()) + + authnCfg := grpcclient.Config{} + if err := env.ParseWithOptions(&authnCfg, env.Options{Prefix: envPrefixAuth}); err != nil { + logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err)) + exitCode = 1 + return + } + + authn, authnHandler, err := authsvc.NewAuthentication(ctx, authnCfg) + if err != nil { + logger.Error(err.Error()) + exitCode = 1 + return + } + defer authnHandler.Close() + logger.Info("authn successfully connected to auth gRPC server " + authnHandler.Secure()) + tp, err := jaegerclient.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio) if err != nil { logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err)) @@ -128,7 +165,7 @@ func main() { defer nps.Close() nps = brokerstracing.NewPubSub(targetServerConfig, tracer, nps) - svc := newService(thingsClient, nps, logger, tracer) + svc := newService(thingsClient, channelsClient, nps, logger, tracer) hs := httpserver.NewServer(ctx, cancel, svcName, targetServerConfig, api.MakeHandler(ctx, svc, logger, cfg.InstanceID), logger) @@ -141,7 +178,7 @@ func main() { g.Go(func() error { return hs.Start() }) - handler := ws.NewHandler(nps, logger, thingsClient) + handler := ws.NewHandler(nps, logger, authn, thingsClient, channelsClient) return proxyWS(ctx, httpServerConfig, targetServerConfig, logger, handler) }) @@ -154,8 +191,8 @@ func main() { } } -func newService(thingsClient magistrala.ThingsServiceClient, nps messaging.PubSub, logger *slog.Logger, tracer trace.Tracer) ws.Service { - svc := ws.New(thingsClient, nps) +func newService(thingsClient grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient, nps messaging.PubSub, logger *slog.Logger, tracer trace.Tracer) ws.Service { + svc := ws.New(thingsClient, channels, nps) svc = tracing.New(tracer, svc) svc = api.LoggingMiddleware(svc, logger) counter, latency := prometheus.MakeMetrics("ws_adapter", "api") diff --git a/coap/adapter.go b/coap/adapter.go index 92c0fc01e4..fc23c6ee84 100644 --- a/coap/adapter.go +++ b/coap/adapter.go @@ -10,7 +10,8 @@ import ( "context" "fmt" - "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" @@ -37,51 +38,78 @@ var _ Service = (*adapterService)(nil) // Observers is a map of maps,. type adapterService struct { - things magistrala.ThingsServiceClient - pubsub messaging.PubSub + things grpcThingsV1.ThingsServiceClient + channels grpcChannelsV1.ChannelsServiceClient + pubsub messaging.PubSub } // New instantiates the CoAP adapter implementation. -func New(thingsClient magistrala.ThingsServiceClient, pubsub messaging.PubSub) Service { +func New(things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient, pubsub messaging.PubSub) Service { as := &adapterService{ - things: thingsClient, - pubsub: pubsub, + things: things, + channels: channels, + pubsub: pubsub, } return as } func (svc *adapterService) Publish(ctx context.Context, key string, msg *messaging.Message) error { - ar := &magistrala.ThingsAuthzReq{ - Permission: policies.PublishPermission, - ThingKey: key, - ChannelID: msg.GetChannel(), + + authnRes, err := svc.things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ + ThingKey: key, + }) + if err != nil { + return errors.Wrap(svcerr.ErrAuthentication, err) } - res, err := svc.things.Authorize(ctx, ar) + if !authnRes.Authenticated { + return svcerr.ErrAuthentication + } + + authzRes, err := svc.channels.Authorize(ctx, &grpcChannelsV1.AuthzReq{ + ClientId: authnRes.GetId(), + ClientType: policies.ThingType, + Permission: policies.PublishPermission, + ChannelId: msg.GetChannel(), + }) + if err != nil { return errors.Wrap(svcerr.ErrAuthorization, err) } - if !res.GetAuthorized() { + if !authzRes.Authorized { return svcerr.ErrAuthorization } - msg.Publisher = res.GetId() + + msg.Publisher = authnRes.GetId() return svc.pubsub.Publish(ctx, msg.GetChannel(), msg) } func (svc *adapterService) Subscribe(ctx context.Context, key, chanID, subtopic string, c Client) error { - ar := &magistrala.ThingsAuthzReq{ - Permission: policies.SubscribePermission, - ThingKey: key, - ChannelID: chanID, + authnRes, err := svc.things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ + ThingKey: key, + }) + if err != nil { + return errors.Wrap(svcerr.ErrAuthentication, err) + } + if !authnRes.Authenticated { + return svcerr.ErrAuthentication } - res, err := svc.things.Authorize(ctx, ar) + + authzRes, err := svc.channels.Authorize(ctx, &grpcChannelsV1.AuthzReq{ + ClientId: authnRes.GetId(), + ClientType: policies.ThingType, + Permission: policies.SubscribePermission, + ChannelId: chanID, + }) + if err != nil { return errors.Wrap(svcerr.ErrAuthorization, err) } - if !res.GetAuthorized() { + if !authzRes.Authorized { return svcerr.ErrAuthorization } + subject := fmt.Sprintf("%s.%s", chansPrefix, chanID) if subtopic != "" { subject = fmt.Sprintf("%s.%s", subject, subtopic) @@ -95,18 +123,31 @@ func (svc *adapterService) Subscribe(ctx context.Context, key, chanID, subtopic } func (svc *adapterService) Unsubscribe(ctx context.Context, key, chanID, subtopic, token string) error { - ar := &magistrala.ThingsAuthzReq{ - Permission: policies.SubscribePermission, - ThingKey: key, - ChannelID: chanID, + authnRes, err := svc.things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ + ThingKey: key, + }) + if err != nil { + return errors.Wrap(svcerr.ErrAuthentication, err) } - res, err := svc.things.Authorize(ctx, ar) + if !authnRes.Authenticated { + return svcerr.ErrAuthentication + } + + authzRes, err := svc.channels.Authorize(ctx, &grpcChannelsV1.AuthzReq{ + DomainId: "", + ClientId: authnRes.GetId(), + ClientType: policies.ThingType, + Permission: policies.SubscribePermission, + ChannelId: chanID, + }) + if err != nil { return errors.Wrap(svcerr.ErrAuthorization, err) } - if !res.GetAuthorized() { + if !authzRes.Authorized { return svcerr.ErrAuthorization } + subject := fmt.Sprintf("%s.%s", chansPrefix, chanID) if subtopic != "" { subject = fmt.Sprintf("%s.%s", subject, subtopic) diff --git a/docker/.env b/docker/.env index 305d2c062d..022e45b394 100644 --- a/docker/.env +++ b/docker/.env @@ -76,11 +76,11 @@ MG_POSTGRES_MAX_CONNECTIONS=100 ### Auth MG_AUTH_LOG_LEVEL=debug MG_AUTH_HTTP_HOST=auth -MG_AUTH_HTTP_PORT=8189 +MG_AUTH_HTTP_PORT=9001 MG_AUTH_HTTP_SERVER_CERT= MG_AUTH_HTTP_SERVER_KEY= MG_AUTH_GRPC_HOST=auth -MG_AUTH_GRPC_PORT=8181 +MG_AUTH_GRPC_PORT=7001 MG_AUTH_GRPC_SERVER_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-server.crt}${GRPC_TLS:+./ssl/certs/auth-grpc-server.crt} MG_AUTH_GRPC_SERVER_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-server.key}${GRPC_TLS:+./ssl/certs/auth-grpc-server.key} MG_AUTH_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}${GRPC_TLS:+./ssl/certs/ca.crt} @@ -99,15 +99,41 @@ MG_AUTH_REFRESH_TOKEN_DURATION="24h" MG_AUTH_INVITATION_DURATION="168h" MG_AUTH_ADAPTER_INSTANCE_ID= -#### Auth GRPC Client Config -MG_AUTH_GRPC_URL=auth:8181 +#### Auth Client Config +MG_AUTH_URL=auth:9001 +MG_AUTH_GRPC_URL=auth:7001 MG_AUTH_GRPC_TIMEOUT=300s MG_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt} MG_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key} MG_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} +### Domains +MG_DOMAINS_LOG_LEVEL=debug +MG_DOMAINS_HTTP_HOST=domains +MG_DOMAINS_HTTP_PORT=9003 +MG_DOMAINS_HTTP_SERVER_KEY= +MG_DOMAINS_HTTP_SERVER_CERT= +MG_DOMAINS_GRPC_HOST=domains +MG_DOMAINS_GRPC_PORT=7003 +MG_DOMAINS_DB_HOST=domains-db +MG_DOMAINS_DB_PORT=5432 +MG_DOMAINS_DB_NAME=domains +MG_DOMAINS_DB_USER=magistrala +MG_DOMAINS_DB_PASS=magistrala +MG_DOMAINS_DB_SSL_MODE= +MG_DOMAINS_DB_SSL_KEY= +MG_DOMAINS_DB_SSL_CERT= +MG_DOMAINS_DB_SSL_ROOT_CERT= +MG_DOMAINS_INSTANCE_ID= + #### Domains Client Config -MG_DOMAINS_URL=http://auth:8189 +MG_DOMAINS_URL=http://domains:9003 +MG_DOMAINS_GRPC_URL=domains:7003 +MG_DOMAINS_GRPC_TIMEOUT=300s +MG_DOMAINS_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt} +MG_DOMAINS_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key} +MG_DOMAINS_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} + ### SpiceDB Datastore config MG_SPICEDB_DB_USER=magistrala @@ -144,10 +170,10 @@ MG_UI_LOG_LEVEL=debug MG_UI_PORT=9095 MG_HTTP_ADAPTER_URL=http://http-adapter:8008 MG_READER_URL=http://timescale-reader:9011 -MG_THINGS_URL=http://things:9000 +MG_THINGS_URL=http://things:9006 MG_USERS_URL=http://users:9002 MG_INVITATIONS_URL=http://invitations:9020 -MG_DOMAINS_URL=http://auth:8189 +MG_DOMAINS_URL=http://domains:9003 MG_BOOTSTRAP_URL=http://bootstrap:9013 MG_UI_HOST_URL=http://localhost:9095 MG_UI_VERIFICATION_TLS=false @@ -193,12 +219,22 @@ MG_USERS_DB_SSL_KEY= MG_USERS_DB_SSL_ROOT_CERT= MG_USERS_RESET_PWD_TEMPLATE=users.tmpl MG_USERS_INSTANCE_ID= +MG_USERS_SECRET_KEY=HyE2D4RUt9nnKG6v8zKEqAp6g6ka8hhZsqUpzgKvnwpXrNVQSH +MG_USERS_ADMIN_EMAIL=admin@example.com +MG_USERS_ADMIN_PASSWORD=12345678 +MG_USERS_PASS_REGEX=^.{8,}$ +MG_USERS_ACCESS_TOKEN_DURATION=15m +MG_USERS_REFRESH_TOKEN_DURATION=24h +MG_TOKEN_RESET_ENDPOINT=/reset-request MG_USERS_ALLOW_SELF_REGISTER=true MG_OAUTH_UI_REDIRECT_URL=http://localhost:9095${MG_UI_PATH_PREFIX}/tokens/secure MG_OAUTH_UI_ERROR_URL=http://localhost:9095${MG_UI_PATH_PREFIX}/error MG_USERS_DELETE_INTERVAL=24h MG_USERS_DELETE_AFTER=720h +#### Users Client Config +MG_USERS_URL=users:9002 + ### Email utility MG_EMAIL_HOST=smtp.mailtrap.io MG_EMAIL_PORT=2525 @@ -214,15 +250,45 @@ MG_GOOGLE_CLIENT_SECRET= MG_GOOGLE_REDIRECT_URL= MG_GOOGLE_STATE= +### Groups +MG_GROUPS_LOG_LEVEL=debug +MG_GROUPS_HTTP_HOST=groups +MG_GROUPS_HTTP_PORT=9004 +MG_GROUPS_HTTP_SERVER_CERT= +MG_GROUPS_HTTP_SERVER_KEY= +MG_GROUPS_GRPC_HOST=groups +MG_GROUPS_GRPC_PORT=7004 +MG_GROUPS_GRPC_SERVER_CERT=${GRPC_MTLS:+./ssl/certs/groups-grpc-server.crt}${GRPC_TLS:+./ssl/certs/groups-grpc-server.crt} +MG_GROUPS_GRPC_SERVER_KEY=${GRPC_MTLS:+./ssl/certs/groups-grpc-server.key}${GRPC_TLS:+./ssl/certs/groups-grpc-server.key} +MG_GROUPS_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}${GRPC_TLS:+./ssl/certs/ca.crt} +MG_GROUPS_DB_HOST=groups-db +MG_GROUPS_DB_PORT=5432 +MG_GROUPS_DB_USER=magistrala +MG_GROUPS_DB_PASS=magistrala +MG_GROUPS_DB_NAME=groups +MG_GROUPS_DB_SSL_MODE=disable +MG_GROUPS_DB_SSL_CERT= +MG_GROUPS_DB_SSL_KEY= +MG_GROUPS_DB_SSL_ROOT_CERT= +MG_GROUPS_INSTANCE_ID= + +#### Groups Client Config +MG_GROUPS_URL=groups:9004 +MG_GROUPS_GRPC_URL=groups:7004 +MG_GROUPS_GRPC_TIMEOUT=300s +MG_GROUPS_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/groups-grpc-client.crt} +MG_GROUPS_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/groups-grpc-client.key} +MG_GROUPS_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} + ### Things MG_THINGS_LOG_LEVEL=debug MG_THINGS_STANDALONE_ID= MG_THINGS_STANDALONE_TOKEN= MG_THINGS_CACHE_KEY_DURATION=10m MG_THINGS_HTTP_HOST=things -MG_THINGS_HTTP_PORT=9000 +MG_THINGS_HTTP_PORT=9006 MG_THINGS_AUTH_GRPC_HOST=things -MG_THINGS_AUTH_GRPC_PORT=7000 +MG_THINGS_AUTH_GRPC_PORT=7006 MG_THINGS_AUTH_GRPC_SERVER_CERT=${GRPC_MTLS:+./ssl/certs/things-grpc-server.crt}${GRPC_TLS:+./ssl/certs/things-grpc-server.crt} MG_THINGS_AUTH_GRPC_SERVER_KEY=${GRPC_MTLS:+./ssl/certs/things-grpc-server.key}${GRPC_TLS:+./ssl/certs/things-grpc-server.key} MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}${GRPC_TLS:+./ssl/certs/ca.crt} @@ -239,13 +305,42 @@ MG_THINGS_DB_SSL_ROOT_CERT= MG_THINGS_INSTANCE_ID= #### Things Client Config -MG_THINGS_URL=http://things:9000 -MG_THINGS_AUTH_GRPC_URL=things:7000 +MG_THINGS_URL=http://things:9006 +MG_THINGS_AUTH_GRPC_URL=things:7006 MG_THINGS_AUTH_GRPC_TIMEOUT=1s MG_THINGS_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/things-grpc-client.crt} MG_THINGS_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/things-grpc-client.key} MG_THINGS_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} +### Channels +MG_CHANNELS_LOG_LEVEL=debug +MG_CHANNELS_HTTP_HOST=channels +MG_CHANNELS_HTTP_PORT=9005 +MG_CHANNELS_GRPC_HOST=channels +MG_CHANNELS_GRPC_PORT=7005 +MG_CHANNELS_GRPC_SERVER_CERT=${GRPC_MTLS:+./ssl/certs/channels-grpc-server.crt}${GRPC_TLS:+./ssl/certs/channels-grpc-server.crt} +MG_CHANNELS_GRPC_SERVER_KEY=${GRPC_MTLS:+./ssl/certs/channels-grpc-server.key}${GRPC_TLS:+./ssl/certs/channels-grpc-server.key} +MG_CHANNELS_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}${GRPC_TLS:+./ssl/certs/ca.crt} +MG_CHANNELS_DB_HOST=channels-db +MG_CHANNELS_DB_PORT=5432 +MG_CHANNELS_DB_USER=magistrala +MG_CHANNELS_DB_PASS=magistrala +MG_CHANNELS_DB_NAME=channels +MG_CHANNELS_DB_SSL_MODE=disable +MG_CHANNELS_DB_SSL_CERT= +MG_CHANNELS_DB_SSL_KEY= +MG_CHANNELS_DB_SSL_ROOT_CERT= +MG_CHANNELS_INSTANCE_ID= + + +#### Channels Client Config +MG_CHANNELS_URL=http://channels:9005 +MG_CHANNELS_GRPC_URL=channels:7005 +MG_CHANNELS_GRPC_TIMEOUT=1s +MG_CHANNELS_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/channels-grpc-client.crt} +MG_CHANNELS_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/channels-grpc-client.key} +MG_CHANNELS_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} + ### HTTP MG_HTTP_ADAPTER_LOG_LEVEL=debug MG_HTTP_ADAPTER_HOST=http-adapter @@ -311,7 +406,7 @@ MG_PROVISION_ENV_CLIENTS_TLS=false MG_PROVISION_SERVER_CERT= MG_PROVISION_SERVER_KEY= MG_PROVISION_USERS_LOCATION=http://users:9002 -MG_PROVISION_THINGS_LOCATION=http://things:9000 +MG_PROVISION_THINGS_LOCATION=http://things:9006 MG_PROVISION_USER= MG_PROVISION_USERNAME= MG_PROVISION_PASS= diff --git a/docker/Dockerfile.livereload b/docker/Dockerfile.livereload new file mode 100644 index 0000000000..d0d482e946 --- /dev/null +++ b/docker/Dockerfile.livereload @@ -0,0 +1,3 @@ +FROM golang:1.23-alpine +RUN go install github.com/air-verse/air@latest +RUN apk update && apk add make git diff --git a/docker/addons/postgres-reader/docker-compose.yml b/docker/addons/postgres-reader/docker-compose.yml index 3b84d6c9bd..bd468eec0e 100644 --- a/docker/addons/postgres-reader/docker-compose.yml +++ b/docker/addons/postgres-reader/docker-compose.yml @@ -35,6 +35,11 @@ services: 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_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} MG_AUTH_GRPC_CLIENT_CERT: ${MG_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} @@ -78,3 +83,35 @@ services: target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + # Channels gRPC mTLS client certificates + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /channels-grpc-server-ca${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + # Auth gRPC mTLS client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true diff --git a/docker/addons/timescale-reader/docker-compose.yml b/docker/addons/timescale-reader/docker-compose.yml index 269e1c6025..9e6855ea6a 100644 --- a/docker/addons/timescale-reader/docker-compose.yml +++ b/docker/addons/timescale-reader/docker-compose.yml @@ -35,6 +35,11 @@ services: 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_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} MG_AUTH_GRPC_CLIENT_CERT: ${MG_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} @@ -78,3 +83,35 @@ services: target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + # Channels gRPC mTLS client certificates + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /channels-grpc-server-ca${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + # Auth gRPC mTLS client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true diff --git a/docker/docker-compose-live.yaml b/docker/docker-compose-live.yaml new file mode 100644 index 0000000000..1ca90a4263 --- /dev/null +++ b/docker/docker-compose-live.yaml @@ -0,0 +1,96 @@ +# Copyright (c) Abstract Machines +# SPDX-License-Identifier: Apache-2.0 + +services: + domains: + image: magistrala/domains-dev + build: + context: . + dockerfile: Dockerfile.livereload + volumes: + - ../:/go/src/github.com/absmach/magistrala + - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache + working_dir: /go/src/github.com/absmach/magistrala + entrypoint: [ "air", + "--build.cmd", "BUILD_DIR=/tmp make domains", + "--build.bin", "/tmp/domains", + "--build.stop_on_error", "true", + "--build.send_interrupt", "true", + "--build.include_file","dockers/.env", + "--build.exclude_dir", ".vscode,.git,.docker,.github,api,build,tools,scripts", + "--build.exclude_regex", "[\"_test\\.go\"" , + "--tmp_dir", "/tmp",] + + users: + image: magistrala/users-dev + build: + context: . + dockerfile: Dockerfile.livereload + volumes: + - ../:/go/src/github.com/absmach/magistrala + - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache + working_dir: /go/src/github.com/absmach/magistrala + entrypoint: [ "air", + "--build.cmd", "BUILD_DIR=/tmp make users", + "--build.bin", "/tmp/users", + "--build.stop_on_error", "true", + "--build.send_interrupt", "true", + "--build.exclude_dir", ".vscode,.git,.docker,.github,api,build,tools,scripts", + "--build.exclude_regex", "[\"_test\\.go\"" , + "--tmp_dir", "/tmp",] + things: + image: magistrala/things-dev + build: + context: . + dockerfile: Dockerfile.livereload + volumes: + - ../:/go/src/github.com/absmach/magistrala + - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache + working_dir: /go/src/github.com/absmach/magistrala + entrypoint: [ "air", + "--build.cmd", "BUILD_DIR=/tmp make things", + "--build.bin", "/tmp/things", + "--build.stop_on_error", "true", + "--build.send_interrupt", "true", + "--build.exclude_dir", ".vscode,.git,.docker,.github,api,build,tools,scripts", + "--build.exclude_regex", "[\"_test\\.go\"" , + "-tmp_dir", "/tmp",] + + channels: + image: magistrala/channels-dev + build: + context: . + dockerfile: Dockerfile.livereload + volumes: + - ../:/go/src/github.com/absmach/magistrala + - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache + working_dir: /go/src/github.com/absmach/magistrala + entrypoint: [ "air", + "--build.cmd", "BUILD_DIR=/tmp make channels", + "--build.bin", "/tmp/channels", + "--build.stop_on_error", "true", + "--build.send_interrupt", "true", + "--build.exclude_dir", ".vscode,.git,.docker,.github,api,build,tools,scripts", + "--build.exclude_regex", "[\"_test\\.go\"" , + "-tmp_dir", "/tmp",] + + channels-db: + command: ["postgres", "-c", "log_statement=all"] + + groups: + image: magistrala/groups-dev + build: + context: . + dockerfile: Dockerfile.livereload + volumes: + - ../:/go/src/github.com/absmach/magistrala + - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache + working_dir: /go/src/github.com/absmach/magistrala + entrypoint: [ "air", + "--build.cmd", "BUILD_DIR=/tmp make groups", + "--build.bin", "/tmp/groups", + "--build.stop_on_error", "true", + "--build.send_interrupt", "true", + "--build.exclude_dir", ".vscode,.git,.docker,.github,api,build,tools,scripts", + "--build.exclude_regex", "[\"_test\\.go\"" , + "-tmp_dir", "/tmp",] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 804389ea8d..6daaa3ef27 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,18 +9,21 @@ networks: volumes: magistrala-users-db-volume: + magistrala-groups-db-volume: magistrala-things-db-volume: + magistrala-channels-db-volume: magistrala-things-redis-volume: magistrala-broker-volume: magistrala-mqtt-broker-volume: magistrala-spicedb-db-volume: magistrala-auth-db-volume: + magistrala-domains-db-volume: magistrala-invitations-db-volume: magistrala-ui-db-volume: services: spicedb: - image: "authzed/spicedb:v1.30.0" + image: "authzed/spicedb:v1.37.0" container_name: magistrala-spicedb command: "serve" restart: "always" @@ -38,7 +41,7 @@ services: - spicedb-migrate spicedb-migrate: - image: "authzed/spicedb:v1.30.0" + image: "authzed/spicedb:v1.37.0" container_name: magistrala-spicedb-migrate command: "migrate head" restart: "on-failure" @@ -69,7 +72,7 @@ services: container_name: magistrala-auth-db restart: on-failure ports: - - 6004:5432 + - 6001:5432 environment: POSTGRES_USER: ${MG_AUTH_DB_USER} POSTGRES_PASSWORD: ${MG_AUTH_DB_PASS} @@ -154,6 +157,111 @@ services: bind: create_host_path: true + domains-db: + image: postgres:16.2-alpine + container_name: magistrala-domains-db + restart: on-failure + ports: + - 6003:5432 + environment: + POSTGRES_USER: ${MG_DOMAINS_DB_USER} + POSTGRES_PASSWORD: ${MG_DOMAINS_DB_PASS} + POSTGRES_DB: ${MG_DOMAINS_DB_NAME} + networks: + - magistrala-base-net + volumes: + - magistrala-domains-db-volume:/var/lib/postgresql/data + + domains: + image: magistrala/domains:${MG_RELEASE_TAG} + container_name: magistrala-domains + depends_on: + - domains-db + - spicedb + expose: + - ${MG_DOMAINS_GRPC_PORT} + restart: on-failure + environment: + MG_DOMAINS_LOG_LEVEL: ${MG_DOMAINS_LOG_LEVEL} + MG_SPICEDB_PRE_SHARED_KEY: ${MG_SPICEDB_PRE_SHARED_KEY} + MG_SPICEDB_HOST: ${MG_SPICEDB_HOST} + MG_SPICEDB_PORT: ${MG_SPICEDB_PORT} + MG_DOMAINS_HTTP_HOST: ${MG_DOMAINS_HTTP_HOST} + MG_DOMAINS_HTTP_PORT: ${MG_DOMAINS_HTTP_PORT} + MG_DOMAINS_HTTP_SERVER_CERT: ${MG_DOMAINS_HTTP_SERVER_CERT} + MG_DOMAINS_HTTP_SERVER_KEY: ${MG_DOMAINS_HTTP_SERVER_KEY} + MG_DOMAINS_GRPC_HOST: ${MG_DOMAINS_GRPC_HOST} + MG_DOMAINS_GRPC_PORT: ${MG_DOMAINS_GRPC_PORT} + ## Compose supports parameter expansion in environment, + ## Eg: ${VAR:+replacement} or ${VAR+replacement} -> replacement if VAR is set and non-empty, otherwise empty + ## Eg :${VAR:-default} or ${VAR-default} -> value of VAR if set and non-empty, otherwise default + MG_DOMAINS_GRPC_SERVER_CERT: ${MG_DOMAINS_GRPC_SERVER_CERT:+/auth-grpc-server.crt} + MG_DOMAINS_GRPC_SERVER_KEY: ${MG_DOMAINS_GRPC_SERVER_KEY:+/auth-grpc-server.key} + MG_DOMAINS_GRPC_SERVER_CA_CERTS: ${MG_DOMAINS_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + MG_DOMAINS_GRPC_CLIENT_CA_CERTS: ${MG_DOMAINS_GRPC_CLIENT_CA_CERTS:+/auth-grpc-client-ca.crt} + MG_DOMAINS_DB_HOST: ${MG_DOMAINS_DB_HOST} + MG_DOMAINS_DB_PORT: ${MG_DOMAINS_DB_PORT} + MG_DOMAINS_DB_USER: ${MG_DOMAINS_DB_USER} + MG_DOMAINS_DB_PASS: ${MG_DOMAINS_DB_PASS} + MG_DOMAINS_DB_NAME: ${MG_DOMAINS_DB_NAME} + MG_DOMAINS_DB_SSL_MODE: ${MG_DOMAINS_DB_SSL_MODE} + MG_DOMAINS_DB_SSL_CERT: ${MG_DOMAINS_DB_SSL_CERT} + MG_DOMAINS_DB_SSL_KEY: ${MG_DOMAINS_DB_SSL_KEY} + MG_DOMAINS_DB_SSL_ROOT_CERT: ${MG_DOMAINS_DB_SSL_ROOT_CERT} + MG_DOMAINS_INSTANCE_ID: ${MG_DOMAINS_INSTANCE_ID} + MG_ES_URL: ${MG_ES_URL} + MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} + MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} + 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_GROUPS_GRPC_URL: ${MG_GROUPS_GRPC_URL} + MG_GROUPS_GRPC_TIMEOUT: ${MG_GROUPS_GRPC_TIMEOUT} + MG_GROUPS_GRPC_CLIENT_CERT: ${MG_GROUPS_GRPC_CLIENT_CERT:+/groups-grpc-client.crt} + MG_GROUPS_GRPC_CLIENT_KEY: ${MG_GROUPS_GRPC_CLIENT_KEY:+/groups-grpc-client.key} + MG_GROUPS_GRPC_SERVER_CA_CERTS: ${MG_GROUPS_GRPC_SERVER_CA_CERTS:+/groups-grpc-server-ca.crt} + MG_CHANNELS_URL: ${MG_CHANNELS_URL} + MG_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-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_JAEGER_URL: ${MG_JAEGER_URL} + MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} + MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} + ports: + - ${MG_DOMAINS_HTTP_PORT}:${MG_DOMAINS_HTTP_PORT} + - ${MG_DOMAINS_GRPC_PORT}:${MG_DOMAINS_GRPC_PORT} + networks: + - magistrala-base-net + volumes: + # Auth gRPC mTLS server certificates + - type: bind + source: ${MG_DOMAINS_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert} + target: /auth-grpc-server${MG_DOMAINS_GRPC_SERVER_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_DOMAINS_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key} + target: /auth-grpc-server${MG_DOMAINS_GRPC_SERVER_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs} + target: /auth-grpc-server-ca${MG_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_DOMAINS_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs} + target: /auth-grpc-client-ca${MG_DOMAINS_GRPC_CLIENT_CA_CERTS:+.crt} + bind: + create_host_path: true + invitations-db: image: postgres:16.2-alpine container_name: magistrala-invitations-db @@ -332,6 +440,18 @@ 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_CHANNELS_URL: ${MG_CHANNELS_URL} + MG_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} + MG_GROUPS_URL: ${MG_GROUPS_URL} + MG_GROUPS_GRPC_URL: ${MG_GROUPS_GRPC_URL} + MG_GROUPS_GRPC_TIMEOUT: ${MG_GROUPS_GRPC_TIMEOUT} + MG_GROUPS_GRPC_CLIENT_CERT: ${MG_GROUPS_GRPC_CLIENT_CERT:+/groups-grpc-client.crt} + MG_GROUPS_GRPC_CLIENT_KEY: ${MG_GROUPS_GRPC_CLIENT_KEY:+/groups-grpc-client.key} + MG_GROUPS_GRPC_SERVER_CA_CERTS: ${MG_GROUPS_GRPC_SERVER_CA_CERTS:+/groups-grpc-server-ca.crt} MG_JAEGER_URL: ${MG_JAEGER_URL} MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} @@ -381,6 +501,116 @@ services: target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + # Channel gRPC client certificates + - type: bind + source: ${MG_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /channels-grpc-client${MG_CHANNELS_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /channels-grpc-client${MG_CHANNELS_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /channels-grpc-server-ca${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + + channels-db: + image: postgres:16.2-alpine + container_name: magistrala-channels-db + restart: on-failure + command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" + environment: + POSTGRES_USER: ${MG_CHANNELS_DB_USER} + POSTGRES_PASSWORD: ${MG_CHANNELS_DB_PASS} + POSTGRES_DB: ${MG_CHANNELS_DB_NAME} + MG_POSTGRES_MAX_CONNECTIONS: ${MG_POSTGRES_MAX_CONNECTIONS} + networks: + - magistrala-base-net + ports: + - 6005:5432 + volumes: + - magistrala-channels-db-volume:/var/lib/postgresql/data + + channels: + image: magistrala/channels:${MG_RELEASE_TAG} + container_name: magistrala-channels + depends_on: + - channels-db + - users + - auth + - nats + restart: on-failure + environment: + MG_CHANNELS_LOG_LEVEL: ${MG_CHANNELS_LOG_LEVEL} + MG_CHANNELS_INSTANCE_ID: ${MG_CHANNELS_INSTANCE_ID} + MG_CHANNELS_HTTP_HOST: ${MG_CHANNELS_HTTP_HOST} + MG_CHANNELS_HTTP_PORT: ${MG_CHANNELS_HTTP_PORT} + MG_CHANNELS_GRPC_HOST: ${MG_CHANNELS_GRPC_HOST} + MG_CHANNELS_GRPC_PORT: ${MG_CHANNELS_GRPC_PORT} + ## Compose supports parameter expansion in environment, + ## Eg: ${VAR:+replacement} or ${VAR+replacement} -> replacement if VAR is set and non-empty, otherwise empty + ## Eg :${VAR:-default} or ${VAR-default} -> value of VAR if set and non-empty, otherwise default + MG_CHANNELS_GRPC_SERVER_CERT: ${MG_CHANNELS_GRPC_SERVER_CERT:+/channels-grpc-server.crt} + MG_CHANNELS_GRPC_SERVER_KEY: ${MG_CHANNELS_GRPC_SERVER_KEY:+/channels-grpc-server.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} + MG_CHANNELS_GRPC_CLIENT_CA_CERTS: ${MG_CHANNELS_GRPC_CLIENT_CA_CERTS:+/channels-grpc-client-ca.crt} + MG_CHANNELS_DB_HOST: ${MG_CHANNELS_DB_HOST} + MG_CHANNELS_DB_PORT: ${MG_CHANNELS_DB_PORT} + MG_CHANNELS_DB_USER: ${MG_CHANNELS_DB_USER} + MG_CHANNELS_DB_PASS: ${MG_CHANNELS_DB_PASS} + MG_CHANNELS_DB_NAME: ${MG_CHANNELS_DB_NAME} + MG_CHANNELS_DB_SSL_MODE: ${MG_CHANNELS_DB_SSL_MODE} + MG_CHANNELS_DB_SSL_CERT: ${MG_CHANNELS_DB_SSL_CERT} + MG_CHANNELS_DB_SSL_KEY: ${MG_CHANNELS_DB_SSL_KEY} + MG_CHANNELS_DB_SSL_ROOT_CERT: ${MG_CHANNELS_DB_SSL_ROOT_CERT} + MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} + MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} + 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_GROUPS_GRPC_URL: ${MG_GROUPS_GRPC_URL} + MG_GROUPS_GRPC_TIMEOUT: ${MG_GROUPS_GRPC_TIMEOUT} + MG_GROUPS_GRPC_CLIENT_CERT: ${MG_GROUPS_GRPC_CLIENT_CERT:+/groups-grpc-client.crt} + MG_GROUPS_GRPC_CLIENT_KEY: ${MG_GROUPS_GRPC_CLIENT_KEY:+/groups-grpc-client.key} + MG_GROUPS_GRPC_SERVER_CA_CERTS: ${MG_GROUPS_GRPC_SERVER_CA_CERTS:+/groups-grpc-server-ca.crt} + MG_ES_URL: ${MG_ES_URL} + MG_JAEGER_URL: ${MG_JAEGER_URL} + MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} + MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} + MG_SPICEDB_PRE_SHARED_KEY: ${MG_SPICEDB_PRE_SHARED_KEY} + MG_SPICEDB_HOST: ${MG_SPICEDB_HOST} + MG_SPICEDB_PORT: ${MG_SPICEDB_PORT} + ports: + - ${MG_CHANNELS_HTTP_PORT}:${MG_CHANNELS_HTTP_PORT} + - ${MG_CHANNELS_GRPC_PORT}:${MG_CHANNELS_GRPC_PORT} + networks: + - magistrala-base-net + volumes: + # Auth gRPC client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true users-db: image: postgres:16.2-alpine @@ -393,7 +623,7 @@ services: POSTGRES_DB: ${MG_USERS_DB_NAME} MG_POSTGRES_MAX_CONNECTIONS: ${MG_POSTGRES_MAX_CONNECTIONS} ports: - - 6000:5432 + - 6002:5432 networks: - magistrala-base-net volumes: @@ -449,6 +679,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_DOMAINS_GRPC_URL: ${MG_DOMAINS_GRPC_URL} + MG_DOMAINS_GRPC_TIMEOUT: ${MG_DOMAINS_GRPC_TIMEOUT} + MG_DOMAINS_GRPC_CLIENT_CERT: ${MG_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt} + MG_DOMAINS_GRPC_CLIENT_KEY: ${MG_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key} + MG_DOMAINS_GRPC_SERVER_CA_CERTS: ${MG_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt} MG_GOOGLE_CLIENT_ID: ${MG_GOOGLE_CLIENT_ID} MG_GOOGLE_CLIENT_SECRET: ${MG_GOOGLE_CLIENT_SECRET} MG_GOOGLE_REDIRECT_URL: ${MG_GOOGLE_REDIRECT_URL} @@ -483,6 +718,103 @@ services: bind: create_host_path: true + + groups-db: + image: postgres:16.2-alpine + container_name: magistrala-groups-db + restart: on-failure + command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" + environment: + POSTGRES_USER: ${MG_GROUPS_DB_USER} + POSTGRES_PASSWORD: ${MG_GROUPS_DB_PASS} + POSTGRES_DB: ${MG_GROUPS_DB_NAME} + MG_POSTGRES_MAX_CONNECTIONS: ${MG_POSTGRES_MAX_CONNECTIONS} + ports: + - 6004:5432 + networks: + - magistrala-base-net + volumes: + - magistrala-groups-db-volume:/var/lib/postgresql/data + + groups: + image: magistrala/groups:${MG_RELEASE_TAG} + container_name: magistrala-groups + depends_on: + - groups-db + - auth + - nats + restart: on-failure + environment: + MG_GROUPS_LOG_LEVEL: ${MG_GROUPS_LOG_LEVEL} + MG_GROUPS_HTTP_HOST: ${MG_GROUPS_HTTP_HOST} + MG_GROUPS_HTTP_PORT: ${MG_GROUPS_HTTP_PORT} + MG_GROUPS_HTTP_SERVER_CERT: ${MG_GROUPS_HTTP_SERVER_CERT} + MG_GROUPS_HTTP_SERVER_KEY: ${MG_GROUPS_HTTP_SERVER_KEY} + MG_GROUPS_GRPC_HOST: ${MG_GROUPS_GRPC_HOST} + MG_GROUPS_GRPC_PORT: ${MG_GROUPS_GRPC_PORT} + ## Compose supports parameter expansion in environment, + ## Eg: ${VAR:+replacement} or ${VAR+replacement} -> replacement if VAR is set and non-empty, otherwise empty + ## Eg :${VAR:-default} or ${VAR-default} -> value of VAR if set and non-empty, otherwise default + MG_GROUPS_GRPC_SERVER_CERT: ${MG_GROUPS_GRPC_SERVER_CERT:+/groups-grpc-server.crt} + MG_GROUPS_GRPC_SERVER_KEY: ${MG_GROUPS_GRPC_SERVER_KEY:+/groups-grpc-server.key} + MG_GROUPS_GRPC_SERVER_CA_CERTS: ${MG_GROUPS_GRPC_SERVER_CA_CERTS:+/groups-grpc-server-ca.crt} + MG_GROUPS_GRPC_CLIENT_CA_CERTS: ${MG_GROUPS_GRPC_CLIENT_CA_CERTS:+/groups-grpc-client-ca.crt} + MG_GROUPS_DB_HOST: ${MG_GROUPS_DB_HOST} + MG_GROUPS_DB_PORT: ${MG_GROUPS_DB_PORT} + MG_GROUPS_DB_USER: ${MG_GROUPS_DB_USER} + MG_GROUPS_DB_PASS: ${MG_GROUPS_DB_PASS} + MG_GROUPS_DB_NAME: ${MG_GROUPS_DB_NAME} + MG_GROUPS_DB_SSL_MODE: ${MG_GROUPS_DB_SSL_MODE} + MG_GROUPS_DB_SSL_CERT: ${MG_GROUPS_DB_SSL_CERT} + MG_GROUPS_DB_SSL_KEY: ${MG_GROUPS_DB_SSL_KEY} + MG_GROUPS_DB_SSL_ROOT_CERT: ${MG_GROUPS_DB_SSL_ROOT_CERT} + MG_CHANNELS_URL: ${MG_CHANNELS_URL} + MG_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-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_ES_URL: ${MG_ES_URL} + MG_JAEGER_URL: ${MG_JAEGER_URL} + MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} + MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} + MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} + MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} + 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_SPICEDB_PRE_SHARED_KEY: ${MG_SPICEDB_PRE_SHARED_KEY} + MG_SPICEDB_HOST: ${MG_SPICEDB_HOST} + MG_SPICEDB_PORT: ${MG_SPICEDB_PORT} + ports: + - ${MG_GROUPS_HTTP_PORT}:${MG_GROUPS_HTTP_PORT} + - ${MG_GROUPS_GRPC_PORT}:${MG_GROUPS_GRPC_PORT} + networks: + - magistrala-base-net + volumes: + # Auth gRPC client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + + jaeger: image: jaegertracing/all-in-one:1.60 container_name: magistrala-jaeger @@ -523,6 +855,11 @@ services: 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_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} MG_JAEGER_URL: ${MG_JAEGER_URL} MG_MESSAGE_BROKER_URL: ${MG_MESSAGE_BROKER_URL} MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} @@ -546,6 +883,22 @@ services: target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + # Channels gRPC mTLS client certificates + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /channels-grpc-server-ca${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true http-adapter: image: magistrala/http:${MG_RELEASE_TAG} @@ -565,6 +918,16 @@ services: 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_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} + MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} + MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} + 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_MESSAGE_BROKER_URL: ${MG_MESSAGE_BROKER_URL} MG_JAEGER_URL: ${MG_JAEGER_URL} MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} @@ -591,6 +954,38 @@ services: target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + # Channels gRPC mTLS client certificates + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /channels-grpc-server-ca${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + # Auth gRPC mTLS client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true coap-adapter: image: magistrala/coap:${MG_RELEASE_TAG} @@ -614,6 +1009,11 @@ services: 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_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} MG_MESSAGE_BROKER_URL: ${MG_MESSAGE_BROKER_URL} MG_JAEGER_URL: ${MG_JAEGER_URL} MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} @@ -641,6 +1041,22 @@ services: target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + # Channels gRPC mTLS client certificates + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /channels-grpc-server-ca${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true ws-adapter: image: magistrala/ws:${MG_RELEASE_TAG} @@ -660,6 +1076,16 @@ services: 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_CHANNELS_GRPC_URL: ${MG_CHANNELS_GRPC_URL} + MG_CHANNELS_GRPC_TIMEOUT: ${MG_CHANNELS_GRPC_TIMEOUT} + MG_CHANNELS_GRPC_CLIENT_CERT: ${MG_CHANNELS_GRPC_CLIENT_CERT:+/channels-grpc-client.crt} + MG_CHANNELS_GRPC_CLIENT_KEY: ${MG_CHANNELS_GRPC_CLIENT_KEY:+/channels-grpc-client.key} + MG_CHANNELS_GRPC_SERVER_CA_CERTS: ${MG_CHANNELS_GRPC_SERVER_CA_CERTS:+/channels-grpc-server-ca.crt} + MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} + MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} + 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_MESSAGE_BROKER_URL: ${MG_MESSAGE_BROKER_URL} MG_JAEGER_URL: ${MG_JAEGER_URL} MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} @@ -686,6 +1112,38 @@ services: target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} bind: create_host_path: true + # Channels gRPC mTLS client certificates + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /channels-grpc-client${MG_CHANNELS_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /channels-grpc-server-ca${MG_CHANNELS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + # Auth gRPC mTLS client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true vernemq: image: magistrala/vernemq:${MG_RELEASE_TAG} diff --git a/docker/nginx/entrypoint.sh b/docker/nginx/entrypoint.sh index 6b90377035..51aa85fc96 100755 --- a/docker/nginx/entrypoint.sh +++ b/docker/nginx/entrypoint.sh @@ -13,10 +13,12 @@ fi envsubst ' ${MG_NGINX_SERVER_NAME} - ${MG_AUTH_HTTP_PORT} + ${MG_DOMAINS_HTTP_PORT} + ${MG_GROUPS_HTTP_PORT} ${MG_USERS_HTTP_PORT} ${MG_THINGS_HTTP_PORT} ${MG_THINGS_AUTH_HTTP_PORT} + ${MG_CHANNELS_HTTP_PORT} ${MG_HTTP_ADAPTER_PORT} ${MG_NGINX_MQTT_PORT} ${MG_NGINX_MQTTS_PORT} diff --git a/docker/nginx/nginx-key.conf b/docker/nginx/nginx-key.conf index 153a7b7a42..b8a88cf73b 100644 --- a/docker/nginx/nginx-key.conf +++ b/docker/nginx/nginx-key.conf @@ -57,93 +57,39 @@ http { add_header Access-Control-Allow-Methods '*'; add_header Access-Control-Allow-Headers '*'; - location ~ ^/(channels)/(.+)/(things)/(.+) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - proxy_pass http://things:${MG_THINGS_HTTP_PORT}; - } - # Proxy pass to users & groups id to things service for listing of channels - # /users/{userID}/channels - Listing of channels belongs to userID - # /groups/{userGroupID}/channels - Listing of channels belongs to userGroupID - location ~ ^/(users|groups)/(.+)/(channels|things) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://things:${MG_THINGS_HTTP_PORT}; - break; - } - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - } - - # Proxy pass to channel id to users service for listing of channels - # /channels/{channelID}/users - Listing of Users belongs to channelID - # /channels/{channelID}/groups - Listing of User Groups belongs to channelID - location ~ ^/(channels|things)/(.+)/(users|groups) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - break; - } - proxy_pass http://things:${MG_THINGS_HTTP_PORT}; - } - - # Proxy pass to user id to auth service for listing of domains - # /users/{userID}/domains - Listing of Domains belongs to userID - location ~ ^/(users)/(.+)/(domains) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; - break; - } - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - } - - # Proxy pass to domain id to users service for listing of users - # /domains/{domainID}/users - Listing of Users belongs to domainID - location ~ ^/(domains)/(.+)/(users) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - break; - } - proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; - } - - - # Proxy pass to auth service + # Proxy pass to domains service location ~ ^/(domains) { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; - proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + proxy_pass http://domains:${MG_DOMAINS_HTTP_PORT}; } # Proxy pass to users service - location ~ ^/(users|groups|password|authorize|oauth/callback/[^/]+) { + location ~ ^/(users|password|authorize|oauth/callback/[^/]+) { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; proxy_pass http://users:${MG_USERS_HTTP_PORT}; } - location ^~ /users/policies { + # Proxy pass to groups service + location ~ "^/([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})/(groups)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; - proxy_pass http://users:${MG_USERS_HTTP_PORT}/policies; + proxy_pass http://groups:${MG_GROUPS_HTTP_PORT}; } # Proxy pass to things service - location ~ ^/(things|channels|connect|disconnect|identify) { + location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(things)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; proxy_pass http://things:${MG_THINGS_HTTP_PORT}; } - location ^~ /things/policies { + # Proxy pass to domains service + location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(channels)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; - proxy_pass http://things:${MG_THINGS_HTTP_PORT}/policies; + proxy_pass http://channels:${MG_CHANNELS_HTTP_PORT}; } # Proxy pass to invitations service diff --git a/docker/nginx/nginx-x509.conf b/docker/nginx/nginx-x509.conf index 1da22b0fb4..268899a0df 100644 --- a/docker/nginx/nginx-x509.conf +++ b/docker/nginx/nginx-x509.conf @@ -66,67 +66,11 @@ http { add_header Access-Control-Allow-Methods '*'; add_header Access-Control-Allow-Headers '*'; - location ~ ^/(channels)/(.+)/(things)/(.+) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - proxy_pass http://things:${MG_THINGS_HTTP_PORT}; - } - # Proxy pass to users & groups id to things service for listing of channels - # /users/{userID}/channels - Listing of channels belongs to userID - # /groups/{userGroupID}/channels - Listing of channels belongs to userGroupID - location ~ ^/(users|groups)/(.+)/(channels|things) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://things:${MG_THINGS_HTTP_PORT}; - break; - } - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - } - - # Proxy pass to channel id to users service for listing of channels - # /channels/{channelID}/users - Listing of Users belongs to channelID - # /channels/{channelID}/groups - Listing of User Groups belongs to channelID - location ~ ^/(channels|things)/(.+)/(users|groups) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - break; - } - proxy_pass http://things:${MG_THINGS_HTTP_PORT}; - } - - # Proxy pass to user id to auth service for listing of domains - # /users/{userID}/domains - Listing of Domains belongs to userID - location ~ ^/(users)/(.+)/(domains) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; - break; - } - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - } - - # Proxy pass to domain id to users service for listing of users - # /domains/{domainID}/users - Listing of Users belongs to domainID - location ~ ^/(domains)/(.+)/(users) { - include snippets/proxy-headers.conf; - add_header Access-Control-Expose-Headers Location; - if ($request_method = GET) { - proxy_pass http://users:${MG_USERS_HTTP_PORT}; - break; - } - proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; - } - - - # Proxy pass to auth service + # Proxy pass to domains service location ~ ^/(domains) { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; - proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + proxy_pass http://domains:${MG_DOMAINS_HTTP_PORT}; } # Proxy pass to users service @@ -136,23 +80,25 @@ http { proxy_pass http://users:${MG_USERS_HTTP_PORT}; } - location ^~ /users/policies { + # Proxy pass to groups service + location ~ "^/([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})/(groups)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; - proxy_pass http://users:${MG_USERS_HTTP_PORT}/policies; + proxy_pass http://groups:${MG_GROUPS_HTTP_PORT}; } # Proxy pass to things service - location ~ ^/(things|channels|connect|disconnect|identify) { + location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(things)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; proxy_pass http://things:${MG_THINGS_HTTP_PORT}; } - location ^~ /things/policies { + # Proxy pass to domains service + location ~ "^/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/(channels)" { include snippets/proxy-headers.conf; add_header Access-Control-Expose-Headers Location; - proxy_pass http://things:${MG_THINGS_HTTP_PORT}/policies; + proxy_pass http://channels:${MG_CHANNELS_HTTP_PORT}; } # Proxy pass to invitations service diff --git a/docker/nginx/snippets/http_access_log.conf b/docker/nginx/snippets/http_access_log.conf index d9adfa1958..4bc37166d6 100644 --- a/docker/nginx/snippets/http_access_log.conf +++ b/docker/nginx/snippets/http_access_log.conf @@ -4,5 +4,9 @@ log_format access_log_format 'HTTP/WS ' '$remote_addr: ' '"$request" $status; ' - 'request time=$request_time upstream connect time=$upstream_connect_time upstream response time=$upstream_response_time'; + 'request time=$request_time ' + 'upstream connect time=$upstream_connect_time ' + 'upstream address $upstream_addr ' + 'upstream status $upstream_status ' + 'upstream response time=$upstream_response_time'; access_log access.log access_log_format; diff --git a/docker/spicedb/schema.zed b/docker/spicedb/schema.zed index 215797a99c..6962b3cbe1 100644 --- a/docker/spicedb/schema.zed +++ b/docker/spicedb/schema.zed @@ -1,74 +1,509 @@ definition user {} + +definition role { + relation entity: domain | group | channel | thing + relation member: user + relation built_in_role: domain | group | channel | thing + + permission delete = entity->manage_role_permission - built_in_role->manage_role_permission + permission update = entity->manage_role_permission - built_in_role->manage_role_permission + permission read = entity->manage_role_permission - built_in_role->manage_role_permission + + permission add_user = entity->add_role_users_permission + permission remove_user = entity->remove_role_users_permission + permission view_user = entity->view_role_users_permission +} + definition thing { - relation administrator: user - relation group: group - relation domain: domain - - permission admin = administrator + group->admin + domain->admin - permission delete = admin - permission edit = admin + group->edit + domain->edit - permission view = edit + group->view + domain->view - permission share = edit - permission publish = group - permission subscribe = group - - // These permission are made for only list purpose. It helps to list users have only particular permission excluding other higher and lower permission. - permission admin_only = admin - permission edit_only = edit - admin - permission view_only = view - - // These permission are made for only list purpose. It helps to list users from external, users who are not in group but have permission on the group through parent group - permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group + relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain + relation parent_group: group + + relation update: role#member + relation read: role#member + relation delete: role#member + relation set_parent_group: role#member + relation connect_to_channel: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + permission update_permission = update + parent_group->thing_update_permission + domain->thing_update_permission + permission read_permission = read + parent_group->thing_read_permission + domain->thing_read_permission + permission delete_permission = delete + parent_group->thing_delete_permission + domain->thing_delete_permission + permission set_parent_group_permission = set_parent_group + parent_group->thing_set_parent_group_permission + domain->thing_set_parent_group_permission + permission connect_to_channel_permission = connect_to_channel + parent_group->thing_connect_to_channel + domain->thing_connect_to_channel_permission + + permission manage_role_permission = manage_role + parent_group->thing_manage_role_permission + domain->thing_manage_role_permission + permission add_role_users_permission = add_role_users + parent_group->thing_add_role_users_permission + domain->thing_add_role_users_permission + permission remove_role_users_permission = remove_role_users + parent_group->thing_remove_role_users_permission + domain->thing_remove_role_users_permission + permission view_role_users_permission = view_role_users + parent_group->thing_view_role_users_permission + domain->thing_view_role_users_permission } -definition group { - relation administrator: user - relation editor: user - relation contributor: user - relation member: user - relation guest: user +definition channel { + relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain + relation parent_group: group + + relation update: role#member + relation read: role#member + relation delete: role#member + relation set_parent_group: role#member + relation connect_to_thing: role#member + relation publish: role#member | thing + relation subscribe: role#member | thing + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + permission update_permission = update + parent_group->channel_update_permission + domain->channel_update_permission + permission read_permission = read + parent_group->channel_read_permission + domain->channel_read_permission + permission delete_permission = delete + parent_group->channel_delete_permission + domain->channel_delete_permission + permission set_parent_group_permission = set_parent_group + parent_group->channel_set_parent_group_permission + domain->channel_set_parent_group_permission + permission connect_to_thing_permission = connect_to_thing + parent_group->channel_connect_to_thing_permission + domain->channel_connect_to_thing + permission publish_permission = publish + parent_group->channel_publish_permission + domain->channel_publish_permission + permission subscribe_permission = subscribe + parent_group->channel_subscribe_permission + domain->channel_subscribe_permission + + permission manage_role_permission = manage_role + parent_group->channel_manage_role_permission + domain->channel_manage_role_permission + permission add_role_users_permission = add_role_users + parent_group->channel_add_role_users_permission + domain->channel_add_role_users_permission + permission remove_role_users_permission = remove_role_users + parent_group->channel_remove_role_users_permission + domain->channel_remove_role_users_permission + permission view_role_users_permission = view_role_users + parent_group->channel_view_role_users_permission + domain->channel_view_role_users_permission +} +definition group { + relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it is safe to add domain relation parent_group: group - relation domain: domain - - permission admin = administrator + parent_group->admin + domain->admin - permission delete = admin - permission edit = admin + editor + parent_group->edit + domain->edit - permission share = edit - permission view = contributor + edit + parent_group->view + domain->view + guest - permission membership = view + member - permission create = membership - guest - - // These permissions are made for listing purposes. They enable listing users who have only particular permission excluding higher-level permissions users. - permission admin_only = admin - permission edit_only = edit - admin - permission view_only = view - permission membership_only = membership - view - - // These permission are made for only list purpose. They enable listing users who have only particular permission from parent group excluding higher-level permissions. - permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group - permission ext_edit = edit - editor // For list of external edit , not having direct relation with group, but have indirect relation from parent group - permission ext_view = view - contributor // For list of external view , not having direct relation with group, but have indirect relation from parent group + + relation update: role#member + relation read: role#member + relation membership: role#member + relation delete: role#member + relation set_child: role#member + relation set_parent: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + relation thing_create: role#member + relation channel_create: role#member + // this allows to add parent for group during the new group creation + relation subgroup_create: role#member + relation subgroup_thing_create: role#member + relation subgroup_channel_create: role#member + + relation thing_update: role#member + relation thing_read: role#member + relation thing_delete: role#member + relation thing_set_parent_group: role#member + relation thing_connect_to_channel: role#member + + relation thing_manage_role: role#member + relation thing_add_role_users: role#member + relation thing_remove_role_users: role#member + relation thing_view_role_users: role#member + + relation channel_update: role#member + relation channel_read: role#member + relation channel_delete: role#member + relation channel_set_parent_group: role#member + relation channel_connect_to_thing: role#member + relation channel_publish: role#member + relation channel_subscribe: role#member + + relation channel_manage_role: role#member + relation channel_add_role_users: role#member + relation channel_remove_role_users: role#member + relation channel_view_role_users: role#member + + relation subgroup_update: role#member + relation subgroup_read: role#member + relation subgroup_membership: role#member + relation subgroup_delete: role#member + relation subgroup_set_child: role#member + relation subgroup_set_parent: role#member + + relation subgroup_manage_role: role#member + relation subgroup_add_role_users: role#member + relation subgroup_remove_role_users: role#member + relation subgroup_view_role_users: role#member + + relation subgroup_thing_update: role#member + relation subgroup_thing_read: role#member + relation subgroup_thing_delete: role#member + relation subgroup_thing_set_parent_group: role#member + relation subgroup_thing_connect_to_channel: role#member + + relation subgroup_thing_manage_role: role#member + relation subgroup_thing_add_role_users: role#member + relation subgroup_thing_remove_role_users: role#member + relation subgroup_thing_view_role_users: role#member + + relation subgroup_channel_update: role#member + relation subgroup_channel_read: role#member + relation subgroup_channel_delete: role#member + relation subgroup_channel_set_parent_group: role#member + relation subgroup_channel_connect_to_thing: role#member + relation subgroup_channel_publish: role#member + relation subgroup_channel_subscribe: role#member + + relation subgroup_channel_manage_role: role#member + relation subgroup_channel_add_role_users: role#member + relation subgroup_channel_remove_role_users: role#member + relation subgroup_channel_view_role_users: role#member + + // Subgroup permission + permission subgroup_create_permission = subgroup_create + parent_group->subgroup_create_permission + permission subgroup_thing_create_permission = subgroup_thing_create + parent_group->subgroup_thing_create_permission + permission subgroup_channel_create_permission = subgroup_channel_create + parent_group->subgroup_channel_create_permission + + permission subgroup_update_permission = subgroup_update + parent_group->subgroup_update_permission + permission subgroup_membership_permission = subgroup_membership + parent_group->subgroup_membership_permission + permission subgroup_read_permission = subgroup_read + parent_group->subgroup_read_permission + permission subgroup_delete_permission = subgroup_delete + parent_group->subgroup_delete_permission + permission subgroup_set_child_permission = subgroup_set_child + parent_group->subgroup_set_child_permission + permission subgroup_set_parent_permission = subgroup_set_parent + parent_group->subgroup_set_parent_permission + + permission subgroup_manage_role_permission = subgroup_manage_role + parent_group->subgroup_manage_role_permission + permission subgroup_add_role_users_permission = subgroup_add_role_users + parent_group->subgroup_add_role_users_permission + permission subgroup_remove_role_users_permission = subgroup_remove_role_users + parent_group->subgroup_remove_role_users_permission + permission subgroup_view_role_users_permission = subgroup_view_role_users + parent_group->subgroup_view_role_users_permission + + // Group permission + permission update_permission = update + parent_group->subgroup_create_permission + domain->group_update_permission + permission membership_permission = membership + parent_group->subgroup_membership_permission + domain->group_membership_permission + permission read_permission = read + parent_group->subgroup_read_permission + domain->group_read_permission + permission delete_permission = delete + parent_group->subgroup_delete_permission + domain->group_delete_permission + permission set_child_permission = set_child + parent_group->subgroup_set_child_permission + domain->group_set_child + permission set_parent_permission = set_parent + parent_group->subgroup_set_parent_permission + domain->group_set_parent + + permission manage_role_permission = manage_role + parent_group->subgroup_manage_role_permission + domain->group_manage_role_permission + permission add_role_users_permission = add_role_users + parent_group->subgroup_add_role_users_permission + domain->group_add_role_users_permission + permission remove_role_users_permission = remove_role_users + parent_group->subgroup_remove_role_users_permission + domain->group_remove_role_users_permission + permission view_role_users_permission = view_role_users + parent_group->subgroup_view_role_users_permission + domain->group_view_role_users_permission + + // Subgroup things permisssion + permission subgroup_thing_update_permission = subgroup_thing_update + parent_group->subgroup_thing_update_permission + permission subgroup_thing_read_permission = subgroup_thing_read + parent_group->subgroup_thing_read_permission + permission subgroup_thing_delete_permission = subgroup_thing_delete + parent_group->subgroup_thing_delete_permission + permission subgroup_thing_set_parent_group_permission = subgroup_thing_set_parent_group + parent_group->subgroup_thing_set_parent_group_permission + permission subgroup_thing_connect_to_channel_permission = subgroup_thing_connect_to_channel + parent_group->subgroup_thing_connect_to_channel_permission + + permission subgroup_thing_manage_role_permission = subgroup_thing_manage_role + parent_group->subgroup_thing_manage_role_permission + permission subgroup_thing_add_role_users_permission = subgroup_thing_add_role_users + parent_group->subgroup_thing_add_role_users_permission + permission subgroup_thing_remove_role_users_permission = subgroup_thing_remove_role_users + parent_group->subgroup_thing_remove_role_users_permission + permission subgroup_thing_view_role_users_permission = subgroup_thing_view_role_users + parent_group->subgroup_thing_view_role_users_permission + + // Group things permisssion + permission thing_create_permission = thing_create + parent_group->subgroup_thing_create + domain->thing_create_permission + permission thing_update_permission = thing_update + parent_group->subgroup_thing_update + domain->thing_update_permission + permission thing_read_permission = thing_read + parent_group->subgroup_thing_read + domain->thing_read_permission + permission thing_delete_permission = thing_delete + parent_group->subgroup_thing_delete + domain->thing_delete_permission + permission thing_set_parent_group_permission = thing_set_parent_group + parent_group->subgroup_thing_set_parent_group + domain->thing_set_parent_group_permission + permission thing_connect_to_channel_permission = thing_connect_to_channel + parent_group->subgroup_thing_connect_to_channel + domain->thing_connect_to_channel_permission + + permission thing_manage_role_permission = thing_manage_role + parent_group->subgroup_thing_manage_role + domain->thing_manage_role_permission + permission thing_add_role_users_permission = thing_add_role_users + parent_group->subgroup_thing_add_role_users + domain->thing_add_role_users_permission + permission thing_remove_role_users_permission = thing_remove_role_users + parent_group->subgroup_thing_remove_role_users + domain->thing_remove_role_users_permission + permission thing_view_role_users_permission = thing_view_role_users + parent_group->subgroup_thing_view_role_users + domain->thing_view_role_users_permission + + // Subgroup channels permisssion + permission subgroup_channel_update_permission = subgroup_channel_update + parent_group->subgroup_channel_update_permission + permission subgroup_channel_read_permission = subgroup_channel_read + parent_group->subgroup_channel_read_permission + permission subgroup_channel_delete_permission = subgroup_channel_delete + parent_group->subgroup_channel_delete_permission + permission subgroup_channel_set_parent_group_permission = subgroup_channel_set_parent_group + parent_group->subgroup_channel_set_parent_group_permission + permission subgroup_channel_connect_to_thing_permission = subgroup_channel_connect_to_thing + parent_group->subgroup_channel_connect_to_thing_permission + permission subgroup_channel_publish_permission = subgroup_channel_publish + parent_group->subgroup_channel_publish_permission + permission subgroup_channel_subscribe_permission = subgroup_channel_subscribe + parent_group->subgroup_channel_subscribe_permission + + permission subgroup_channel_manage_role_permission = subgroup_channel_manage_role + parent_group->subgroup_channel_manage_role_permission + permission subgroup_channel_add_role_users_permission = subgroup_channel_add_role_users + parent_group->subgroup_channel_add_role_users_permission + permission subgroup_channel_remove_role_users_permission = subgroup_channel_remove_role_users + parent_group->subgroup_channel_remove_role_users_permission + permission subgroup_channel_view_role_users_permission = subgroup_channel_view_role_users + parent_group->subgroup_channel_view_role_users_permission + + // Group channels permisssion + permission channel_create_permission = channel_create + parent_group->subgroup_channel_create_permission + domain->channel_create_permission + permission channel_update_permission = channel_update + parent_group->subgroup_channel_update + domain->channel_update_permission + permission channel_read_permission = channel_read + parent_group->subgroup_channel_read + domain->channel_read_permission + permission channel_delete_permission = channel_delete + parent_group->subgroup_channel_delete_permission + domain->channel_delete_permission + permission channel_set_parent_group_permission = channel_set_parent_group + parent_group->subgroup_channel_set_parent_group + domain->channel_set_parent_group_permission + permission channel_connect_to_thing_permission = channel_connect_to_thing + parent_group->subgroup_channel_connect_to_thing + domain->channel_connect_to_thing_permission + permission channel_publish_permission = channel_publish + parent_group->subgroup_channel_publish + domain->channel_publish_permission + permission channel_subscribe_permission = channel_subscribe + parent_group->subgroup_channel_subscribe + domain->channel_subscribe_permission + + permission channel_manage_role_permission = channel_manage_role + parent_group->subgroup_channel_manage_role + domain->channel_manage_role_permission + permission channel_add_role_users_permission = channel_add_role_users + parent_group->subgroup_channel_add_role_users + domain->channel_add_role_users_permission + permission channel_remove_role_users_permission = channel_remove_role_users + parent_group->subgroup_channel_remove_role_users + domain->channel_remove_role_users_permission + permission channel_view_role_users_permission = channel_view_role_users + parent_group->subgroup_channel_view_role_users + domain->channel_view_role_users_permission + + } definition domain { - relation administrator: user // combination domain + user id - relation editor: user - relation contributor: user - relation member: user - relation guest: user + //Replace platoform with organization in future + relation organization: platform + relation team: team + + relation update: role#member | team#member + relation enable: role#member | team#member + relation disable: role#member | team#member + relation membership: role#member | team#member + relation read: role#member | team#member + relation delete: role#member | team#member + + relation manage_role: role#member | team#member + relation add_role_users: role#member | team#member + relation remove_role_users: role#member | team#member + relation view_role_users: role#member | team#member + + relation thing_create: role#member | team#member + relation channel_create: role#member | team#member + relation group_create: role#member | team#member + + relation thing_update: role#member | team#member + relation thing_read: role#member | team#member + relation thing_delete: role#member | team#member + relation thing_set_parent_group: role#member | team#member + relation thing_connect_to_channel: role#member | team#member + relation thing_manage_role: role#member | team#member + relation thing_add_role_users: role#member | team#member + relation thing_remove_role_users: role#member | team#member + relation thing_view_role_users: role#member | team#member + + relation channel_update: role#member | team#member + relation channel_read: role#member | team#member + relation channel_delete: role#member | team#member + relation channel_set_parent_group: role#member | team#member + relation channel_connect_to_thing: role#member | team#member + relation channel_publish: role#member | team#member + relation channel_subscribe: role#member | team#member + + relation channel_manage_role: role#member | team#member + relation channel_add_role_users: role#member | team#member + relation channel_remove_role_users: role#member | team#member + relation channel_view_role_users: role#member | team#member + + relation group_update: role#member | team#member + relation group_membership: role#member | team#member + relation group_read: role#member | team#member + relation group_delete: role#member | team#member + relation group_set_child: role#member | team#member + relation group_set_parent: role#member | team#member + + relation group_manage_role: role#member | team#member + relation group_add_role_users: role#member | team#member + relation group_remove_role_users: role#member | team#member + relation group_view_role_users: role#member | team#member + + permission update_permission = update + team->domain_update + organization->admin + permission read_permission = read + team->domain_read + organization->admin + permission enable_permission = enable + team->domain_update + organization->admin + permission disable_permission = disable + team->domain_update + organization->admin + permission membership_permission = membership + team->domain_membership + organization->admin + permission delete_permission = delete + team->domain_delete + organization->admin + + permission manage_role_permission = manage_role + team->domain_manage_role + organization->admin + permission add_role_users_permission = add_role_users + team->domain_add_role_users + organization->admin + permission remove_role_users_permission = remove_role_users + team->domain_remove_role_users + organization->admin + permission view_role_users_permission = view_role_users + team->domain_view_role_users + organization->admin + + permission thing_create_permission = thing_create + team->thing_create + organization->admin + permission channel_create_permission = channel_create + team->channel_create + organization->admin + permission group_create_permission = group_create + team->group_create + organization->admin + + permission thing_update_permission = thing_update + team->thing_update + organization->admin + permission thing_read_permission = thing_read + team->thing_read + organization->admin + permission thing_delete_permission = thing_delete + team->thing_delete + organization->admin + permission thing_set_parent_group_permission = thing_set_parent_group + team->thing_set_parent_group + organization->admin + permission thing_connect_to_channel_permission = thing_connect_to_channel + team->thing_connect_to_channel + organization->admin + + permission thing_manage_role_permission = thing_manage_role + team->thing_manage_role + organization->admin + permission thing_add_role_users_permission = thing_add_role_users + team->thing_add_role_users + organization->admin + permission thing_remove_role_users_permission = thing_remove_role_users + team->thing_remove_role_users + organization->admin + permission thing_view_role_users_permission = thing_view_role_users + team->thing_view_role_users + organization->admin + + permission channel_update_permission = channel_update + team->channel_update + organization->admin + permission channel_read_permission = channel_read + team->channel_read + organization->admin + permission channel_delete_permission = channel_delete + team->channel_delete + organization->admin + permission channel_set_parent_group_permission = channel_set_parent_group + team->channel_set_parent_group + organization->admin + permission channel_connect_to_thing_permission = channel_connect_to_thing + team->channel_connect_to_thing + organization->admin + permission channel_publish_permission = channel_publish + team->channel_publish + organization->admin + permission channel_subscribe_permission = channel_subscribe + team->channel_subscribe + organization->admin + + permission channel_manage_role_permission = channel_manage_role + team->channel_manage_role + organization->admin + permission channel_add_role_users_permission = channel_add_role_users + team->channel_add_role_users + organization->admin + permission channel_remove_role_users_permission = channel_remove_role_users + team->channel_remove_role_users + organization->admin + permission channel_view_role_users_permission = channel_view_role_users + team->channel_view_role_users + organization->admin + + permission group_update_permission = group_update + team->group_update + organization->admin + permission group_membership_permission = group_membership + team->group_membership + organization->admin + permission group_read_permission = group_read + team->group_read + organization->admin + permission group_delete_permission = group_delete + team->group_delete + organization->admin + permission group_set_child_permission = group_set_child + team->group_set_child + organization->admin + permission group_set_parent_permission = group_set_parent + team->group_set_parent + organization->admin + + permission group_manage_role_permission = group_manage_role + team->group_manage_role + organization->admin + permission group_add_role_users_permission = group_add_role_users + team->group_add_role_users + organization->admin + permission group_remove_role_users_permission = group_remove_role_users + team->group_remove_role_users + organization->admin + permission group_view_role_users_permission = group_view_role_users + team->group_view_role_users + organization->admin + +} + +// Add this realtion and permission in future while adding orgnaization +definition team { + relation organization: organization + relation parent_team: team + + relation delete: role#member + relation enable: role#member | team#member + relation disable: role#member | team#member + relation update: role#member + relation read: role#member + + relation set_parent: role#member + relation set_child: role#member + + relation member: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + relation subteam_delete: role#member + relation subteam_update: role#member + relation subteam_read: role#member + + relation subteam_member: role#member + + relation subteam_set_child: role#member + relation subteam_set_parent: role#member + + relation subteam_manage_role: role#member + relation subteam_add_role_users: role#member + relation subteam_remove_role_users: role#member + relation subteam_view_role_users: role#member + + // Domain related permission + + relation domain_update: role#member | team#member + relation domain_read: role#member | team#member + relation domain_membership: role#member | team#member + relation domain_delete: role#member | team#member + + relation domain_manage_role: role#member | team#member + relation domain_add_role_users: role#member | team#member + relation domain_remove_role_users: role#member | team#member + relation domain_view_role_users: role#member | team#member + + relation thing_create: role#member | team#member + relation channel_create: role#member | team#member + relation group_create: role#member | team#member + + relation thing_update: role#member | team#member + relation thing_read: role#member | team#member + relation thing_delete: role#member | team#member + relation thing_set_parent_group: role#member | team#member + relation thing_connect_to_channel: role#member | team#member + + relation thing_manage_role: role#member | team#member + relation thing_add_role_users: role#member | team#member + relation thing_remove_role_users: role#member | team#member + relation thing_view_role_users: role#member | team#member + + relation channel_update: role#member | team#member + relation channel_read: role#member | team#member + relation channel_delete: role#member | team#member + relation channel_set_parent_group: role#member | team#member + relation channel_connect_to_thing: role#member | team#member + relation channel_publish: role#member | team#member + relation channel_subscribe: role#member | team#member + + relation channel_manage_role: role#member | team#member + relation channel_add_role_users: role#member | team#member + relation channel_remove_role_users: role#member | team#member + relation channel_view_role_users: role#member | team#member + + relation group_update: role#member | team#member + relation group_membership: role#member | team#member + relation group_read: role#member | team#member + relation group_delete: role#member | team#member + relation group_set_child: role#member | team#member + relation group_set_parent: role#member | team#member + + relation group_manage_role: role#member | team#member + relation group_add_role_users: role#member | team#member + relation group_remove_role_users: role#member | team#member + relation group_view_role_users: role#member | team#member + + permission delete_permission = delete + organization->team_delete + parent_team->subteam_delete + organization->admin + permission update_permission = update + organization->team_update + parent_team->subteam_update + organization->admin + permission read_permission = read + organization->team_read + parent_team->subteam_read + organization->admin + + permission set_parent_permission = set_parent + organization->team_set_parent + parent_team->subteam_set_parent + organization->admin + permission set_child_permisssion = set_child + organization->team_set_child + parent_team->subteam_set_child + organization->admin + + permission membership = member + organization->team_member + parent_team->subteam_member + organization->admin + + permission manage_role_permission = manage_role + organization->team_manage_role + parent_team->subteam_manage_role + organization->admin + permission add_role_users_permission = add_role_users + organization->team_add_role_users + parent_team->subteam_add_role_users + organization->admin + permission remove_role_users_permission = remove_role_users + organization->team_remove_role_users + parent_team->subteam_remove_role_users + organization->admin + permission view_role_users_permission = view_role_users + organization->team_view_role_users + parent_team->subteam_view_role_users + organization->admin +} + + +definition organization { relation platform: platform + relation administrator: user + + relation delete: role#member + relation update: role#member + relation read: role#member + + relation member: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + relation team_create: role#member + + relation team_delete: role#member + relation team_update: role#member + relation team_read: role#member + + relation team_member: role#member // Will be member of all the teams in the organization - permission admin = administrator + platform->admin - permission edit = admin + editor - permission share = edit - permission view = edit + contributor + guest - permission membership = view + member - permission create = membership - guest + relation team_set_child: role#member + relation team_set_parent: role#member + + relation team_manage_role: role#member + relation team_add_role_users: role#member + relation team_remove_role_users: role#member + relation team_view_role_users: role#member + + permission admin = administrator + platform->administrator + permission delete_permission = admin + delete->member + permission update_permission = admin + update->member + permission read_permission = admin + read->member + + permission membership = admin + member->member + + permission team_create_permission = admin + team_create->member + + permission manage_role_permission = admin + manage_role + permission add_role_users_permisson = admin + add_role_users + permission remove_role_users_permission = admin + remove_role_users + permission view_role_users_permission = admin + view_role_users } + definition platform { relation administrator: user relation member: user diff --git a/docker/spicedb/schema_V1.zed b/docker/spicedb/schema_V1.zed new file mode 100644 index 0000000000..8b0f0c23ce --- /dev/null +++ b/docker/spicedb/schema_V1.zed @@ -0,0 +1,193 @@ +definition user {} + +definition thing { + relation administrator: user + relation group: group + relation domain: domain + + permission admin = administrator + group->admin + domain->admin + permission delete = admin + permission edit = admin + group->edit + domain->edit + permission view = edit + group->view + domain->view + permission share = edit + + // These permission are made for only list purpose. It helps to list users have only particular permission excluding other higher and lower permission. + permission admin_only = admin + permission edit_only = edit - admin + permission view_only = view + + // These permission are made for only list purpose. It helps to list users from external, users who are not in group but have permission on the group through parent group + permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group +} + +definition channel { + relation administrator: user + relation editor: user + relation contributor: user + relation member: user + relation guest: user + + relation publisher: thing | user + relation subscriber: thing | user + relation publisher_subscriber: thing | user + + relation parent_group: group + relation domain: domain + + permission admin = administrator + parent_group->admin + domain->admin + permission delete = admin + permission edit = admin + editor + parent_group->edit + domain->edit + permission share = edit + permission view = contributor + edit + parent_group->view + domain->view + guest + permission membership = view + member + permission create = membership - guest + + permission publish = publisher + edit + publisher_subscriber + permission subscribe = subscriber + view + publisher_subscriber + + // These permissions are made for listing purposes. They enable listing users who have only particular permission excluding higher-level permissions users. + permission admin_only = admin + permission edit_only = edit - admin + permission view_only = view + permission membership_only = membership - view + + // These permission are made for only list purpose. They enable listing users who have only particular permission from parent group excluding higher-level permissions. + permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group + permission ext_edit = edit - editor // For list of external edit , not having direct relation with group, but have indirect relation from parent group + permission ext_view = view - contributor // For list of external view , not having direct relation with group, but have indirect relation from parent group +} + +definition group { + relation administrator: user + relation editor: user + relation contributor: user + relation member: user + relation guest: user + + relation parent_group: group + relation domain: domain + + permission admin = administrator + parent_group->admin + domain->admin + permission delete = admin + permission edit = admin + editor + parent_group->edit + domain->edit + permission share = edit + permission view = contributor + edit + parent_group->view + domain->view + guest + permission membership = view + member + permission create = membership - guest + + // These permissions are made for listing purposes. They enable listing users who have only particular permission excluding higher-level permissions users. + permission admin_only = admin + permission edit_only = edit - admin + permission view_only = view + permission membership_only = membership - view + + // These permission are made for only list purpose. They enable listing users who have only particular permission from parent group excluding higher-level permissions. + permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group + permission ext_edit = edit - editor // For list of external edit , not having direct relation with group, but have indirect relation from parent group + permission ext_view = view - contributor // For list of external view , not having direct relation with group, but have indirect relation from parent group +} + +definition domain { + relation organization: organization + relation org_group: organization_group#membership + + // Collaborator can be a user or an organization group member + relation collabrator_adminstrator: user | organization_group#membership // collabrator organization group + relation collabrator_editor: user | organization_group#membership // collabrator organization group + relation collabrator_contributor: user | organization_group#membership // collabrator organization group + relation collabrator_member: user | organization_group#membership // collabrator organization group + relation collabrator_guest: user | organization_group#membership // collabrator organization group + + permission admin = organization->admin + org_group->admin + collabrator_adminstrator->admin + permission edit = admin + org_group->edit + collabrator_adminstrator->edit + collabrator_editor->edit + permission share = edit + permission view = edit + org_group->view + collabrator_adminstrator->view + collabrator_editor->view + collabrator_guest->view + permission membership = view + org_group->membership + collabrator_adminstrator->membership + collabrator_editor->membership + permission create = membership - org_group->guest + collabrator_adminstrator->guest + collabrator_editor->guest +} + +definition organization_group { + // users should be withing the organization , user outside the organization could not have thes following relations + relation ou_administrator: user + relation ou_editor: user + relation ou_contributor: user + relation ou_delegate: user + relation ou_guest: user + relation ou_scoped_guest: user + + // Internal organiztion groups + relation iog_admininstrator: organization_group#membership + relation iog_editor: organization_group#membership + relation iog_contributor: organization_group#membership + relation iog_delegate: organization_group#membership + relation iog_guest: organization_group#membership + relation iog_scoped_guest: organization_group#membership + + // An organization_group can have only one organization + relation organization: organization + + // External Users relatiuons + relation eu_administrator: user + relation eu_editor: user + relation eu_contributor: user + relation eu_delegate: user + relation eu_guest: user + relation eu_scoped_guest: user + + + // External organization group + relation eog_admininstrator: organization_group#membership + relation eog_editor: organization_group#membership + relation eog_contributor: organization_group#membership + relation eog_delegate: organization_group#membership + relation eog_guest: organization_group#membership + relation eog_scoped_guest: organization_group#membership + + permission admin = organization->admin + + ou_administrator + iog_admininstrator->admin + + eu_administrator + eog_admininstrator->admin + + permission edit = admin + ou_editor + iog_editor->edit + + eu_editor + eog_editor->edit + + iog_admininstrator->edit + eog_admininstrator->edit + + permission share = edit + + permission contribute = edit + ou_contributor + iog_contributor->contribute + + eu_contributor + eog_contributor->contribute + + iog_admininstrator->contribute + eog_admininstrator->contribute + + iog_editor->contribute + eog_editor->contribute + + permission create = contribute + ou_delegate + iog_delegate->create + + eu_delegate + eog_delegate->create + + iog_admininstrator->create + eog_admininstrator->create + + iog_editor->create + eog_editor->create + + iog_contributor-create + eog_contributor->create + + + permission view = contribute + ou_guest + iog_guest->contribute + + eu_guest + eog_guest->contribute + + permission membership = create + ou_guest + iog_guest->contribute + + eu_guest + eog_guest->contribute + + ou_scoped_guest + eu_scoped_guest + + iog_scoped_guest->contribute + eog_scoped_guest->contribute +} + +definition organization { + relation administrator: user + relation member: user + + relation platform: platform + + permission admin = administrator + platform->admin + permission membership = admin + member +} + +definition platform { + relation administrator: user + relation member: user + + permission admin = administrator + permission membership = administrator + member +} diff --git a/docker/spicedb/schemav2.zed b/docker/spicedb/schemav2.zed new file mode 100644 index 0000000000..cb050d5f42 --- /dev/null +++ b/docker/spicedb/schemav2.zed @@ -0,0 +1,176 @@ +definition user {} + + +definition thing { + relation administrator: user + + + relation parent_group: group + + relation add_role: role#member + relation edit_role: role#member + relation delete_role: role#member + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + relation set_parent: role#member + relation connect_to_channel: role#member + + permission admin = administrator + + permission update_perm = admin + update + parent_group->update_thing + permission share_perm = admin + share + parent_group->share_thing + permission read_perm = admin + read + parent_group->read_thing + permission delete_perm = admin + delete + parent_group->delete_thing + permission connect_to_channel_perm = admin + connect_to_channel + parent_group->connect_thing_to_channel + + +} + +definition channel { + relation administrator: user + + + relation parent_group: group + + relation add_role: role#member + relation edit_role: role#member + relation delete_role: role#member + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + relation set_parent: role#member + relation connect_to_thing: role#member + relation publish: role#member | thing + relation subscribe: role#member | thing + + permission admin = administrator + + permission update_perm = admin + update + parent_group->update_thing_permission + permission share_perm = admin + share + parent_group->update_thing_permission + permission read_perm = admin + read + parent_group->read_thing_permission + permission delete_perm = admin + delete + parent_group->delete_thing_permission + permission connect_to_channel_perm = admin + connect_to_thing + parent_group->connect_to_channel_permission +} + +definition group { + + relation domain: domain // This can't be clubed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain + relation parent_group: group + + relation add_role: role#member + relation edit_role: role#member + relation delete_role: role#member + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + relation set_child: role#member + relation set_parent: role#member + + relation create_thing: role#member + relation update_thing: role#member + relation share_thing: role#member + relation read_thing: role#member + relation delete_thing: role#member + relation set_parent_group_thing: role#member + relation connect_thing_to_channel: role#member + + relation create_channel: role#member + relation update_channel: role#member + relation share_channel: role#member + relation read_channel: role#member + relation delete_channel: role#member + relation set_parent_group_channel: role#member + relation connect_to_thing: role#member + relation publish_channel: role#member + relation subscribe_channel: role#member + + relation create_subgroup: role#member // this allows to add parent for group during the new group creation + relation update_subgroup: role#member + relation share_subgroup: role#member + relation read_subgroup: role#member + relation delete_subgroup: role#member + relation set_child_subgroup: role#member + relation set_parent_subgroup: role#member + + + + permission update_perm = update + parent_group->update_subgroup + domain->update_group + permission share_perm = share parent_group->share_subgroup + domain->share_group + permission read_perm = read + parent_group->read_subgroup + domain->read_group + permission delete_perm = delete + parent_group->delete_subgroup + domain->delete_group + permission set_child_perm = set_child + parent_group->set_child_subgroup + domain->set_child_group + permission set_parent_perm = set_parent + parent_group->set_parent_subgroup + domain->set_parent_group + + + permission create_thing_permission = create_thing + parent_group->create_thing_permission + permission update_thing_permission = update_thing + parent_group->update_thing_permission + permission share_thing_permission = share_thing + parent_group->share_thing_permission + permission read_thing_permission = read_thing + parent_group->read_thing_permission + permission delete_thing_permission = delete_thing + parent_group->delete_thing_permission + permission set_parent_group_thing_permission = set_parent_group_thing + parent_group->set_parent_group_thing_permission + permission connect_thing_to_channel_permission = connect_thing_to_channel + + +} + +definition domain { + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + + relation add_role: role#member + relation edit_role: role#member + relation delete_role: role#member + + relation create_thing: role#member + relation update_thing: role#member + relation share_thing: role#member + relation read_thing: role#member + relation delete_thing: role#member + relation set_parent_group_thing: role#member + relation connect_to_channel: role#member + + relation create_channel: role#member + relation update_channel: role#member + relation share_channel: role#member + relation read_channel: role#member + relation delete_channel: role#member + relation set_parent_group_channel: role#member + relation connect_to_thing: role#member + relation publish_channel: role#member + relation subscribe_channel: role#member + + relation create_group: role#member + relation update_group: role#member + relation share_group: role#member + relation read_group: role#member + relation delete_group: role#member + relation set_child_group: role#member + relation set_parent_group: role#member + +} + + +definition role { + relation entity: domain | group | channel | thing + relation member: user +} + + + +definition platform { + relation administrator: user + relation member: user + + permission admin = administrator + permission membership = administrator + member +} diff --git a/docker/spicedb/schemav3.zed b/docker/spicedb/schemav3.zed new file mode 100644 index 0000000000..01f3ac2123 --- /dev/null +++ b/docker/spicedb/schemav3.zed @@ -0,0 +1,284 @@ +definition user {} + + +definition thing { + + + relation domain: domain + relation parent_group: group + + relation role_manager: role#member + relation manage_role_users: role#member + relation view_role_users: role#member + + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + relation set_parent: role#member + relation connect_to_channel: role#member + + + permission update_perm = parent_group->update_thing + domain->update_thing + update + permission share_perm = share + parent_group->share_thing + domain->share_thing + permission read_perm = read + parent_group->read_thing + domain->read_thing + permission delete_perm = delete + parent_group->delete_thing + domain->delete_thing + permission connect_to_channel_perm = connect_to_channel + parent_group->connect_thing_to_channel + domain->connect_thing_to_channel + + +} + +definition channel { + + + relation parent_group: group + + relation role_manager: role#member + relation manage_role_users: role#member + relation view_role_users: role#member + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + relation set_parent: role#member + relation connect_to_thing: role#member + relation publish: role#member | thing + relation subscribe: role#member | thing + + + permission update_perm = update + parent_group->update_thing_permission + permission share_perm = share + parent_group->update_thing_permission + permission read_perm = read + parent_group->read_thing_permission + permission delete_perm = delete + parent_group->delete_thing_permission + permission connect_to_channel_perm = connect_to_thing + parent_group->connect_to_channel_permission +} + +definition group { + + relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain + relation parent_group: group + + relation role_manager: role#member + relation manage_role_users: role#member + relation view_role_users: role#member + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + relation set_child: role#member + relation set_parent: role#member + + relation create_thing: role#member + relation update_thing: role#member + relation share_thing: role#member + relation read_thing: role#member + relation delete_thing: role#member + relation set_parent_group_thing: role#member + relation connect_thing_to_channel: role#member + + relation create_channel: role#member + relation update_channel: role#member + relation share_channel: role#member + relation read_channel: role#member + relation delete_channel: role#member + relation set_parent_group_channel: role#member + relation connect_to_thing: role#member + relation publish_channel: role#member + relation subscribe_channel: role#member + + relation create_subgroup: role#member // this allows to add parent for group during the new group creation + relation update_subgroup: role#member + relation share_subgroup: role#member + relation read_subgroup: role#member + relation delete_subgroup: role#member + relation set_child_subgroup: role#member + relation set_parent_subgroup: role#member + + + + permission update_perm = update + parent_group->update_subgroup + domain->update_group + permission share_perm = share + parent_group->share_subgroup + domain->share_group + permission read_perm = read + parent_group->read_subgroup + domain->read_group + permission delete_perm = delete + parent_group->delete_subgroup + domain->delete_group + permission set_child_perm = set_child + parent_group->set_child_subgroup + domain->set_child_group + permission set_parent_perm = set_parent + parent_group->set_parent_subgroup + domain->set_parent_group + + + permission create_thing_permission = create_thing + parent_group->create_thing_permission + permission update_thing_permission = update_thing + parent_group->update_thing_permission + permission share_thing_permission = share_thing + parent_group->share_thing_permission + permission read_thing_permission = read_thing + parent_group->read_thing_permission + permission delete_thing_permission = delete_thing + parent_group->delete_thing_permission + permission set_parent_group_thing_permission = set_parent_group_thing + parent_group->set_parent_group_thing_permission + permission connect_thing_to_channel_permission = connect_thing_to_channel + + +} + +definition domain { + + relation update: role#member + relation share: role#member + relation read: role#member + relation delete: role#member + + relation role_manager: role#member + relation manage_role_users: role#member + + relation create_thing: role#member + relation update_thing: role#member + relation share_thing: role#member + relation read_thing: role#member + relation delete_thing: role#member + relation set_parent_group_thing: role#member + relation connect_to_channel: role#member + + relation create_channel: role#member + relation update_channel: role#member + relation share_channel: role#member + relation read_channel: role#member + relation delete_channel: role#member + relation set_parent_group_channel: role#member + relation connect_to_thing: role#member + relation publish_channel: role#member + relation subscribe_channel: role#member + + relation create_group: role#member + relation update_group: role#member + relation share_group: role#member + relation read_group: role#member + relation delete_group: role#member + relation set_child_group: role#member + relation set_parent_group: role#member + +} + + +definition role { + relation entity: domain | group | channel | thing + relation member: user | team + relation built_in_role: domain | group | channel | thing + + permission delete = entity->manage_role_perm - built_in_role->manage_role_perm + permission update = entity->manage_role_perm - built_in_role->manage_role_perm + permission read = entity->manage_role_perm - built_in_role->manage_role_perm + + permission add_user = entity->add_role_users_perm + permission remove_user = entity->remove_role_users_perm + permission view_user = entity->view_role_users_perm + + permission add_permission = entity->manage_role_perm - built_in_role->manage_role_perm + permission remove_permission = entity->manage_role_perm - built_in_role->manage_role_perm +} + + + + +definition team { + + relation organization: organization + relation parent_team: team + + relation delete: role#member + relation update: role#member + relation read: role#member + + relation set_parent : role#member + relation set_child : role#member + + relation member: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + + relation delete_subteam: role#member + relation update_subteam: role#member + relation read_subteam: role#member + + relation member_subteam: role#member + + relation set_child_subteam: role#member + relation set_parent_subteam: role#member + + relation manage_role_subteam: role#member + relation add_role_users_subteam: role#member + relation remove_role_users_subteam: role#member + relation view_role_users_subteam: role#member + + permission delete_perm = delete->member + organization->delete_team + parent_team->delete_subteam + permission update_perm = update->member + organization->update_team + parent_team->update_team + permission read_perm = read->member + organization->read_group + parent_team->read_team + + permission set_parent_perm = set_parent->member + organization->set_parent_team + parent_team->set_parent_subteam + permission set_child_perm = set_child->member + organization->set_child_team + parent_team->set_child_subteam + + permission membership = member->member + organization->member_team + parent_team->member_subteam + + permission manage_role_perm = manage_role + organization->manage_role_team + parent_team->manage_role_subteam + permission add_role_users_perm = add_role_users + organization->add_role_users_team + parent_team->add_role_users_subteam + permission remove_role_users_perm = remove_role_users + organization->remove_role_users_team + parent_team->remove_role_users_subteam + permission view_role_users_perm = view_role_users + organization->view_role_users_team + parent_team->view_role_users_subteam +} + + +definition organization { + + relation platform: platform + + relation delete: role#member + relation update: role#member + relation read: role#member + + relation member: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + relation create_team : role#member + + relation delete_team: role#member + relation update_team: role#member + relation read_team: role#member + + relation member_team: role#member + + relation set_child_team: role#member + relation set_parent_team: role#member + + relation manage_role_team: role#member + relation add_role_users_team: role#member + relation remove_role_users_team: role#member + relation view_role_users_team: role#member + + permission delete_perm = delete->member + permission update_perm = update->member + permission read_perm = read->member + + permission membership = member->member + + permission create_team_perm = create_team->member + + permission manage_role_perm = manage_role + permission add_role_users_perm = add_role_users + permission remove_role_users_perm = remove_role_users + permission view_role_users_perm = view_role_users + +} + + +definition platform { + relation administrator: user + relation member: user + + permission admin = administrator + permission membership = administrator + member +}definition user {} + diff --git a/docker/spicedb/schemav4.zed b/docker/spicedb/schemav4.zed new file mode 100644 index 0000000000..9de6ceb0a7 --- /dev/null +++ b/docker/spicedb/schemav4.zed @@ -0,0 +1,492 @@ +definition user {} + + +definition role { + relation entity: domain | group | channel | thing + relation member: user + relation built_in_role: domain | group | channel | thing + + permission delete = entity->manage_role_permission - built_in_role->manage_role_permission + permission update = entity->manage_role_permission - built_in_role->manage_role_permission + permission read = entity->manage_role_permission - built_in_role->manage_role_permission + + permission add_user = entity->add_role_users_permission + permission remove_user = entity->remove_role_users_permission + permission view_user = entity->view_role_users_permission + + +} + +definition thing { + relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain + relation parent_group: group + + relation update: role#member + relation read: role#member + relation delete: role#member + relation set_parent_group: role#member + relation connect_to_channel: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + permission update_permission = update + parent_group->thing_update_permission + domain->thing_update_permission + permission read_permission = read + parent_group->thing_read_permission + domain->thing_read_permission + permission delete_permission = delete + parent_group->thing_delete_permission + domain->thing_delete_permission + permission set_parent_group_permission = set_parent_group + parent_group->thing_set_parent_group_permission + domain->thing_set_parent_group_permission + permission connect_to_channel_permission = connect_to_channel + parent_group->thing_connect_to_channel + domain->thing_connect_to_channel_permission + + permission manage_role_permission = manage_role + parent_group->thing_manage_role_permission + domain->thing_manage_role_permission + permission add_role_users_permission = add_role_users + parent_group->thing_add_role_users_permission + domain->thing_add_role_users_permission + permission remove_role_users_permission = remove_role_users + parent_group->thing_remove_role_users_permission + domain->thing_remove_role_users_permission + permission view_role_users_permission = view_role_users + parent_group->thing_view_role_users_permission + domain->thing_view_role_users_permission +} + +definition channel { + relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain + relation parent_group: group + + relation update: role#member + relation read: role#member + relation delete: role#member + relation set_parent_group: role#member + relation connect_to_thing: role#member + relation publish: role#member | thing + relation subscribe: role#member | thing + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + permission update_permission = update + parent_group->channel_update_permission + domain->channel_update_permission + permission read_permission = read + parent_group->channel_read_permission + domain->channel_read_permission + permission delete_permission = delete + parent_group->channel_delete_permission + domain->channel_delete_permission + permission set_parent_group_permission = set_parent_group + parent_group->channel_set_parent_group_permission + domain->channel_set_parent_group_permission + permission connect_to_thing_permission = connect_to_thing + parent_group->channel_connect_to_thing_permission + domain->channel_connect_to_thing + permission publish_permission = publish + parent_group->channel_publish_permission + domain->channel_publish_permission + permission subscribe_permission = subscribe + parent_group->channel_subscribe_permission + domain->channel_subscribe_permission + + permission manage_role_permission = manage_role + parent_group->channel_manage_role_permission + domain->channel_manage_role_permission + permission add_role_users_permission = add_role_users + parent_group->channel_add_role_users_permission + domain->channel_add_role_users_permission + permission remove_role_users_permission = remove_role_users + parent_group->channel_remove_role_users_permission + domain->channel_remove_role_users_permission + permission view_role_users_permission = view_role_users + parent_group->channel_view_role_users_permission + domain->channel_view_role_users_permission +} + +definition group { + relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it is safe to add domain + relation parent_group: group + + relation update: role#member + relation read: role#member + relation delete: role#member + relation set_child: role#member + relation set_parent: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + relation thing_create: role#member + relation thing_update: role#member + relation thing_read: role#member + relation thing_delete: role#member + relation thing_set_parent_group: role#member + relation thing_connect_to_channel: role#member + + relation thing_manage_role: role#member + relation thing_add_role_users: role#member + relation thing_remove_role_users: role#member + relation thing_view_role_users: role#member + + relation channel_create: role#member + relation channel_update: role#member + relation channel_read: role#member + relation channel_delete: role#member + relation channel_set_parent_group: role#member + relation channel_connect_to_thing: role#member + relation channel_publish: role#member + relation channel_subscribe: role#member + + relation channel_manage_role: role#member + relation channel_add_role_users: role#member + relation channel_remove_role_users: role#member + relation channel_view_role_users: role#member + + relation subgroup_create: role#member // this allows to add parent for group during the new group creation + relation subgroup_update: role#member + relation subgroup_read: role#member + relation subgroup_delete: role#member + relation subgroup_set_child: role#member + relation subgroup_set_parent: role#member + + relation subgroup_manage_role: role#member + relation subgroup_add_role_users: role#member + relation subgroup_remove_role_users: role#member + relation subgroup_view_role_users: role#member + + relation subgroup_thing_create: role#member + relation subgroup_thing_update: role#member + relation subgroup_thing_read: role#member + relation subgroup_thing_delete: role#member + relation subgroup_thing_set_parent_group: role#member + relation subgroup_thing_connect_to_channel: role#member + + relation subgroup_thing_manage_role: role#member + relation subgroup_thing_add_role_users: role#member + relation subgroup_thing_remove_role_users: role#member + relation subgroup_thing_view_role_users: role#member + + relation subgroup_channel_create: role#member + relation subgroup_channel_update: role#member + relation subgroup_channel_read: role#member + relation subgroup_channel_delete: role#member + relation subgroup_channel_set_parent_group: role#member + relation subgroup_channel_connect_to_thing: role#member + relation subgroup_channel_publish: role#member + relation subgroup_channel_subscribe: role#member + + relation subgroup_channel_manage_role: role#member + relation subgroup_channel_add_role_users: role#member + relation subgroup_channel_remove_role_users: role#member + relation subgroup_channel_view_role_users: role#member + + // Subgroup permission + permission subgroup_create_permission = subgroup_create + parent_group->subgroup_create_permission + permission subgroup_update_permission = subgroup_update + parent_group->subgroup_update_permission + permission subgroup_read_permission = subgroup_read + parent_group->subgroup_read_permission + permission subgroup_delete_permission = subgroup_delete + parent_group->subgroup_delete_permission + permission subgroup_set_child_permission = subgroup_set_child + parent_group->subgroup_set_child_permission + permission subgroup_set_parent_permission = subgroup_set_parent + parent_group->subgroup_set_parent_permission + + permission subgroup_manage_role_permission = subgroup_manage_role + parent_group->subgroup_manage_role_permission + permission subgroup_add_role_users_permission = subgroup_add_role_users + parent_group->subgroup_add_role_users_permission + permission subgroup_remove_role_users_permission = subgroup_remove_role_users + parent_group->subgroup_remove_role_users_permission + permission subgroup_view_role_users_permission = subgroup_view_role_users + parent_group->subgroup_view_role_users_permission + + // Group permission + permission update_permission = update + parent_group->subgroup_create_permission + domain->group_update_permission + permission read_permission = read + parent_group->subgroup_read_permission + domain->group_read_permission + permission delete_permission = delete + parent_group->subgroup_delete_permission + domain->group_delete_permission + permission set_child_permission = set_child + parent_group->subgroup_set_child_permission + domain->group_set_child + permission set_parent_permission = set_parent + parent_group->subgroup_set_parent_permission + domain->group_set_parent + + permission manage_role_permission = manage_role + parent_group->subgroup_manage_role_permission + domain->group_manage_role_permission + permission add_role_users_permission = add_role_users + parent_group->subgroup_add_role_users_permission + domain->group_add_role_users_permission + permission remove_role_users_permission = remove_role_users + parent_group->subgroup_remove_role_users_permission + domain->group_remove_role_users_permission + permission view_role_users_permission = view_role_users + parent_group->subgroup_view_role_users_permission + domain->group_view_role_users_permission + + // Subgroup things permisssion + permission subgroup_thing_create_permission = subgroup_thing_create + parent_group->subgroup_thing_create_permission + permission subgroup_thing_update_permission = subgroup_thing_update + parent_group->subgroup_thing_update_permission + permission subgroup_thing_read_permission = subgroup_thing_read + parent_group->subgroup_thing_read_permission + permission subgroup_thing_delete_permission = subgroup_thing_delete + parent_group->subgroup_thing_delete_permission + permission subgroup_thing_set_parent_group_permission = subgroup_thing_set_parent_group + parent_group->subgroup_thing_set_parent_group_permission + permission subgroup_thing_connect_to_channel_permission = subgroup_thing_connect_to_channel + parent_group->subgroup_thing_connect_to_channel_permission + + permission subgroup_thing_manage_role_permission = subgroup_thing_manage_role + parent_group->subgroup_thing_manage_role_permission + permission subgroup_thing_add_role_users_permission = subgroup_thing_add_role_users + parent_group->subgroup_thing_add_role_users_permission + permission subgroup_thing_remove_role_users_permission = subgroup_thing_remove_role_users + parent_group->subgroup_thing_remove_role_users_permission + permission subgroup_thing_view_role_users_permission = subgroup_thing_view_role_users + parent_group->subgroup_thing_view_role_users_permission + + // Group things permisssion + permission thing_create_permission = thing_create + parent_group->subgroup_thing_create + domain->thing_create_permission + permission thing_update_permission = thing_update + parent_group->subgroup_thing_update + domain->thing_update_permission + permission thing_read_permission = thing_read + parent_group->subgroup_thing_read + domain->thing_read_permission + permission thing_delete_permission = thing_delete + parent_group->subgroup_thing_delete + domain->thing_delete_permission + permission thing_set_parent_group_permission = thing_set_parent_group + parent_group->subgroup_thing_set_parent_group + domain->thing_set_parent_group_permission + permission thing_connect_to_channel_permission = thing_connect_to_channel + parent_group->subgroup_thing_connect_to_channel + domain->thing_connect_to_channel_permission + + permission thing_manage_role_permission = thing_manage_role + parent_group->subgroup_thing_manage_role + domain->thing_manage_role_permission + permission thing_add_role_users_permission = thing_add_role_users + parent_group->subgroup_thing_add_role_users + domain->thing_add_role_users_permission + permission thing_remove_role_users_permission = thing_remove_role_users + parent_group->subgroup_thing_remove_role_users + domain->thing_remove_role_users_permission + permission thing_view_role_users_permission = thing_view_role_users + parent_group->subgroup_thing_view_role_users + domain->thing_view_role_users_permission + + // Subgroup channels permisssion + permission subgroup_channel_create_permission = subgroup_channel_create + parent_group->subgroup_channel_create_permission + permission subgroup_channel_update_permission = subgroup_channel_update + parent_group->subgroup_channel_update_permission + permission subgroup_channel_read_permission = subgroup_channel_read + parent_group->subgroup_channel_read_permission + permission subgroup_channel_delete_permission = subgroup_channel_delete + parent_group->subgroup_channel_delete_permission + permission subgroup_channel_set_parent_group_permission = subgroup_channel_set_parent_group + parent_group->subgroup_channel_set_parent_group_permission + permission subgroup_channel_connect_to_thing_permission = subgroup_channel_connect_to_thing + parent_group->subgroup_channel_connect_to_thing_permission + permission subgroup_channel_publish_permission = subgroup_channel_publish + parent_group->subgroup_channel_publish_permission + permission subgroup_channel_subscribe_permission = subgroup_channel_subscribe + parent_group->subgroup_channel_subscribe_permission + + permission subgroup_channel_manage_role_permission = subgroup_channel_manage_role + parent_group->subgroup_channel_manage_role_permission + permission subgroup_channel_add_role_users_permission = subgroup_channel_add_role_users + parent_group->subgroup_channel_add_role_users_permission + permission subgroup_channel_remove_role_users_permission = subgroup_channel_remove_role_users + parent_group->subgroup_channel_remove_role_users_permission + permission subgroup_channel_view_role_users_permission = subgroup_channel_view_role_users + parent_group->subgroup_channel_view_role_users_permission + + // Group channels permisssion + permission channel_create_permission = channel_create + parent_group->subgroup_channel_create_permission + domain->channel_create_permission + permission channel_update_permission = channel_update + parent_group->subgroup_channel_update + domain->channel_update_permission + permission channel_read_permission = channel_read + parent_group->subgroup_channel_read + domain->channel_read_permission + permission channel_delete_permission = channel_delete + parent_group->subgroup_channel_delete_permission + domain->channel_delete_permission + permission channel_set_parent_group_permission = channel_set_parent_group + parent_group->subgroup_channel_set_parent_group + domain->channel_set_parent_group_permission + permission channel_connect_to_thing_permission = channel_connect_to_thing + parent_group->subgroup_channel_connect_to_thing + domain->channel_connect_to_thing_permission + permission channel_publish_permission = channel_publish + parent_group->subgroup_channel_publish + domain->channel_publish_permission + permission channel_subscribe_permission = channel_subscribe + parent_group->subgroup_channel_subscribe + domain->channel_subscribe_permission + + permission channel_manage_role_permission = channel_manage_role + parent_group->subgroup_channel_manage_role + domain->channel_manage_role_permission + permission channel_add_role_users_permission = channel_add_role_users + parent_group->subgroup_channel_add_role_users + domain->channel_add_role_users_permission + permission channel_remove_role_users_permission = channel_remove_role_users + parent_group->subgroup_channel_remove_role_users + domain->channel_remove_role_users_permission + permission channel_view_role_users_permission = channel_view_role_users + parent_group->subgroup_channel_view_role_users + domain->channel_view_role_users_permission + + +} + +definition domain { + relation organization: organization + relation team: team + + relation update: role#member | team#member + relation read: role#member | team#member + relation delete: role#member | team#member + + relation manage_role: role#member | team#member + relation add_role_users: role#member | team#member + relation remove_role_users: role#member | team#member + relation view_role_users: role#member | team#member + + relation thing_create: role#member | team#member + relation thing_update: role#member | team#member + relation thing_read: role#member | team#member + relation thing_delete: role#member | team#member + relation thing_set_parent_group: role#member | team#member + relation thing_connect_to_channel: role#member | team#member + + relation thing_manage_role: role#member | team#member + relation thing_add_role_users: role#member | team#member + relation thing_remove_role_users: role#member | team#member + relation thing_view_role_users: role#member | team#member + + relation channel_create: role#member | team#member + relation channel_update: role#member | team#member + relation channel_read: role#member | team#member + relation channel_delete: role#member | team#member + relation channel_set_parent_group: role#member | team#member + relation channel_connect_to_thing: role#member | team#member + relation channel_publish: role#member | team#member + relation channel_subscribe: role#member | team#member + + relation channel_manage_role: role#member | team#member + relation channel_add_role_users: role#member | team#member + relation channel_remove_role_users: role#member | team#member + relation channel_view_role_users: role#member | team#member + + relation group_create: role#member | team#member + relation group_update: role#member | team#member + relation group_read: role#member | team#member + relation group_delete: role#member | team#member + relation group_set_child: role#member | team#member + relation group_set_parent: role#member | team#member + + relation group_manage_role: role#member | team#member + relation group_add_role_users: role#member | team#member + relation group_remove_role_users: role#member | team#member + relation group_view_role_users: role#member | team#member + + permission update_permission = update + team->domain_update + organization->admin + permission read_permission = read + team->domain_read + organization->admin + permission delete_permission = delete + team->domain_delete + organization->admin + + permission manage_role_permission = manage_role + team->domain_manage_role + organization->admin + permission add_role_users_permission = add_role_users + team->domain_add_role_users + organization->admin + permission remove_role_users_permission = remove_role_users + team->domain_remove_role_users + organization->admin + permission view_role_users_permission = view_role_users + team->domain_view_role_users + organization->admin + + permission thing_create_permission = thing_create + team->thing_create + organization->admin + permission thing_update_permission = thing_update + team->thing_update + organization->admin + permission thing_read_permission = thing_read + team->thing_read + organization->admin + permission thing_delete_permission = thing_delete + team->thing_delete + organization->admin + permission thing_set_parent_group_permission = thing_set_parent_group + team->thing_set_parent_group + organization->admin + permission thing_connect_to_channel_permission = thing_connect_to_channel + team->thing_connect_to_channel + organization->admin + + permission thing_manage_role_permission = thing_manage_role + team->thing_manage_role + organization->admin + permission thing_add_role_users_permission = thing_add_role_users + team->thing_add_role_users + organization->admin + permission thing_remove_role_users_permission = thing_remove_role_users + team->thing_remove_role_users + organization->admin + permission thing_view_role_users_permission = thing_view_role_users + team->thing_view_role_users + organization->admin + + permission channel_create_permission = channel_create + team->channel_create + organization->admin + permission channel_update_permission = channel_update + team->channel_update + organization->admin + permission channel_read_permission = channel_read + team->channel_read + organization->admin + permission channel_delete_permission = channel_delete + team->channel_delete + organization->admin + permission channel_set_parent_group_permission = channel_set_parent_group + team->channel_set_parent_group + organization->admin + permission channel_connect_to_thing_permission = channel_connect_to_thing + team->channel_connect_to_thing + organization->admin + permission channel_publish_permission = channel_publish + team->channel_publish + organization->admin + permission channel_subscribe_permission = channel_subscribe + team->channel_subscribe + organization->admin + + permission channel_manage_role_permission = channel_manage_role + team->channel_manage_role + organization->admin + permission channel_add_role_users_permission = channel_add_role_users + team->channel_add_role_users + organization->admin + permission channel_remove_role_users_permission = channel_remove_role_users + team->channel_remove_role_users + organization->admin + permission channel_view_role_users_permission = channel_view_role_users + team->channel_view_role_users + organization->admin + + permission group_create_permission = group_create + team->group_create + organization->admin + permission group_update_permission = group_update + team->group_update + organization->admin + permission group_read_permission = group_read + team->group_read + organization->admin + permission group_delete_permission = group_delete + team->group_delete + organization->admin + permission group_set_child_permission = group_set_child + team->group_set_child + organization->admin + permission group_set_parent_permission = group_set_parent + team->group_set_parent + organization->admin + + permission group_manage_role_permission = group_manage_role + team->group_manage_role + organization->admin + permission group_add_role_users_permission = group_add_role_users + team->group_add_role_users + organization->admin + permission group_remove_role_users_permission = group_remove_role_users + team->group_remove_role_users + organization->admin + permission group_view_role_users_permission = group_view_role_users + team->group_view_role_users + organization->admin + +} + + +definition team { + relation organization: organization + relation parent_team: team + + relation delete: role#member + relation update: role#member + relation read: role#member + + relation set_parent: role#member + relation set_child: role#member + + relation member: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + relation subteam_delete: role#member + relation subteam_update: role#member + relation subteam_read: role#member + + relation subteam_member: role#member + + relation subteam_set_child: role#member + relation subteam_set_parent: role#member + + relation subteam_manage_role: role#member + relation subteam_add_role_users: role#member + relation subteam_remove_role_users: role#member + relation subteam_view_role_users: role#member + + // Domain related permission + + relation domain_update: role#member | team#member + relation domain_read: role#member | team#member + relation domain_delete: role#member | team#member + + relation domain_manage_role: role#member | team#member + relation domain_add_role_users: role#member | team#member + relation domain_remove_role_users: role#member | team#member + relation domain_view_role_users: role#member | team#member + + relation thing_create: role#member | team#member + relation thing_update: role#member | team#member + relation thing_read: role#member | team#member + relation thing_delete: role#member | team#member + relation thing_set_parent_group: role#member | team#member + relation thing_connect_to_channel: role#member | team#member + + relation thing_manage_role: role#member | team#member + relation thing_add_role_users: role#member | team#member + relation thing_remove_role_users: role#member | team#member + relation thing_view_role_users: role#member | team#member + + relation channel_create: role#member | team#member + relation channel_update: role#member | team#member + relation channel_read: role#member | team#member + relation channel_delete: role#member | team#member + relation channel_set_parent_group: role#member | team#member + relation channel_connect_to_thing: role#member | team#member + relation channel_publish: role#member | team#member + relation channel_subscribe: role#member | team#member + + relation channel_manage_role: role#member | team#member + relation channel_add_role_users: role#member | team#member + relation channel_remove_role_users: role#member | team#member + relation channel_view_role_users: role#member | team#member + + relation group_create: role#member | team#member + relation group_update: role#member | team#member + relation group_read: role#member | team#member + relation group_delete: role#member | team#member + relation group_set_child: role#member | team#member + relation group_set_parent: role#member | team#member + + relation group_manage_role: role#member | team#member + relation group_add_role_users: role#member | team#member + relation group_remove_role_users: role#member | team#member + relation group_view_role_users: role#member | team#member + + permission delete_permission = delete + organization->team_delete + parent_team->subteam_delete + organization->admin + permission update_permission = update + organization->team_update + parent_team->subteam_update + organization->admin + permission read_permission = read + organization->team_read + parent_team->subteam_read + organization->admin + + permission set_parent_permission = set_parent + organization->team_set_parent + parent_team->subteam_set_parent + organization->admin + permission set_child_permisssion = set_child + organization->team_set_child + parent_team->subteam_set_child + organization->admin + + permission membership = member + organization->team_member + parent_team->subteam_member + organization->admin + + permission manage_role_permission = manage_role + organization->team_manage_role + parent_team->subteam_manage_role + organization->admin + permission add_role_users_permission = add_role_users + organization->team_add_role_users + parent_team->subteam_add_role_users + organization->admin + permission remove_role_users_permission = remove_role_users + organization->team_remove_role_users + parent_team->subteam_remove_role_users + organization->admin + permission view_role_users_permission = view_role_users + organization->team_view_role_users + parent_team->subteam_view_role_users + organization->admin +} + + +definition organization { + relation platform: platform + relation administrator : user + + relation delete: role#member + relation update: role#member + relation read: role#member + + relation member: role#member + + relation manage_role: role#member + relation add_role_users: role#member + relation remove_role_users: role#member + relation view_role_users: role#member + + relation team_create: role#member + + relation team_delete: role#member + relation team_update: role#member + relation team_read: role#member + + relation team_member: role#member // Will be member of all the teams in the organization + + relation team_set_child: role#member + relation team_set_parent: role#member + + relation team_manage_role: role#member + relation team_add_role_users: role#member + relation team_remove_role_users: role#member + relation team_view_role_users: role#member + + permission admin = administrator + platform->administrator + permission delete_permission = admin + delete->member + permission update_permission = admin + update->member + permission read_permission = admin + read->member + + permission membership = admin + member->member + + permission team_create_permission = admin + team_create->member + + permission manage_role_permission = admin + manage_role + permission add_role_users_permisson = admin + add_role_users + permission remove_role_users_permission = admin + remove_role_users + permission view_role_users_permission = admin + view_role_users +} + + +definition platform { + relation administrator: user + relation member: user + + permission admin = administrator + permission membership = administrator + member +} diff --git a/auth/api/grpc/domains/client.go b/domains/api/grpc/client.go similarity index 68% rename from auth/api/grpc/domains/client.go rename to domains/api/grpc/client.go index 1b952afc12..9f603b7ce8 100644 --- a/auth/api/grpc/domains/client.go +++ b/domains/api/grpc/client.go @@ -1,22 +1,22 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package grpc import ( "context" "time" - "github.com/absmach/magistrala" grpcapi "github.com/absmach/magistrala/auth/api/grpc" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" "github.com/go-kit/kit/endpoint" kitgrpc "github.com/go-kit/kit/transport/grpc" "google.golang.org/grpc" ) -const domainsSvcName = "magistrala.DomainsService" +const domainsSvcName = "domains.v1.DomainsService" -var _ magistrala.DomainsServiceClient = (*domainsGrpcClient)(nil) +var _ grpcDomainsV1.DomainsServiceClient = (*domainsGrpcClient)(nil) type domainsGrpcClient struct { deleteUserFromDomains endpoint.Endpoint @@ -24,7 +24,7 @@ type domainsGrpcClient struct { } // NewDomainsClient returns new domains gRPC client instance. -func NewDomainsClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.DomainsServiceClient { +func NewDomainsClient(conn *grpc.ClientConn, timeout time.Duration) grpcDomainsV1.DomainsServiceClient { return &domainsGrpcClient{ deleteUserFromDomains: kitgrpc.NewClient( conn, @@ -32,14 +32,14 @@ func NewDomainsClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.D "DeleteUserFromDomains", encodeDeleteUserRequest, decodeDeleteUserResponse, - magistrala.DeleteUserRes{}, + grpcDomainsV1.DeleteUserRes{}, ).Endpoint(), timeout: timeout, } } -func (client domainsGrpcClient) DeleteUserFromDomains(ctx context.Context, in *magistrala.DeleteUserReq, opts ...grpc.CallOption) (*magistrala.DeleteUserRes, error) { +func (client domainsGrpcClient) DeleteUserFromDomains(ctx context.Context, in *grpcDomainsV1.DeleteUserReq, opts ...grpc.CallOption) (*grpcDomainsV1.DeleteUserRes, error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() @@ -47,21 +47,21 @@ func (client domainsGrpcClient) DeleteUserFromDomains(ctx context.Context, in *m ID: in.GetId(), }) if err != nil { - return &magistrala.DeleteUserRes{}, grpcapi.DecodeError(err) + return &grpcDomainsV1.DeleteUserRes{}, grpcapi.DecodeError(err) } dpr := res.(deleteUserRes) - return &magistrala.DeleteUserRes{Deleted: dpr.deleted}, nil + return &grpcDomainsV1.DeleteUserRes{Deleted: dpr.deleted}, nil } func decodeDeleteUserResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { - res := grpcRes.(*magistrala.DeleteUserRes) + res := grpcRes.(*grpcDomainsV1.DeleteUserRes) return deleteUserRes{deleted: res.GetDeleted()}, nil } func encodeDeleteUserRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { req := grpcReq.(deleteUserPoliciesReq) - return &magistrala.DeleteUserReq{ + return &grpcDomainsV1.DeleteUserReq{ Id: req.ID, }, nil } diff --git a/auth/api/grpc/domains/doc.go b/domains/api/grpc/doc.go similarity index 90% rename from auth/api/grpc/domains/doc.go rename to domains/api/grpc/doc.go index 4ae689977a..ecbe29d553 100644 --- a/auth/api/grpc/domains/doc.go +++ b/domains/api/grpc/doc.go @@ -2,4 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 // Package grpc contains implementation of Domains service gRPC API. -package domains +package grpc diff --git a/auth/api/grpc/domains/endpoint.go b/domains/api/grpc/endpoint.go similarity index 78% rename from auth/api/grpc/domains/endpoint.go rename to domains/api/grpc/endpoint.go index 5bbb047e6d..4ced78cbee 100644 --- a/auth/api/grpc/domains/endpoint.go +++ b/domains/api/grpc/endpoint.go @@ -1,16 +1,16 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package grpc import ( "context" - "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/domains" "github.com/go-kit/kit/endpoint" ) -func deleteUserFromDomainsEndpoint(svc auth.Service) endpoint.Endpoint { +func deleteUserFromDomainsEndpoint(svc domains.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(deleteUserPoliciesReq) if err := req.validate(); err != nil { diff --git a/auth/api/grpc/domains/endpoint_test.go b/domains/api/grpc/endpoint_test.go similarity index 76% rename from auth/api/grpc/domains/endpoint_test.go rename to domains/api/grpc/endpoint_test.go index 3bddb69141..f83c95afcd 100644 --- a/auth/api/grpc/domains/endpoint_test.go +++ b/domains/api/grpc/endpoint_test.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains_test +package grpc_test import ( "context" @@ -10,9 +10,9 @@ import ( "testing" "time" - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/auth" - grpcapi "github.com/absmach/magistrala/auth/api/grpc/domains" + "github.com/absmach/magistrala/domains" + grpcapi "github.com/absmach/magistrala/domains/api/grpc" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" "github.com/stretchr/testify/assert" @@ -44,10 +44,10 @@ const ( var authAddr = fmt.Sprintf("localhost:%d", port) -func startGRPCServer(svc auth.Service, port int) *grpc.Server { +func startGRPCServer(svc domains.Service, port int) *grpc.Server { listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port)) server := grpc.NewServer() - magistrala.RegisterDomainsServiceServer(server, grpcapi.NewDomainsServer(svc)) + grpcDomainsV1.RegisterDomainsServiceServer(server, grpcapi.NewDomainsServer(svc)) go func() { err := server.Serve(listener) assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err)) @@ -64,33 +64,33 @@ func TestDeleteUserFromDomains(t *testing.T) { cases := []struct { desc string token string - deleteUserReq *magistrala.DeleteUserReq - deleteUserRes *magistrala.DeleteUserRes + deleteUserReq *grpcDomainsV1.DeleteUserReq + deleteUserRes *grpcDomainsV1.DeleteUserRes err error }{ { desc: "delete valid req", token: validToken, - deleteUserReq: &magistrala.DeleteUserReq{ + deleteUserReq: &grpcDomainsV1.DeleteUserReq{ Id: id, }, - deleteUserRes: &magistrala.DeleteUserRes{Deleted: true}, + deleteUserRes: &grpcDomainsV1.DeleteUserRes{Deleted: true}, err: nil, }, { desc: "delete invalid req with invalid token", token: inValidToken, - deleteUserReq: &magistrala.DeleteUserReq{}, - deleteUserRes: &magistrala.DeleteUserRes{Deleted: false}, + deleteUserReq: &grpcDomainsV1.DeleteUserReq{}, + deleteUserRes: &grpcDomainsV1.DeleteUserRes{Deleted: false}, err: apiutil.ErrMissingID, }, { desc: "delete invalid req with invalid token", token: inValidToken, - deleteUserReq: &magistrala.DeleteUserReq{ + deleteUserReq: &grpcDomainsV1.DeleteUserReq{ Id: id, }, - deleteUserRes: &magistrala.DeleteUserRes{Deleted: false}, + deleteUserRes: &grpcDomainsV1.DeleteUserRes{Deleted: false}, err: apiutil.ErrMissingPolicyEntityType, }, } diff --git a/auth/api/grpc/domains/requests.go b/domains/api/grpc/requests.go similarity index 94% rename from auth/api/grpc/domains/requests.go rename to domains/api/grpc/requests.go index 8e98928798..ef9607dfff 100644 --- a/auth/api/grpc/domains/requests.go +++ b/domains/api/grpc/requests.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package grpc import ( "github.com/absmach/magistrala/pkg/apiutil" diff --git a/auth/api/grpc/domains/responses.go b/domains/api/grpc/responses.go similarity index 88% rename from auth/api/grpc/domains/responses.go rename to domains/api/grpc/responses.go index 09b883089a..c5749399dc 100644 --- a/auth/api/grpc/domains/responses.go +++ b/domains/api/grpc/responses.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package grpc type deleteUserRes struct { deleted bool diff --git a/auth/api/grpc/domains/server.go b/domains/api/grpc/server.go similarity index 62% rename from auth/api/grpc/domains/server.go rename to domains/api/grpc/server.go index fdfc55ce32..4831275705 100644 --- a/auth/api/grpc/domains/server.go +++ b/domains/api/grpc/server.go @@ -1,25 +1,25 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package grpc import ( "context" - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/auth" grpcapi "github.com/absmach/magistrala/auth/api/grpc" + "github.com/absmach/magistrala/domains" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" kitgrpc "github.com/go-kit/kit/transport/grpc" ) -var _ magistrala.DomainsServiceServer = (*domainsGrpcServer)(nil) +var _ grpcDomainsV1.DomainsServiceServer = (*domainsGrpcServer)(nil) type domainsGrpcServer struct { - magistrala.UnimplementedDomainsServiceServer + grpcDomainsV1.UnimplementedDomainsServiceServer deleteUserFromDomains kitgrpc.Handler } -func NewDomainsServer(svc auth.Service) magistrala.DomainsServiceServer { +func NewDomainsServer(svc domains.Service) grpcDomainsV1.DomainsServiceServer { return &domainsGrpcServer{ deleteUserFromDomains: kitgrpc.NewServer( (deleteUserFromDomainsEndpoint(svc)), @@ -30,7 +30,7 @@ func NewDomainsServer(svc auth.Service) magistrala.DomainsServiceServer { } func decodeDeleteUserRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { - req := grpcReq.(*magistrala.DeleteUserReq) + req := grpcReq.(*grpcDomainsV1.DeleteUserReq) return deleteUserPoliciesReq{ ID: req.GetId(), }, nil @@ -38,13 +38,13 @@ func decodeDeleteUserRequest(_ context.Context, grpcReq interface{}) (interface{ func encodeDeleteUserResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { res := grpcRes.(deleteUserRes) - return &magistrala.DeleteUserRes{Deleted: res.deleted}, nil + return &grpcDomainsV1.DeleteUserRes{Deleted: res.deleted}, nil } -func (s *domainsGrpcServer) DeleteUserFromDomains(ctx context.Context, req *magistrala.DeleteUserReq) (*magistrala.DeleteUserRes, error) { +func (s *domainsGrpcServer) DeleteUserFromDomains(ctx context.Context, req *grpcDomainsV1.DeleteUserReq) (*grpcDomainsV1.DeleteUserRes, error) { _, res, err := s.deleteUserFromDomains.ServeGRPC(ctx, req) if err != nil { return nil, grpcapi.EncodeError(err) } - return res.(*magistrala.DeleteUserRes), nil + return res.(*grpcDomainsV1.DeleteUserRes), nil } diff --git a/auth/api/grpc/domains/setup_test.go b/domains/api/grpc/setup_test.go similarity index 81% rename from auth/api/grpc/domains/setup_test.go rename to domains/api/grpc/setup_test.go index d65f23e793..220fc4904f 100644 --- a/auth/api/grpc/domains/setup_test.go +++ b/domains/api/grpc/setup_test.go @@ -1,13 +1,13 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains_test +package grpc_test import ( "os" "testing" - "github.com/absmach/magistrala/auth/mocks" + "github.com/absmach/magistrala/domains/mocks" ) var svc *mocks.Service diff --git a/auth/api/http/domains/decode.go b/domains/api/http/decode.go similarity index 65% rename from auth/api/http/domains/decode.go rename to domains/api/http/decode.go index e0c58ecc52..543b4ffdd1 100644 --- a/auth/api/http/domains/decode.go +++ b/domains/api/http/decode.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package http import ( "context" @@ -9,7 +9,7 @@ import ( "net/http" "strings" - "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/domains" "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" @@ -20,9 +20,7 @@ func decodeCreateDomainRequest(_ context.Context, r *http.Request) (interface{}, if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) } - req := createDomainReq{ - token: apiutil.ExtractBearerToken(r), - } + req := createDomainReq{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) } @@ -32,15 +30,6 @@ func decodeCreateDomainRequest(_ context.Context, r *http.Request) (interface{}, func decodeRetrieveDomainRequest(_ context.Context, r *http.Request) (interface{}, error) { req := retrieveDomainRequest{ - token: apiutil.ExtractBearerToken(r), - domainID: chi.URLParam(r, "domainID"), - } - return req, nil -} - -func decodeRetrieveDomainPermissionsRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := retrieveDomainPermissionsRequest{ - token: apiutil.ExtractBearerToken(r), domainID: chi.URLParam(r, "domainID"), } return req, nil @@ -52,7 +41,6 @@ func decodeUpdateDomainRequest(_ context.Context, r *http.Request) (interface{}, } req := updateDomainReq{ - token: apiutil.ExtractBearerToken(r), domainID: chi.URLParam(r, "domainID"), } @@ -78,7 +66,6 @@ func decodeListDomainRequest(ctx context.Context, r *http.Request) (interface{}, func decodeEnableDomainRequest(_ context.Context, r *http.Request) (interface{}, error) { req := enableDomainReq{ - token: apiutil.ExtractBearerToken(r), domainID: chi.URLParam(r, "domainID"), } return req, nil @@ -86,7 +73,6 @@ func decodeEnableDomainRequest(_ context.Context, r *http.Request) (interface{}, func decodeDisableDomainRequest(_ context.Context, r *http.Request) (interface{}, error) { req := disableDomainReq{ - token: apiutil.ExtractBearerToken(r), domainID: chi.URLParam(r, "domainID"), } return req, nil @@ -94,63 +80,17 @@ func decodeDisableDomainRequest(_ context.Context, r *http.Request) (interface{} func decodeFreezeDomainRequest(_ context.Context, r *http.Request) (interface{}, error) { req := freezeDomainReq{ - token: apiutil.ExtractBearerToken(r), domainID: chi.URLParam(r, "domainID"), } return req, nil } -func decodeAssignUsersRequest(_ 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 := assignUsersReq{ - token: apiutil.ExtractBearerToken(r), - domainID: chi.URLParam(r, "domainID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - - return req, nil -} - -func decodeUnassignUserRequest(_ 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 := unassignUserReq{ - token: apiutil.ExtractBearerToken(r), - domainID: chi.URLParam(r, "domainID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - - return req, nil -} - -func decodeListUserDomainsRequest(ctx context.Context, r *http.Request) (interface{}, error) { - page, err := decodePageRequest(ctx, r) - if err != nil { - return nil, err - } - req := listUserDomainsReq{ - token: apiutil.ExtractBearerToken(r), - userID: chi.URLParam(r, "userID"), - page: page, - } - return req, nil -} - func decodePageRequest(_ context.Context, r *http.Request) (page, error) { s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefClientStatus) if err != nil { return page{}, errors.Wrap(apiutil.ErrValidation, err) } - st, err := auth.ToStatus(s) + st, err := domains.ToStatus(s) if err != nil { return page{}, errors.Wrap(apiutil.ErrValidation, err) } diff --git a/domains/api/http/endpoint.go b/domains/api/http/endpoint.go new file mode 100644 index 0000000000..e332d82f08 --- /dev/null +++ b/domains/api/http/endpoint.go @@ -0,0 +1,182 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/go-kit/kit/endpoint" +) + +func createDomainEndpoint(svc domains.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(createDomainReq) + if err := req.validate(); err != nil { + return nil, err + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + + d := domains.Domain{ + Name: req.Name, + Metadata: req.Metadata, + Tags: req.Tags, + Alias: req.Alias, + } + domain, err := svc.CreateDomain(ctx, session, d) + if err != nil { + return nil, err + } + + return createDomainRes{domain}, nil + } +} + +func retrieveDomainEndpoint(svc domains.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(retrieveDomainRequest) + if err := req.validate(); err != nil { + return nil, err + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + + domain, err := svc.RetrieveDomain(ctx, session, req.domainID) + if err != nil { + return nil, err + } + return retrieveDomainRes{domain}, nil + } +} + +func updateDomainEndpoint(svc domains.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(updateDomainReq) + if err := req.validate(); err != nil { + return nil, err + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + + var metadata domains.Metadata + if req.Metadata != nil { + metadata = *req.Metadata + } + d := domains.DomainReq{ + Name: req.Name, + Metadata: &metadata, + Tags: req.Tags, + Alias: req.Alias, + } + domain, err := svc.UpdateDomain(ctx, session, req.domainID, d) + if err != nil { + return nil, err + } + + return updateDomainRes{domain}, nil + } +} + +func listDomainsEndpoint(svc domains.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listDomainsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + + page := domains.Page{ + Offset: req.offset, + Limit: req.limit, + Name: req.name, + Metadata: req.metadata, + Order: req.order, + Dir: req.dir, + Tag: req.tag, + Permission: req.permission, + Status: req.status, + } + dp, err := svc.ListDomains(ctx, session, page) + if err != nil { + return nil, err + } + return listDomainsRes{dp}, nil + } +} + +func enableDomainEndpoint(svc domains.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(enableDomainReq) + if err := req.validate(); err != nil { + return nil, err + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + + if _, err := svc.EnableDomain(ctx, session, req.domainID); err != nil { + return nil, err + } + return enableDomainRes{}, nil + } +} + +func disableDomainEndpoint(svc domains.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(disableDomainReq) + if err := req.validate(); err != nil { + return nil, err + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + + if _, err := svc.DisableDomain(ctx, session, req.domainID); err != nil { + return nil, err + } + return disableDomainRes{}, nil + } +} + +func freezeDomainEndpoint(svc domains.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(freezeDomainReq) + if err := req.validate(); err != nil { + return nil, err + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthorization + } + + if _, err := svc.FreezeDomain(ctx, session, req.domainID); err != nil { + return nil, err + } + return freezeDomainRes{}, nil + } +} diff --git a/auth/api/http/domains/endpoint_test.go b/domains/api/http/endpoint_test.go similarity index 90% rename from auth/api/http/domains/endpoint_test.go rename to domains/api/http/endpoint_test.go index 2fe1fd7d38..974f511081 100644 --- a/auth/api/http/domains/endpoint_test.go +++ b/domains/api/http/endpoint_test.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains_test +package http_test import ( "encoding/json" @@ -13,29 +13,31 @@ import ( "testing" "time" - "github.com/absmach/magistrala/auth" - httpapi "github.com/absmach/magistrala/auth/api/http/domains" - "github.com/absmach/magistrala/auth/mocks" + "github.com/absmach/magistrala/domains" + httpapi "github.com/absmach/magistrala/domains/api/http" + "github.com/absmach/magistrala/domains/mocks" "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" + authnmock "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" policies "github.com/absmach/magistrala/pkg/policies" + mgclients "github.com/absmach/magistrala/things" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) var ( - validCMetadata = auth.Metadata{"role": "client"} + validCMetadata = domains.Metadata{"role": "client"} ID = testsutil.GenerateUUID(&testing.T{}) - domain = auth.Domain{ + domain = domains.Domain{ ID: ID, Name: "domainname", Tags: []string{"tag1", "tag2"}, Metadata: validCMetadata, - Status: auth.EnabledStatus, + Status: domains.EnabledStatus, Alias: "mydomain", } validToken = "token" @@ -91,7 +93,9 @@ func newDomainsServer() (*httptest.Server, *mocks.Service) { logger := mglog.NewMock() mux := chi.NewRouter() svc := new(mocks.Service) - httpapi.MakeHandler(svc, mux, logger) + authn := new(authnmock.Authentication) + mux = chi.NewMux() + httpapi.MakeHandler(svc, authn, mux, logger, "") return httptest.NewServer(mux), svc } @@ -101,7 +105,7 @@ func TestCreateDomain(t *testing.T) { cases := []struct { desc string - domain auth.Domain + domain domains.Domain token string contentType string svcErr error @@ -110,10 +114,10 @@ func TestCreateDomain(t *testing.T) { }{ { desc: "register a new domain successfully", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -124,10 +128,10 @@ func TestCreateDomain(t *testing.T) { }, { desc: "register a new domain with empty token", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -138,10 +142,10 @@ func TestCreateDomain(t *testing.T) { }, { desc: "register a new domain with invalid token", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -153,10 +157,10 @@ func TestCreateDomain(t *testing.T) { }, { desc: "register a new domain with an empty name", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -167,10 +171,10 @@ func TestCreateDomain(t *testing.T) { }, { desc: "register a new domain with an empty alias", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "", }, @@ -181,10 +185,10 @@ func TestCreateDomain(t *testing.T) { }, { desc: "register a new domain with invalid content type", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -195,7 +199,7 @@ func TestCreateDomain(t *testing.T) { }, { desc: "register a new domain that cant be marshalled", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", Metadata: map[string]interface{}{ @@ -222,7 +226,7 @@ func TestCreateDomain(t *testing.T) { body: strings.NewReader(data), } - svcCall := svc.On("CreateDomain", mock.Anything, mock.Anything, mock.Anything).Return(auth.Domain{}, tc.svcErr) + svcCall := svc.On("CreateDomain", mock.Anything, mock.Anything, mock.Anything).Return(domains.Domain{}, tc.svcErr) res, err := req.make() assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) var errRes respBody @@ -245,7 +249,7 @@ func TestListDomains(t *testing.T) { desc string token string query string - listDomainsRequest auth.DomainsPage + listDomainsRequest domains.DomainsPage status int svcErr error err error @@ -254,9 +258,9 @@ func TestListDomains(t *testing.T) { desc: "list domains with valid token", token: validToken, status: http.StatusOK, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, err: nil, }, @@ -276,9 +280,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with offset", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "offset=1", status: http.StatusOK, @@ -294,9 +298,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with limit", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "limit=1", status: http.StatusOK, @@ -312,9 +316,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with name", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "name=domainname", status: http.StatusOK, @@ -337,9 +341,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with status", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "status=enabled", status: http.StatusOK, @@ -362,9 +366,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with tags", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "tag=tag1,tag2", status: http.StatusOK, @@ -387,9 +391,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with metadata", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&", status: http.StatusOK, @@ -412,9 +416,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with permissions", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "permission=view", status: http.StatusOK, @@ -437,9 +441,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with order", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "order=name", status: http.StatusOK, @@ -461,9 +465,9 @@ func TestListDomains(t *testing.T) { { desc: "list domains with dir", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "dir=asc", status: http.StatusOK, @@ -544,7 +548,7 @@ func TestViewDomain(t *testing.T) { token: tc.token, } - svcCall := svc.On("RetrieveDomain", mock.Anything, mock.Anything, mock.Anything).Return(auth.Domain{}, tc.svcErr) + svcCall := svc.On("RetrieveDomain", mock.Anything, mock.Anything, mock.Anything).Return(domains.Domain{}, tc.svcErr) res, err := req.make() assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) var errRes respBody @@ -633,7 +637,7 @@ func TestUpdateDomain(t *testing.T) { cases := []struct { desc string token string - domain auth.Domain + domain domains.Domain contentType string status int svcErr error @@ -642,10 +646,10 @@ func TestUpdateDomain(t *testing.T) { { desc: "update domain successfully", token: validToken, - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -656,10 +660,10 @@ func TestUpdateDomain(t *testing.T) { { desc: "update domain with empty token", token: "", - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -670,10 +674,10 @@ func TestUpdateDomain(t *testing.T) { { desc: "update domain with invalid token", token: inValidToken, - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -685,10 +689,10 @@ func TestUpdateDomain(t *testing.T) { { desc: "update domain with invalid content type", token: validToken, - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", - Metadata: auth.Metadata{"role": "domain"}, + Metadata: domains.Metadata{"role": "domain"}, Tags: []string{"tag1", "tag2"}, Alias: "test", }, @@ -699,7 +703,7 @@ func TestUpdateDomain(t *testing.T) { { desc: "update domain with data that cant be marshalled", token: validToken, - domain: auth.Domain{ + domain: domains.Domain{ ID: ID, Name: "test", Metadata: map[string]interface{}{ @@ -725,7 +729,7 @@ func TestUpdateDomain(t *testing.T) { token: tc.token, } - svcCall := svc.On("UpdateDomain", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(auth.Domain{}, tc.svcErr) + svcCall := svc.On("UpdateDomain", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(domains.Domain{}, tc.svcErr) res, err := req.make() assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) var errRes respBody @@ -746,12 +750,12 @@ func TestEnableDomain(t *testing.T) { defer ds.Close() disabledDomain := domain - disabledDomain.Status = auth.DisabledStatus + disabledDomain.Status = domains.DisabledStatus cases := []struct { desc string - domain auth.Domain - response auth.Domain + domain domains.Domain + response domains.Domain token string status int svcErr error @@ -760,9 +764,9 @@ func TestEnableDomain(t *testing.T) { { desc: "enable domain with valid token", domain: disabledDomain, - response: auth.Domain{ + response: domains.Domain{ ID: domain.ID, - Status: auth.EnabledStatus, + Status: domains.EnabledStatus, }, token: validToken, status: http.StatusOK, @@ -785,7 +789,7 @@ func TestEnableDomain(t *testing.T) { }, { desc: "enable domain with empty id", - domain: auth.Domain{ + domain: domains.Domain{ ID: "", }, token: validToken, @@ -794,7 +798,7 @@ func TestEnableDomain(t *testing.T) { }, { desc: "enable domain with invalid id", - domain: auth.Domain{ + domain: domains.Domain{ ID: "invalid", }, token: validToken, @@ -828,8 +832,8 @@ func TestDisableDomain(t *testing.T) { cases := []struct { desc string - domain auth.Domain - response auth.Domain + domain domains.Domain + response domains.Domain token string status int svcErr error @@ -838,9 +842,9 @@ func TestDisableDomain(t *testing.T) { { desc: "disable domain with valid token", domain: domain, - response: auth.Domain{ + response: domains.Domain{ ID: domain.ID, - Status: auth.DisabledStatus, + Status: domains.DisabledStatus, }, token: validToken, status: http.StatusOK, @@ -863,7 +867,7 @@ func TestDisableDomain(t *testing.T) { }, { desc: "disable domain with empty id", - domain: auth.Domain{ + domain: domains.Domain{ ID: "", }, token: validToken, @@ -872,7 +876,7 @@ func TestDisableDomain(t *testing.T) { }, { desc: "disable domain with invalid id", - domain: auth.Domain{ + domain: domains.Domain{ ID: "invalid", }, token: validToken, @@ -906,8 +910,8 @@ func TestFreezeDomain(t *testing.T) { cases := []struct { desc string - domain auth.Domain - response auth.Domain + domain domains.Domain + response domains.Domain token string status int svcErr error @@ -916,9 +920,9 @@ func TestFreezeDomain(t *testing.T) { { desc: "freeze domain with valid token", domain: domain, - response: auth.Domain{ + response: domains.Domain{ ID: domain.ID, - Status: auth.FreezeStatus, + Status: domains.FreezeStatus, }, token: validToken, status: http.StatusOK, @@ -941,7 +945,7 @@ func TestFreezeDomain(t *testing.T) { }, { desc: "freeze domain with empty id", - domain: auth.Domain{ + domain: domains.Domain{ ID: "", }, token: validToken, @@ -950,7 +954,7 @@ func TestFreezeDomain(t *testing.T) { }, { desc: "freeze domain with invalid id", - domain: auth.Domain{ + domain: domains.Domain{ ID: "invalid", }, token: validToken, @@ -1205,7 +1209,7 @@ func TestListDomainsByUserID(t *testing.T) { desc string token string query string - listDomainsRequest auth.DomainsPage + listDomainsRequest domains.DomainsPage userID string status int svcErr error @@ -1215,9 +1219,9 @@ func TestListDomainsByUserID(t *testing.T) { desc: "list domains by user id with valid token", token: validToken, status: http.StatusOK, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, userID: validID, err: nil, @@ -1246,9 +1250,9 @@ func TestListDomainsByUserID(t *testing.T) { { desc: "list domains by user id with offset", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "offset=1", userID: validID, @@ -1265,9 +1269,9 @@ func TestListDomainsByUserID(t *testing.T) { { desc: "list domains by user id with limit", token: validToken, - listDomainsRequest: auth.DomainsPage{ + listDomainsRequest: domains.DomainsPage{ Total: 1, - Domains: []auth.Domain{domain}, + Domains: []domains.Domain{domain}, }, query: "limit=1", userID: validID, @@ -1300,11 +1304,11 @@ func TestListDomainsByUserID(t *testing.T) { } type respBody struct { - Err string `json:"error"` - Message string `json:"message"` - Total int `json:"total"` - Permissions []string `json:"permissions"` - ID string `json:"id"` - Tags []string `json:"tags"` - Status auth.Status `json:"status"` + Err string `json:"error"` + Message string `json:"message"` + Total int `json:"total"` + Permissions []string `json:"permissions"` + ID string `json:"id"` + Tags []string `json:"tags"` + Status mgclients.Status `json:"status"` } diff --git a/auth/api/http/domains/requests.go b/domains/api/http/requests.go similarity index 52% rename from auth/api/http/domains/requests.go rename to domains/api/http/requests.go index 5abbddd0df..722e4cfa32 100644 --- a/auth/api/http/domains/requests.go +++ b/domains/api/http/requests.go @@ -1,10 +1,10 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package http import ( - "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/domains" "github.com/absmach/magistrala/pkg/apiutil" ) @@ -17,11 +17,10 @@ type page struct { metadata map[string]interface{} tag string permission string - status auth.Status + status domains.Status } type createDomainReq struct { - token string Name string `json:"name"` Metadata map[string]interface{} `json:"metadata,omitempty"` Tags []string `json:"tags,omitempty"` @@ -29,10 +28,6 @@ type createDomainReq struct { } func (req createDomainReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.Name == "" { return apiutil.ErrMissingName } @@ -45,32 +40,10 @@ func (req createDomainReq) validate() error { } type retrieveDomainRequest struct { - token string domainID string } func (req retrieveDomainRequest) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - - if req.domainID == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type retrieveDomainPermissionsRequest struct { - token string - domainID string -} - -func (req retrieveDomainPermissionsRequest) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.domainID == "" { return apiutil.ErrMissingID } @@ -79,7 +52,6 @@ func (req retrieveDomainPermissionsRequest) validate() error { } type updateDomainReq struct { - token string domainID string Name *string `json:"name,omitempty"` Metadata *map[string]interface{} `json:"metadata,omitempty"` @@ -88,10 +60,6 @@ type updateDomainReq struct { } func (req updateDomainReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.domainID == "" { return apiutil.ErrMissingID } @@ -113,15 +81,10 @@ func (req listDomainsReq) validate() error { } type enableDomainReq struct { - token string domainID string } func (req enableDomainReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.domainID == "" { return apiutil.ErrMissingID } @@ -130,15 +93,10 @@ func (req enableDomainReq) validate() error { } type disableDomainReq struct { - token string domainID string } func (req disableDomainReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.domainID == "" { return apiutil.ErrMissingID } @@ -147,85 +105,13 @@ func (req disableDomainReq) validate() error { } type freezeDomainReq struct { - token string domainID string } func (req freezeDomainReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - - if req.domainID == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type assignUsersReq struct { - token string - domainID string - UserIDs []string `json:"user_ids"` - Relation string `json:"relation"` -} - -func (req assignUsersReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - if req.domainID == "" { return apiutil.ErrMissingID } - if len(req.UserIDs) == 0 { - return apiutil.ErrMissingID - } - - if req.Relation == "" { - return apiutil.ErrMissingRelation - } - - return nil -} - -type unassignUserReq struct { - token string - domainID string - UserID string `json:"user_id"` -} - -func (req unassignUserReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - - if req.domainID == "" { - return apiutil.ErrMissingID - } - - if req.UserID == "" { - return apiutil.ErrMalformedPolicy - } - - return nil -} - -type listUserDomainsReq struct { - token string - userID string - page -} - -func (req listUserDomainsReq) validate() error { - if req.token == "" { - return apiutil.ErrBearerToken - } - - if req.userID == "" { - return apiutil.ErrMissingID - } - return nil } diff --git a/auth/api/http/domains/responses.go b/domains/api/http/responses.go similarity index 95% rename from auth/api/http/domains/responses.go rename to domains/api/http/responses.go index 3eb277ef06..fd9fe227e2 100644 --- a/auth/api/http/domains/responses.go +++ b/domains/api/http/responses.go @@ -1,13 +1,13 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package http import ( "net/http" "github.com/absmach/magistrala" - "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/domains" ) var ( @@ -19,7 +19,7 @@ var ( ) type createDomainRes struct { - auth.Domain + domains.Domain } func (res createDomainRes) Code() int { @@ -35,7 +35,7 @@ func (res createDomainRes) Empty() bool { } type retrieveDomainRes struct { - auth.Domain + domains.Domain } func (res retrieveDomainRes) Code() int { @@ -67,7 +67,7 @@ func (res retrieveDomainPermissionsRes) Empty() bool { } type updateDomainRes struct { - auth.Domain + domains.Domain } func (res updateDomainRes) Code() int { @@ -83,7 +83,7 @@ func (res updateDomainRes) Empty() bool { } type listDomainsRes struct { - auth.DomainsPage + domains.DomainsPage } func (res listDomainsRes) Code() int { @@ -169,7 +169,7 @@ func (res unassignUsersRes) Empty() bool { } type listUserDomainsRes struct { - auth.DomainsPage + domains.DomainsPage } func (res listUserDomainsRes) Code() int { diff --git a/auth/api/http/domains/transport.go b/domains/api/http/transport.go similarity index 64% rename from auth/api/http/domains/transport.go rename to domains/api/http/transport.go index 332e9b78ac..910038c211 100644 --- a/auth/api/http/domains/transport.go +++ b/domains/api/http/transport.go @@ -1,25 +1,32 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package domains +package http import ( "log/slog" - "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/domains" "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/authn" + roleManagerHttp "github.com/absmach/magistrala/pkg/roles/rolemanager/api" "github.com/go-chi/chi/v5" kithttp "github.com/go-kit/kit/transport/http" + "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux { +func MakeHandler(svc domains.Service, authn authn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string) *chi.Mux { opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), } + d := roleManagerHttp.NewDecoder("domainID") mux.Route("/domains", func(r chi.Router) { + r.Use(api.AuthenticateMiddleware(authn, false)) + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( createDomainEndpoint(svc), decodeCreateDomainRequest, @@ -34,6 +41,8 @@ func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux { opts..., ), "list_domains").ServeHTTP) + r = roleManagerHttp.EntityAvailableActionsRouter(svc, d, r, opts) + r.Route("/{domainID}", func(r chi.Router) { r.Get("/", otelhttp.NewHandler(kithttp.NewServer( retrieveDomainEndpoint(svc), @@ -42,13 +51,6 @@ func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux { opts..., ), "view_domain").ServeHTTP) - r.Get("/permissions", otelhttp.NewHandler(kithttp.NewServer( - retrieveDomainPermissionsEndpoint(svc), - decodeRetrieveDomainPermissionsRequest, - api.EncodeResponse, - opts..., - ), "view_domain_permissions").ServeHTTP) - r.Patch("/", otelhttp.NewHandler(kithttp.NewServer( updateDomainEndpoint(svc), decodeUpdateDomainRequest, @@ -76,30 +78,13 @@ func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux { api.EncodeResponse, opts..., ), "freeze_domain").ServeHTTP) + roleManagerHttp.EntityRoleMangerRouter(svc, d, r, opts) - r.Route("/users", func(r chi.Router) { - r.Post("/assign", otelhttp.NewHandler(kithttp.NewServer( - assignDomainUsersEndpoint(svc), - decodeAssignUsersRequest, - api.EncodeResponse, - opts..., - ), "assign_domain_users").ServeHTTP) - - r.Post("/unassign", otelhttp.NewHandler(kithttp.NewServer( - unassignDomainUserEndpoint(svc), - decodeUnassignUserRequest, - api.EncodeResponse, - opts..., - ), "unassign_domain_users").ServeHTTP) - }) }) }) - mux.Get("/users/{userID}/domains", otelhttp.NewHandler(kithttp.NewServer( - listUserDomainsEndpoint(svc), - decodeListUserDomainsRequest, - api.EncodeResponse, - opts..., - ), "list_domains_by_user_id").ServeHTTP) + + mux.Get("/health", magistrala.Health("auth", instanceID)) + mux.Handle("/metrics", promhttp.Handler()) return mux } diff --git a/auth/domains.go b/domains/domains.go similarity index 55% rename from auth/domains.go rename to domains/domains.go index e9efc58034..ea4334a215 100644 --- a/auth/domains.go +++ b/domains/domains.go @@ -1,7 +1,7 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package auth +package domains import ( "context" @@ -9,8 +9,10 @@ import ( "strings" "time" + "github.com/absmach/magistrala/pkg/authn" svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/roles" + "github.com/absmach/magistrala/pkg/svcutil" ) // Status represents Domain status. @@ -24,6 +26,8 @@ const ( DisabledStatus // FreezeStatus represents domain is in freezed state. FreezeStatus + // DeletedStatus represents domain is in deleted state. + DeletedStatus // AllStatus is used for querying purposes to list Domains irrespective // of their status - enabled, disabled, freezed, deleting. It is never stored in the @@ -37,6 +41,7 @@ const ( Disabled = "disabled" Enabled = "enabled" Freezed = "freezed" + Deleted = "deleted" All = "all" Unknown = "unknown" ) @@ -52,6 +57,8 @@ func (s Status) String() string { return All case FreezeStatus: return Freezed + case DeletedStatus: + return Deleted default: return Unknown } @@ -66,6 +73,8 @@ func ToStatus(status string) (Status, error) { return DisabledStatus, nil case Freezed: return FreezeStatus, nil + case Deleted: + return DeletedStatus, nil case All: return AllStatus, nil } @@ -85,6 +94,34 @@ func (s *Status) UnmarshalJSON(data []byte) error { return err } +const ( + OpUpdateDomain svcutil.Operation = iota + OpRetrieveDomain + OpEnableDomain + OpDisableDomain +) + +var expectedOperations = []svcutil.Operation{ + OpRetrieveDomain, + OpUpdateDomain, + OpEnableDomain, + OpDisableDomain, +} + +var operationNames = []string{ + "OpRetrieveDomain", + "OpUpdateDomain", + "OpEnableDomain", + "OpDisableDomain", +} + +func NewOperationPerm() svcutil.OperationPerm { + return svcutil.NewOperationPerm(expectedOperations, operationNames) +} + +// Metadata represents arbitrary JSON. +type Metadata map[string]interface{} + type DomainReq struct { Name *string `json:"name,omitempty"` Metadata *Metadata `json:"metadata,omitempty"` @@ -106,9 +143,6 @@ type Domain struct { UpdatedAt time.Time `json:"updated_at,omitempty"` } -// Metadata represents arbitrary JSON. -type Metadata map[string]interface{} - type Page struct { Total uint64 `json:"total"` Offset uint64 `json:"offset"` @@ -157,32 +191,29 @@ type Policy struct { ObjectID string `json:"object_id,omitempty"` } -type Domains interface { - CreateDomain(ctx context.Context, token string, d Domain) (Domain, error) - RetrieveDomain(ctx context.Context, token string, id string) (Domain, error) - RetrieveDomainPermissions(ctx context.Context, token string, id string) (policies.Permissions, error) - UpdateDomain(ctx context.Context, token string, id string, d DomainReq) (Domain, error) - ChangeDomainStatus(ctx context.Context, token string, id string, d DomainReq) (Domain, error) - ListDomains(ctx context.Context, token string, page Page) (DomainsPage, error) - AssignUsers(ctx context.Context, token string, id string, userIds []string, relation string) error - UnassignUser(ctx context.Context, token string, id string, userID string) error - ListUserDomains(ctx context.Context, token string, userID string, page Page) (DomainsPage, error) +//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines" +type Service interface { + CreateDomain(ctx context.Context, sesssion authn.Session, d Domain) (Domain, error) + RetrieveDomain(ctx context.Context, sesssion authn.Session, id string) (Domain, error) + UpdateDomain(ctx context.Context, sesssion authn.Session, id string, d DomainReq) (Domain, error) + EnableDomain(ctx context.Context, sesssion authn.Session, id string) (Domain, error) + DisableDomain(ctx context.Context, sesssion authn.Session, id string) (Domain, error) + FreezeDomain(ctx context.Context, sesssion authn.Session, id string) (Domain, error) + ListDomains(ctx context.Context, sesssion authn.Session, page Page) (DomainsPage, error) DeleteUserFromDomains(ctx context.Context, id string) error + roles.RoleManager } -// DomainsRepository specifies Domain persistence API. +// Repository specifies Domain persistence API. // -//go:generate mockery --name DomainsRepository --output=./mocks --filename domains.go --quiet --note "Copyright (c) Abstract Machines" -type DomainsRepository interface { +//go:generate mockery --name Repository --output=./mocks --filename repository.go --quiet --note "Copyright (c) Abstract Machines" +type Repository interface { // Save creates db insert transaction for the given domain. Save(ctx context.Context, d Domain) (Domain, error) // RetrieveByID retrieves Domain by its unique ID. RetrieveByID(ctx context.Context, id string) (Domain, error) - // RetrievePermissions retrieves domain permissions. - RetrievePermissions(ctx context.Context, subject, id string) ([]string, error) - // RetrieveAllByIDs retrieves for given Domain IDs. RetrieveAllByIDs(ctx context.Context, pm Page) (DomainsPage, error) @@ -192,18 +223,60 @@ type DomainsRepository interface { // Delete Delete(ctx context.Context, id string) error - // SavePolicies save policies in domains database - SavePolicies(ctx context.Context, pcs ...Policy) error - - // DeletePolicies delete policies from domains database - DeletePolicies(ctx context.Context, pcs ...Policy) error - // ListDomains list all the domains ListDomains(ctx context.Context, pm Page) (DomainsPage, error) - // CheckPolicy check policies in domains database. - CheckPolicy(ctx context.Context, pc Policy) error + roles.Repository +} + +// Below codes should moved out of service, may be can be kept in `cmd//main.go` + +const ( + updatePermission = "update_permission" + enablePermission = "enable_permission" + disablePermission = "disable_permission" + readPermission = "read_permission" + membershipPermission = "membership_permission" + deletePermission = "delete_permission" + manageRolePermission = "manage_role_permission" + addRoleUsersPermission = "add_role_users_permission" + removeRoleUsersPermission = "remove_role_users_permission" + viewRoleUsersPermission = "view_role_users_permission" +) - // DeleteUserPolicies deletes user policies from domains database. - DeleteUserPolicies(ctx context.Context, id string) (err error) +const ( + ThingCreatePermission = "thing_create_permission" + ChannelCreatePermission = "channel_create_permission" + GroupCreatePermission = "group_create_permission" +) + +func NewOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + OpRetrieveDomain: readPermission, + OpUpdateDomain: updatePermission, + OpEnableDomain: enablePermission, + OpDisableDomain: disablePermission, + } + return opPerm +} + +func NewRolesOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + roles.OpAddRole: manageRolePermission, + roles.OpRemoveRole: manageRolePermission, + roles.OpUpdateRoleName: manageRolePermission, + roles.OpRetrieveRole: manageRolePermission, + roles.OpRetrieveAllRoles: manageRolePermission, + roles.OpRoleAddActions: manageRolePermission, + roles.OpRoleListActions: manageRolePermission, + roles.OpRoleCheckActionsExists: manageRolePermission, + roles.OpRoleRemoveActions: manageRolePermission, + roles.OpRoleRemoveAllActions: manageRolePermission, + roles.OpRoleAddMembers: addRoleUsersPermission, + roles.OpRoleListMembers: viewRoleUsersPermission, + roles.OpRoleCheckMembersExists: viewRoleUsersPermission, + roles.OpRoleRemoveMembers: removeRoleUsersPermission, + roles.OpRoleRemoveAllMembers: manageRolePermission, + } + return opPerm } diff --git a/auth/domains_test.go b/domains/domains_test.go similarity index 78% rename from auth/domains_test.go rename to domains/domains_test.go index 82875bccd5..7e55ff3b2b 100644 --- a/auth/domains_test.go +++ b/domains/domains_test.go @@ -1,12 +1,12 @@ // Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 -package auth_test +package domains_test import ( "testing" - "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/domains" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/stretchr/testify/assert" ) @@ -14,32 +14,32 @@ import ( func TestStatusString(t *testing.T) { cases := []struct { desc string - status auth.Status + status domains.Status expected string }{ { desc: "Enabled", - status: auth.EnabledStatus, + status: domains.EnabledStatus, expected: "enabled", }, { desc: "Disabled", - status: auth.DisabledStatus, + status: domains.DisabledStatus, expected: "disabled", }, { desc: "Freezed", - status: auth.FreezeStatus, + status: domains.FreezeStatus, expected: "freezed", }, { desc: "All", - status: auth.AllStatus, + status: domains.AllStatus, expected: "all", }, { desc: "Unknown", - status: auth.Status(100), + status: domains.Status(100), expected: "unknown", }, } @@ -56,44 +56,44 @@ func TestToStatus(t *testing.T) { cases := []struct { desc string status string - expetcted auth.Status + expetcted domains.Status err error }{ { desc: "Enabled", status: "enabled", - expetcted: auth.EnabledStatus, + expetcted: domains.EnabledStatus, err: nil, }, { desc: "Disabled", status: "disabled", - expetcted: auth.DisabledStatus, + expetcted: domains.DisabledStatus, err: nil, }, { desc: "Freezed", status: "freezed", - expetcted: auth.FreezeStatus, + expetcted: domains.FreezeStatus, err: nil, }, { desc: "All", status: "all", - expetcted: auth.AllStatus, + expetcted: domains.AllStatus, err: nil, }, { desc: "Unknown", status: "unknown", - expetcted: auth.Status(0), + expetcted: domains.Status(0), err: svcerr.ErrInvalidStatus, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { - got, err := auth.ToStatus(tc.status) + got, err := domains.ToStatus(tc.status) assert.Equal(t, tc.err, err, "ToStatus() error = %v, expected %v", err, tc.err) assert.Equal(t, tc.expetcted, got, "ToStatus() = %v, expected %v", got, tc.expetcted) }) @@ -104,31 +104,31 @@ func TestStatusMarshalJSON(t *testing.T) { cases := []struct { desc string expected []byte - status auth.Status + status domains.Status err error }{ { desc: "Enabled", expected: []byte(`"enabled"`), - status: auth.EnabledStatus, + status: domains.EnabledStatus, err: nil, }, { desc: "Disabled", expected: []byte(`"disabled"`), - status: auth.DisabledStatus, + status: domains.DisabledStatus, err: nil, }, { desc: "All", expected: []byte(`"all"`), - status: auth.AllStatus, + status: domains.AllStatus, err: nil, }, { desc: "Unknown", expected: []byte(`"unknown"`), - status: auth.Status(100), + status: domains.Status(100), err: nil, }, } @@ -145,31 +145,31 @@ func TestStatusMarshalJSON(t *testing.T) { func TestStatusUnmarshalJSON(t *testing.T) { cases := []struct { desc string - expected auth.Status + expected domains.Status status []byte err error }{ { desc: "Enabled", - expected: auth.EnabledStatus, + expected: domains.EnabledStatus, status: []byte(`"enabled"`), err: nil, }, { desc: "Disabled", - expected: auth.DisabledStatus, + expected: domains.DisabledStatus, status: []byte(`"disabled"`), err: nil, }, { desc: "All", - expected: auth.AllStatus, + expected: domains.AllStatus, status: []byte(`"all"`), err: nil, }, { desc: "Unknown", - expected: auth.Status(0), + expected: domains.Status(0), status: []byte(`"unknown"`), err: svcerr.ErrInvalidStatus, }, @@ -177,7 +177,7 @@ func TestStatusUnmarshalJSON(t *testing.T) { for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { - var s auth.Status + var s domains.Status err := s.UnmarshalJSON(tc.status) assert.Equal(t, tc.err, err, "UnmarshalJSON() error = %v, expected %v", err, tc.err) assert.Equal(t, tc.expected, s, "UnmarshalJSON() = %v, expected %v", s, tc.expected) diff --git a/auth/events/doc.go b/domains/events/doc.go similarity index 100% rename from auth/events/doc.go rename to domains/events/doc.go diff --git a/auth/events/events.go b/domains/events/events.go similarity index 52% rename from auth/events/events.go rename to domains/events/events.go index e0fe609a0a..94135ffd79 100644 --- a/auth/events/events.go +++ b/domains/events/events.go @@ -6,38 +6,34 @@ package events import ( "time" - "github.com/absmach/magistrala/auth" + "github.com/absmach/magistrala/domains" "github.com/absmach/magistrala/pkg/events" - "github.com/absmach/magistrala/pkg/policies" ) const ( - domainPrefix = "domain." - domainCreate = domainPrefix + "create" - domainRetrieve = domainPrefix + "retrieve" - domainRetrievePermissions = domainPrefix + "retrieve_permissions" - domainUpdate = domainPrefix + "update" - domainChangeStatus = domainPrefix + "change_status" - domainList = domainPrefix + "list" - domainAssign = domainPrefix + "assign" - domainUnassign = domainPrefix + "unassign" - domainUserList = domainPrefix + "user_list" + domainPrefix = "domain." + domainCreate = domainPrefix + "create" + domainRetrieve = domainPrefix + "retrieve" + domainUpdate = domainPrefix + "update" + domainEnable = domainPrefix + "enable" + domainDisable = domainPrefix + "disable" + domainFreeze = domainPrefix + "freeze" + domainList = domainPrefix + "list" + domainUserDelete = domainPrefix + "user_delete" ) var ( _ events.Event = (*createDomainEvent)(nil) _ events.Event = (*retrieveDomainEvent)(nil) - _ events.Event = (*retrieveDomainPermissionsEvent)(nil) _ events.Event = (*updateDomainEvent)(nil) - _ events.Event = (*changeDomainStatusEvent)(nil) + _ events.Event = (*enableDomainEvent)(nil) + _ events.Event = (*disableDomainEvent)(nil) + _ events.Event = (*freezeDomainEvent)(nil) _ events.Event = (*listDomainsEvent)(nil) - _ events.Event = (*assignUsersEvent)(nil) - _ events.Event = (*unassignUsersEvent)(nil) - _ events.Event = (*listUserDomainsEvent)(nil) ) type createDomainEvent struct { - auth.Domain + domains.Domain } func (cde createDomainEvent) Encode() (map[string]interface{}, error) { @@ -67,7 +63,7 @@ func (cde createDomainEvent) Encode() (map[string]interface{}, error) { } type retrieveDomainEvent struct { - auth.Domain + domains.Domain } func (rde retrieveDomainEvent) Encode() (map[string]interface{}, error) { @@ -98,26 +94,8 @@ func (rde retrieveDomainEvent) Encode() (map[string]interface{}, error) { return val, nil } -type retrieveDomainPermissionsEvent struct { - domainID string - permissions policies.Permissions -} - -func (rpe retrieveDomainPermissionsEvent) Encode() (map[string]interface{}, error) { - val := map[string]interface{}{ - "operation": domainRetrievePermissions, - "domain_id": rpe.domainID, - } - - if rpe.permissions != nil { - val["permissions"] = rpe.permissions - } - - return val, nil -} - type updateDomainEvent struct { - auth.Domain + domains.Domain } func (ude updateDomainEvent) Encode() (map[string]interface{}, error) { @@ -145,25 +123,53 @@ func (ude updateDomainEvent) Encode() (map[string]interface{}, error) { return val, nil } -type changeDomainStatusEvent struct { +type enableDomainEvent struct { + domainID string + updatedAt time.Time + updatedBy string +} + +func (cdse enableDomainEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": domainEnable, + "id": cdse.domainID, + "updated_at": cdse.updatedAt, + "updated_by": cdse.updatedBy, + }, nil +} + +type disableDomainEvent struct { domainID string - status auth.Status updatedAt time.Time updatedBy string } -func (cdse changeDomainStatusEvent) Encode() (map[string]interface{}, error) { +func (cdse disableDomainEvent) Encode() (map[string]interface{}, error) { return map[string]interface{}{ - "operation": domainChangeStatus, + "operation": domainDisable, + "id": cdse.domainID, + "updated_at": cdse.updatedAt, + "updated_by": cdse.updatedBy, + }, nil +} + +type freezeDomainEvent struct { + domainID string + updatedAt time.Time + updatedBy string +} + +func (cdse freezeDomainEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": domainFreeze, "id": cdse.domainID, - "status": cdse.status.String(), "updated_at": cdse.updatedAt, "updated_by": cdse.updatedBy, }, nil } type listDomainsEvent struct { - auth.Page + domains.Page total uint64 } @@ -212,85 +218,14 @@ func (lde listDomainsEvent) Encode() (map[string]interface{}, error) { return val, nil } -type assignUsersEvent struct { - userIDs []string - domainID string - relation string -} - -func (ase assignUsersEvent) Encode() (map[string]interface{}, error) { - val := map[string]interface{}{ - "operation": domainAssign, - "user_ids": ase.userIDs, - "domain_id": ase.domainID, - "relation": ase.relation, - } - - return val, nil -} - -type unassignUsersEvent struct { - userID string - domainID string -} - -func (use unassignUsersEvent) Encode() (map[string]interface{}, error) { - val := map[string]interface{}{ - "operation": domainUnassign, - "user_id": use.userID, - "domain_id": use.domainID, - } - - return val, nil -} - -type listUserDomainsEvent struct { - auth.Page +type deleteUserFromDomainsEvent struct { userID string } -func (lde listUserDomainsEvent) Encode() (map[string]interface{}, error) { +func (dude deleteUserFromDomainsEvent) Encode() (map[string]interface{}, error) { val := map[string]interface{}{ - "operation": domainUserList, - "total": lde.Total, - "offset": lde.Offset, - "limit": lde.Limit, - "user_id": lde.userID, - } - - if lde.Name != "" { - val["name"] = lde.Name - } - if lde.Order != "" { - val["order"] = lde.Order + "operation": domainUserDelete, + "user_id": dude.userID, } - if lde.Dir != "" { - val["dir"] = lde.Dir - } - if lde.Metadata != nil { - val["metadata"] = lde.Metadata - } - if lde.Tag != "" { - val["tag"] = lde.Tag - } - if lde.Permission != "" { - val["permission"] = lde.Permission - } - if lde.Status.String() != "" { - val["status"] = lde.Status.String() - } - if lde.ID != "" { - val["id"] = lde.ID - } - if len(lde.IDs) > 0 { - val["ids"] = lde.IDs - } - if lde.Identity != "" { - val["identity"] = lde.Identity - } - if lde.SubjectID != "" { - val["subject_id"] = lde.SubjectID - } - return val, nil } diff --git a/domains/events/streams.go b/domains/events/streams.go new file mode 100644 index 0000000000..6f53b84af7 --- /dev/null +++ b/domains/events/streams.go @@ -0,0 +1,180 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/events" + "github.com/absmach/magistrala/pkg/events/store" + rmEvents "github.com/absmach/magistrala/pkg/roles/rolemanager/events" +) + +const streamID = "magistrala.domains" + +var _ domains.Service = (*eventStore)(nil) + +type eventStore struct { + events.Publisher + svc domains.Service + rmEvents.RoleManagerEventStore +} + +// NewEventStoreMiddleware returns wrapper around auth service that sends +// events to event store. +func NewEventStoreMiddleware(ctx context.Context, svc domains.Service, url string) (domains.Service, error) { + publisher, err := store.NewPublisher(ctx, url, streamID) + if err != nil { + return nil, err + } + + res := rmEvents.NewRoleManagerEventStore("domains", svc, publisher) + + return &eventStore{ + svc: svc, + Publisher: publisher, + RoleManagerEventStore: res, + }, nil +} + +func (es *eventStore) CreateDomain(ctx context.Context, session authn.Session, domain domains.Domain) (domains.Domain, error) { + domain, err := es.svc.CreateDomain(ctx, session, domain) + if err != nil { + return domain, err + } + + event := createDomainEvent{ + domain, + } + + if err := es.Publish(ctx, event); err != nil { + return domain, err + } + + return domain, nil +} + +func (es *eventStore) RetrieveDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + domain, err := es.svc.RetrieveDomain(ctx, session, id) + if err != nil { + return domain, err + } + + event := retrieveDomainEvent{ + domain, + } + + if err := es.Publish(ctx, event); err != nil { + return domain, err + } + + return domain, nil +} + +func (es *eventStore) UpdateDomain(ctx context.Context, session authn.Session, id string, d domains.DomainReq) (domains.Domain, error) { + domain, err := es.svc.UpdateDomain(ctx, session, id, d) + if err != nil { + return domain, err + } + + event := updateDomainEvent{ + domain, + } + + if err := es.Publish(ctx, event); err != nil { + return domain, err + } + + return domain, nil +} + +func (es *eventStore) EnableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + domain, err := es.svc.EnableDomain(ctx, session, id) + if err != nil { + return domain, err + } + + event := enableDomainEvent{ + domainID: id, + updatedAt: domain.UpdatedAt, + updatedBy: domain.UpdatedBy, + } + + if err := es.Publish(ctx, event); err != nil { + return domain, err + } + + return domain, nil +} + +func (es *eventStore) DisableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + domain, err := es.svc.DisableDomain(ctx, session, id) + if err != nil { + return domain, err + } + + event := disableDomainEvent{ + domainID: id, + updatedAt: domain.UpdatedAt, + updatedBy: domain.UpdatedBy, + } + + if err := es.Publish(ctx, event); err != nil { + return domain, err + } + + return domain, nil +} + +func (es *eventStore) FreezeDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + domain, err := es.svc.FreezeDomain(ctx, session, id) + if err != nil { + return domain, err + } + + event := freezeDomainEvent{ + domainID: id, + updatedAt: domain.UpdatedAt, + updatedBy: domain.UpdatedBy, + } + + if err := es.Publish(ctx, event); err != nil { + return domain, err + } + + return domain, nil +} + +func (es *eventStore) ListDomains(ctx context.Context, session authn.Session, p domains.Page) (domains.DomainsPage, error) { + dp, err := es.svc.ListDomains(ctx, session, p) + if err != nil { + return dp, err + } + + event := listDomainsEvent{ + p, dp.Total, + } + + if err := es.Publish(ctx, event); err != nil { + return dp, err + } + + return dp, nil +} + +func (es *eventStore) DeleteUserFromDomains(ctx context.Context, userID string) error { + if err := es.svc.DeleteUserFromDomains(ctx, userID); err != nil { + return err + } + + event := deleteUserFromDomainsEvent{userID} + + if err := es.Publish(ctx, event); err != nil { + return err + } + + return nil +} diff --git a/domains/middleware/authorization.go b/domains/middleware/authorization.go new file mode 100644 index 0000000000..7e0e92a1a3 --- /dev/null +++ b/domains/middleware/authorization.go @@ -0,0 +1,152 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/authz" + mgauthz "github.com/absmach/magistrala/pkg/authz" + "github.com/absmach/magistrala/pkg/policies" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" + "github.com/absmach/magistrala/pkg/svcutil" +) + +var _ domains.Service = (*authorizationMiddleware)(nil) + +type authorizationMiddleware struct { + svc domains.Service + authz mgauthz.Authorization + opp svcutil.OperationPerm + rmMW.RoleManagerAuthorizationMiddleware +} + +// AuthorizationMiddleware adds authorization to the clients service. +func AuthorizationMiddleware(entityType string, svc domains.Service, authz mgauthz.Authorization, domainsOpPerm, rolesOpPerm map[svcutil.Operation]svcutil.Permission) (domains.Service, error) { + opp := domains.NewOperationPerm() + if err := opp.AddOperationPermissionMap(domainsOpPerm); err != nil { + return nil, err + } + if err := opp.Validate(); err != nil { + return nil, err + } + + ram, err := rmMW.NewRoleManagerAuthorizationMiddleware(entityType, svc, authz, rolesOpPerm) + if err != nil { + return nil, err + } + return &authorizationMiddleware{ + svc: svc, + authz: authz, + opp: opp, + RoleManagerAuthorizationMiddleware: ram, + }, nil +} + +func (am *authorizationMiddleware) CreateDomain(ctx context.Context, session authn.Session, d domains.Domain) (domains.Domain, error) { + return am.svc.CreateDomain(ctx, session, d) +} + +func (am *authorizationMiddleware) RetrieveDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + if err := am.authorize(ctx, domains.OpRetrieveDomain, authz.PolicyReq{ + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: id, + ObjectType: policies.DomainType, + }); err != nil { + return domains.Domain{}, err + } + return am.svc.RetrieveDomain(ctx, session, id) +} + +func (am *authorizationMiddleware) UpdateDomain(ctx context.Context, session authn.Session, id string, d domains.DomainReq) (domains.Domain, error) { + if err := am.authorize(ctx, domains.OpUpdateDomain, authz.PolicyReq{ + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: id, + ObjectType: policies.DomainType, + }); err != nil { + return domains.Domain{}, err + } + return am.svc.UpdateDomain(ctx, session, id, d) +} + +func (am *authorizationMiddleware) EnableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + if err := am.authorize(ctx, domains.OpEnableDomain, authz.PolicyReq{ + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: id, + ObjectType: policies.DomainType, + }); err != nil { + return domains.Domain{}, err + } + + return am.svc.EnableDomain(ctx, session, id) +} + +func (am *authorizationMiddleware) DisableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + if err := am.authorize(ctx, domains.OpDisableDomain, authz.PolicyReq{ + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: id, + ObjectType: policies.DomainType, + }); err != nil { + return domains.Domain{}, err + } + + return am.svc.DisableDomain(ctx, session, id) +} + +func (am *authorizationMiddleware) FreezeDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + // Only SuperAdmin can freeze the domain + if err := am.authz.Authorize(ctx, authz.PolicyReq{ + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Permission: policies.AdminPermission, + Object: id, + ObjectType: policies.DomainType, + }); err != nil { + return domains.Domain{}, err + } + return am.svc.FreezeDomain(ctx, session, id) +} + +func (am *authorizationMiddleware) ListDomains(ctx context.Context, session authn.Session, page domains.Page) (domains.DomainsPage, error) { + if err := am.authz.Authorize(ctx, authz.PolicyReq{ + Subject: session.UserID, + SubjectType: policies.UserType, + Permission: policies.AdminPermission, + ObjectType: policies.PlatformType, + Object: policies.MagistralaObject, + }); err == nil { + session.SuperAdmin = true + } + + return am.svc.ListDomains(ctx, session, page) +} + +func (am *authorizationMiddleware) DeleteUserFromDomains(ctx context.Context, id string) (err error) { + return am.svc.DeleteUserFromDomains(ctx, id) +} + +func (am *authorizationMiddleware) authorize(ctx context.Context, op svcutil.Operation, authReq authz.PolicyReq) error { + perm, err := am.opp.GetPermission(op) + if err != nil { + return err + } + authReq.Permission = perm.String() + + if err := am.authz.Authorize(ctx, authReq); err != nil { + return err + } + + return nil +} diff --git a/domains/middleware/logging.go b/domains/middleware/logging.go new file mode 100644 index 0000000000..bf114d812b --- /dev/null +++ b/domains/middleware/logging.go @@ -0,0 +1,180 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +//go:build !test + +package middleware + +import ( + "context" + "log/slog" + "time" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/pkg/authn" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" +) + +var _ domains.Service = (*loggingMiddleware)(nil) + +type loggingMiddleware struct { + logger *slog.Logger + svc domains.Service + rmMW.RoleManagerLoggingMiddleware +} + +// LoggingMiddleware adds logging facilities to the core service. +func LoggingMiddleware(svc domains.Service, logger *slog.Logger) domains.Service { + rmlm := rmMW.NewRoleManagerLoggingMiddleware("domains", svc, logger) + return &loggingMiddleware{ + logger: logger, + svc: svc, + RoleManagerLoggingMiddleware: rmlm, + } +} + +func (lm *loggingMiddleware) CreateDomain(ctx context.Context, session authn.Session, d domains.Domain) (do domains.Domain, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("domain", + slog.String("id", d.ID), + slog.String("name", d.Name), + ), + } + if err != nil { + args := append(args, slog.String("error", err.Error())) + lm.logger.Warn("Create domain failed", args...) + return + } + lm.logger.Info("Create domain completed successfully", args...) + }(time.Now()) + return lm.svc.CreateDomain(ctx, session, d) +} + +func (lm *loggingMiddleware) RetrieveDomain(ctx context.Context, session authn.Session, id string) (do domains.Domain, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("domain_id", id), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Retrieve domain failed", args...) + return + } + lm.logger.Info("Retrieve domain completed successfully", args...) + }(time.Now()) + return lm.svc.RetrieveDomain(ctx, session, id) +} + +func (lm *loggingMiddleware) UpdateDomain(ctx context.Context, session authn.Session, id string, d domains.DomainReq) (do domains.Domain, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("domain", + slog.String("id", id), + slog.Any("name", d.Name), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Update domain failed", args...) + return + } + lm.logger.Info("Update domain completed successfully", args...) + }(time.Now()) + return lm.svc.UpdateDomain(ctx, session, id, d) +} + +func (lm *loggingMiddleware) EnableDomain(ctx context.Context, session authn.Session, id string) (do domains.Domain, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("domain", + slog.String("id", id), + slog.String("name", do.Name), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Enable domain failed", args...) + return + } + lm.logger.Info("Enable domain completed successfully", args...) + }(time.Now()) + return lm.svc.EnableDomain(ctx, session, id) +} + +func (lm *loggingMiddleware) DisableDomain(ctx context.Context, session authn.Session, id string) (do domains.Domain, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("domain", + slog.String("id", id), + slog.String("name", do.Name), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Disable domain failed", args...) + return + } + lm.logger.Info("Disable domain completed successfully", args...) + }(time.Now()) + return lm.svc.DisableDomain(ctx, session, id) +} + +func (lm *loggingMiddleware) FreezeDomain(ctx context.Context, session authn.Session, id string) (do domains.Domain, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("domain", + slog.String("id", id), + slog.String("name", do.Name), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Freeze domain failed", args...) + return + } + lm.logger.Info("Freeze domain completed successfully", args...) + }(time.Now()) + return lm.svc.FreezeDomain(ctx, session, id) +} + +func (lm *loggingMiddleware) ListDomains(ctx context.Context, session authn.Session, page domains.Page) (do domains.DomainsPage, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("page", + slog.Uint64("limit", page.Limit), + slog.Uint64("offset", page.Offset), + slog.Uint64("total", page.Total), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("List domains failed", args...) + return + } + lm.logger.Info("List domains completed successfully", args...) + }(time.Now()) + return lm.svc.ListDomains(ctx, session, page) +} +func (lm *loggingMiddleware) DeleteUserFromDomains(ctx context.Context, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("id", id), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Delete entity policies failed to complete successfully", args...) + return + } + lm.logger.Info("Delete entity policies completed successfully", args...) + }(time.Now()) + return lm.svc.DeleteUserFromDomains(ctx, id) +} diff --git a/domains/middleware/metrics.go b/domains/middleware/metrics.go new file mode 100644 index 0000000000..e7378190bc --- /dev/null +++ b/domains/middleware/metrics.go @@ -0,0 +1,101 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +//go:build !test + +package middleware + +import ( + "context" + "time" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/pkg/authn" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" + "github.com/go-kit/kit/metrics" +) + +var _ domains.Service = (*metricsMiddleware)(nil) + +type metricsMiddleware struct { + counter metrics.Counter + latency metrics.Histogram + svc domains.Service + rmMW.RoleManagerMetricsMiddleware +} + +// MetricsMiddleware instruments core service by tracking request count and latency. +func MetricsMiddleware(svc domains.Service, counter metrics.Counter, latency metrics.Histogram) domains.Service { + rmmw := rmMW.NewRoleManagerMetricsMiddleware("domains", svc, counter, latency) + + return &metricsMiddleware{ + counter: counter, + latency: latency, + svc: svc, + RoleManagerMetricsMiddleware: rmmw, + } +} + +func (ms *metricsMiddleware) CreateDomain(ctx context.Context, session authn.Session, d domains.Domain) (domains.Domain, error) { + defer func(begin time.Time) { + ms.counter.With("method", "create_domain").Add(1) + ms.latency.With("method", "create_domain").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.CreateDomain(ctx, session, d) +} + +func (ms *metricsMiddleware) RetrieveDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + defer func(begin time.Time) { + ms.counter.With("method", "retrieve_domain").Add(1) + ms.latency.With("method", "retrieve_domain").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.RetrieveDomain(ctx, session, id) +} + +func (ms *metricsMiddleware) UpdateDomain(ctx context.Context, session authn.Session, id string, d domains.DomainReq) (domains.Domain, error) { + defer func(begin time.Time) { + ms.counter.With("method", "update_domain").Add(1) + ms.latency.With("method", "update_domain").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.UpdateDomain(ctx, session, id, d) +} + +func (ms *metricsMiddleware) EnableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + defer func(begin time.Time) { + ms.counter.With("method", "enable_domain").Add(1) + ms.latency.With("method", "enable_domain").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.EnableDomain(ctx, session, id) +} + +func (ms *metricsMiddleware) DisableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + defer func(begin time.Time) { + ms.counter.With("method", "disable_domain").Add(1) + ms.latency.With("method", "disable_domain").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.DisableDomain(ctx, session, id) +} + +func (ms *metricsMiddleware) FreezeDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + defer func(begin time.Time) { + ms.counter.With("method", "freeze_domain").Add(1) + ms.latency.With("method", "freeze_domain").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.FreezeDomain(ctx, session, id) +} + +func (ms *metricsMiddleware) ListDomains(ctx context.Context, session authn.Session, page domains.Page) (domains.DomainsPage, error) { + defer func(begin time.Time) { + ms.counter.With("method", "list_domains").Add(1) + ms.latency.With("method", "list_domains").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.ListDomains(ctx, session, page) +} + +func (ms *metricsMiddleware) DeleteUserFromDomains(ctx context.Context, id string) error { + defer func(begin time.Time) { + ms.counter.With("method", "delete_user_from_domains").Add(1) + ms.latency.With("method", "delete_user_from_domains").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.DeleteUserFromDomains(ctx, id) +} diff --git a/auth/mocks/domains_client.go b/domains/mocks/domains_client.go similarity index 71% rename from auth/mocks/domains_client.go rename to domains/mocks/domains_client.go index 7950316f88..e664e6b384 100644 --- a/auth/mocks/domains_client.go +++ b/domains/mocks/domains_client.go @@ -11,9 +11,9 @@ import ( grpc "google.golang.org/grpc" - magistrala "github.com/absmach/magistrala" - mock "github.com/stretchr/testify/mock" + + v1 "github.com/absmach/magistrala/internal/grpc/domains/v1" ) // DomainsServiceClient is an autogenerated mock type for the DomainsServiceClient type @@ -30,7 +30,7 @@ func (_m *DomainsServiceClient) EXPECT() *DomainsServiceClient_Expecter { } // DeleteUserFromDomains provides a mock function with given fields: ctx, in, opts -func (_m *DomainsServiceClient) DeleteUserFromDomains(ctx context.Context, in *magistrala.DeleteUserReq, opts ...grpc.CallOption) (*magistrala.DeleteUserRes, error) { +func (_m *DomainsServiceClient) DeleteUserFromDomains(ctx context.Context, in *v1.DeleteUserReq, opts ...grpc.CallOption) (*v1.DeleteUserRes, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] @@ -44,20 +44,20 @@ func (_m *DomainsServiceClient) DeleteUserFromDomains(ctx context.Context, in *m panic("no return value specified for DeleteUserFromDomains") } - var r0 *magistrala.DeleteUserRes + var r0 *v1.DeleteUserRes var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.DeleteUserReq, ...grpc.CallOption) (*magistrala.DeleteUserRes, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.DeleteUserReq, ...grpc.CallOption) (*v1.DeleteUserRes, error)); ok { return rf(ctx, in, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.DeleteUserReq, ...grpc.CallOption) *magistrala.DeleteUserRes); ok { + if rf, ok := ret.Get(0).(func(context.Context, *v1.DeleteUserReq, ...grpc.CallOption) *v1.DeleteUserRes); ok { r0 = rf(ctx, in, opts...) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*magistrala.DeleteUserRes) + r0 = ret.Get(0).(*v1.DeleteUserRes) } } - if rf, ok := ret.Get(1).(func(context.Context, *magistrala.DeleteUserReq, ...grpc.CallOption) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *v1.DeleteUserReq, ...grpc.CallOption) error); ok { r1 = rf(ctx, in, opts...) } else { r1 = ret.Error(1) @@ -73,14 +73,14 @@ type DomainsServiceClient_DeleteUserFromDomains_Call struct { // DeleteUserFromDomains is a helper method to define mock.On call // - ctx context.Context -// - in *magistrala.DeleteUserReq +// - in *v1.DeleteUserReq // - opts ...grpc.CallOption func (_e *DomainsServiceClient_Expecter) DeleteUserFromDomains(ctx interface{}, in interface{}, opts ...interface{}) *DomainsServiceClient_DeleteUserFromDomains_Call { return &DomainsServiceClient_DeleteUserFromDomains_Call{Call: _e.mock.On("DeleteUserFromDomains", append([]interface{}{ctx, in}, opts...)...)} } -func (_c *DomainsServiceClient_DeleteUserFromDomains_Call) Run(run func(ctx context.Context, in *magistrala.DeleteUserReq, opts ...grpc.CallOption)) *DomainsServiceClient_DeleteUserFromDomains_Call { +func (_c *DomainsServiceClient_DeleteUserFromDomains_Call) Run(run func(ctx context.Context, in *v1.DeleteUserReq, opts ...grpc.CallOption)) *DomainsServiceClient_DeleteUserFromDomains_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]grpc.CallOption, len(args)-2) for i, a := range args[2:] { @@ -88,17 +88,17 @@ func (_c *DomainsServiceClient_DeleteUserFromDomains_Call) Run(run func(ctx cont variadicArgs[i] = a.(grpc.CallOption) } } - run(args[0].(context.Context), args[1].(*magistrala.DeleteUserReq), variadicArgs...) + run(args[0].(context.Context), args[1].(*v1.DeleteUserReq), variadicArgs...) }) return _c } -func (_c *DomainsServiceClient_DeleteUserFromDomains_Call) Return(_a0 *magistrala.DeleteUserRes, _a1 error) *DomainsServiceClient_DeleteUserFromDomains_Call { +func (_c *DomainsServiceClient_DeleteUserFromDomains_Call) Return(_a0 *v1.DeleteUserRes, _a1 error) *DomainsServiceClient_DeleteUserFromDomains_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *DomainsServiceClient_DeleteUserFromDomains_Call) RunAndReturn(run func(context.Context, *magistrala.DeleteUserReq, ...grpc.CallOption) (*magistrala.DeleteUserRes, error)) *DomainsServiceClient_DeleteUserFromDomains_Call { +func (_c *DomainsServiceClient_DeleteUserFromDomains_Call) RunAndReturn(run func(context.Context, *v1.DeleteUserReq, ...grpc.CallOption) (*v1.DeleteUserRes, error)) *DomainsServiceClient_DeleteUserFromDomains_Call { _c.Call.Return(run) return _c } diff --git a/domains/mocks/repository.go b/domains/mocks/repository.go new file mode 100644 index 0000000000..d0d9ec4b47 --- /dev/null +++ b/domains/mocks/repository.go @@ -0,0 +1,654 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + domains "github.com/absmach/magistrala/domains" + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// Repository is an autogenerated mock type for the Repository type +type Repository struct { + mock.Mock +} + +// AddRoles provides a mock function with given fields: ctx, rps +func (_m *Repository) AddRoles(ctx context.Context, rps []roles.RoleProvision) ([]roles.Role, error) { + ret := _m.Called(ctx, rps) + + if len(ret) == 0 { + panic("no return value specified for AddRoles") + } + + var r0 []roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) ([]roles.Role, error)); ok { + return rf(ctx, rps) + } + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) []roles.Role); ok { + r0 = rf(ctx, rps) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []roles.RoleProvision) error); ok { + r1 = rf(ctx, rps) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *Repository) Delete(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ListDomains provides a mock function with given fields: ctx, pm +func (_m *Repository) ListDomains(ctx context.Context, pm domains.Page) (domains.DomainsPage, error) { + ret := _m.Called(ctx, pm) + + if len(ret) == 0 { + panic("no return value specified for ListDomains") + } + + var r0 domains.DomainsPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, domains.Page) (domains.DomainsPage, error)); ok { + return rf(ctx, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, domains.Page) domains.DomainsPage); ok { + r0 = rf(ctx, pm) + } else { + r0 = ret.Get(0).(domains.DomainsPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, domains.Page) error); ok { + r1 = rf(ctx, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, members +func (_m *Repository) RemoveMemberFromAllRoles(ctx context.Context, members string) error { + ret := _m.Called(ctx, members) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRoles provides a mock function with given fields: ctx, roleIDs +func (_m *Repository) RemoveRoles(ctx context.Context, roleIDs []string) error { + ret := _m.Called(ctx, roleIDs) + + if len(ret) == 0 { + panic("no return value specified for RemoveRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok { + r0 = rf(ctx, roleIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAllByIDs provides a mock function with given fields: ctx, pm +func (_m *Repository) RetrieveAllByIDs(ctx context.Context, pm domains.Page) (domains.DomainsPage, error) { + ret := _m.Called(ctx, pm) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllByIDs") + } + + var r0 domains.DomainsPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, domains.Page) (domains.DomainsPage, error)); ok { + return rf(ctx, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, domains.Page) domains.DomainsPage); ok { + r0 = rf(ctx, pm) + } else { + r0 = ret.Get(0).(domains.DomainsPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, domains.Page) error); ok { + r1 = rf(ctx, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, entityID, limit, offset +func (_m *Repository) RetrieveAllRoles(ctx context.Context, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveByID provides a mock function with given fields: ctx, id +func (_m *Repository) RetrieveByID(ctx context.Context, id string) (domains.Domain, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for RetrieveByID") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (domains.Domain, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) domains.Domain); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveEntitiesRolesActionsMembers provides a mock function with given fields: ctx, entityIDs +func (_m *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) { + ret := _m.Called(ctx, entityIDs) + + if len(ret) == 0 { + panic("no return value specified for RetrieveEntitiesRolesActionsMembers") + } + + var r0 []roles.EntityActionRole + var r1 []roles.EntityMemberRole + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error)); ok { + return rf(ctx, entityIDs) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []roles.EntityActionRole); ok { + r0 = rf(ctx, entityIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.EntityActionRole) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) []roles.EntityMemberRole); ok { + r1 = rf(ctx, entityIDs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]roles.EntityMemberRole) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, []string) error); ok { + r2 = rf(ctx, entityIDs) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RetrieveRole provides a mock function with given fields: ctx, roleID +func (_m *Repository) RetrieveRole(ctx context.Context, roleID string) (roles.Role, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (roles.Role, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) roles.Role); ok { + r0 = rf(ctx, roleID) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRoleByEntityIDAndName provides a mock function with given fields: ctx, entityID, roleName +func (_m *Repository) RetrieveRoleByEntityIDAndName(ctx context.Context, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRoleByEntityIDAndName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (roles.Role, error)); ok { + return rf(ctx, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) roles.Role); ok { + r0 = rf(ctx, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleAddActions(ctx context.Context, role roles.Role, actions []string) ([]string, error) { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleAddMembers(ctx context.Context, role roles.Role, members []string) ([]string, error) { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, members) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, roleID, actions +func (_m *Repository) RoleCheckActionsExists(ctx context.Context, roleID string, actions []string) (bool, error) { + ret := _m.Called(ctx, roleID, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, roleID, members +func (_m *Repository) RoleCheckMembersExists(ctx context.Context, roleID string, members []string) (bool, error) { + ret := _m.Called(ctx, roleID, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, members) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, roleID +func (_m *Repository) RoleListActions(ctx context.Context, roleID string) ([]string, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, roleID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, roleID, limit, offset +func (_m *Repository) RoleListMembers(ctx context.Context, roleID string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, roleID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, roleID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, roleID, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, roleID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleRemoveActions(ctx context.Context, role roles.Role, actions []string) error { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllActions(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllMembers(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleRemoveMembers(ctx context.Context, role roles.Role, members []string) error { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Save provides a mock function with given fields: ctx, d +func (_m *Repository) Save(ctx context.Context, d domains.Domain) (domains.Domain, error) { + ret := _m.Called(ctx, d) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, domains.Domain) (domains.Domain, error)); ok { + return rf(ctx, d) + } + if rf, ok := ret.Get(0).(func(context.Context, domains.Domain) domains.Domain); ok { + r0 = rf(ctx, d) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, domains.Domain) error); ok { + r1 = rf(ctx, d) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, id, userID, d +func (_m *Repository) Update(ctx context.Context, id string, userID string, d domains.DomainReq) (domains.Domain, error) { + ret := _m.Called(ctx, id, userID, d) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, domains.DomainReq) (domains.Domain, error)); ok { + return rf(ctx, id, userID, d) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, domains.DomainReq) domains.Domain); ok { + r0 = rf(ctx, id, userID, d) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, domains.DomainReq) error); ok { + r1 = rf(ctx, id, userID, d) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRole provides a mock function with given fields: ctx, ro +func (_m *Repository) UpdateRole(ctx context.Context, ro roles.Role) (roles.Role, error) { + ret := _m.Called(ctx, ro) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) (roles.Role, error)); ok { + return rf(ctx, ro) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) roles.Role); ok { + r0 = rf(ctx, ro) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role) error); ok { + r1 = rf(ctx, ro) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRepository creates a new instance of Repository. 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 NewRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *Repository { + mock := &Repository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/domains/mocks/service.go b/domains/mocks/service.go new file mode 100644 index 0000000000..76d98e7c99 --- /dev/null +++ b/domains/mocks/service.go @@ -0,0 +1,674 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + authn "github.com/absmach/magistrala/pkg/authn" + + domains "github.com/absmach/magistrala/domains" + + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +// AddRole provides a mock function with given fields: ctx, session, entityID, roleName, optionalActions, optionalMembers +func (_m *Service) AddRole(ctx context.Context, session authn.Session, entityID string, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName, optionalActions, optionalMembers) + + if len(ret) == 0 { + panic("no return value specified for AddRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateDomain provides a mock function with given fields: ctx, sesssion, d +func (_m *Service) CreateDomain(ctx context.Context, sesssion authn.Session, d domains.Domain) (domains.Domain, error) { + ret := _m.Called(ctx, sesssion, d) + + if len(ret) == 0 { + panic("no return value specified for CreateDomain") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, domains.Domain) (domains.Domain, error)); ok { + return rf(ctx, sesssion, d) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, domains.Domain) domains.Domain); ok { + r0 = rf(ctx, sesssion, d) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, domains.Domain) error); ok { + r1 = rf(ctx, sesssion, d) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteUserFromDomains provides a mock function with given fields: ctx, id +func (_m *Service) DeleteUserFromDomains(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteUserFromDomains") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DisableDomain provides a mock function with given fields: ctx, sesssion, id +func (_m *Service) DisableDomain(ctx context.Context, sesssion authn.Session, id string) (domains.Domain, error) { + ret := _m.Called(ctx, sesssion, id) + + if len(ret) == 0 { + panic("no return value specified for DisableDomain") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (domains.Domain, error)); ok { + return rf(ctx, sesssion, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) domains.Domain); ok { + r0 = rf(ctx, sesssion, id) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, sesssion, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EnableDomain provides a mock function with given fields: ctx, sesssion, id +func (_m *Service) EnableDomain(ctx context.Context, sesssion authn.Session, id string) (domains.Domain, error) { + ret := _m.Called(ctx, sesssion, id) + + if len(ret) == 0 { + panic("no return value specified for EnableDomain") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (domains.Domain, error)); ok { + return rf(ctx, sesssion, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) domains.Domain); ok { + r0 = rf(ctx, sesssion, id) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, sesssion, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FreezeDomain provides a mock function with given fields: ctx, sesssion, id +func (_m *Service) FreezeDomain(ctx context.Context, sesssion authn.Session, id string) (domains.Domain, error) { + ret := _m.Called(ctx, sesssion, id) + + if len(ret) == 0 { + panic("no return value specified for FreezeDomain") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (domains.Domain, error)); ok { + return rf(ctx, sesssion, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) domains.Domain); ok { + r0 = rf(ctx, sesssion, id) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, sesssion, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListAvailableActions provides a mock function with given fields: ctx, session +func (_m *Service) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + ret := _m.Called(ctx, session) + + if len(ret) == 0 { + panic("no return value specified for ListAvailableActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) ([]string, error)); ok { + return rf(ctx, session) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) []string); ok { + r0 = rf(ctx, session) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session) error); ok { + r1 = rf(ctx, session) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListDomains provides a mock function with given fields: ctx, sesssion, page +func (_m *Service) ListDomains(ctx context.Context, sesssion authn.Session, page domains.Page) (domains.DomainsPage, error) { + ret := _m.Called(ctx, sesssion, page) + + if len(ret) == 0 { + panic("no return value specified for ListDomains") + } + + var r0 domains.DomainsPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, domains.Page) (domains.DomainsPage, error)); ok { + return rf(ctx, sesssion, page) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, domains.Page) domains.DomainsPage); ok { + r0 = rf(ctx, sesssion, page) + } else { + r0 = ret.Get(0).(domains.DomainsPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, domains.Page) error); ok { + r1 = rf(ctx, sesssion, page) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, session, memberID +func (_m *Service) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) error { + ret := _m.Called(ctx, session, memberID) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, memberID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RemoveRole(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RemoveRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, session, entityID, limit, offset +func (_m *Service) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, session, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, session, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, session, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveDomain provides a mock function with given fields: ctx, sesssion, id +func (_m *Service) RetrieveDomain(ctx context.Context, sesssion authn.Session, id string) (domains.Domain, error) { + ret := _m.Called(ctx, sesssion, id) + + if len(ret) == 0 { + panic("no return value specified for RetrieveDomain") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (domains.Domain, error)); ok { + return rf(ctx, sesssion, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) domains.Domain); ok { + r0 = rf(ctx, sesssion, id) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, sesssion, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RetrieveRole(ctx context.Context, session authn.Session, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleAddActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleAddMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleListActions(ctx context.Context, session authn.Session, entityID string, roleName string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) []string); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, session, entityID, roleName, limit, offset +func (_m *Service) RoleListMembers(ctx context.Context, session authn.Session, entityID string, roleName string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, session, entityID, roleName, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, session, entityID, roleName, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleRemoveActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) error { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) error { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateDomain provides a mock function with given fields: ctx, sesssion, id, d +func (_m *Service) UpdateDomain(ctx context.Context, sesssion authn.Session, id string, d domains.DomainReq) (domains.Domain, error) { + ret := _m.Called(ctx, sesssion, id, d) + + if len(ret) == 0 { + panic("no return value specified for UpdateDomain") + } + + var r0 domains.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, domains.DomainReq) (domains.Domain, error)); ok { + return rf(ctx, sesssion, id, d) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, domains.DomainReq) domains.Domain); ok { + r0 = rf(ctx, sesssion, id, d) + } else { + r0 = ret.Get(0).(domains.Domain) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, domains.DomainReq) error); ok { + r1 = rf(ctx, sesssion, id, d) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRoleName provides a mock function with given fields: ctx, session, entityID, oldRoleName, newRoleName +func (_m *Service) UpdateRoleName(ctx context.Context, session authn.Session, entityID string, oldRoleName string, newRoleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, oldRoleName, newRoleName) + + if len(ret) == 0 { + panic("no return value specified for UpdateRoleName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, oldRoleName, newRoleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string) error); ok { + r1 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewService creates a new instance of Service. 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 NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/domains/postgres/doc.go b/domains/postgres/doc.go new file mode 100644 index 0000000000..ac5c81ae14 --- /dev/null +++ b/domains/postgres/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package postgres contains Key repository implementations using +// PostgreSQL as the underlying database. +package postgres diff --git a/auth/postgres/domains.go b/domains/postgres/domains.go similarity index 55% rename from auth/postgres/domains.go rename to domains/postgres/domains.go index 40ef9682e9..6c129f1fa9 100644 --- a/auth/postgres/domains.go +++ b/domains/postgres/domains.go @@ -11,61 +11,70 @@ import ( "strings" "time" - "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/domains" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" "github.com/absmach/magistrala/pkg/postgres" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" "github.com/jackc/pgtype" "github.com/jmoiron/sqlx" ) -var _ auth.DomainsRepository = (*domainRepo)(nil) +var _ domains.Repository = (*domainRepo)(nil) + +const ( + rolesTableNamePrefix = "domains" + entityTableName = "domains" + entityIDColumnName = "id" +) type domainRepo struct { db postgres.Database + rolesPostgres.Repository } -// NewDomainRepository instantiates a PostgreSQL +// New instantiates a PostgreSQL // implementation of Domain repository. -func NewDomainRepository(db postgres.Database) auth.DomainsRepository { +func New(db postgres.Database) domains.Repository { + rmsvcRepo := rolesPostgres.NewRepository(db, rolesTableNamePrefix, entityTableName, entityIDColumnName) return &domainRepo{ - db: db, + db: db, + Repository: rmsvcRepo, } } -func (repo domainRepo) Save(ctx context.Context, d auth.Domain) (ad auth.Domain, err error) { +func (repo domainRepo) Save(ctx context.Context, d domains.Domain) (dd domains.Domain, err error) { q := `INSERT INTO domains (id, name, tags, alias, metadata, created_at, updated_at, updated_by, created_by, status) VALUES (:id, :name, :tags, :alias, :metadata, :created_at, :updated_at, :updated_by, :created_by, :status) RETURNING id, name, tags, alias, metadata, created_at, updated_at, updated_by, created_by, status;` dbd, err := toDBDomain(d) if err != nil { - return auth.Domain{}, errors.Wrap(repoerr.ErrCreateEntity, errors.ErrRollbackTx) + return domains.Domain{}, errors.Wrap(repoerr.ErrCreateEntity, errors.ErrRollbackTx) } row, err := repo.db.NamedQueryContext(ctx, q, dbd) if err != nil { - return auth.Domain{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + return domains.Domain{}, postgres.HandleError(repoerr.ErrCreateEntity, err) } defer row.Close() row.Next() dbd = dbDomain{} if err := row.StructScan(&dbd); err != nil { - return auth.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + return domains.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } domain, err := toDomain(dbd) if err != nil { - return auth.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + return domains.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } return domain, nil } // RetrieveByID retrieves Domain by its unique ID. -func (repo domainRepo) RetrieveByID(ctx context.Context, id string) (auth.Domain, error) { +func (repo domainRepo) RetrieveByID(ctx context.Context, id string) (domains.Domain, error) { q := `SELECT d.id as id, d.name as name, d.tags as tags, d.alias as alias, d.metadata as metadata, d.created_at as created_at, d.updated_at as updated_at, d.updated_by as updated_by, d.created_by as created_by, d.status as status FROM domains d WHERE d.id = :id` @@ -75,64 +84,35 @@ func (repo domainRepo) RetrieveByID(ctx context.Context, id string) (auth.Domain rows, err := repo.db.NamedQueryContext(ctx, q, dbdp) if err != nil { - return auth.Domain{}, postgres.HandleError(repoerr.ErrViewEntity, err) + return domains.Domain{}, postgres.HandleError(repoerr.ErrViewEntity, err) } defer rows.Close() dbd := dbDomain{} if rows.Next() { if err = rows.StructScan(&dbd); err != nil { - return auth.Domain{}, postgres.HandleError(repoerr.ErrViewEntity, err) + return domains.Domain{}, postgres.HandleError(repoerr.ErrViewEntity, err) } domain, err := toDomain(dbd) if err != nil { - return auth.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + return domains.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } return domain, nil } - return auth.Domain{}, repoerr.ErrNotFound -} - -func (repo domainRepo) RetrievePermissions(ctx context.Context, subject, id string) ([]string, error) { - q := `SELECT pc.relation as relation - FROM domains as d - JOIN policies pc - ON pc.object_id = d.id - WHERE d.id = $1 - AND pc.subject_id = $2 - ` - - rows, err := repo.db.QueryxContext(ctx, q, id, subject) - if err != nil { - return []string{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) - } - defer rows.Close() - - domains, err := repo.processRows(rows) - if err != nil { - return []string{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) - } - - permissions := []string{} - for _, domain := range domains { - if domain.Permission != "" { - permissions = append(permissions, domain.Permission) - } - } - return permissions, nil + return domains.Domain{}, repoerr.ErrNotFound } // RetrieveAllByIDs retrieves for given Domain IDs . -func (repo domainRepo) RetrieveAllByIDs(ctx context.Context, pm auth.Page) (auth.DomainsPage, error) { +func (repo domainRepo) RetrieveAllByIDs(ctx context.Context, pm domains.Page) (domains.DomainsPage, error) { var q string if len(pm.IDs) == 0 { - return auth.DomainsPage{}, nil + return domains.DomainsPage{}, nil } query, err := buildPageQuery(pm) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } q = `SELECT d.id as id, d.name as name, d.tags as tags, d.alias as alias, d.metadata as metadata, d.created_at as created_at, d.updated_at as updated_at, d.updated_by as updated_by, d.created_by as created_by, d.status as status @@ -141,18 +121,18 @@ func (repo domainRepo) RetrieveAllByIDs(ctx context.Context, pm auth.Page) (auth dbPage, err := toDBClientsPage(pm) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() - domains, err := repo.processRows(rows) + doms, err := repo.processRows(rows) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } cq := "SELECT COUNT(*) FROM domains d" @@ -162,60 +142,64 @@ func (repo domainRepo) RetrieveAllByIDs(ctx context.Context, pm auth.Page) (auth total, err := postgres.Total(ctx, repo.db, cq, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - return auth.DomainsPage{ + return domains.DomainsPage{ Total: total, Offset: pm.Offset, Limit: pm.Limit, - Domains: domains, + Domains: doms, }, nil } // ListDomains list domains of user. -func (repo domainRepo) ListDomains(ctx context.Context, pm auth.Page) (auth.DomainsPage, error) { - var q string +func (repo domainRepo) ListDomains(ctx context.Context, pm domains.Page) (domains.DomainsPage, error) { query, err := buildPageQuery(pm) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } - q = `SELECT d.id as id, d.name as name, d.tags as tags, d.alias as alias, d.metadata as metadata, d.created_at as created_at, d.updated_at as updated_at, d.updated_by as updated_by, d.created_by as created_by, d.status as status, pc.relation as relation + q := `SELECT d.id as id, d.name as name, d.tags as tags, d.alias as alias, d.metadata as metadata, d.created_at as created_at, d.updated_at as updated_at, d.updated_by as updated_by, d.created_by as created_by, d.status as status FROM domains as d - JOIN policies pc - ON pc.object_id = d.id` + JOIN domains_roles dr + ON dr.entity_id = d.id + JOIN domains_role_members drm + ON drm.role_id = dr.id + ` - // The service sends the user ID in the pagemeta subject field, which filters domains by joining with the policies table. - // For SuperAdmins, access to domains is granted without the policies filter. - // If the user making the request is a super admin, the service will assign an empty value to the pagemeta subject field. - // In the repository, when the pagemeta subject is empty, the query should be constructed without applying the policies filter. if pm.SubjectID == "" { q = `SELECT d.id as id, d.name as name, d.tags as tags, d.alias as alias, d.metadata as metadata, d.created_at as created_at, d.updated_at as updated_at, d.updated_by as updated_by, d.created_by as created_by, d.status as status FROM domains as d` } - q = fmt.Sprintf("%s %s LIMIT %d OFFSET %d", q, query, pm.Limit, pm.Offset) + q = fmt.Sprintf("%s %s LIMIT :limit OFFSET :offset", q, query) dbPage, err := toDBClientsPage(pm) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() - domains, err := repo.processRows(rows) + doms, err := repo.processRows(rows) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - cq := "SELECT COUNT(*) FROM domains d JOIN policies pc ON pc.object_id = d.id" + cq := `SELECT COUNT(*) + FROM domains as d + JOIN domains_roles dr + ON dr.entity_id = d.id + JOIN domains_role_members drm + ON drm.role_id = dr.id + ` if pm.SubjectID == "" { - cq = "SELECT COUNT(*) FROM domains d" + cq = `SELECT COUNT(*) + FROM domains as d` } if query != "" { cq = fmt.Sprintf(" %s %s", cq, query) @@ -223,23 +207,23 @@ func (repo domainRepo) ListDomains(ctx context.Context, pm auth.Page) (auth.Doma total, err := postgres.Total(ctx, repo.db, cq, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + return domains.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - return auth.DomainsPage{ + return domains.DomainsPage{ Total: total, Offset: pm.Offset, Limit: pm.Limit, - Domains: domains, + Domains: doms, }, nil } // Update updates the client name and metadata. -func (repo domainRepo) Update(ctx context.Context, id, userID string, dr auth.DomainReq) (auth.Domain, error) { +func (repo domainRepo) Update(ctx context.Context, id, userID string, dr domains.DomainReq) (domains.Domain, error) { var query []string var upq string var ws string = "AND status = :status" - d := auth.Domain{ID: id} + d := domains.Domain{ID: id} if dr.Name != nil && *dr.Name != "" { query = append(query, "name = :name, ") d.Name = *dr.Name @@ -273,23 +257,23 @@ func (repo domainRepo) Update(ctx context.Context, id, userID string, dr auth.Do dbd, err := toDBDomain(d) if err != nil { - return auth.Domain{}, errors.Wrap(repoerr.ErrUpdateEntity, err) + return domains.Domain{}, errors.Wrap(repoerr.ErrUpdateEntity, err) } row, err := repo.db.NamedQueryContext(ctx, q, dbd) if err != nil { - return auth.Domain{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) + return domains.Domain{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) } // defer row.Close() row.Next() dbd = dbDomain{} if err := row.StructScan(&dbd); err != nil { - return auth.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + return domains.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } domain, err := toDomain(dbd) if err != nil { - return auth.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + return domains.Domain{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } return domain, nil @@ -310,97 +294,8 @@ func (repo domainRepo) Delete(ctx context.Context, id string) error { return nil } -// SavePolicies save policies in domains database. -func (repo domainRepo) SavePolicies(ctx context.Context, pcs ...auth.Policy) error { - q := `INSERT INTO policies (subject_type, subject_id, subject_relation, relation, object_type, object_id) - VALUES (:subject_type, :subject_id, :subject_relation, :relation, :object_type, :object_id) - RETURNING subject_type, subject_id, subject_relation, relation, object_type, object_id;` - - dbpc := toDBPolicies(pcs...) - row, err := repo.db.NamedQueryContext(ctx, q, dbpc) - if err != nil { - return postgres.HandleError(repoerr.ErrCreateEntity, err) - } - defer row.Close() - - return nil -} - -// CheckPolicy check policy in domains database. -func (repo domainRepo) CheckPolicy(ctx context.Context, pc auth.Policy) error { - q := ` - SELECT - subject_type, subject_id, subject_relation, relation, object_type, object_id FROM policies - WHERE - subject_type = :subject_type - AND subject_id = :subject_id - AND subject_relation = :subject_relation - AND relation = :relation - AND object_type = :object_type - AND object_id = :object_id - LIMIT 1 - ` - dbpc := toDBPolicy(pc) - row, err := repo.db.NamedQueryContext(ctx, q, dbpc) - if err != nil { - return postgres.HandleError(repoerr.ErrCreateEntity, err) - } - defer row.Close() - row.Next() - if err := row.StructScan(&dbpc); err != nil { - return errors.Wrap(repoerr.ErrNotFound, err) - } - return nil -} - -// DeletePolicies delete policies from domains database. -func (repo domainRepo) DeletePolicies(ctx context.Context, pcs ...auth.Policy) (err error) { - tx, err := repo.db.BeginTxx(ctx, nil) - if err != nil { - return err - } - defer func() { - if err != nil { - if errRollback := tx.Rollback(); errRollback != nil { - err = errors.Wrap(apiutil.ErrRollbackTx, errRollback) - } - } - }() - - for _, pc := range pcs { - q := ` - DELETE FROM - policies - WHERE - subject_type = :subject_type - AND subject_id = :subject_id - AND subject_relation = :subject_relation - AND object_type = :object_type - AND object_id = :object_id - ;` - - dbpc := toDBPolicy(pc) - row, err := tx.NamedQuery(q, dbpc) - if err != nil { - return postgres.HandleError(repoerr.ErrRemoveEntity, err) - } - defer row.Close() - } - return tx.Commit() -} - -func (repo domainRepo) DeleteUserPolicies(ctx context.Context, id string) (err error) { - q := "DELETE FROM policies WHERE subject_id = $1;" - - if _, err := repo.db.ExecContext(ctx, q, id); err != nil { - return postgres.HandleError(repoerr.ErrRemoveEntity, err) - } - - return nil -} - -func (repo domainRepo) processRows(rows *sqlx.Rows) ([]auth.Domain, error) { - var items []auth.Domain +func (repo domainRepo) processRows(rows *sqlx.Rows) ([]domains.Domain, error) { + var items []domains.Domain for rows.Next() { dbd := dbDomain{} if err := rows.StructScan(&dbd); err != nil { @@ -421,7 +316,7 @@ type dbDomain struct { Metadata []byte `db:"metadata,omitempty"` Tags pgtype.TextArray `db:"tags,omitempty"` Alias *string `db:"alias,omitempty"` - Status auth.Status `db:"status"` + Status domains.Status `db:"status"` Permission string `db:"relation"` CreatedBy string `db:"created_by"` CreatedAt time.Time `db:"created_at"` @@ -429,7 +324,7 @@ type dbDomain struct { UpdatedAt sql.NullTime `db:"updated_at,omitempty"` } -func toDBDomain(d auth.Domain) (dbDomain, error) { +func toDBDomain(d domains.Domain) (dbDomain, error) { data := []byte("{}") if len(d.Metadata) > 0 { b, err := json.Marshal(d.Metadata) @@ -471,11 +366,11 @@ func toDBDomain(d auth.Domain) (dbDomain, error) { }, nil } -func toDomain(d dbDomain) (auth.Domain, error) { - var metadata auth.Metadata +func toDomain(d dbDomain) (domains.Domain, error) { + var metadata domains.Metadata if d.Metadata != nil { if err := json.Unmarshal([]byte(d.Metadata), &metadata); err != nil { - return auth.Domain{}, errors.Wrap(errors.ErrMalformedEntity, err) + return domains.Domain{}, errors.Wrap(errors.ErrMalformedEntity, err) } } var tags []string @@ -495,7 +390,7 @@ func toDomain(d dbDomain) (auth.Domain, error) { updatedAt = d.UpdatedAt.Time } - return auth.Domain{ + return domains.Domain{ ID: d.ID, Name: d.Name, Metadata: metadata, @@ -511,22 +406,22 @@ func toDomain(d dbDomain) (auth.Domain, error) { } type dbDomainsPage struct { - Total uint64 `db:"total"` - Limit uint64 `db:"limit"` - Offset uint64 `db:"offset"` - Order string `db:"order"` - Dir string `db:"dir"` - Name string `db:"name"` - Permission string `db:"permission"` - ID string `db:"id"` - IDs []string `db:"ids"` - Metadata []byte `db:"metadata"` - Tag string `db:"tag"` - Status auth.Status `db:"status"` - SubjectID string `db:"subject_id"` + Total uint64 `db:"total"` + Limit uint64 `db:"limit"` + Offset uint64 `db:"offset"` + Order string `db:"order"` + Dir string `db:"dir"` + Name string `db:"name"` + Permission string `db:"permission"` + ID string `db:"id"` + IDs []string `db:"ids"` + Metadata []byte `db:"metadata"` + Tag string `db:"tag"` + Status domains.Status `db:"status"` + SubjectID string `db:"subject_id"` } -func toDBClientsPage(pm auth.Page) (dbDomainsPage, error) { +func toDBClientsPage(pm domains.Page) (dbDomainsPage, error) { _, data, err := postgres.CreateMetadataQuery("", pm.Metadata) if err != nil { return dbDomainsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) @@ -548,7 +443,7 @@ func toDBClientsPage(pm auth.Page) (dbDomainsPage, error) { }, nil } -func buildPageQuery(pm auth.Page) (string, error) { +func buildPageQuery(pm domains.Page) (string, error) { var query []string var emq string @@ -560,10 +455,10 @@ func buildPageQuery(pm auth.Page) (string, error) { query = append(query, fmt.Sprintf("d.id IN ('%s')", strings.Join(pm.IDs, "','"))) } - if (pm.Status >= auth.EnabledStatus) && (pm.Status < auth.AllStatus) { + if (pm.Status >= domains.EnabledStatus) && (pm.Status < domains.AllStatus) { query = append(query, "d.status = :status") } else { - query = append(query, fmt.Sprintf("d.status < %d", auth.AllStatus)) + query = append(query, fmt.Sprintf("d.status < %d", domains.AllStatus)) } if pm.Name != "" { @@ -571,11 +466,11 @@ func buildPageQuery(pm auth.Page) (string, error) { } if pm.SubjectID != "" { - query = append(query, "pc.subject_id = :subject_id") + query = append(query, "drm.member_id = :subject_id") } if pm.Permission != "" && pm.SubjectID != "" { - query = append(query, "pc.relation = :permission") + query = append(query, "dr.name = :permission") } if pm.Tag != "" { @@ -606,7 +501,7 @@ type dbPolicy struct { ObjectID string `db:"object_id,omitempty"` } -func toDBPolicies(pcs ...auth.Policy) []dbPolicy { +func toDBPolicies(pcs ...domains.Policy) []dbPolicy { var dbpcs []dbPolicy for _, pc := range pcs { dbpcs = append(dbpcs, dbPolicy{ @@ -621,7 +516,7 @@ func toDBPolicies(pcs ...auth.Policy) []dbPolicy { return dbpcs } -func toDBPolicy(pc auth.Policy) dbPolicy { +func toDBPolicy(pc domains.Policy) dbPolicy { return dbPolicy{ SubjectType: pc.SubjectType, SubjectID: pc.SubjectID, diff --git a/domains/postgres/domains_test.go b/domains/postgres/domains_test.go new file mode 100644 index 0000000000..d383a988c8 --- /dev/null +++ b/domains/postgres/domains_test.go @@ -0,0 +1,611 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/domains/postgres" + "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + inValid = "invalid" +) + +var ( + domainID = testsutil.GenerateUUID(&testing.T{}) + userID = testsutil.GenerateUUID(&testing.T{}) +) + +func TestSave(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM domains") + require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) + }) + + repo := postgres.New(database) + + cases := []struct { + desc string + domain domains.Domain + err error + }{ + { + desc: "add new domain with all fields successfully", + domain: domains.Domain{ + ID: domainID, + Name: "test", + Alias: "test", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + }, + err: nil, + }, + { + desc: "add the same domain again", + domain: domains.Domain{ + ID: domainID, + Name: "test", + Alias: "test", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + }, + err: repoerr.ErrConflict, + }, + { + desc: "add domain with empty ID", + domain: domains.Domain{ + ID: "", + Name: "test1", + Alias: "test1", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + }, + err: nil, + }, + { + desc: "add domain with empty alias", + domain: domains.Domain{ + ID: testsutil.GenerateUUID(&testing.T{}), + Name: "test1", + Alias: "", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + }, + err: repoerr.ErrCreateEntity, + }, + { + desc: "add domain with malformed metadata", + domain: domains.Domain{ + ID: domainID, + Name: "test1", + Alias: "test1", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "key": make(chan int), + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + }, + err: repoerr.ErrCreateEntity, + }, + } + + for _, tc := range cases { + _, err := repo.Save(context.Background(), tc.domain) + { + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } + } +} + +func TestRetrieveByID(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM domains") + require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) + }) + + repo := postgres.New(database) + + domain := domains.Domain{ + ID: domainID, + Name: "test", + Alias: "test", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + } + + _, err := repo.Save(context.Background(), domain) + require.Nil(t, err, fmt.Sprintf("failed to save client %s", domain.ID)) + + cases := []struct { + desc string + domainID string + response domains.Domain + err error + }{ + { + desc: "retrieve existing client", + domainID: domain.ID, + response: domain, + err: nil, + }, + { + desc: "retrieve non-existing client", + domainID: inValid, + response: domains.Domain{}, + err: repoerr.ErrNotFound, + }, + { + desc: "retrieve with empty client id", + domainID: "", + response: domains.Domain{}, + err: repoerr.ErrNotFound, + }, + } + + for _, tc := range cases { + d, err := repo.RetrieveByID(context.Background(), tc.domainID) + assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) + } +} + +func TestRetrieveAllByIDs(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM domains") + require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) + }) + + repo := postgres.New(database) + + items := []domains.Domain{} + for i := 0; i < 10; i++ { + domain := domains.Domain{ + ID: testsutil.GenerateUUID(t), + Name: fmt.Sprintf(`"test%d"`, i), + Alias: fmt.Sprintf(`"test%d"`, i), + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + } + if i%5 == 0 { + domain.Status = domains.DisabledStatus + domain.Tags = []string{"test", "admin"} + domain.Metadata = map[string]interface{}{ + "test1": "test1", + } + } + _, err := repo.Save(context.Background(), domain) + require.Nil(t, err, fmt.Sprintf("save domain unexpected error: %s", err)) + items = append(items, domain) + } + + cases := []struct { + desc string + pm domains.Page + response domains.DomainsPage + err error + }{ + { + desc: "retrieve by ids successfully", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{items[1].ID, items[2].ID}, + }, + response: domains.DomainsPage{ + Total: 2, + Offset: 0, + Limit: 10, + Domains: []domains.Domain{items[1], items[2]}, + }, + err: nil, + }, + { + desc: "retrieve by ids with empty ids", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{}, + }, + response: domains.DomainsPage{ + Total: 0, + Offset: 0, + Limit: 0, + }, + err: nil, + }, + { + desc: "retrieve by ids with invalid ids", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{inValid}, + }, + response: domains.DomainsPage{ + Total: 0, + Offset: 0, + Limit: 10, + }, + err: nil, + }, + { + desc: "retrieve by ids and status", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{items[0].ID, items[1].ID}, + Status: domains.DisabledStatus, + }, + response: domains.DomainsPage{ + Total: 1, + Offset: 0, + Limit: 10, + Domains: []domains.Domain{items[0]}, + }, + }, + { + desc: "retrieve by ids and status with invalid status", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{items[0].ID, items[1].ID}, + Status: 5, + }, + response: domains.DomainsPage{ + Total: 2, + Offset: 0, + Limit: 10, + Domains: []domains.Domain{items[0], items[1]}, + }, + }, + { + desc: "retrieve by ids and tags", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{items[0].ID, items[1].ID}, + Tag: "test", + }, + response: domains.DomainsPage{ + Total: 1, + Offset: 0, + Limit: 10, + Domains: []domains.Domain{items[1]}, + }, + }, + { + desc: " retrieve by ids and metadata", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{items[1].ID, items[2].ID}, + Metadata: map[string]interface{}{ + "test": "test", + }, + Status: domains.EnabledStatus, + }, + response: domains.DomainsPage{ + Total: 2, + Offset: 0, + Limit: 10, + Domains: items[1:3], + }, + }, + { + desc: "retrieve by ids and metadata with invalid metadata", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{items[1].ID, items[2].ID}, + Metadata: map[string]interface{}{ + "test1": "test1", + }, + Status: domains.EnabledStatus, + }, + response: domains.DomainsPage{ + Total: 0, + Offset: 0, + Limit: 10, + }, + }, + { + desc: "retrieve by ids and malfomed metadata", + pm: domains.Page{ + Offset: 0, + Limit: 10, + IDs: []string{items[1].ID, items[2].ID}, + Metadata: map[string]interface{}{ + "key": make(chan int), + }, + Status: domains.EnabledStatus, + }, + response: domains.DomainsPage{}, + err: repoerr.ErrViewEntity, + }, + { + desc: "retrieve all by ids and id", + pm: domains.Page{ + Offset: 0, + Limit: 10, + ID: items[1].ID, + IDs: []string{items[1].ID, items[2].ID}, + }, + response: domains.DomainsPage{ + Total: 1, + Offset: 0, + Limit: 10, + Domains: []domains.Domain{items[1]}, + }, + }, + { + desc: "retrieve all by ids and id with invalid id", + pm: domains.Page{ + Offset: 0, + Limit: 10, + ID: inValid, + IDs: []string{items[1].ID, items[2].ID}, + }, + response: domains.DomainsPage{ + Total: 0, + Offset: 0, + Limit: 10, + }, + }, + { + desc: "retrieve all by ids and name", + pm: domains.Page{ + Offset: 0, + Limit: 10, + Name: items[1].Name, + IDs: []string{items[1].ID, items[2].ID}, + }, + response: domains.DomainsPage{ + Total: 1, + Offset: 0, + Limit: 10, + Domains: []domains.Domain{items[1]}, + }, + }, + { + desc: "retrieve all by ids with empty page", + pm: domains.Page{}, + response: domains.DomainsPage{}, + }, + } + + for _, tc := range cases { + d, err := repo.RetrieveAllByIDs(context.Background(), tc.pm) + assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) + } +} + +func TestUpdate(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM domains") + require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) + }) + + updatedName := "test1" + updatedMetadata := domains.Metadata{ + "test1": "test1", + } + updatedTags := []string{"test1"} + updatedStatus := domains.DisabledStatus + updatedAlias := "test1" + + repo := postgres.New(database) + + domain := domains.Domain{ + ID: domainID, + Name: "test", + Alias: "test", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + } + + _, err := repo.Save(context.Background(), domain) + require.Nil(t, err, fmt.Sprintf("failed to save client %s", domain.ID)) + + cases := []struct { + desc string + domainID string + d domains.DomainReq + response domains.Domain + err error + }{ + { + desc: "update existing domain name and metadata", + domainID: domain.ID, + d: domains.DomainReq{ + Name: &updatedName, + Metadata: &updatedMetadata, + }, + response: domains.Domain{ + ID: domainID, + Name: "test1", + Alias: "test", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test1": "test1", + }, + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + UpdatedAt: time.Now(), + }, + err: nil, + }, + { + desc: "update existing domain name, metadata, tags, status and alias", + domainID: domain.ID, + d: domains.DomainReq{ + Name: &updatedName, + Metadata: &updatedMetadata, + Tags: &updatedTags, + Status: &updatedStatus, + Alias: &updatedAlias, + }, + response: domains.Domain{ + ID: domainID, + Name: "test1", + Alias: "test1", + Tags: []string{"test1"}, + Metadata: map[string]interface{}{ + "test1": "test1", + }, + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.DisabledStatus, + UpdatedAt: time.Now(), + }, + err: nil, + }, + { + desc: "update non-existing domain", + domainID: inValid, + d: domains.DomainReq{ + Name: &updatedName, + Metadata: &updatedMetadata, + }, + response: domains.Domain{}, + err: repoerr.ErrFailedOpDB, + }, + { + desc: "update domain with empty ID", + domainID: "", + d: domains.DomainReq{ + Name: &updatedName, + Metadata: &updatedMetadata, + }, + response: domains.Domain{}, + err: repoerr.ErrFailedOpDB, + }, + { + desc: "update domain with malformed metadata", + domainID: domainID, + d: domains.DomainReq{ + Name: &updatedName, + Metadata: &domains.Metadata{"key": make(chan int)}, + }, + response: domains.Domain{}, + err: repoerr.ErrUpdateEntity, + }, + } + + for _, tc := range cases { + d, err := repo.Update(context.Background(), tc.domainID, userID, tc.d) + d.UpdatedAt = tc.response.UpdatedAt + assert.Equal(t, tc.response, d, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, d)) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} + +func TestDelete(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM domains") + require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err)) + }) + + repo := postgres.New(database) + + domain := domains.Domain{ + ID: domainID, + Name: "test", + Alias: "test", + Tags: []string{"test"}, + Metadata: map[string]interface{}{ + "test": "test", + }, + CreatedBy: userID, + UpdatedBy: userID, + Status: domains.EnabledStatus, + } + + _, err := repo.Save(context.Background(), domain) + require.Nil(t, err, fmt.Sprintf("failed to save client %s", domain.ID)) + + cases := []struct { + desc string + domainID string + err error + }{ + { + desc: "delete existing domain", + domainID: domain.ID, + err: nil, + }, + { + desc: "delete non-existing domain", + domainID: inValid, + err: repoerr.ErrNotFound, + }, + { + desc: "delete domain with empty ID", + domainID: "", + err: repoerr.ErrNotFound, + }, + } + + for _, tc := range cases { + err := repo.Delete(context.Background(), tc.domainID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} diff --git a/domains/postgres/init.go b/domains/postgres/init.go new file mode 100644 index 0000000000..0d0a8d22f6 --- /dev/null +++ b/domains/postgres/init.go @@ -0,0 +1,50 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "github.com/absmach/magistrala/pkg/errors" + + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" + _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access + migrate "github.com/rubenv/sql-migrate" +) + +// Migration of Auth service. +func Migration() (*migrate.MemoryMigrationSource, error) { + rolesMigration, err := rolesPostgres.Migration(rolesTableNamePrefix, entityTableName, entityIDColumnName) + if err != nil { + return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err) + } + + domainMigrations := &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: "domain_1", + Up: []string{ + `CREATE TABLE IF NOT EXISTS domains ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(254), + tags TEXT[], + metadata JSONB, + alias VARCHAR(254) NOT NULL UNIQUE, + created_at TIMESTAMP, + updated_at TIMESTAMP, + updated_by VARCHAR(254), + created_by VARCHAR(254), + status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0) + );`, + }, + Down: []string{ + `DROP TABLE IF EXISTS domains`, + }, + }, + }, + } + + domainMigrations.Migrations = append(domainMigrations.Migrations, rolesMigration.Migrations...) + + return domainMigrations, nil +} diff --git a/domains/postgres/setup_test.go b/domains/postgres/setup_test.go new file mode 100644 index 0000000000..89a6b21368 --- /dev/null +++ b/domains/postgres/setup_test.go @@ -0,0 +1,95 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package postgres_test contains tests for PostgreSQL repository +// implementations. +package postgres_test + +import ( + "database/sql" + "fmt" + "log" + "os" + "testing" + "time" + + apostgres "github.com/absmach/magistrala/auth/postgres" + "github.com/absmach/magistrala/pkg/postgres" + pgclient "github.com/absmach/magistrala/pkg/postgres" + "github.com/jmoiron/sqlx" + dockertest "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "go.opentelemetry.io/otel" +) + +var ( + db *sqlx.DB + database postgres.Database + tracer = otel.Tracer("repo_tests") +) + +func TestMain(m *testing.M) { + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + + container, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "postgres", + Tag: "16.2-alpine", + Env: []string{ + "POSTGRES_USER=test", + "POSTGRES_PASSWORD=test", + "POSTGRES_DB=test", + "listen_addresses = '*'", + }, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + if err != nil { + log.Fatalf("Could not start container: %s", err) + } + + port := container.GetPort("5432/tcp") + + pool.MaxWait = 120 * time.Second + if err := pool.Retry(func() error { + url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port) + db, err := sql.Open("pgx", url) + if err != nil { + return err + } + return db.Ping() + }); err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + + dbConfig := pgclient.Config{ + Host: "localhost", + Port: port, + User: "test", + Pass: "test", + Name: "test", + SSLMode: "disable", + SSLCert: "", + SSLKey: "", + SSLRootCert: "", + } + + if db, err = pgclient.Setup(dbConfig, *apostgres.Migration()); err != nil { + log.Fatalf("Could not setup test DB connection: %s", err) + } + + database = postgres.NewDatabase(db, dbConfig, tracer) + + code := m.Run() + + // Defers will not be run when using os.Exit + db.Close() + if err := pool.Purge(container); err != nil { + log.Fatalf("Could not purge container: %s", err) + } + + os.Exit(code) +} diff --git a/domains/roleactions.go b/domains/roleactions.go new file mode 100644 index 0000000000..c6df84915d --- /dev/null +++ b/domains/roleactions.go @@ -0,0 +1,124 @@ +package domains + +import "github.com/absmach/magistrala/pkg/roles" + +const ( + // Domain Roles : Actions related to manage the domain. + Update roles.Action = "update" + Enable roles.Action = "enable" + Disable roles.Action = "disable" + Read roles.Action = "read" + Delete roles.Action = "delete" + Membership roles.Action = "membership" + ManageRole roles.Action = "manage_role" + AddRoleUsers roles.Action = "add_role_users" + RemoveRoleUsers roles.Action = "remove_role_users" + ViewRoleUsers roles.Action = "view_role_users" + + // Domain Roles : Actions related to entity creation and entity listing within domain + ThingCreate roles.Action = "thing_create" + ChannelCreate roles.Action = "channel_create" + GroupCreate roles.Action = "group_create" + + // Domain Things Roles: Actions related to things present within the Domain + ThingUpdate roles.Action = "thing_update" + ThingRead roles.Action = "thing_read" + ThingDelete roles.Action = "thing_delete" + ThingSetParentGroup roles.Action = "thing_set_parent_group" + ThingConnectToChannel roles.Action = "thing_connect_to_channel" + ThingManageRole roles.Action = "thing_manage_role" + ThingAddRoleUsers roles.Action = "thing_add_role_users" + ThingRemoveRoleUsers roles.Action = "thing_remove_role_users" + ThingViewRoleUsers roles.Action = "thing_view_role_users" + + // Domain Channels Roles: Actions related to channels present within the Domain + ChannelUpdate roles.Action = "channel_update" + ChannelRead roles.Action = "channel_read" + ChannelDelete roles.Action = "channel_delete" + ChannelSetParentGroup roles.Action = "channel_set_parent_group" + ChannelConnectToThing roles.Action = "channel_connect_to_thing" + ChannelPublish roles.Action = "channel_publish" + ChannelSubscribe roles.Action = "channel_subscribe" + ChannelManageRole roles.Action = "channel_manage_role" + ChannelAddRoleUsers roles.Action = "channel_add_role_users" + ChannelRemoveRoleUsers roles.Action = "channel_remove_role_users" + ChannelViewRoleUsers roles.Action = "channel_view_role_users" + + // Domain Groups Roles: Actions related to Groups present within the Domain + GroupUpdate roles.Action = "group_update" + GroupMembership roles.Action = "group_membership" + GroupRead roles.Action = "group_read" + GroupDelete roles.Action = "group_delete" + GroupSetChild roles.Action = "group_set_child" + GroupSetParent roles.Action = "group_set_parent" + GroupManageRole roles.Action = "group_manage_role" + GroupAddRoleUsers roles.Action = "group_add_role_users" + GroupRemoveRoleUsers roles.Action = "group_remove_role_users" + GroupViewRoleUsers roles.Action = "group_view_role_users" +) + +const ( + BuiltInRoleAdmin = "admin" + BuiltInRoleMembership = "membership" +) + +func AvailableActions() []roles.Action { + return []roles.Action{ + Update, + Enable, + Disable, + Read, + Delete, + Membership, + ManageRole, + AddRoleUsers, + RemoveRoleUsers, + ViewRoleUsers, + ThingCreate, + ChannelCreate, + GroupCreate, + ThingUpdate, + ThingRead, + ThingDelete, + ThingSetParentGroup, + ThingConnectToChannel, + ThingManageRole, + ThingAddRoleUsers, + ThingRemoveRoleUsers, + ThingViewRoleUsers, + ChannelUpdate, + ChannelRead, + ChannelDelete, + ChannelSetParentGroup, + ChannelConnectToThing, + ChannelPublish, + ChannelSubscribe, + ChannelManageRole, + ChannelAddRoleUsers, + ChannelRemoveRoleUsers, + ChannelViewRoleUsers, + GroupUpdate, + GroupMembership, + GroupRead, + GroupDelete, + GroupSetChild, + GroupSetParent, + GroupManageRole, + GroupAddRoleUsers, + GroupRemoveRoleUsers, + GroupViewRoleUsers, + } +} + +func membershipRoleActions() []roles.Action { + return []roles.Action{ + Membership, + } +} + +func BuiltInRoles() map[roles.BuiltInRoleName][]roles.Action { + return map[roles.BuiltInRoleName][]roles.Action{ + BuiltInRoleAdmin: AvailableActions(), + BuiltInRoleMembership: membershipRoleActions(), + } +} diff --git a/domains/service.go b/domains/service.go new file mode 100644 index 0000000000..97956bb008 --- /dev/null +++ b/domains/service.go @@ -0,0 +1,195 @@ +package domains + +import ( + "context" + "time" + + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/roles" +) + +const defLimit = 100 + +var ( + errCreateDomainPolicy = errors.New("failed to create domain policy") + errRollbackRepo = errors.New("failed to rollback repo") + errRemovePolicyEngine = errors.New("failed to remove from policy engine") +) + +type service struct { + repo Repository + policy policies.Service + idProvider magistrala.IDProvider + roles.ProvisionManageService +} + +var _ Service = (*service)(nil) + +func New(repo Repository, policy policies.Service, idProvider magistrala.IDProvider, sidProvider magistrala.IDProvider) (Service, error) { + + rpms, err := roles.NewProvisionManageService(policies.DomainType, repo, policy, sidProvider, AvailableActions(), BuiltInRoles()) + if err != nil { + return nil, err + } + + return &service{ + repo: repo, + policy: policy, + idProvider: idProvider, + ProvisionManageService: rpms, + }, nil +} + +func (svc service) CreateDomain(ctx context.Context, session authn.Session, d Domain) (do Domain, err error) { + + d.CreatedBy = session.UserID + + domainID, err := svc.idProvider.ID() + if err != nil { + return Domain{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + d.ID = domainID + + if d.Status != DisabledStatus && d.Status != EnabledStatus { + return Domain{}, svcerr.ErrInvalidStatus + } + + d.CreatedAt = time.Now() + + // Domain is created in repo first, because Roles table have foreign key relation with Domain ID + dom, err := svc.repo.Save(ctx, d) + if err != nil { + return Domain{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + defer func() { + if err != nil { + if errRollBack := svc.repo.Delete(ctx, domainID); errRollBack != nil { + err = errors.Wrap(err, errors.Wrap(errRollbackRepo, errRollBack)) + } + } + }() + + newBuiltInRoleMembers := map[roles.BuiltInRoleName][]roles.Member{ + BuiltInRoleAdmin: {roles.Member(session.UserID)}, + BuiltInRoleMembership: {}, + } + + optionalPolicies := []policies.Policy{ + { + Subject: policies.MagistralaObject, + SubjectType: policies.PlatformType, + Relation: "organization", + Object: d.ID, + ObjectType: policies.DomainType, + }, + } + + if _, err := svc.AddNewEntitiesRoles(ctx, domainID, session.UserID, []string{domainID}, optionalPolicies, newBuiltInRoleMembers); err != nil { + return Domain{}, errors.Wrap(errCreateDomainPolicy, err) + } + + return dom, nil +} + +func (svc service) RetrieveDomain(ctx context.Context, session authn.Session, id string) (Domain, error) { + domain, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { + return Domain{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return domain, nil +} + +func (svc service) UpdateDomain(ctx context.Context, session authn.Session, id string, d DomainReq) (Domain, error) { + dom, err := svc.repo.Update(ctx, id, session.UserID, d) + if err != nil { + return Domain{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return dom, nil +} + +func (svc service) EnableDomain(ctx context.Context, session authn.Session, id string) (Domain, error) { + status := EnabledStatus + dom, err := svc.repo.Update(ctx, id, session.UserID, DomainReq{Status: &status}) + if err != nil { + return Domain{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return dom, nil +} + +func (svc service) DisableDomain(ctx context.Context, session authn.Session, id string) (Domain, error) { + status := DisabledStatus + dom, err := svc.repo.Update(ctx, id, session.UserID, DomainReq{Status: &status}) + if err != nil { + return Domain{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return dom, nil +} + +// Only SuperAdmin can freeze the domain +func (svc service) FreezeDomain(ctx context.Context, session authn.Session, id string) (Domain, error) { + status := FreezeStatus + dom, err := svc.repo.Update(ctx, id, session.UserID, DomainReq{Status: &status}) + if err != nil { + return Domain{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return dom, nil +} + +func (svc service) ListDomains(ctx context.Context, session authn.Session, p Page) (DomainsPage, error) { + p.SubjectID = session.UserID + //ToDo : Check list without below function and confirm and decide to remove or not + if session.SuperAdmin { + p.SubjectID = "" + } + + dp, err := svc.repo.ListDomains(ctx, p) + if err != nil { + return DomainsPage{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return dp, nil +} + +func (svc service) DeleteUserFromDomains(ctx context.Context, id string) (err error) { + domainsPage, err := svc.repo.ListDomains(ctx, Page{SubjectID: id, Limit: defLimit}) + if err != nil { + return err + } + + if domainsPage.Total > defLimit { + for i := defLimit; i < int(domainsPage.Total); i += defLimit { + page := Page{SubjectID: id, Offset: uint64(i), Limit: defLimit} + dp, err := svc.repo.ListDomains(ctx, page) + if err != nil { + return err + } + domainsPage.Domains = append(domainsPage.Domains, dp.Domains...) + } + } + + // if err := svc.RemoveMembersFromAllRoles(ctx, authn.Session{}, []string{id}); err != nil { + // return err + // } + ////////////ToDo////////////// + // Remove user from all roles in all domains + ////////////////////////// + + // for _, domain := range domainsPage.Domains { + // req := policies.Policy{ + // Subject: policies.EncodeDomainUserID(domain.ID, id), + // SubjectType: policies.UserType, + // } + // if err := svc.policies.DeletePolicyFilter(ctx, req); err != nil { + // return err + // } + // } + + // if err := svc.repo.DeleteUserPolicies(ctx, id); err != nil { + // return err + // } + + return nil +} diff --git a/domains/service_test.go b/domains/service_test.go new file mode 100644 index 0000000000..9b782a09e2 --- /dev/null +++ b/domains/service_test.go @@ -0,0 +1,393 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package domains_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/domains/mocks" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" + policiesMocks "github.com/absmach/magistrala/pkg/policies/mocks" + "github.com/absmach/magistrala/pkg/sid" + "github.com/absmach/magistrala/pkg/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const ( + secret = "secret" + email = "test@example.com" + id = "testID" + groupName = "mgx" + description = "Description" + + memberRelation = "member" + authoritiesObj = "authorities" + loginDuration = 30 * time.Minute + refreshDuration = 24 * time.Hour + invalidDuration = 7 * 24 * time.Hour + validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22" +) + +var ( + errIssueUser = errors.New("failed to issue new login key") + errCreateDomainPolicy = errors.New("failed to create domain policy") + errRetrieve = errors.New("failed to retrieve key data") + ErrExpiry = errors.New("session is expired") + errRollbackPolicy = errors.New("failed to rollback policy") + errAddPolicies = errors.New("failed to add policies") + errPlatform = errors.New("invalid platform id") + inValid = "invalid" + valid = "valid" + domain = domains.Domain{ + ID: validID, + Name: groupName, + Tags: []string{"tag1", "tag2"}, + Alias: "test", + Permission: policies.AdminPermission, + CreatedBy: validID, + UpdatedBy: validID, + } + validSession = authn.Session{} + inValidSession = authn.Session{} +) + +var ( + drepo *mocks.Repository + policyMock *policiesMocks.Service +) + +func newService() domains.Service { + drepo = new(mocks.Repository) + idProvider := uuid.NewMock() + sidProvider := sid.NewMock() + policyMock = new(policiesMocks.Service) + ds, _ := domains.New(drepo, policyMock, idProvider, sidProvider) + return ds +} + +func TestCreateDomain(t *testing.T) { + svc := newService() + + cases := []struct { + desc string + d domains.Domain + session authn.Session + userID string + addPolicyErr error + savePolicyErr error + saveDomainErr error + deleteDomainErr error + deletePoliciesErr error + err error + }{ + { + desc: "create domain successfully", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + err: nil, + }, + { + desc: "create domain with invalid session", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: inValidSession, + err: svcerr.ErrAuthentication, + }, + { + desc: "create domain with invalid status", + d: domains.Domain{ + Status: domains.AllStatus, + }, + session: validSession, + err: svcerr.ErrInvalidStatus, + }, + { + desc: "create domain with failed policy request", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + addPolicyErr: errors.ErrMalformedEntity, + err: errors.ErrMalformedEntity, + }, + { + desc: "create domain with failed save policy request", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + savePolicyErr: errors.ErrMalformedEntity, + err: errCreateDomainPolicy, + }, + { + desc: "create domain with failed save domain request", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + saveDomainErr: errors.ErrMalformedEntity, + err: svcerr.ErrCreateEntity, + }, + { + desc: "create domain with rollback error", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + savePolicyErr: errors.ErrMalformedEntity, + deleteDomainErr: errors.ErrMalformedEntity, + err: errors.ErrMalformedEntity, + }, + { + desc: "create domain with rollback error and failed to delete policies", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + savePolicyErr: errors.ErrMalformedEntity, + deleteDomainErr: errors.ErrMalformedEntity, + deletePoliciesErr: errors.ErrMalformedEntity, + err: errors.ErrMalformedEntity, + }, + { + desc: "create domain with failed to create and failed rollback", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + saveDomainErr: errors.ErrMalformedEntity, + deletePoliciesErr: errors.ErrMalformedEntity, + err: errRollbackPolicy, + }, + { + desc: "create domain with failed to create and failed rollback", + d: domains.Domain{ + Status: domains.EnabledStatus, + }, + session: validSession, + saveDomainErr: errors.ErrMalformedEntity, + deleteDomainErr: errors.ErrMalformedEntity, + err: errors.ErrMalformedEntity, + }, + } + + for _, tc := range cases { + repoCall := drepo.On("Save", mock.Anything, mock.Anything).Return(domains.Domain{}, tc.saveDomainErr) + _, err := svc.CreateDomain(context.Background(), tc.session, tc.d) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Unset() + } +} + +func TestRetrieveDomain(t *testing.T) { + svc := newService() + + cases := []struct { + desc string + session authn.Session + domainID string + domainRepoErr error + domainRepoErr1 error + checkPolicyErr error + err error + }{ + { + desc: "retrieve domain successfully", + session: validSession, + domainID: validID, + err: nil, + }, + { + desc: "retrieve domain with invalid session", + session: inValidSession, + domainID: validID, + err: svcerr.ErrAuthentication, + }, + { + desc: "retrieve domain with empty domain id", + session: validSession, + domainID: "", + err: svcerr.ErrViewEntity, + domainRepoErr1: repoerr.ErrNotFound, + }, + { + desc: "retrieve non-existing domain", + session: validSession, + domainID: inValid, + domainRepoErr: repoerr.ErrNotFound, + err: svcerr.ErrViewEntity, + domainRepoErr1: repoerr.ErrNotFound, + }, + { + desc: "retrieve domain with failed to retrieve by id", + session: validSession, + domainID: validID, + domainRepoErr1: repoerr.ErrNotFound, + err: svcerr.ErrNotFound, + }, + } + + for _, tc := range cases { + repoCall := drepo.On("RetrieveByID", mock.Anything, groupName).Return(domains.Domain{}, tc.domainRepoErr) + repoCall1 := drepo.On("RetrieveByID", mock.Anything, tc.domainID).Return(domains.Domain{}, tc.domainRepoErr1) + _, err := svc.RetrieveDomain(context.Background(), tc.session, tc.domainID) + 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() + } +} + +func TestUpdateDomain(t *testing.T) { + svc := newService() + + cases := []struct { + desc string + session authn.Session + domainID string + domReq domains.DomainReq + checkPolicyErr error + retrieveByIDErr error + updateErr error + err error + }{ + { + desc: "update domain successfully", + session: validSession, + domainID: validID, + domReq: domains.DomainReq{ + Name: &valid, + Alias: &valid, + }, + err: nil, + }, + { + desc: "update domain with invalid session", + session: inValidSession, + domainID: validID, + domReq: domains.DomainReq{ + Name: &valid, + Alias: &valid, + }, + err: svcerr.ErrAuthentication, + }, + { + desc: "update domain with empty domainID", + session: validSession, + domainID: "", + domReq: domains.DomainReq{ + Name: &valid, + Alias: &valid, + }, + checkPolicyErr: svcerr.ErrAuthorization, + err: svcerr.ErrDomainAuthorization, + }, + { + desc: "update domain with failed to retrieve by id", + session: validSession, + domainID: validID, + domReq: domains.DomainReq{ + Name: &valid, + Alias: &valid, + }, + retrieveByIDErr: repoerr.ErrNotFound, + err: svcerr.ErrNotFound, + }, + { + desc: "update domain with failed to update", + session: validSession, + domainID: validID, + domReq: domains.DomainReq{ + Name: &valid, + Alias: &valid, + }, + updateErr: errors.ErrMalformedEntity, + err: errors.ErrMalformedEntity, + }, + } + + for _, tc := range cases { + repoCall1 := drepo.On("RetrieveByID", mock.Anything, mock.Anything).Return(domains.Domain{}, tc.retrieveByIDErr) + repoCall2 := drepo.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(domains.Domain{}, tc.updateErr) + _, err := svc.UpdateDomain(context.Background(), tc.session, tc.domainID, tc.domReq) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) + repoCall1.Unset() + repoCall2.Unset() + } +} + +func TestListDomains(t *testing.T) { + svc := newService() + + cases := []struct { + desc string + session authn.Session + domainID string + authReq domains.Page + listDomainsRes domains.DomainsPage + retreiveByIDErr error + checkPolicyErr error + listDomainErr error + err error + }{ + { + desc: "list domains successfully", + session: validSession, + domainID: validID, + authReq: domains.Page{ + Offset: 0, + Limit: 10, + Permission: policies.AdminPermission, + Status: domains.EnabledStatus, + }, + listDomainsRes: domains.DomainsPage{ + Domains: []domains.Domain{domain}, + }, + err: nil, + }, + { + desc: "list domains with invalid session", + session: inValidSession, + domainID: validID, + authReq: domains.Page{ + Offset: 0, + Limit: 10, + Permission: policies.AdminPermission, + Status: domains.EnabledStatus, + }, + err: svcerr.ErrAuthentication, + }, + { + desc: "list domains with repository error on list domains", + session: validSession, + domainID: validID, + authReq: domains.Page{ + Offset: 0, + Limit: 10, + Permission: policies.AdminPermission, + Status: domains.EnabledStatus, + }, + listDomainErr: errors.ErrMalformedEntity, + err: svcerr.ErrViewEntity, + }, + } + + for _, tc := range cases { + repoCall1 := drepo.On("ListDomains", mock.Anything, mock.Anything).Return(tc.listDomainsRes, tc.listDomainErr) + _, err := svc.ListDomains(context.Background(), tc.session, domains.Page{}) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) + repoCall1.Unset() + } +} diff --git a/domains/tracing/tracing.go b/domains/tracing/tracing.go new file mode 100644 index 0000000000..ed9f2753b3 --- /dev/null +++ b/domains/tracing/tracing.go @@ -0,0 +1,84 @@ +package tracing + +import ( + "context" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/pkg/authn" + rmTrace "github.com/absmach/magistrala/pkg/roles/rolemanager/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var _ domains.Service = (*tracingMiddleware)(nil) + +type tracingMiddleware struct { + tracer trace.Tracer + svc domains.Service + rmTrace.RoleManagerTracing +} + +// New returns a new group service with tracing capabilities. +func New(svc domains.Service, tracer trace.Tracer) domains.Service { + return &tracingMiddleware{tracer, svc, rmTrace.NewRoleManagerTracing("domain", svc, tracer)} +} + +func (tm *tracingMiddleware) CreateDomain(ctx context.Context, session authn.Session, d domains.Domain) (domains.Domain, error) { + ctx, span := tm.tracer.Start(ctx, "create_domain", trace.WithAttributes( + attribute.String("name", d.Name), + )) + defer span.End() + return tm.svc.CreateDomain(ctx, session, d) +} + +func (tm *tracingMiddleware) RetrieveDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + ctx, span := tm.tracer.Start(ctx, "view_domain", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + return tm.svc.RetrieveDomain(ctx, session, id) +} + +func (tm *tracingMiddleware) UpdateDomain(ctx context.Context, session authn.Session, id string, d domains.DomainReq) (domains.Domain, error) { + ctx, span := tm.tracer.Start(ctx, "update_domain", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + return tm.svc.UpdateDomain(ctx, session, id, d) +} + +func (tm *tracingMiddleware) EnableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + ctx, span := tm.tracer.Start(ctx, "enable_domain", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + return tm.svc.EnableDomain(ctx, session, id) +} + +func (tm *tracingMiddleware) DisableDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + ctx, span := tm.tracer.Start(ctx, "disable_domain", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + return tm.svc.DisableDomain(ctx, session, id) +} + +func (tm *tracingMiddleware) FreezeDomain(ctx context.Context, session authn.Session, id string) (domains.Domain, error) { + ctx, span := tm.tracer.Start(ctx, "freeze_domain", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + return tm.svc.FreezeDomain(ctx, session, id) +} + +func (tm *tracingMiddleware) ListDomains(ctx context.Context, session authn.Session, p domains.Page) (domains.DomainsPage, error) { + ctx, span := tm.tracer.Start(ctx, "list_domains") + defer span.End() + return tm.svc.ListDomains(ctx, session, p) +} + +func (tm *tracingMiddleware) DeleteUserFromDomains(ctx context.Context, id string) error { + ctx, span := tm.tracer.Start(ctx, "delete_user_from_domains") + defer span.End() + return tm.svc.DeleteUserFromDomains(ctx, id) +} diff --git a/go.mod b/go.mod index 240970ec41..e3fcd80cf8 100644 --- a/go.mod +++ b/go.mod @@ -152,8 +152,10 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/sqids/sqids-go v0.4.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index d701219c4b..d1b6e437f5 100644 --- a/go.sum +++ b/go.sum @@ -406,6 +406,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw= +github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -426,6 +428,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLqFypcxvLmlvEciCHL7+Lv+4vwZqecI= +github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/groups/api/grpc/client.go b/groups/api/grpc/client.go new file mode 100644 index 0000000000..e65c5c0cf4 --- /dev/null +++ b/groups/api/grpc/client.go @@ -0,0 +1,94 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + "fmt" + "time" + + grpcCommonV1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/go-kit/kit/endpoint" + kitgrpc "github.com/go-kit/kit/transport/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const svcName = "groups.v1.GroupsService" + +var _ grpcGroupsV1.GroupsServiceClient = (*grpcClient)(nil) + +type grpcClient struct { + timeout time.Duration + retrieveEntity endpoint.Endpoint +} + +// NewClient returns new gRPC client instance. +func NewClient(conn *grpc.ClientConn, timeout time.Duration) grpcGroupsV1.GroupsServiceClient { + return &grpcClient{ + + retrieveEntity: kitgrpc.NewClient( + conn, + svcName, + "RetrieveEntity", + encodeRetrieveEntityRequest, + decodeRetrieveEntityResponse, + grpcCommonV1.RetrieveEntityRes{}, + ).Endpoint(), + + timeout: timeout, + } +} + +func (client grpcClient) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq, _ ...grpc.CallOption) (r *grpcCommonV1.RetrieveEntityRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + res, err := client.retrieveEntity(ctx, req) + if err != nil { + return &grpcCommonV1.RetrieveEntityRes{}, decodeError(err) + } + typedRes := res.(*grpcCommonV1.RetrieveEntityRes) + + return typedRes, nil +} + +func encodeRetrieveEntityRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + return grpcReq, nil +} + +func decodeRetrieveEntityResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + return grpcRes, nil +} + +func decodeError(err error) error { + if st, ok := status.FromError(err); ok { + switch st.Code() { + case codes.Unauthenticated: + return errors.Wrap(svcerr.ErrAuthentication, errors.New(st.Message())) + case codes.PermissionDenied: + return errors.Wrap(svcerr.ErrAuthorization, errors.New(st.Message())) + case codes.InvalidArgument: + return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message())) + case codes.FailedPrecondition: + return errors.Wrap(errors.ErrMalformedEntity, errors.New(st.Message())) + case codes.NotFound: + return errors.Wrap(svcerr.ErrNotFound, errors.New(st.Message())) + case codes.AlreadyExists: + return errors.Wrap(svcerr.ErrConflict, errors.New(st.Message())) + case codes.OK: + if msg := st.Message(); msg != "" { + return errors.Wrap(errors.ErrUnidentified, errors.New(msg)) + } + return nil + default: + return errors.Wrap(fmt.Errorf("unexpected gRPC status: %s (status code:%v)", st.Code().String(), st.Code()), errors.New(st.Message())) + } + } + return err +} diff --git a/groups/api/grpc/doc.go b/groups/api/grpc/doc.go new file mode 100644 index 0000000000..20956ee50b --- /dev/null +++ b/groups/api/grpc/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package grpc contains implementation of Auth service gRPC API. +package grpc diff --git a/groups/api/grpc/endpoint.go b/groups/api/grpc/endpoint.go new file mode 100644 index 0000000000..f49059b322 --- /dev/null +++ b/groups/api/grpc/endpoint.go @@ -0,0 +1,26 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + + groups "github.com/absmach/magistrala/groups/private" + "github.com/go-kit/kit/endpoint" +) + +func retrieveEntityEndpoint(svc groups.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + + req := request.(retrieveEntityReq) + group, err := svc.RetrieveById(ctx, req.Id) + + if err != nil { + return retrieveEntityRes{}, err + } + + return retrieveEntityRes{id: group.ID, domain: group.Domain, status: uint8(group.Status)}, nil + + } +} diff --git a/groups/api/grpc/request.go b/groups/api/grpc/request.go new file mode 100644 index 0000000000..4c8286e109 --- /dev/null +++ b/groups/api/grpc/request.go @@ -0,0 +1,8 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +type retrieveEntityReq struct { + Id string +} diff --git a/groups/api/grpc/responses.go b/groups/api/grpc/responses.go new file mode 100644 index 0000000000..6ae8047c53 --- /dev/null +++ b/groups/api/grpc/responses.go @@ -0,0 +1,12 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +type groupBasic struct { + id string + domain string + status uint8 +} + +type retrieveEntityRes groupBasic diff --git a/groups/api/grpc/server.go b/groups/api/grpc/server.go new file mode 100644 index 0000000000..974ff04c80 --- /dev/null +++ b/groups/api/grpc/server.go @@ -0,0 +1,88 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package grpc + +import ( + "context" + + mgauth "github.com/absmach/magistrala/auth" + groups "github.com/absmach/magistrala/groups/private" + grpcCommonV1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + kitgrpc "github.com/go-kit/kit/transport/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var _ grpcGroupsV1.GroupsServiceServer = (*grpcServer)(nil) + +type grpcServer struct { + grpcGroupsV1.UnimplementedGroupsServiceServer + retrieveEntity kitgrpc.Handler +} + +// NewServer returns new AuthServiceServer instance. +func NewServer(svc groups.Service) grpcGroupsV1.GroupsServiceServer { + return &grpcServer{ + retrieveEntity: kitgrpc.NewServer( + retrieveEntityEndpoint(svc), + decodeRetrieveEntityRequest, + encodeRetrieveEntityResponse, + ), + } +} + +func (s *grpcServer) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq) (*grpcCommonV1.RetrieveEntityRes, error) { + _, res, err := s.retrieveEntity.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcCommonV1.RetrieveEntityRes), nil +} + +func decodeRetrieveEntityRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcCommonV1.RetrieveEntityReq) + return retrieveEntityReq{ + Id: req.GetId(), + }, nil +} + +func encodeRetrieveEntityResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(retrieveEntityRes) + + return &grpcCommonV1.RetrieveEntityRes{ + Entity: &grpcCommonV1.EntityBasic{ + Id: res.id, + DomainId: res.domain, + Status: uint32(res.status), + }, + }, nil +} + +func encodeError(err error) error { + switch { + case errors.Contains(err, nil): + return nil + case errors.Contains(err, errors.ErrMalformedEntity), + err == apiutil.ErrInvalidAuthKey, + err == apiutil.ErrMissingID, + err == apiutil.ErrMissingMemberType, + err == apiutil.ErrMissingPolicySub, + err == apiutil.ErrMissingPolicyObj, + err == apiutil.ErrMalformedPolicyAct: + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Contains(err, svcerr.ErrAuthentication), + errors.Contains(err, mgauth.ErrKeyExpired), + err == apiutil.ErrMissingEmail, + err == apiutil.ErrBearerToken: + return status.Error(codes.Unauthenticated, err.Error()) + case errors.Contains(err, svcerr.ErrAuthorization): + return status.Error(codes.PermissionDenied, err.Error()) + default: + return status.Error(codes.Internal, err.Error()) + } +} diff --git a/internal/groups/api/decode.go b/groups/api/http/decode.go similarity index 52% rename from internal/groups/api/decode.go rename to groups/api/http/decode.go index c560f5083c..de3d3a68d9 100644 --- a/internal/groups/api/decode.go +++ b/groups/api/http/decode.go @@ -9,164 +9,86 @@ import ( "net/http" "strings" + mggroups "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" - mggroups "github.com/absmach/magistrala/pkg/groups" "github.com/go-chi/chi/v5" ) -func DecodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) { - pm, err := decodePageMeta(r) - if err != nil { - return nil, err - } - - level, err := apiutil.ReadNumQuery[uint64](r, api.LevelKey, api.DefLevel) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - parentID, err := apiutil.ReadStringQuery(r, api.ParentKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - tree, err := apiutil.ReadBoolQuery(r, api.TreeKey, false) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - dir, err := apiutil.ReadNumQuery[int64](r, api.DirKey, -1) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) +func DecodeGroupCreate(_ 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) } - - memberKind, err := apiutil.ReadStringQuery(r, api.MemberKindKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) + var g mggroups.Group + if err := json.NewDecoder(r.Body).Decode(&g); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) } - - permission, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) + req := createGroupReq{ + Group: g, } - listPerms, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - req := listGroupsReq{ - tree: tree, - memberKind: memberKind, - memberID: chi.URLParam(r, "memberID"), - Page: mggroups.Page{ - Level: level, - ParentID: parentID, - Permission: permission, - PageMeta: pm, - Direction: dir, - ListPerms: listPerms, - }, - } return req, nil } -func DecodeListParentsRequest(_ context.Context, r *http.Request) (interface{}, error) { +func DecodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) { pm, err := decodePageMeta(r) if err != nil { return nil, err } - level, err := apiutil.ReadNumQuery[uint64](r, api.LevelKey, api.DefLevel) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) + req := listGroupsReq{ + PageMeta: pm, } + return req, nil +} - tree, err := apiutil.ReadBoolQuery(r, api.TreeKey, false) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - permission, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) +func DecodeGroupUpdate(_ 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) } - - listPerms, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) + req := updateGroupReq{ + id: chi.URLParam(r, "groupID"), } - req := listGroupsReq{ - tree: tree, - Page: mggroups.Page{ - Level: level, - ParentID: chi.URLParam(r, "groupID"), - Permission: permission, - PageMeta: pm, - Direction: +1, - ListPerms: listPerms, - }, + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) } return req, nil } -func DecodeListChildrenRequest(_ context.Context, r *http.Request) (interface{}, error) { - pm, err := decodePageMeta(r) - if err != nil { - return nil, err +func DecodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) { + req := groupReq{ + id: chi.URLParam(r, "groupID"), } + return req, nil +} - level, err := apiutil.ReadNumQuery[uint64](r, api.LevelKey, api.DefLevel) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) +func DecodeChangeGroupStatusRequest(_ context.Context, r *http.Request) (interface{}, error) { + req := changeGroupStatusReq{ + id: chi.URLParam(r, "groupID"), } + return req, nil +} - tree, err := apiutil.ReadBoolQuery(r, api.TreeKey, false) +func decodeRetrieveGroupHierarchy(_ context.Context, r *http.Request) (interface{}, error) { + hm, err := decodeHierarchyPageMeta(r) if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - permission, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) + return nil, err } - listPerms, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - req := listGroupsReq{ - tree: tree, - Page: mggroups.Page{ - Level: level, - ParentID: chi.URLParam(r, "groupID"), - Permission: permission, - PageMeta: pm, - Direction: -1, - ListPerms: listPerms, - }, + req := retrieveGroupHierarchyReq{ + id: chi.URLParam(r, "groupID"), + HierarchyPageMeta: hm, } return req, nil } -func DecodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) { +func decodeAddParentGroupRequest(_ 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) } - var g mggroups.Group - if err := json.NewDecoder(r.Body).Decode(&g); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - req := createGroupReq{ - Group: g, - } - return req, nil -} - -func DecodeGroupUpdate(_ 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 := updateGroupReq{ + req := addParentGroupReq{ id: chi.URLParam(r, "groupID"), } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -175,33 +97,32 @@ func DecodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) return req, nil } -func DecodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := groupReq{ +func decodeRemoveParentGroupRequest(_ context.Context, r *http.Request) (interface{}, error) { + req := removeParentGroupReq{ id: chi.URLParam(r, "groupID"), } return req, nil } -func DecodeGroupPermsRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := groupPermsReq{ - id: chi.URLParam(r, "groupID"), +func decodeAddChildrenGroupsRequest(_ 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) } - return req, nil -} - -func DecodeChangeGroupStatus(_ context.Context, r *http.Request) (interface{}, error) { - req := changeGroupStatusReq{ + req := addChildrenGroupsReq{ id: chi.URLParam(r, "groupID"), } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) + } return req, nil } -func DecodeAssignMembersRequest(_ context.Context, r *http.Request) (interface{}, error) { +func decodeRemoveChildrenGroupsRequest(_ 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 := assignReq{ - groupID: chi.URLParam(r, "groupID"), + req := removeChildrenGroupsReq{ + id: chi.URLParam(r, "groupID"), } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) @@ -209,36 +130,47 @@ func DecodeAssignMembersRequest(_ context.Context, r *http.Request) (interface{} return req, nil } -func DecodeUnassignMembersRequest(_ 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) +func decodeRemoveAllChildrenGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) { + req := removeAllChildrenGroupsReq{ + id: chi.URLParam(r, "groupID"), } - req := unassignReq{ - groupID: chi.URLParam(r, "groupID"), + return req, nil +} + +func decodeListChildrenGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) { + pm, err := decodePageMeta(r) + if err != nil { + return nil, err } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) + + req := listChildrenGroupsReq{ + id: chi.URLParam(r, "groupID"), + PageMeta: pm, } return req, nil } -func DecodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) { - memberKind, err := apiutil.ReadStringQuery(r, api.MemberKindKey, "") +func decodeHierarchyPageMeta(r *http.Request) (mggroups.HierarchyPageMeta, error) { + level, err := apiutil.ReadNumQuery[uint64](r, api.LevelKey, api.DefLevel) if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) + return mggroups.HierarchyPageMeta{}, errors.Wrap(apiutil.ErrValidation, err) } - permission, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) + + tree, err := apiutil.ReadBoolQuery(r, api.TreeKey, false) if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) + return mggroups.HierarchyPageMeta{}, errors.Wrap(apiutil.ErrValidation, err) } - req := listMembersReq{ - groupID: chi.URLParam(r, "groupID"), - permission: permission, - memberKind: memberKind, + hierarchyDir, err := apiutil.ReadNumQuery[int64](r, api.DirKey, -1) + if err != nil { + return mggroups.HierarchyPageMeta{}, errors.Wrap(apiutil.ErrValidation, err) } - return req, nil -} + return mggroups.HierarchyPageMeta{ + Level: level, + Direction: hierarchyDir, + Tree: tree, + }, nil +} func decodePageMeta(r *http.Request) (mggroups.PageMeta, error) { s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefGroupStatus) if err != nil { @@ -268,14 +200,23 @@ func decodePageMeta(r *http.Request) (mggroups.PageMeta, error) { if err != nil { return mggroups.PageMeta{}, errors.Wrap(apiutil.ErrValidation, err) } - + permission, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) + if err != nil { + return mggroups.PageMeta{}, errors.Wrap(apiutil.ErrValidation, err) + } + listPerms, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) + if err != nil { + return mggroups.PageMeta{}, errors.Wrap(apiutil.ErrValidation, err) + } ret := mggroups.PageMeta{ - Offset: offset, - Limit: limit, - Name: name, - ID: id, - Metadata: meta, - Status: st, + Offset: offset, + Limit: limit, + Name: name, + ID: id, + Metadata: meta, + Status: st, + Permission: permission, + ListPerms: listPerms, } return ret, nil } diff --git a/internal/groups/api/decode_test.go b/groups/api/http/decode_test.go similarity index 64% rename from internal/groups/api/decode_test.go rename to groups/api/http/decode_test.go index 2e45e34800..f003120737 100644 --- a/internal/groups/api/decode_test.go +++ b/groups/api/http/decode_test.go @@ -8,13 +8,14 @@ import ( "fmt" "net/http" "net/url" + "reflect" "strings" "testing" + "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/groups" "github.com/stretchr/testify/assert" ) @@ -31,12 +32,9 @@ func TestDecodeListGroupsRequest(t *testing.T) { url: "http://localhost:8080", header: map[string][]string{}, resp: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, + PageMeta: groups.PageMeta{ + Limit: 10, Permission: api.DefPermission, - Direction: -1, }, }, err: nil, @@ -48,24 +46,17 @@ func TestDecodeListGroupsRequest(t *testing.T) { "Authorization": {"Bearer 123"}, }, resp: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Status: groups.EnabledStatus, - Offset: 10, - Limit: 10, - Name: "random", - Metadata: groups.Metadata{ - "test": "test", - }, - }, - Level: 2, - ParentID: "random", - Permission: "random", - Direction: -1, + PageMeta: groups.PageMeta{ + Status: groups.EnabledStatus, + Offset: 10, + Limit: 10, + Name: "random", ListPerms: true, + Permission: "random", + Metadata: groups.Metadata{ + "test": "test", + }, }, - tree: true, - memberKind: "random", }, err: nil, }, @@ -133,7 +124,7 @@ func TestDecodeListGroupsRequest(t *testing.T) { } } -func TestDecodeListParentsRequest(t *testing.T) { +func TestDecodeRetrieveGroupHierarchy(t *testing.T) { cases := []struct { desc string url string @@ -145,14 +136,8 @@ func TestDecodeListParentsRequest(t *testing.T) { desc: "valid request with no parameters", url: "http://localhost:8080", header: map[string][]string{}, - resp: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - Permission: api.DefPermission, - Direction: +1, - }, + resp: retrieveGroupHierarchyReq{ + HierarchyPageMeta: groups.HierarchyPageMeta{}, }, err: nil, }, @@ -162,23 +147,8 @@ func TestDecodeListParentsRequest(t *testing.T) { header: map[string][]string{ "Authorization": {"Bearer 123"}, }, - resp: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Status: groups.EnabledStatus, - Offset: 10, - Limit: 10, - Name: "random", - Metadata: groups.Metadata{ - "test": "test", - }, - }, - Level: 2, - Permission: "random", - Direction: +1, - ListPerms: true, - }, - tree: true, + resp: retrieveGroupHierarchyReq{ + HierarchyPageMeta: groups.HierarchyPageMeta{}, }, err: nil, }, @@ -222,7 +192,7 @@ func TestDecodeListParentsRequest(t *testing.T) { URL: parsedURL, Header: tc.header, } - resp, err := DecodeListParentsRequest(context.Background(), req) + resp, err := decodeRetrieveGroupHierarchy(context.Background(), req) assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.resp, resp)) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) } @@ -241,12 +211,9 @@ func TestDecodeListChildrenRequest(t *testing.T) { url: "http://localhost:8080", header: map[string][]string{}, resp: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, + PageMeta: groups.PageMeta{ + Limit: 10, Permission: api.DefPermission, - Direction: -1, }, }, err: nil, @@ -258,22 +225,17 @@ func TestDecodeListChildrenRequest(t *testing.T) { "Authorization": {"Bearer 123"}, }, resp: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Status: groups.EnabledStatus, - Offset: 10, - Limit: 10, - Name: "random", - Metadata: groups.Metadata{ - "test": "test", - }, + PageMeta: groups.PageMeta{ + Status: groups.EnabledStatus, + Offset: 10, + Limit: 10, + Name: "random", + Metadata: groups.Metadata{ + "test": "test", }, - Level: 2, Permission: "random", - Direction: -1, ListPerms: true, }, - tree: true, }, err: nil, }, @@ -317,64 +279,7 @@ func TestDecodeListChildrenRequest(t *testing.T) { URL: parsedURL, Header: tc.header, } - resp, err := DecodeListChildrenRequest(context.Background(), req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - } -} - -func TestDecodeListMembersRequest(t *testing.T) { - cases := []struct { - desc string - url string - header map[string][]string - resp interface{} - err error - }{ - { - desc: "valid request with no parameters", - url: "http://localhost:8080", - header: map[string][]string{}, - resp: listMembersReq{ - permission: api.DefPermission, - }, - err: nil, - }, - { - desc: "valid request with all parameters", - url: "http://localhost:8080?member_kind=random&permission=random", - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - }, - resp: listMembersReq{ - memberKind: "random", - permission: "random", - }, - err: nil, - }, - { - desc: "valid request with invalid permission", - url: "http://localhost:8080?permission=random&permission=random", - resp: nil, - err: apiutil.ErrValidation, - }, - { - desc: "valid request with invalid member kind", - url: "http://localhost:8080?member_kind=random&member_kind=random", - resp: nil, - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - parsedURL, err := url.Parse(tc.url) - assert.NoError(t, err) - - req := &http.Request{ - URL: parsedURL, - Header: tc.header, - } - resp, err := DecodeListMembersRequest(context.Background(), req) + resp, err := decodeListChildrenGroupsRequest(context.Background(), req) assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.resp, resp)) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) } @@ -598,7 +503,7 @@ func TestDecodeGroupRequest(t *testing.T) { } } -func TestDecodeGroupPermsRequest(t *testing.T) { +func TestDecodeChangeGroupStatus(t *testing.T) { cases := []struct { desc string header map[string][]string @@ -610,12 +515,12 @@ func TestDecodeGroupPermsRequest(t *testing.T) { header: map[string][]string{ "Authorization": {"Bearer 123"}, }, - resp: groupPermsReq{}, + resp: changeGroupStatusReq{}, err: nil, }, { desc: "empty token", - resp: groupPermsReq{}, + resp: changeGroupStatusReq{}, err: nil, }, } @@ -624,146 +529,276 @@ func TestDecodeGroupPermsRequest(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", http.NoBody) assert.NoError(t, err) req.Header = tc.header - resp, err := DecodeGroupPermsRequest(context.Background(), req) + resp, err := DecodeChangeGroupStatusRequest(context.Background(), req) assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.resp, resp)) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) } } -func TestDecodeChangeGroupStatus(t *testing.T) { - cases := []struct { - desc string - header map[string][]string - resp interface{} - err error +func TestDecodeChangeGroupStatusRequest(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool }{ - { - desc: "valid request", - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - }, - resp: changeGroupStatusReq{}, - err: nil, - }, - { - desc: "empty token", - resp: changeGroupStatusReq{}, - err: nil, - }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := DecodeChangeGroupStatusRequest(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("DecodeChangeGroupStatusRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DecodeChangeGroupStatusRequest() = %v, want %v", got, tt.want) + } + }) } +} - for _, tc := range cases { - req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", http.NoBody) - assert.NoError(t, err) - req.Header = tc.header - resp, err := DecodeChangeGroupStatus(context.Background(), req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) +func Test_decodeRetrieveGroupHierarchy(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeRetrieveGroupHierarchy(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeRetrieveGroupHierarchy() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeRetrieveGroupHierarchy() = %v, want %v", got, tt.want) + } + }) } } -func TestDecodeAssignMembersRequest(t *testing.T) { - cases := []struct { - desc string - body string - header map[string][]string - resp interface{} - err error +func Test_decodeAddParentGroupRequest(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool }{ - { - desc: "valid request", - body: `{"member_kind": "random", "members": ["random"]}`, - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - "Content-Type": {api.ContentType}, - }, - resp: assignReq{ - MemberKind: "random", - Members: []string{"random"}, - }, - err: nil, - }, - { - desc: "invalid content type", - body: `{"member_kind": "random", "members": ["random"]}`, - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - "Content-Type": {"text/plain"}, - }, - resp: nil, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "invalid request body", - body: `data`, - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - "Content-Type": {api.ContentType}, - }, - resp: nil, - err: errors.ErrMalformedEntity, - }, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeAddParentGroupRequest(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeAddParentGroupRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeAddParentGroupRequest() = %v, want %v", got, tt.want) + } + }) } +} - for _, tc := range cases { - req, err := http.NewRequest(http.MethodPost, "http://localhost:8080", strings.NewReader(tc.body)) - assert.NoError(t, err) - req.Header = tc.header - resp, err := DecodeAssignMembersRequest(context.Background(), req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) +func Test_decodeRemoveParentGroupRequest(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeRemoveParentGroupRequest(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeRemoveParentGroupRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeRemoveParentGroupRequest() = %v, want %v", got, tt.want) + } + }) } } -func TestDecodeUnassignMembersRequest(t *testing.T) { - cases := []struct { - desc string - body string - header map[string][]string - resp interface{} - err error +func Test_decodeAddChildrenGroupsRequest(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool }{ - { - desc: "valid request", - body: `{"member_kind": "random", "members": ["random"]}`, - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - "Content-Type": {api.ContentType}, - }, - resp: unassignReq{ - MemberKind: "random", - Members: []string{"random"}, - }, - err: nil, - }, - { - desc: "invalid content type", - body: `{"member_kind": "random", "members": ["random"]}`, - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - "Content-Type": {"text/plain"}, - }, - resp: nil, - err: apiutil.ErrUnsupportedContentType, - }, - { - desc: "invalid request body", - body: `data`, - header: map[string][]string{ - "Authorization": {"Bearer 123"}, - "Content-Type": {api.ContentType}, - }, - resp: nil, - err: errors.ErrMalformedEntity, - }, + // TODO: Add test cases. } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeAddChildrenGroupsRequest(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeAddChildrenGroupsRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeAddChildrenGroupsRequest() = %v, want %v", got, tt.want) + } + }) + } +} - for _, tc := range cases { - req, err := http.NewRequest(http.MethodPost, "http://localhost:8080", strings.NewReader(tc.body)) - assert.NoError(t, err) - req.Header = tc.header - resp, err := DecodeUnassignMembersRequest(context.Background(), req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) +func Test_decodeRemoveChildrenGroupsRequest(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeRemoveChildrenGroupsRequest(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeRemoveChildrenGroupsRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeRemoveChildrenGroupsRequest() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_decodeRemoveAllChildrenGroupsRequest(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeRemoveAllChildrenGroupsRequest(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeRemoveAllChildrenGroupsRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeRemoveAllChildrenGroupsRequest() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_decodeListChildrenGroupsRequest(t *testing.T) { + type args struct { + in0 context.Context + r *http.Request + } + tests := []struct { + name string + args args + want interface{} + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeListChildrenGroupsRequest(tt.args.in0, tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeListChildrenGroupsRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeListChildrenGroupsRequest() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_decodeHierarchyPageMeta(t *testing.T) { + type args struct { + r *http.Request + } + tests := []struct { + name string + args args + want groups.HierarchyPageMeta + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeHierarchyPageMeta(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodeHierarchyPageMeta() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodeHierarchyPageMeta() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_decodePageMeta(t *testing.T) { + type args struct { + r *http.Request + } + tests := []struct { + name string + args args + want groups.PageMeta + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodePageMeta(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("decodePageMeta() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decodePageMeta() = %v, want %v", got, tt.want) + } + }) } } diff --git a/internal/groups/api/doc.go b/groups/api/http/doc.go similarity index 100% rename from internal/groups/api/doc.go rename to groups/api/http/doc.go diff --git a/groups/api/http/endpoint_test.go b/groups/api/http/endpoint_test.go new file mode 100644 index 0000000000..7cd03c095f --- /dev/null +++ b/groups/api/http/endpoint_test.go @@ -0,0 +1,4 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api diff --git a/internal/groups/api/endpoints.go b/groups/api/http/endpoints.go similarity index 57% rename from internal/groups/api/endpoints.go rename to groups/api/http/endpoints.go index 7082c3e58c..f802451f22 100644 --- a/internal/groups/api/endpoints.go +++ b/groups/api/http/endpoints.go @@ -6,18 +6,18 @@ package api import ( "context" + "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" "github.com/go-kit/kit/endpoint" ) const groupTypeChannels = "channels" -func CreateGroupEndpoint(svc groups.Service, kind string) endpoint.Endpoint { +func CreateGroupEndpoint(svc groups.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(createGroupReq) if err := req.validate(); err != nil { @@ -26,10 +26,10 @@ func CreateGroupEndpoint(svc groups.Service, kind string) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return createGroupRes{created: false}, svcerr.ErrAuthorization + return createGroupRes{created: false}, svcerr.ErrAuthentication } - group, err := svc.CreateGroup(ctx, session, kind, req.Group) + group, err := svc.CreateGroup(ctx, session, req.Group) if err != nil { return createGroupRes{created: false}, err } @@ -47,7 +47,7 @@ func ViewGroupEndpoint(svc groups.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return viewGroupRes{}, svcerr.ErrAuthorization + return viewGroupRes{}, svcerr.ErrAuthentication } group, err := svc.ViewGroup(ctx, session, req.id) @@ -59,27 +59,6 @@ func ViewGroupEndpoint(svc groups.Service) endpoint.Endpoint { } } -func ViewGroupPermsEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(groupPermsReq) - if err := req.validate(); err != nil { - return viewGroupPermsRes{}, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return viewGroupPermsRes{}, svcerr.ErrAuthorization - } - - p, err := svc.ViewGroupPerms(ctx, session, req.id) - if err != nil { - return viewGroupPermsRes{}, err - } - - return viewGroupPermsRes{Permissions: p}, nil - } -} - func UpdateGroupEndpoint(svc groups.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(updateGroupReq) @@ -89,7 +68,7 @@ func UpdateGroupEndpoint(svc groups.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return updateGroupRes{}, svcerr.ErrAuthorization + return updateGroupRes{}, svcerr.ErrAuthentication } group := groups.Group{ @@ -117,7 +96,7 @@ func EnableGroupEndpoint(svc groups.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return changeStatusRes{}, svcerr.ErrAuthorization + return changeStatusRes{}, svcerr.ErrAuthentication } group, err := svc.EnableGroup(ctx, session, req.id) @@ -137,7 +116,7 @@ func DisableGroupEndpoint(svc groups.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return changeStatusRes{}, svcerr.ErrAuthorization + return changeStatusRes{}, svcerr.ErrAuthentication } group, err := svc.DisableGroup(ctx, session, req.id) @@ -148,142 +127,208 @@ func DisableGroupEndpoint(svc groups.Service) endpoint.Endpoint { } } -func ListGroupsEndpoint(svc groups.Service, groupType, memberKind string) endpoint.Endpoint { +func ListGroupsEndpoint(svc groups.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(listGroupsReq) - if memberKind != "" { - req.memberKind = memberKind - } + if err := req.validate(); err != nil { - if groupType == groupTypeChannels { - return channelPageRes{}, errors.Wrap(apiutil.ErrValidation, err) - } return groupPageRes{}, errors.Wrap(apiutil.ErrValidation, err) } session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - if groupType == groupTypeChannels { - return channelPageRes{}, svcerr.ErrAuthorization - } - return groupPageRes{}, svcerr.ErrAuthorization + return groupPageRes{}, svcerr.ErrAuthentication } - page, err := svc.ListGroups(ctx, session, req.memberKind, req.memberID, req.Page) + page, err := svc.ListGroups(ctx, session, req.PageMeta) if err != nil { - if groupType == groupTypeChannels { - return channelPageRes{}, err - } return groupPageRes{}, err } - if req.tree { - return buildGroupsResponseTree(page), nil + groups := []viewGroupRes{} + for _, g := range page.Groups { + groups = append(groups, toViewGroupRes(g)) } - filterByID := req.Page.ParentID != "" - if groupType == groupTypeChannels { - return buildChannelsResponse(page, filterByID), nil - } - return buildGroupsResponse(page, filterByID), nil + return groupPageRes{pageRes: pageRes{ + Limit: page.Limit, + Offset: page.Offset, + Total: page.Total, + }, + Groups: groups, + }, nil + } } -func ListMembersEndpoint(svc groups.Service, memberKind string) endpoint.Endpoint { +func DeleteGroupEndpoint(svc groups.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(listMembersReq) - if memberKind != "" { - req.memberKind = memberKind + req := request.(groupReq) + if err := req.validate(); err != nil { + return deleteGroupRes{}, errors.Wrap(apiutil.ErrValidation, err) } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return deleteGroupRes{}, svcerr.ErrAuthentication + } + if err := svc.DeleteGroup(ctx, session, req.id); err != nil { + return deleteGroupRes{}, err + } + return deleteGroupRes{deleted: true}, nil + } +} + +func retrieveGroupHierarchyEndpoint(svc groups.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(retrieveGroupHierarchyReq) if err := req.validate(); err != nil { - return listMembersRes{}, errors.Wrap(apiutil.ErrValidation, err) + return retrieveGroupHierarchyRes{}, errors.Wrap(apiutil.ErrValidation, err) } session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return listMembersRes{}, svcerr.ErrAuthorization + return changeStatusRes{}, svcerr.ErrAuthentication } - page, err := svc.ListMembers(ctx, session, req.groupID, req.permission, req.memberKind) + hp, err := svc.RetrieveGroupHierarchy(ctx, session, req.id, req.HierarchyPageMeta) if err != nil { - return listMembersRes{}, err + return retrieveGroupHierarchyRes{}, err } - return listMembersRes{ - pageRes: pageRes{ - Limit: page.Limit, - Offset: page.Offset, - Total: page.Total, - }, - Members: page.Members, - }, nil + groups := []viewGroupRes{} + for _, g := range hp.Groups { + groups = append(groups, toViewGroupRes(g)) + } + return retrieveGroupHierarchyRes{Level: hp.Level, Direction: hp.Direction, Groups: groups}, nil + } } -func AssignMembersEndpoint(svc groups.Service, relation, memberKind string) endpoint.Endpoint { +func addParentGroupEndpoint(svc groups.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignReq) - if relation != "" { - req.Relation = relation + req := request.(addParentGroupReq) + if err := req.validate(); err != nil { + return addParentGroupRes{}, errors.Wrap(apiutil.ErrValidation, err) } - if memberKind != "" { - req.MemberKind = memberKind + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return changeStatusRes{}, svcerr.ErrAuthentication + } + + if err := svc.AddParentGroup(ctx, session, req.id, req.ParentID); err != nil { + return addParentGroupRes{}, err } + return addParentGroupRes{}, nil + } +} + +func removeParentGroupEndpoint(svc groups.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(removeParentGroupReq) if err := req.validate(); err != nil { - return assignRes{}, errors.Wrap(apiutil.ErrValidation, err) + return removeParentGroupRes{}, errors.Wrap(apiutil.ErrValidation, err) } + session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return assignRes{}, svcerr.ErrAuthorization + return changeStatusRes{}, svcerr.ErrAuthentication } - if err := svc.Assign(ctx, session, req.groupID, req.Relation, req.MemberKind, req.Members...); err != nil { - return assignRes{}, err + if err := svc.RemoveParentGroup(ctx, session, req.id); err != nil { + return removeParentGroupRes{}, err } - return assignRes{assigned: true}, nil + return removeParentGroupRes{}, nil } } -func UnassignMembersEndpoint(svc groups.Service, relation, memberKind string) endpoint.Endpoint { +func addChildrenGroupsEndpoint(svc groups.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(unassignReq) - if relation != "" { - req.Relation = relation + req := request.(addChildrenGroupsReq) + if err := req.validate(); err != nil { + return addChildrenGroupsRes{}, errors.Wrap(apiutil.ErrValidation, err) } - if memberKind != "" { - req.MemberKind = memberKind + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return changeStatusRes{}, svcerr.ErrAuthentication } + + if err := svc.AddChildrenGroups(ctx, session, req.id, req.ChildrenIDs); err != nil { + return addChildrenGroupsRes{}, err + } + return addChildrenGroupsRes{}, nil + } +} + +func removeChildrenGroupsEndpoint(svc groups.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(removeChildrenGroupsReq) if err := req.validate(); err != nil { - return unassignRes{}, errors.Wrap(apiutil.ErrValidation, err) + return removeChildrenGroupsRes{}, errors.Wrap(apiutil.ErrValidation, err) } + session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return unassignRes{}, svcerr.ErrAuthorization + return changeStatusRes{}, svcerr.ErrAuthentication } - if err := svc.Unassign(ctx, session, req.groupID, req.Relation, req.MemberKind, req.Members...); err != nil { - return unassignRes{}, err + if err := svc.RemoveChildrenGroups(ctx, session, req.id, req.ChildrenIDs); err != nil { + return removeChildrenGroupsRes{}, err } - return unassignRes{unassigned: true}, nil + return removeChildrenGroupsRes{}, nil } } -func DeleteGroupEndpoint(svc groups.Service) endpoint.Endpoint { +func removeAllChildrenGroupsEndpoint(svc groups.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(groupReq) + req := request.(removeAllChildrenGroupsReq) if err := req.validate(); err != nil { - return deleteGroupRes{}, errors.Wrap(apiutil.ErrValidation, err) + return removeAllChildrenGroupsRes{}, errors.Wrap(apiutil.ErrValidation, err) } session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return deleteGroupRes{}, svcerr.ErrAuthorization + return changeStatusRes{}, svcerr.ErrAuthentication } - if err := svc.DeleteGroup(ctx, session, req.id); err != nil { - return deleteGroupRes{}, err + if err := svc.RemoveAllChildrenGroups(ctx, session, req.id); err != nil { + return removeAllChildrenGroupsRes{}, err } - return deleteGroupRes{deleted: true}, nil + return removeAllChildrenGroupsRes{}, nil + } +} + +func listChildrenGroupsEndpoint(svc groups.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listChildrenGroupsReq) + if err := req.validate(); err != nil { + return listChildrenGroupsRes{}, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return changeStatusRes{}, svcerr.ErrAuthentication + } + + gp, err := svc.ListChildrenGroups(ctx, session, req.id, req.PageMeta) + if err != nil { + return listChildrenGroupsRes{}, err + } + viewGroups := []viewGroupRes{} + + for _, group := range gp.Groups { + viewGroups = append(viewGroups, toViewGroupRes(group)) + } + return listChildrenGroupsRes{ + pageRes: pageRes{ + Limit: gp.Limit, + Offset: gp.Offset, + Total: gp.Total, + }, + Groups: viewGroups, + }, nil } } @@ -310,7 +355,6 @@ func buildGroupsResponseTree(page groups.Page) groupPageRes { Limit: page.Limit, Offset: page.Offset, Total: page.Total, - Level: page.Level, }, Groups: []viewGroupRes{}, } @@ -342,7 +386,6 @@ func buildGroupsResponse(gp groups.Page, filterByID bool) groupPageRes { res := groupPageRes{ pageRes: pageRes{ Total: gp.Total, - Level: gp.Level, }, Groups: []viewGroupRes{}, } @@ -359,25 +402,3 @@ func buildGroupsResponse(gp groups.Page, filterByID bool) groupPageRes { return res } - -func buildChannelsResponse(cp groups.Page, filterByID bool) channelPageRes { - res := channelPageRes{ - pageRes: pageRes{ - Total: cp.Total, - Level: cp.Level, - }, - Channels: []viewGroupRes{}, - } - - for _, channel := range cp.Groups { - if filterByID && channel.Level == 0 { - continue - } - view := viewGroupRes{ - Group: channel, - } - res.Channels = append(res.Channels, view) - } - - return res -} diff --git a/groups/api/http/requests.go b/groups/api/http/requests.go new file mode 100644 index 0000000000..ad07254889 --- /dev/null +++ b/groups/api/http/requests.go @@ -0,0 +1,187 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "github.com/absmach/magistrala/groups" + mggroups "github.com/absmach/magistrala/groups" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" +) + +type createGroupReq struct { + mggroups.Group +} + +func (req createGroupReq) validate() error { + if len(req.Name) > api.MaxNameSize || req.Name == "" { + return apiutil.ErrNameSize + } + + return nil +} + +type updateGroupReq struct { + id string + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +func (req updateGroupReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + if len(req.Name) > api.MaxNameSize { + return apiutil.ErrNameSize + } + return nil +} + +type listGroupsReq struct { + mggroups.PageMeta +} + +func (req listGroupsReq) validate() error { + + if req.Limit > api.MaxLimitSize || req.Limit < 1 { + return apiutil.ErrLimitSize + } + + return nil +} + +type groupReq struct { + id string +} + +func (req groupReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + + return nil +} + +type changeGroupStatusReq struct { + id string +} + +func (req changeGroupStatusReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + return nil +} + +type retrieveGroupHierarchyReq struct { + mggroups.HierarchyPageMeta + id string +} + +func (req retrieveGroupHierarchyReq) validate() error { + if req.Level > groups.MaxLevel { + return apiutil.ErrLevel + } + + return nil +} + +type addParentGroupReq struct { + id string + ParentID string `json:"parent_id"` +} + +func (req addParentGroupReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + if err := api.ValidateUUID(req.ParentID); err != nil { + return err + } + if req.id == req.ParentID { + return apiutil.ErrSelfParentingNotAllowed + } + return nil +} + +type removeParentGroupReq struct { + id string +} + +func (req removeParentGroupReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + return nil +} + +type addChildrenGroupsReq struct { + id string + ChildrenIDs []string `json:"children_ids"` +} + +func (req addChildrenGroupsReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + if len(req.ChildrenIDs) == 0 { + return apiutil.ErrMissingChildrenGroupIDs + } + for _, childID := range req.ChildrenIDs { + if err := api.ValidateUUID(childID); err != nil { + return err + } + if req.id == childID { + return apiutil.ErrSelfParentingNotAllowed + } + } + return nil +} + +type removeChildrenGroupsReq struct { + id string + ChildrenIDs []string `json:"children_ids"` +} + +func (req removeChildrenGroupsReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + if len(req.ChildrenIDs) == 0 { + return apiutil.ErrMissingChildrenGroupIDs + } + for _, childID := range req.ChildrenIDs { + if err := api.ValidateUUID(childID); err != nil { + return err + } + } + return nil +} + +type removeAllChildrenGroupsReq struct { + id string +} + +func (req removeAllChildrenGroupsReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + return nil +} + +type listChildrenGroupsReq struct { + id string + mggroups.PageMeta +} + +func (req listChildrenGroupsReq) validate() error { + if req.id == "" { + return apiutil.ErrMissingID + } + if req.Limit > api.MaxLimitSize || req.Limit < 1 { + return apiutil.ErrLimitSize + } + return nil +} diff --git a/groups/api/http/requests_test.go b/groups/api/http/requests_test.go new file mode 100644 index 0000000000..703c0e9ed6 --- /dev/null +++ b/groups/api/http/requests_test.go @@ -0,0 +1,400 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "fmt" + "strings" + "testing" + + "github.com/absmach/magistrala/groups" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/stretchr/testify/assert" +) + +var valid = "valid" + +func TestCreateGroupReqValidation(t *testing.T) { + cases := []struct { + desc string + req createGroupReq + err error + }{ + { + desc: "valid request", + req: createGroupReq{ + Group: groups.Group{ + Name: valid, + }, + }, + err: nil, + }, + { + desc: "long name", + req: createGroupReq{ + Group: groups.Group{ + Name: strings.Repeat("a", api.MaxNameSize+1), + }, + }, + err: apiutil.ErrNameSize, + }, + { + desc: "empty name", + req: createGroupReq{ + Group: groups.Group{}, + }, + err: apiutil.ErrNameSize, + }, + } + + for _, tc := range cases { + err := tc.req.validate() + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} + +func TestUpdateGroupReqValidation(t *testing.T) { + cases := []struct { + desc string + req updateGroupReq + err error + }{ + { + desc: "valid request", + req: updateGroupReq{ + id: valid, + Name: valid, + }, + err: nil, + }, + { + desc: "long name", + req: updateGroupReq{ + id: valid, + Name: strings.Repeat("a", api.MaxNameSize+1), + }, + err: apiutil.ErrNameSize, + }, + { + desc: "empty id", + req: updateGroupReq{ + Name: valid, + }, + err: apiutil.ErrMissingID, + }, + } + + for _, tc := range cases { + err := tc.req.validate() + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} + +func TestListGroupReqValidation(t *testing.T) { + cases := []struct { + desc string + req listGroupsReq + err error + }{ + { + desc: "valid request", + req: listGroupsReq{ + PageMeta: groups.PageMeta{ + Limit: 10, + }, + }, + err: nil, + }, + { + desc: "invalid upper level", + req: listGroupsReq{ + PageMeta: groups.PageMeta{ + Limit: 10, + }, + }, + err: apiutil.ErrInvalidLevel, + }, + { + desc: "invalid lower limit", + req: listGroupsReq{ + PageMeta: groups.PageMeta{ + Limit: 0, + }, + }, + err: apiutil.ErrLimitSize, + }, + { + desc: "invalid upper limit", + req: listGroupsReq{ + PageMeta: groups.PageMeta{ + Limit: api.MaxLimitSize + 1, + }, + }, + err: apiutil.ErrLimitSize, + }, + } + + for _, tc := range cases { + err := tc.req.validate() + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} + +func TestGroupReqValidation(t *testing.T) { + cases := []struct { + desc string + req groupReq + err error + }{ + { + desc: "valid request", + req: groupReq{ + id: valid, + }, + err: nil, + }, + + { + desc: "empty id", + req: groupReq{}, + err: apiutil.ErrMissingID, + }, + } + + for _, tc := range cases { + err := tc.req.validate() + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} + +func TestChangeGroupStatusReqValidation(t *testing.T) { + cases := []struct { + desc string + req changeGroupStatusReq + err error + }{ + { + desc: "valid request", + req: changeGroupStatusReq{ + id: valid, + }, + err: nil, + }, + { + desc: "empty id", + req: changeGroupStatusReq{}, + err: apiutil.ErrMissingID, + }, + } + + for _, tc := range cases { + err := tc.req.validate() + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} + +func Test_createGroupReq_validate(t *testing.T) { + tests := []struct { + name string + req createGroupReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("createGroupReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_updateGroupReq_validate(t *testing.T) { + tests := []struct { + name string + req updateGroupReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("updateGroupReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_listGroupsReq_validate(t *testing.T) { + tests := []struct { + name string + req listGroupsReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("listGroupsReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_groupReq_validate(t *testing.T) { + tests := []struct { + name string + req groupReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("groupReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_changeGroupStatusReq_validate(t *testing.T) { + tests := []struct { + name string + req changeGroupStatusReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("changeGroupStatusReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_retrieveGroupHierarchyReq_validate(t *testing.T) { + tests := []struct { + name string + req retrieveGroupHierarchyReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("retrieveGroupHierarchyReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_addParentGroupReq_validate(t *testing.T) { + tests := []struct { + name string + req addParentGroupReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("addParentGroupReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_removeParentGroupReq_validate(t *testing.T) { + tests := []struct { + name string + req removeParentGroupReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("removeParentGroupReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_addChildrenGroupsReq_validate(t *testing.T) { + tests := []struct { + name string + req addChildrenGroupsReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("addChildrenGroupsReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_removeChildrenGroupsReq_validate(t *testing.T) { + tests := []struct { + name string + req removeChildrenGroupsReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("removeChildrenGroupsReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_removeAllChildrenGroupsReq_validate(t *testing.T) { + tests := []struct { + name string + req removeAllChildrenGroupsReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("removeAllChildrenGroupsReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_listChildrenGroupsReq_validate(t *testing.T) { + tests := []struct { + name string + req listChildrenGroupsReq + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.req.validate(); (err != nil) != tt.wantErr { + t.Errorf("listChildrenGroupsReq.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/groups/api/responses.go b/groups/api/http/responses.go similarity index 54% rename from internal/groups/api/responses.go rename to groups/api/http/responses.go index a2c30795b8..afe8cb8f49 100644 --- a/internal/groups/api/responses.go +++ b/groups/api/http/responses.go @@ -8,7 +8,7 @@ import ( "net/http" "github.com/absmach/magistrala" - "github.com/absmach/magistrala/pkg/groups" + "github.com/absmach/magistrala/groups" ) var ( @@ -17,8 +17,13 @@ var ( _ magistrala.Response = (*changeStatusRes)(nil) _ magistrala.Response = (*viewGroupRes)(nil) _ magistrala.Response = (*updateGroupRes)(nil) - _ magistrala.Response = (*assignRes)(nil) - _ magistrala.Response = (*unassignRes)(nil) + _ magistrala.Response = (*retrieveGroupHierarchyRes)(nil) + _ magistrala.Response = (*addParentGroupRes)(nil) + _ magistrala.Response = (*removeParentGroupRes)(nil) + _ magistrala.Response = (*addChildrenGroupsRes)(nil) + _ magistrala.Response = (*removeChildrenGroupsRes)(nil) + _ magistrala.Response = (*removeAllChildrenGroupsRes)(nil) + _ magistrala.Response = (*listChildrenGroupsRes)(nil) ) type viewGroupRes struct { @@ -37,22 +42,6 @@ func (res viewGroupRes) Empty() bool { return false } -type viewGroupPermsRes struct { - Permissions []string `json:"permissions"` -} - -func (res viewGroupPermsRes) Code() int { - return http.StatusOK -} - -func (res viewGroupPermsRes) Headers() map[string]string { - return map[string]string{} -} - -func (res viewGroupPermsRes) Empty() bool { - return false -} - type createGroupRes struct { groups.Group `json:",inline"` created bool @@ -89,7 +78,6 @@ type pageRes struct { Limit uint64 `json:"limit,omitempty"` Offset uint64 `json:"offset"` Total uint64 `json:"total"` - Level uint64 `json:"level,omitempty"` } func (res groupPageRes) Code() int { @@ -104,23 +92,6 @@ func (res groupPageRes) Empty() bool { return false } -type channelPageRes struct { - pageRes - Channels []viewGroupRes `json:"channels"` -} - -func (res channelPageRes) Code() int { - return http.StatusOK -} - -func (res channelPageRes) Headers() map[string]string { - return map[string]string{} -} - -func (res channelPageRes) Empty() bool { - return false -} - type updateGroupRes struct { groups.Group `json:",inline"` } @@ -153,79 +124,132 @@ func (res changeStatusRes) Empty() bool { return false } -type assignRes struct { - assigned bool +type deleteGroupRes struct { + deleted bool } -func (res assignRes) Code() int { - if res.assigned { - return http.StatusCreated +func (res deleteGroupRes) Code() int { + if res.deleted { + return http.StatusNoContent } return http.StatusBadRequest } -func (res assignRes) Headers() map[string]string { +func (res deleteGroupRes) Headers() map[string]string { return map[string]string{} } -func (res assignRes) Empty() bool { +func (res deleteGroupRes) Empty() bool { return true } -type unassignRes struct { - unassigned bool +type retrieveGroupHierarchyRes struct { + Level uint64 `json:"level"` + Direction int64 `json:"direction"` + Groups []viewGroupRes `json:"groups"` } -func (res unassignRes) Code() int { - if res.unassigned { - return http.StatusCreated - } +func (res retrieveGroupHierarchyRes) Code() int { + return http.StatusOK +} - return http.StatusBadRequest +func (res retrieveGroupHierarchyRes) Headers() map[string]string { + return map[string]string{} +} + +func (res retrieveGroupHierarchyRes) Empty() bool { + return false } -func (res unassignRes) Headers() map[string]string { +type addParentGroupRes struct { +} + +func (res addParentGroupRes) Code() int { + return http.StatusNoContent +} + +func (res addParentGroupRes) Headers() map[string]string { return map[string]string{} } -func (res unassignRes) Empty() bool { +func (res addParentGroupRes) Empty() bool { return true } -type listMembersRes struct { - pageRes - Members []groups.Member `json:"members"` +type removeParentGroupRes struct { } -func (res listMembersRes) Code() int { - return http.StatusOK +func (res removeParentGroupRes) Code() int { + return http.StatusNoContent } -func (res listMembersRes) Headers() map[string]string { +func (res removeParentGroupRes) Headers() map[string]string { return map[string]string{} } -func (res listMembersRes) Empty() bool { - return false +func (res removeParentGroupRes) Empty() bool { + return true } -type deleteGroupRes struct { - deleted bool +type addChildrenGroupsRes struct { } -func (res deleteGroupRes) Code() int { - if res.deleted { - return http.StatusNoContent - } +func (res addChildrenGroupsRes) Code() int { + return http.StatusNoContent +} - return http.StatusBadRequest +func (res addChildrenGroupsRes) Headers() map[string]string { + return map[string]string{} } -func (res deleteGroupRes) Headers() map[string]string { +func (res addChildrenGroupsRes) Empty() bool { + return true +} + +type removeChildrenGroupsRes struct { +} + +func (res removeChildrenGroupsRes) Code() int { + return http.StatusNoContent +} + +func (res removeChildrenGroupsRes) Headers() map[string]string { return map[string]string{} } -func (res deleteGroupRes) Empty() bool { +func (res removeChildrenGroupsRes) Empty() bool { + return true +} + +type removeAllChildrenGroupsRes struct { +} + +func (res removeAllChildrenGroupsRes) Code() int { + return http.StatusNoContent +} + +func (res removeAllChildrenGroupsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res removeAllChildrenGroupsRes) Empty() bool { return true } + +type listChildrenGroupsRes struct { + pageRes + Groups []viewGroupRes `json:"groups"` +} + +func (res listChildrenGroupsRes) Code() int { + return http.StatusOK +} + +func (res listChildrenGroupsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res listChildrenGroupsRes) Empty() bool { + return false +} diff --git a/groups/api/http/transport.go b/groups/api/http/transport.go new file mode 100644 index 0000000000..98b26c6ff6 --- /dev/null +++ b/groups/api/http/transport.go @@ -0,0 +1,139 @@ +package api + +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +import ( + "log/slog" + + "github.com/absmach/magistrala/groups" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/authn" + roleManagerHttp "github.com/absmach/magistrala/pkg/roles/rolemanager/api" + "github.com/go-chi/chi/v5" + kithttp "github.com/go-kit/kit/transport/http" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +// MakeHandler returns a HTTP handler for Groups API endpoints. +func MakeHandler(svc groups.Service, authn authn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string) *chi.Mux { + opts := []kithttp.ServerOption{ + kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), + } + d := roleManagerHttp.NewDecoder("groupID") + + mux.Route("/{domainID}/groups", func(r chi.Router) { + r.Use(api.AuthenticateMiddleware(authn, true)) + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + CreateGroupEndpoint(svc), + DecodeGroupCreate, + api.EncodeResponse, + opts..., + ), "create_group").ServeHTTP) + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + ListGroupsEndpoint(svc), + DecodeListGroupsRequest, + api.EncodeResponse, + opts..., + ), "list_groups").ServeHTTP) + r = roleManagerHttp.EntityAvailableActionsRouter(svc, d, r, opts) + + r.Route("/{groupID}", func(r chi.Router) { + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + ViewGroupEndpoint(svc), + DecodeGroupRequest, + api.EncodeResponse, + opts..., + ), "view_group").ServeHTTP) + + r.Put("/", otelhttp.NewHandler(kithttp.NewServer( + UpdateGroupEndpoint(svc), + DecodeGroupUpdate, + api.EncodeResponse, + opts..., + ), "update_group").ServeHTTP) + + r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( + DeleteGroupEndpoint(svc), + DecodeGroupRequest, + api.EncodeResponse, + opts..., + ), "delete_group").ServeHTTP) + + r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer( + EnableGroupEndpoint(svc), + DecodeChangeGroupStatusRequest, + api.EncodeResponse, + opts..., + ), "enable_group").ServeHTTP) + + r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer( + DisableGroupEndpoint(svc), + DecodeChangeGroupStatusRequest, + api.EncodeResponse, + opts..., + ), "disable_group").ServeHTTP) + + r = roleManagerHttp.EntityRoleMangerRouter(svc, d, r, opts) + + r.Get("/hierarchy", otelhttp.NewHandler(kithttp.NewServer( + retrieveGroupHierarchyEndpoint(svc), + decodeRetrieveGroupHierarchy, + api.EncodeResponse, + opts..., + ), "retrieve_group_hierarchy").ServeHTTP) + + r.Route("/parent", func(r chi.Router) { + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + addParentGroupEndpoint(svc), + decodeAddParentGroupRequest, + api.EncodeResponse, + opts..., + ), "add_parent_group").ServeHTTP) + + r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( + removeParentGroupEndpoint(svc), + decodeRemoveParentGroupRequest, + api.EncodeResponse, + opts..., + ), "remove_parent_group").ServeHTTP) + + }) + + r.Route("/children", func(r chi.Router) { + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + addChildrenGroupsEndpoint(svc), + decodeAddChildrenGroupsRequest, + api.EncodeResponse, + opts..., + ), "add_children_groups").ServeHTTP) + + r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( + removeChildrenGroupsEndpoint(svc), + decodeRemoveChildrenGroupsRequest, + api.EncodeResponse, + opts..., + ), "remove_children_groups").ServeHTTP) + + r.Delete("/all", otelhttp.NewHandler(kithttp.NewServer( + removeAllChildrenGroupsEndpoint(svc), + decodeRemoveAllChildrenGroupsRequest, + api.EncodeResponse, + opts..., + ), "remove_all_children_groups").ServeHTTP) + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + listChildrenGroupsEndpoint(svc), + decodeListChildrenGroupsRequest, + api.EncodeResponse, + opts..., + ), "list_children_groups").ServeHTTP) + }) + }) + + }) + + return mux +} diff --git a/pkg/groups/doc.go b/groups/doc.go similarity index 100% rename from pkg/groups/doc.go rename to groups/doc.go diff --git a/pkg/groups/errors.go b/groups/errors.go similarity index 100% rename from pkg/groups/errors.go rename to groups/errors.go diff --git a/internal/groups/events/doc.go b/groups/events/doc.go similarity index 100% rename from internal/groups/events/doc.go rename to groups/events/doc.go diff --git a/internal/groups/events/events.go b/groups/events/events.go similarity index 51% rename from internal/groups/events/events.go rename to groups/events/events.go index eb65fd411a..525a39e364 100644 --- a/internal/groups/events/events.go +++ b/groups/events/events.go @@ -6,27 +6,29 @@ package events import ( "time" + groups "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/pkg/events" - groups "github.com/absmach/magistrala/pkg/groups" ) var ( - groupPrefix = "group." - groupCreate = groupPrefix + "create" - groupUpdate = groupPrefix + "update" - groupChangeStatus = groupPrefix + "change_status" - groupView = groupPrefix + "view" - groupViewPerms = groupPrefix + "view_perms" - groupList = groupPrefix + "list" - groupListMemberships = groupPrefix + "list_by_user" - groupRemove = groupPrefix + "remove" - groupAssign = groupPrefix + "assign" - groupUnassign = groupPrefix + "unassign" + groupPrefix = "group." + groupCreate = groupPrefix + "create" + groupUpdate = groupPrefix + "update" + groupChangeStatus = groupPrefix + "change_status" + groupView = groupPrefix + "view" + groupList = groupPrefix + "list" + groupRemove = groupPrefix + "remove" + groupRetrieveGroupHierarchy = groupPrefix + "retrieve_group_hierarchy" + groupAddParentGroup = groupPrefix + "add_parent_group" + groupRemoveParentGroup = groupPrefix + "remove_parent_group" + groupViewParentGroup = groupPrefix + "view_parent_group" + groupAddChildrenGroups = groupPrefix + "add_children_groups" + groupRemoveChildrenGroups = groupPrefix + "remove_children_groups" + groupRemoveAllChildrenGroups = groupPrefix + "remove_all_children_groups" + groupListChildrenGroups = groupPrefix + "list_children_groups" ) var ( - _ events.Event = (*assignEvent)(nil) - _ events.Event = (*unassignEvent)(nil) _ events.Event = (*createGroupEvent)(nil) _ events.Event = (*updateGroupEvent)(nil) _ events.Event = (*changeStatusGroupEvent)(nil) @@ -34,43 +36,16 @@ var ( _ events.Event = (*deleteGroupEvent)(nil) _ events.Event = (*viewGroupEvent)(nil) _ events.Event = (*listGroupEvent)(nil) - _ events.Event = (*listGroupMembershipEvent)(nil) + _ events.Event = (*addParentGroupEvent)(nil) + _ events.Event = (*removeParentGroupEvent)(nil) + _ events.Event = (*viewParentGroupEvent)(nil) + _ events.Event = (*addChildrenGroupsEvent)(nil) + _ events.Event = (*removeChildrenGroupsEvent)(nil) + _ events.Event = (*removeAllChildrenGroupsEvent)(nil) + _ events.Event = (*listChildrenGroupsEvent)(nil) + _ events.Event = (*retrieveGroupHierarchyEvent)(nil) ) -type assignEvent struct { - memberIDs []string - relation string - memberKind string - groupID string -} - -func (cge assignEvent) Encode() (map[string]interface{}, error) { - return map[string]interface{}{ - "operation": groupAssign, - "member_ids": cge.memberIDs, - "relation": cge.relation, - "memberKind": cge.memberKind, - "group_id": cge.groupID, - }, nil -} - -type unassignEvent struct { - memberIDs []string - relation string - memberKind string - groupID string -} - -func (cge unassignEvent) Encode() (map[string]interface{}, error) { - return map[string]interface{}{ - "operation": groupUnassign, - "member_ids": cge.memberIDs, - "relation": cge.relation, - "memberKind": cge.memberKind, - "group_id": cge.groupID, - }, nil -} - type createGroupEvent struct { groups.Group } @@ -202,19 +177,8 @@ func (vge viewGroupEvent) Encode() (map[string]interface{}, error) { return val, nil } -type viewGroupPermsEvent struct { - permissions []string -} - -func (vgpe viewGroupPermsEvent) Encode() (map[string]interface{}, error) { - return map[string]interface{}{ - "operation": groupViewPerms, - "permissions": vgpe.permissions, - }, nil -} - type listGroupEvent struct { - groups.Page + groups.PageMeta } func (lge listGroupEvent) Encode() (map[string]interface{}, error) { @@ -244,28 +208,132 @@ func (lge listGroupEvent) Encode() (map[string]interface{}, error) { return val, nil } -type listGroupMembershipEvent struct { - groupID string - permission string - memberKind string +type deleteGroupEvent struct { + id string } -func (lgme listGroupMembershipEvent) Encode() (map[string]interface{}, error) { +func (rge deleteGroupEvent) Encode() (map[string]interface{}, error) { return map[string]interface{}{ - "operation": groupListMemberships, - "id": lgme.groupID, - "permission": lgme.permission, - "member_kind": lgme.memberKind, + "operation": groupRemove, + "id": rge.id, }, nil } -type deleteGroupEvent struct { +type retrieveGroupHierarchyEvent struct { id string + groups.HierarchyPageMeta } -func (rge deleteGroupEvent) Encode() (map[string]interface{}, error) { +func (vcge retrieveGroupHierarchyEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": groupRetrieveGroupHierarchy, + "id": vcge.id, + "level": vcge.Level, + "direction": vcge.Direction, + "tree": vcge.Tree, + } + return val, nil +} + +type addParentGroupEvent struct { + id string + parentID string +} + +func (apge addParentGroupEvent) Encode() (map[string]interface{}, error) { return map[string]interface{}{ - "operation": groupRemove, - "id": rge.id, + "operation": groupAddParentGroup, + "id": apge.id, + "parent_id": apge.parentID, + }, nil +} + +type removeParentGroupEvent struct { + id string +} + +func (rpge removeParentGroupEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": groupRemoveParentGroup, + "id": rpge.id, + }, nil +} + +type viewParentGroupEvent struct { + id string +} + +func (vpge viewParentGroupEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": groupViewParentGroup, + "id": vpge.id, + }, nil +} + +type addChildrenGroupsEvent struct { + id string + childrenIDs []string +} + +func (acge addChildrenGroupsEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": groupAddChildrenGroups, + "id": acge.id, + "childre_ids": acge.childrenIDs, + }, nil +} + +type removeChildrenGroupsEvent struct { + id string + childrenIDs []string +} + +func (rcge removeChildrenGroupsEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": groupRemoveChildrenGroups, + "id": rcge.id, + "children_ids": rcge.childrenIDs, }, nil } + +type removeAllChildrenGroupsEvent struct { + id string +} + +func (racge removeAllChildrenGroupsEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": groupRemoveAllChildrenGroups, + "id": racge.id, + }, nil +} + +type listChildrenGroupsEvent struct { + id string + groups.PageMeta +} + +func (vcge listChildrenGroupsEvent) Encode() (map[string]interface{}, error) { + val := map[string]interface{}{ + "operation": groupListChildrenGroups, + "id": vcge.id, + "total": vcge.Total, + "offset": vcge.Offset, + "limit": vcge.Limit, + } + if vcge.Name != "" { + val["name"] = vcge.Name + } + if vcge.DomainID != "" { + val["domain_id"] = vcge.DomainID + } + if vcge.Tag != "" { + val["tag"] = vcge.Tag + } + if vcge.Metadata != nil { + val["metadata"] = vcge.Metadata + } + if vcge.Status.String() != "" { + val["status"] = vcge.Status.String() + } + return val, nil +} diff --git a/internal/groups/events/streams.go b/groups/events/streams.go similarity index 51% rename from internal/groups/events/streams.go rename to groups/events/streams.go index b473c5e1f2..3b5dfa6e17 100644 --- a/internal/groups/events/streams.go +++ b/groups/events/streams.go @@ -6,35 +6,41 @@ package events import ( "context" + "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/events" "github.com/absmach/magistrala/pkg/events/store" - "github.com/absmach/magistrala/pkg/groups" + rmEvents "github.com/absmach/magistrala/pkg/roles/rolemanager/events" ) +const streamID = "magistrala.groups" + var _ groups.Service = (*eventStore)(nil) type eventStore struct { events.Publisher svc groups.Service + rmEvents.RoleManagerEventStore } // NewEventStoreMiddleware returns wrapper around things service that sends // events to event store. -func NewEventStoreMiddleware(ctx context.Context, svc groups.Service, url, streamID string) (groups.Service, error) { +func New(ctx context.Context, svc groups.Service, url string) (groups.Service, error) { publisher, err := store.NewPublisher(ctx, url, streamID) if err != nil { return nil, err } + rmes := rmEvents.NewRoleManagerEventStore("groups", svc, publisher) return &eventStore{ - svc: svc, - Publisher: publisher, + svc: svc, + Publisher: publisher, + RoleManagerEventStore: rmes, }, nil } -func (es eventStore) CreateGroup(ctx context.Context, session authn.Session, kind string, group groups.Group) (groups.Group, error) { - group, err := es.svc.CreateGroup(ctx, session, kind, group) +func (es eventStore) CreateGroup(ctx context.Context, session authn.Session, group groups.Group) (groups.Group, error) { + group, err := es.svc.CreateGroup(ctx, session, group) if err != nil { return group, err } @@ -83,24 +89,8 @@ func (es eventStore) ViewGroup(ctx context.Context, session authn.Session, id st return group, nil } -func (es eventStore) ViewGroupPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - permissions, err := es.svc.ViewGroupPerms(ctx, session, id) - if err != nil { - return permissions, err - } - event := viewGroupPermsEvent{ - permissions, - } - - if err := es.Publish(ctx, event); err != nil { - return permissions, err - } - - return permissions, nil -} - -func (es eventStore) ListGroups(ctx context.Context, session authn.Session, memberKind, memberID string, pm groups.Page) (groups.Page, error) { - gp, err := es.svc.ListGroups(ctx, session, memberKind, memberID, pm) +func (es eventStore) ListGroups(ctx context.Context, session authn.Session, pm groups.PageMeta) (groups.Page, error) { + gp, err := es.svc.ListGroups(ctx, session, pm) if err != nil { return gp, err } @@ -115,22 +105,6 @@ func (es eventStore) ListGroups(ctx context.Context, session authn.Session, memb return gp, nil } -func (es eventStore) ListMembers(ctx context.Context, session authn.Session, groupID, permission, memberKind string) (groups.MembersPage, error) { - mp, err := es.svc.ListMembers(ctx, session, groupID, permission, memberKind) - if err != nil { - return mp, err - } - event := listGroupMembershipEvent{ - groupID, permission, memberKind, - } - - if err := es.Publish(ctx, event); err != nil { - return mp, err - } - - return mp, nil -} - func (es eventStore) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { group, err := es.svc.EnableGroup(ctx, session, id) if err != nil { @@ -140,43 +114,6 @@ func (es eventStore) EnableGroup(ctx context.Context, session authn.Session, id return es.changeStatus(ctx, group) } -func (es eventStore) Assign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) error { - if err := es.svc.Assign(ctx, session, groupID, relation, memberKind, memberIDs...); err != nil { - return err - } - - event := assignEvent{ - groupID: groupID, - relation: relation, - memberKind: memberKind, - memberIDs: memberIDs, - } - - if err := es.Publish(ctx, event); err != nil { - return err - } - - return nil -} - -func (es eventStore) Unassign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) error { - if err := es.svc.Unassign(ctx, session, groupID, relation, memberKind, memberIDs...); err != nil { - return err - } - - event := unassignEvent{ - groupID: groupID, - relation: relation, - memberKind: memberKind, - memberIDs: memberIDs, - } - - if err := es.Publish(ctx, event); err != nil { - return err - } - return es.svc.Unassign(ctx, session, groupID, relation, memberKind, memberIDs...) -} - func (es eventStore) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { group, err := es.svc.DisableGroup(ctx, session, id) if err != nil { @@ -210,3 +147,75 @@ func (es eventStore) DeleteGroup(ctx context.Context, session authn.Session, id } return nil } + +func (es eventStore) RetrieveGroupHierarchy(ctx context.Context, session authn.Session, id string, hm groups.HierarchyPageMeta) (groups.HierarchyPage, error) { + g, err := es.svc.RetrieveGroupHierarchy(ctx, session, id, hm) + if err != nil { + return g, err + } + if err := es.Publish(ctx, retrieveGroupHierarchyEvent{id, hm}); err != nil { + return g, err + } + return g, nil +} + +func (es eventStore) AddParentGroup(ctx context.Context, session authn.Session, id, parentID string) error { + if err := es.svc.AddParentGroup(ctx, session, id, parentID); err != nil { + return err + } + if err := es.Publish(ctx, addParentGroupEvent{id, parentID}); err != nil { + return err + } + return nil +} + +func (es eventStore) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + if err := es.svc.RemoveParentGroup(ctx, session, id); err != nil { + return err + } + if err := es.Publish(ctx, removeParentGroupEvent{id}); err != nil { + return err + } + return nil +} + +func (es eventStore) AddChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + if err := es.svc.AddChildrenGroups(ctx, session, id, childrenGroupIDs); err != nil { + return err + } + if err := es.Publish(ctx, addChildrenGroupsEvent{id, childrenGroupIDs}); err != nil { + return err + } + return nil +} + +func (es eventStore) RemoveChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + if err := es.svc.RemoveChildrenGroups(ctx, session, id, childrenGroupIDs); err != nil { + return err + } + if err := es.Publish(ctx, removeChildrenGroupsEvent{id, childrenGroupIDs}); err != nil { + return err + } + + return nil +} +func (es eventStore) RemoveAllChildrenGroups(ctx context.Context, session authn.Session, id string) error { + if err := es.svc.RemoveAllChildrenGroups(ctx, session, id); err != nil { + return err + } + if err := es.Publish(ctx, removeAllChildrenGroupsEvent{id}); err != nil { + return err + } + return nil +} + +func (es eventStore) ListChildrenGroups(ctx context.Context, session authn.Session, id string, pm groups.PageMeta) (groups.Page, error) { + g, err := es.svc.ListChildrenGroups(ctx, session, id, pm) + if err != nil { + return g, err + } + if err := es.Publish(ctx, listChildrenGroupsEvent{id, pm}); err != nil { + return g, err + } + return g, nil +} diff --git a/groups/groups.go b/groups/groups.go new file mode 100644 index 0000000000..8890f05887 --- /dev/null +++ b/groups/groups.go @@ -0,0 +1,302 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package groups + +import ( + "context" + "time" + + "github.com/absmach/magistrala/domains" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/roles" + "github.com/absmach/magistrala/pkg/svcutil" +) + +// MaxLevel represents the maximum group hierarchy level. +const ( + MaxLevel = uint64(20) + MaxPathLength = 20 +) + +// Metadata represents arbitrary JSON. +type Metadata map[string]interface{} + +// Group represents the group of Clients. +// Indicates a level in tree hierarchy. Root node is level 1. +// Path in a tree consisting of group IDs +// Paths are unique per domain. +type Group struct { + ID string `json:"id"` + Domain string `json:"domain_id,omitempty"` + Parent string `json:"parent_id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + Level int `json:"level,omitempty"` + Path string `json:"path,omitempty"` + Children []*Group `json:"children,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + UpdatedBy string `json:"updated_by,omitempty"` + Status Status `json:"status"` + Permissions []string `json:"permissions,omitempty"` +} + +type Member struct { + ID string `json:"id"` + Type string `json:"type"` +} + +// Memberships contains page related metadata as well as list of memberships that +// belong to this page. +type MembersPage struct { + Total uint64 `json:"total"` + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Members []Member `json:"members"` +} + +// Page contains page related metadata as well as list +// of Groups that belong to the page. +type Page struct { + PageMeta + Groups []Group +} + +type HierarchyPageMeta struct { + Level uint64 `json:"level"` + Direction int64 `json:"direction"` // ancestors (+1) or descendants (-1) + // - `true` - result is JSON tree representing groups hierarchy, + // - `false` - result is JSON array of groups. + // ToDo: Tree is build in API layer now, not in service layer. This need to be fine tuned. + Tree bool `json:"tree"` +} +type HierarchyPage struct { + HierarchyPageMeta + Groups []Group +} + +// Repository specifies a group persistence API. +// +//go:generate mockery --name Repository --output=./mocks --filename repository.go --quiet --note "Copyright (c) Abstract Machines" --unroll-variadic=false +type Repository interface { + // Save group. + Save(ctx context.Context, g Group) (Group, error) + + // Update a group. + Update(ctx context.Context, g Group) (Group, error) + + // RetrieveByID retrieves group by its id. + RetrieveByID(ctx context.Context, id string) (Group, error) + + // RetrieveAll retrieves all groups. + RetrieveAll(ctx context.Context, pm PageMeta) (Page, error) + + // RetrieveByIDs retrieves group by ids and query. + RetrieveByIDs(ctx context.Context, pm PageMeta, ids ...string) (Page, error) + + RetrieveHierarchy(ctx context.Context, id string, hm HierarchyPageMeta) (HierarchyPage, error) + + // ChangeStatus changes groups status to active or inactive + ChangeStatus(ctx context.Context, group Group) (Group, error) + + // AssignParentGroup assigns parent group id to a given group id + AssignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error + + // UnassignParentGroup unassign parent group id fr given group id + UnassignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error + + UnassignAllChildrenGroup(ctx context.Context, id string) error + + // Delete a group + Delete(ctx context.Context, groupID string) error + + roles.Repository +} + +//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines" --unroll-variadic=false +type Service interface { + // CreateGroup creates new group. + CreateGroup(ctx context.Context, session authn.Session, g Group) (Group, error) + + // UpdateGroup updates the group identified by the provided ID. + UpdateGroup(ctx context.Context, session authn.Session, g Group) (Group, error) + + // ViewGroup retrieves data about the group identified by ID. + ViewGroup(ctx context.Context, session authn.Session, id string) (Group, error) + + // ListGroups retrieves + ListGroups(ctx context.Context, session authn.Session, pm PageMeta) (Page, error) + + // EnableGroup logically enables the group identified with the provided ID. + EnableGroup(ctx context.Context, session authn.Session, id string) (Group, error) + + // DisableGroup logically disables the group identified with the provided ID. + DisableGroup(ctx context.Context, session authn.Session, id string) (Group, error) + + // DeleteGroup delete the given group id + DeleteGroup(ctx context.Context, session authn.Session, id string) error + + RetrieveGroupHierarchy(ctx context.Context, session authn.Session, id string, hm HierarchyPageMeta) (HierarchyPage, error) + + AddParentGroup(ctx context.Context, session authn.Session, id, parentID string) error + + RemoveParentGroup(ctx context.Context, session authn.Session, id string) error + + AddChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error + + RemoveChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error + + RemoveAllChildrenGroups(ctx context.Context, session authn.Session, id string) error + + ListChildrenGroups(ctx context.Context, session authn.Session, id string, pm PageMeta) (Page, error) + + roles.RoleManager +} + +const ( + OpCreateGroup svcutil.Operation = iota + OpListGroups + OpViewGroup + OpUpdateGroup + OpEnableGroup + OpDisableGroup + OpRetrieveGroupHierarchy + OpAddParentGroup + OpRemoveParentGroup + OpViewParentGroup + OpAddChildrenGroups + OpRemoveChildrenGroups + OpRemoveAllChildrenGroups + OpListChildrenGroups + OpAddChannels + OpRemoveChannels + OpRemoveAllChannels + OpListChannels + OpAddThings + OpRemoveThings + OpRemoveAllThings + OpListThings + OpDeleteGroup +) + +var expectedOperations = []svcutil.Operation{ + OpCreateGroup, + OpListGroups, + OpViewGroup, + OpUpdateGroup, + OpEnableGroup, + OpDisableGroup, + OpRetrieveGroupHierarchy, + OpAddParentGroup, + OpRemoveParentGroup, + OpViewParentGroup, + OpAddChildrenGroups, + OpRemoveChildrenGroups, + OpRemoveAllChildrenGroups, + OpListChildrenGroups, + OpAddChannels, + OpRemoveChannels, + OpRemoveAllChannels, + OpListChannels, + OpAddThings, + OpRemoveThings, + OpRemoveAllThings, + OpListThings, + OpDeleteGroup, +} + +var operationNames = []string{ + "OpCreateGroup", + "OpListGroups", + "OpViewGroup", + "OpUpdateGroup", + "OpEnableGroup", + "OpDisableGroup", + "OpRetrieveGroupHierarchy", + "OpAddParentGroup", + "OpRemoveParentGroup", + "OpViewParentGroup", + "OpAddChildrenGroups", + "OpRemoveChildrenGroups", + "OpRemoveAllChildrenGroups", + "OpListChildrenGroups", + "OpAddChannels", + "OpRemoveChannels", + "OpRemoveAllChannels", + "OpListChannels", + "OpAddThings", + "OpRemoveThings", + "OpRemoveAllThings", + "OpListThings", + "OpDeleteGroup", +} + +func NewOperationPerm() svcutil.OperationPerm { + return svcutil.NewOperationPerm(expectedOperations, operationNames) +} + +// Below codes should moved out of service, may be can be kept in `cmd//main.go` +const ( + updatePermission = "update_permission" + readPermission = "read_permission" + membershipPermission = "membership_permission" + deletePermission = "delete_permission" + setChildPermission = "set_child_permission" + setParentPermission = "set_parent_permission" + manageRolePermission = "manage_role_permission" + addRoleUsersPermission = "add_role_users_permission" + removeRoleUsersPermission = "remove_role_users_permission" + viewRoleUsersPermission = "view_role_users_permission" +) + +func NewOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + OpCreateGroup: domains.GroupCreatePermission, + OpListGroups: readPermission, + OpViewGroup: readPermission, + OpUpdateGroup: updatePermission, + OpEnableGroup: updatePermission, + OpDisableGroup: updatePermission, + OpRetrieveGroupHierarchy: readPermission, + OpAddParentGroup: setParentPermission, + OpRemoveParentGroup: setParentPermission, + OpViewParentGroup: readPermission, + OpAddChildrenGroups: setChildPermission, + OpRemoveChildrenGroups: setChildPermission, + OpRemoveAllChildrenGroups: setChildPermission, + OpListChildrenGroups: readPermission, + OpAddChannels: "", + OpRemoveChannels: "", + OpRemoveAllChannels: "", + OpListChannels: "", + OpAddThings: "", + OpRemoveThings: "", + OpRemoveAllThings: "", + OpListThings: "", + OpDeleteGroup: deletePermission, + } + return opPerm +} + +func NewRolesOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + roles.OpAddRole: manageRolePermission, + roles.OpRemoveRole: manageRolePermission, + roles.OpUpdateRoleName: manageRolePermission, + roles.OpRetrieveRole: manageRolePermission, + roles.OpRetrieveAllRoles: manageRolePermission, + roles.OpRoleAddActions: manageRolePermission, + roles.OpRoleListActions: manageRolePermission, + roles.OpRoleCheckActionsExists: manageRolePermission, + roles.OpRoleRemoveActions: manageRolePermission, + roles.OpRoleRemoveAllActions: manageRolePermission, + roles.OpRoleAddMembers: addRoleUsersPermission, + roles.OpRoleListMembers: viewRoleUsersPermission, + roles.OpRoleCheckMembersExists: viewRoleUsersPermission, + roles.OpRoleRemoveMembers: removeRoleUsersPermission, + roles.OpRoleRemoveAllMembers: manageRolePermission, + } + return opPerm +} diff --git a/groups/middleware/authorization.go b/groups/middleware/authorization.go new file mode 100644 index 0000000000..509a983f47 --- /dev/null +++ b/groups/middleware/authorization.go @@ -0,0 +1,314 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + + "github.com/absmach/magistrala/groups" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/authz" + mgauthz "github.com/absmach/magistrala/pkg/authz" + "github.com/absmach/magistrala/pkg/errors" + "github.com/absmach/magistrala/pkg/policies" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" + "github.com/absmach/magistrala/pkg/svcutil" +) + +var errParentUnAuthz = errors.New("parent group unauthorized") + +var _ groups.Service = (*authorizationMiddleware)(nil) + +type authorizationMiddleware struct { + svc groups.Service + authz mgauthz.Authorization + opp svcutil.OperationPerm + rmMW.RoleManagerAuthorizationMiddleware +} + +// AuthorizationMiddleware adds authorization to the clients service. +func AuthorizationMiddleware(entityType string, svc groups.Service, authz mgauthz.Authorization, groupsOpPerm, rolesOpPerm map[svcutil.Operation]svcutil.Permission) (groups.Service, error) { + opp := groups.NewOperationPerm() + if err := opp.AddOperationPermissionMap(groupsOpPerm); err != nil { + return nil, err + } + if err := opp.Validate(); err != nil { + return nil, err + } + + ram, err := rmMW.NewRoleManagerAuthorizationMiddleware(entityType, svc, authz, rolesOpPerm) + if err != nil { + return nil, err + } + return &authorizationMiddleware{ + svc: svc, + authz: authz, + opp: opp, + RoleManagerAuthorizationMiddleware: ram, + }, nil +} + +func (am *authorizationMiddleware) CreateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { + + if err := am.authorize(ctx, groups.OpCreateGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + }); err != nil { + return groups.Group{}, errors.Wrap(errParentUnAuthz, err) + } + + if g.Parent != "" { + if err := am.authorize(ctx, groups.OpAddChildrenGroups, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: g.Parent, + ObjectType: policies.GroupType, + }); err != nil { + return groups.Group{}, errors.Wrap(errParentUnAuthz, err) + } + } + + return am.svc.CreateGroup(ctx, session, g) +} + +func (am *authorizationMiddleware) UpdateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { + if err := am.authorize(ctx, groups.OpUpdateGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: g.ID, + ObjectType: policies.GroupType, + }); err != nil { + return groups.Group{}, errors.Wrap(errParentUnAuthz, err) + } + + return am.svc.UpdateGroup(ctx, session, g) +} + +func (am *authorizationMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + if err := am.authorize(ctx, groups.OpViewGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return groups.Group{}, errors.Wrap(errParentUnAuthz, err) + } + + return am.svc.ViewGroup(ctx, session, id) +} + +func (am *authorizationMiddleware) ListGroups(ctx context.Context, session authn.Session, gm groups.PageMeta) (groups.Page, error) { + err := am.checkSuperAdmin(ctx, session.UserID) + switch { + case err == nil: + session.SuperAdmin = true + default: + if err := am.authorize(ctx, groups.OpListGroups, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Subject: session.DomainUserID, + Object: session.DomainID, + ObjectType: policies.DomainType, + }); err != nil { + return groups.Page{}, errors.Wrap(errParentUnAuthz, err) + } + } + + return am.svc.ListGroups(ctx, session, gm) +} + +func (am *authorizationMiddleware) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + if err := am.authorize(ctx, groups.OpEnableGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return groups.Group{}, err + } + + return am.svc.EnableGroup(ctx, session, id) +} + +func (am *authorizationMiddleware) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + if err := am.authorize(ctx, groups.OpDisableGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return groups.Group{}, err + } + + return am.svc.DisableGroup(ctx, session, id) +} + +func (am *authorizationMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) error { + if err := am.authorize(ctx, groups.OpDeleteGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + + return am.svc.DeleteGroup(ctx, session, id) +} + +func (am *authorizationMiddleware) RetrieveGroupHierarchy(ctx context.Context, session authn.Session, id string, hm groups.HierarchyPageMeta) (groups.HierarchyPage, error) { + if err := am.authorize(ctx, groups.OpRetrieveGroupHierarchy, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return groups.HierarchyPage{}, err + } + return am.svc.RetrieveGroupHierarchy(ctx, session, id, hm) +} + +func (am *authorizationMiddleware) AddParentGroup(ctx context.Context, session authn.Session, id, parentID string) error { + if err := am.authorize(ctx, groups.OpAddParentGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + + if err := am.authorize(ctx, groups.OpAddChildrenGroups, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: parentID, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + return am.svc.AddParentGroup(ctx, session, id, parentID) +} + +func (am *authorizationMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + if err := am.authorize(ctx, groups.OpRemoveParentGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + + return am.svc.RemoveParentGroup(ctx, session, id) +} + +func (am *authorizationMiddleware) AddChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + if err := am.authorize(ctx, groups.OpAddChildrenGroups, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + + for _, childID := range childrenGroupIDs { + if err := am.authorize(ctx, groups.OpAddParentGroup, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: childID, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + } + + return am.svc.AddChildrenGroups(ctx, session, id, childrenGroupIDs) +} + +func (am *authorizationMiddleware) RemoveChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + if err := am.authorize(ctx, groups.OpRemoveChildrenGroups, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + + return am.svc.RemoveChildrenGroups(ctx, session, id, childrenGroupIDs) +} + +func (am *authorizationMiddleware) RemoveAllChildrenGroups(ctx context.Context, session authn.Session, id string) error { + if err := am.authorize(ctx, groups.OpRemoveAllChildrenGroups, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return err + } + return am.svc.RemoveAllChildrenGroups(ctx, session, id) +} + +func (am *authorizationMiddleware) ListChildrenGroups(ctx context.Context, session authn.Session, id string, pm groups.PageMeta) (groups.Page, error) { + if err := am.authorize(ctx, groups.OpListChildrenGroups, mgauthz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + Object: id, + ObjectType: policies.GroupType, + }); err != nil { + return groups.Page{}, err + } + + return am.svc.ListChildrenGroups(ctx, session, id, pm) +} + +func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error { + if err := am.authz.Authorize(ctx, authz.PolicyReq{ + SubjectType: policies.UserType, + Subject: adminID, + Permission: policies.AdminPermission, + ObjectType: policies.PlatformType, + Object: policies.MagistralaObject, + }); err != nil { + return err + } + return nil +} + +func (am *authorizationMiddleware) authorize(ctx context.Context, op svcutil.Operation, pr authz.PolicyReq) error { + perm, err := am.opp.GetPermission(op) + if err != nil { + return err + } + pr.Permission = perm.String() + if err := am.authz.Authorize(ctx, pr); err != nil { + return err + } + return nil +} diff --git a/internal/groups/middleware/doc.go b/groups/middleware/doc.go similarity index 100% rename from internal/groups/middleware/doc.go rename to groups/middleware/doc.go diff --git a/internal/groups/middleware/logging.go b/groups/middleware/logging.go similarity index 53% rename from internal/groups/middleware/logging.go rename to groups/middleware/logging.go index 220f924d09..488a7a0cab 100644 --- a/internal/groups/middleware/logging.go +++ b/groups/middleware/logging.go @@ -8,8 +8,9 @@ import ( "log/slog" "time" + "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/groups" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" ) var _ groups.Service = (*loggingMiddleware)(nil) @@ -17,16 +18,17 @@ var _ groups.Service = (*loggingMiddleware)(nil) type loggingMiddleware struct { logger *slog.Logger svc groups.Service + rmMW.RoleManagerLoggingMiddleware } // LoggingMiddleware adds logging facilities to the groups service. func LoggingMiddleware(svc groups.Service, logger *slog.Logger) groups.Service { - return &loggingMiddleware{logger, svc} + return &loggingMiddleware{logger, svc, rmMW.NewRoleManagerLoggingMiddleware("groups", svc, logger)} } -// CreateGroup logs the create_group request. It logs the group name, id and session and the time it took to complete the request. +// CreateGroup logs the create_group request. It logs the group name, id and token and the time it took to complete the request. // If the request fails, it logs the error. -func (lm *loggingMiddleware) CreateGroup(ctx context.Context, session authn.Session, kind string, group groups.Group) (g groups.Group, err error) { +func (lm *loggingMiddleware) CreateGroup(ctx context.Context, session authn.Session, group groups.Group) (g groups.Group, err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), @@ -36,13 +38,13 @@ func (lm *loggingMiddleware) CreateGroup(ctx context.Context, session authn.Sess ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Create group failed", args...) return } lm.logger.Info("Create group completed successfully", args...) }(time.Now()) - return lm.svc.CreateGroup(ctx, session, kind, group) + return lm.svc.CreateGroup(ctx, session, group) } // UpdateGroup logs the update_group request. It logs the group name, id and the time it took to complete the request. @@ -58,7 +60,7 @@ func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, session authn.Sess ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Update group failed", args...) return } @@ -79,7 +81,7 @@ func (lm *loggingMiddleware) ViewGroup(ctx context.Context, session authn.Sessio ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("View group failed", args...) return } @@ -88,48 +90,26 @@ func (lm *loggingMiddleware) ViewGroup(ctx context.Context, session authn.Sessio return lm.svc.ViewGroup(ctx, session, id) } -// ViewGroupPerms logs the view_group request. It logs the group id and the time it took to complete the request. -// If the request fails, it logs the error. -func (lm *loggingMiddleware) ViewGroupPerms(ctx context.Context, session authn.Session, id string) (p []string, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("group_id", id), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("View group permissions failed", args...) - return - } - lm.logger.Info("View group permissions completed successfully", args...) - }(time.Now()) - return lm.svc.ViewGroupPerms(ctx, session, id) -} - // ListGroups logs the list_groups request. It logs the page metadata and the time it took to complete the request. // If the request fails, it logs the error. -func (lm *loggingMiddleware) ListGroups(ctx context.Context, session authn.Session, memberKind, memberID string, gp groups.Page) (cg groups.Page, err error) { +func (lm *loggingMiddleware) ListGroups(ctx context.Context, session authn.Session, pm groups.PageMeta) (cg groups.Page, err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), - slog.Group("member", - slog.String("id", memberID), - slog.String("kind", memberKind), - ), slog.Group("page", - slog.Uint64("limit", gp.Limit), - slog.Uint64("offset", gp.Offset), + slog.Uint64("limit", pm.Limit), + slog.Uint64("offset", pm.Offset), slog.Uint64("total", cg.Total), ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("List groups failed", args...) return } lm.logger.Info("List groups completed successfully", args...) }(time.Now()) - return lm.svc.ListGroups(ctx, session, memberKind, memberID, gp) + return lm.svc.ListGroups(ctx, session, pm) } // EnableGroup logs the enable_group request. It logs the group name, id and the time it took to complete the request. @@ -144,7 +124,7 @@ func (lm *loggingMiddleware) EnableGroup(ctx context.Context, session authn.Sess ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Enable group failed", args...) return } @@ -165,7 +145,7 @@ func (lm *loggingMiddleware) DisableGroup(ctx context.Context, session authn.Ses ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Disable group failed", args...) return } @@ -174,78 +154,143 @@ func (lm *loggingMiddleware) DisableGroup(ctx context.Context, session authn.Ses return lm.svc.DisableGroup(ctx, session, id) } -// ListMembers logs the list_members request. It logs the groupID and the time it took to complete the request. -// If the request fails, it logs the error. -func (lm *loggingMiddleware) ListMembers(ctx context.Context, session authn.Session, groupID, permission, memberKind string) (mp groups.MembersPage, err error) { +func (lm *loggingMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), - slog.String("group_id", groupID), - slog.String("permission", permission), - slog.String("member_kind", memberKind), + slog.String("group_id", id), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("List members failed", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Delete group failed", args...) return } - lm.logger.Info("List members completed successfully", args...) + lm.logger.Info("Delete group completed successfully", args...) }(time.Now()) - return lm.svc.ListMembers(ctx, session, groupID, permission, memberKind) + return lm.svc.DeleteGroup(ctx, session, id) } -func (lm *loggingMiddleware) Assign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) { +func (lm *loggingMiddleware) RetrieveGroupHierarchy(ctx context.Context, session authn.Session, id string, hm groups.HierarchyPageMeta) (gp groups.HierarchyPage, err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), - slog.String("group_id", groupID), - slog.String("relation", relation), - slog.String("member_kind", memberKind), - slog.Any("member_ids", memberIDs), + slog.String("group_id", id), + slog.Group("page", + slog.Uint64("limit", hm.Level), + slog.Int64("offset", hm.Direction), + slog.Bool("total", hm.Tree), + ), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Assign member to group failed", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Retrieve group hierarchy failed", args...) return } - lm.logger.Info("Assign member to group completed successfully", args...) + lm.logger.Info("Retrieve group hierarchy completed successfully", args...) }(time.Now()) + return lm.svc.RetrieveGroupHierarchy(ctx, session, id, hm) +} - return lm.svc.Assign(ctx, session, groupID, relation, memberKind, memberIDs...) +func (lm *loggingMiddleware) AddParentGroup(ctx context.Context, session authn.Session, id, parentID string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("group_id", id), + slog.String("parent_group_id", parentID), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Add parent group failed", args...) + return + } + lm.logger.Info("Add parent group completed successfully", args...) + }(time.Now()) + return lm.svc.AddParentGroup(ctx, session, id, parentID) } -func (lm *loggingMiddleware) Unassign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) { +func (lm *loggingMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), - slog.String("group_id", groupID), - slog.String("relation", relation), - slog.String("member_kind", memberKind), - slog.Any("member_ids", memberIDs), + slog.String("group_id", id), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Unassign member to group failed", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Remove parent group failed", args...) return } - lm.logger.Info("Unassign member to group completed successfully", args...) + lm.logger.Info("Remove parent group completed successfully", args...) }(time.Now()) + return lm.svc.RemoveParentGroup(ctx, session, id) +} - return lm.svc.Unassign(ctx, session, groupID, relation, memberKind, memberIDs...) +func (lm *loggingMiddleware) AddChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("group_id", id), + slog.Any("children_group_ids", childrenGroupIDs), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Add children groups failed", args...) + return + } + lm.logger.Info("Add parent group completed successfully", args...) + }(time.Now()) + return lm.svc.AddChildrenGroups(ctx, session, id, childrenGroupIDs) } -func (lm *loggingMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) (err error) { +func (lm *loggingMiddleware) RemoveChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), slog.String("group_id", id), + slog.Any("children_group_ids", childrenGroupIDs), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Delete group failed", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Remove children groups failed", args...) return } - lm.logger.Info("Delete group completed successfully", args...) + lm.logger.Info("Remove parent group completed successfully", args...) }(time.Now()) - return lm.svc.DeleteGroup(ctx, session, id) + return lm.svc.RemoveChildrenGroups(ctx, session, id, childrenGroupIDs) +} + +func (lm *loggingMiddleware) RemoveAllChildrenGroups(ctx context.Context, session authn.Session, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("group_id", id), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Remove all children groups failed", args...) + return + } + lm.logger.Info("Remove all parent group completed successfully", args...) + }(time.Now()) + return lm.svc.RemoveAllChildrenGroups(ctx, session, id) +} + +func (lm *loggingMiddleware) ListChildrenGroups(ctx context.Context, session authn.Session, id string, pm groups.PageMeta) (gp groups.Page, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("group_id", id), + slog.Group("page", + slog.Uint64("limit", pm.Limit), + slog.Uint64("offset", pm.Offset), + slog.Uint64("total", gp.Total), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("List children groups failed", args...) + return + } + lm.logger.Info("List children groups completed successfully", args...) + }(time.Now()) + return lm.svc.ListChildrenGroups(ctx, session, id, pm) } diff --git a/internal/groups/middleware/metrics.go b/groups/middleware/metrics.go similarity index 52% rename from internal/groups/middleware/metrics.go rename to groups/middleware/metrics.go index 7d6fa13f7f..e5379b7cb4 100644 --- a/internal/groups/middleware/metrics.go +++ b/groups/middleware/metrics.go @@ -7,8 +7,9 @@ import ( "context" "time" + "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/groups" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" "github.com/go-kit/kit/metrics" ) @@ -18,24 +19,27 @@ type metricsMiddleware struct { counter metrics.Counter latency metrics.Histogram svc groups.Service + rmMW.RoleManagerMetricsMiddleware } // MetricsMiddleware instruments policies service by tracking request count and latency. func MetricsMiddleware(svc groups.Service, counter metrics.Counter, latency metrics.Histogram) groups.Service { + rmm := rmMW.NewRoleManagerMetricsMiddleware("group", svc, counter, latency) return &metricsMiddleware{ - counter: counter, - latency: latency, - svc: svc, + counter: counter, + latency: latency, + svc: svc, + RoleManagerMetricsMiddleware: rmm, } } // CreateGroup instruments CreateGroup method with metrics. -func (ms *metricsMiddleware) CreateGroup(ctx context.Context, session authn.Session, kind string, g groups.Group) (groups.Group, error) { +func (ms *metricsMiddleware) CreateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { defer func(begin time.Time) { ms.counter.With("method", "create_group").Add(1) ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.CreateGroup(ctx, session, kind, g) + return ms.svc.CreateGroup(ctx, session, g) } // UpdateGroup instruments UpdateGroup method with metrics. @@ -56,22 +60,13 @@ func (ms *metricsMiddleware) ViewGroup(ctx context.Context, session authn.Sessio return ms.svc.ViewGroup(ctx, session, id) } -// ViewGroupPerms instruments ViewGroup method with metrics. -func (ms *metricsMiddleware) ViewGroupPerms(ctx context.Context, session authn.Session, id string) (p []string, err error) { - defer func(begin time.Time) { - ms.counter.With("method", "view_group_perms").Add(1) - ms.latency.With("method", "view_group_perms").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.ViewGroupPerms(ctx, session, id) -} - // ListGroups instruments ListGroups method with metrics. -func (ms *metricsMiddleware) ListGroups(ctx context.Context, session authn.Session, memberKind, memberID string, gp groups.Page) (cg groups.Page, err error) { +func (ms *metricsMiddleware) ListGroups(ctx context.Context, session authn.Session, pm groups.PageMeta) (cg groups.Page, err error) { defer func(begin time.Time) { ms.counter.With("method", "list_groups").Add(1) ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.ListGroups(ctx, session, memberKind, memberID, gp) + return ms.svc.ListGroups(ctx, session, pm) } // EnableGroup instruments EnableGroup method with metrics. @@ -92,39 +87,66 @@ func (ms *metricsMiddleware) DisableGroup(ctx context.Context, session authn.Ses return ms.svc.DisableGroup(ctx, session, id) } -// ListMembers instruments ListMembers method with metrics. -func (ms *metricsMiddleware) ListMembers(ctx context.Context, session authn.Session, groupID, permission, memberKind string) (mp groups.MembersPage, err error) { +func (ms *metricsMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) (err error) { + defer func(begin time.Time) { + ms.counter.With("method", "delete_group").Add(1) + ms.latency.With("method", "delete_group").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.DeleteGroup(ctx, session, id) +} + +func (ms *metricsMiddleware) RetrieveGroupHierarchy(ctx context.Context, session authn.Session, id string, hm groups.HierarchyPageMeta) (groups.HierarchyPage, error) { + defer func(begin time.Time) { + ms.counter.With("method", "list_parent_groups").Add(1) + ms.latency.With("method", "list_parent_groups").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.RetrieveGroupHierarchy(ctx, session, id, hm) +} + +func (ms *metricsMiddleware) AddParentGroup(ctx context.Context, session authn.Session, id, parentID string) error { defer func(begin time.Time) { - ms.counter.With("method", "list_memberships").Add(1) - ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "add_parent_group").Add(1) + ms.latency.With("method", "add_parent_group").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.ListMembers(ctx, session, groupID, permission, memberKind) + return ms.svc.AddParentGroup(ctx, session, id, parentID) } -// Assign instruments Assign method with metrics. -func (ms *metricsMiddleware) Assign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) { +func (ms *metricsMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { defer func(begin time.Time) { - ms.counter.With("method", "assign").Add(1) - ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "remove_parent_group").Add(1) + ms.latency.With("method", "remove_parent_group").Observe(time.Since(begin).Seconds()) }(time.Now()) + return ms.svc.RemoveParentGroup(ctx, session, id) +} - return ms.svc.Assign(ctx, session, groupID, relation, memberKind, memberIDs...) +func (ms *metricsMiddleware) AddChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + defer func(begin time.Time) { + ms.counter.With("method", "add_children_groups").Add(1) + ms.latency.With("method", "add_children_groups").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.AddChildrenGroups(ctx, session, id, childrenGroupIDs) } -// Unassign instruments Unassign method with metrics. -func (ms *metricsMiddleware) Unassign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) { +func (ms *metricsMiddleware) RemoveChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { defer func(begin time.Time) { - ms.counter.With("method", "unassign").Add(1) - ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "remove_children_groups").Add(1) + ms.latency.With("method", "remove_children_groups").Observe(time.Since(begin).Seconds()) }(time.Now()) + return ms.svc.RemoveChildrenGroups(ctx, session, id, childrenGroupIDs) +} - return ms.svc.Unassign(ctx, session, groupID, relation, memberKind, memberIDs...) +func (ms *metricsMiddleware) RemoveAllChildrenGroups(ctx context.Context, session authn.Session, id string) error { + defer func(begin time.Time) { + ms.counter.With("method", "remove_all_children_groups").Add(1) + ms.latency.With("method", "remove_all_children_groups").Observe(time.Since(begin).Seconds()) + }(time.Now()) + return ms.svc.RemoveAllChildrenGroups(ctx, session, id) } -func (ms *metricsMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) (err error) { +func (ms *metricsMiddleware) ListChildrenGroups(ctx context.Context, session authn.Session, id string, pm groups.PageMeta) (groups.Page, error) { defer func(begin time.Time) { - ms.counter.With("method", "delete_group").Add(1) - ms.latency.With("method", "delete_group").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "list_children_groups").Add(1) + ms.latency.With("method", "list_children_groups").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.DeleteGroup(ctx, session, id) + return ms.svc.ListChildrenGroups(ctx, session, id, pm) } diff --git a/pkg/groups/mocks/doc.go b/groups/mocks/doc.go similarity index 100% rename from pkg/groups/mocks/doc.go rename to groups/mocks/doc.go diff --git a/groups/mocks/groups_client.go b/groups/mocks/groups_client.go new file mode 100644 index 0000000000..fb92cf927b --- /dev/null +++ b/groups/mocks/groups_client.go @@ -0,0 +1,118 @@ +// 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" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + v1 "github.com/absmach/magistrala/internal/grpc/common/v1" +) + +// GroupsServiceClient is an autogenerated mock type for the GroupsServiceClient type +type GroupsServiceClient struct { + mock.Mock +} + +type GroupsServiceClient_Expecter struct { + mock *mock.Mock +} + +func (_m *GroupsServiceClient) EXPECT() *GroupsServiceClient_Expecter { + return &GroupsServiceClient_Expecter{mock: &_m.Mock} +} + +// RetrieveEntity provides a mock function with given fields: ctx, in, opts +func (_m *GroupsServiceClient) RetrieveEntity(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption) (*v1.RetrieveEntityRes, 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 RetrieveEntity") + } + + var r0 *v1.RetrieveEntityRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) (*v1.RetrieveEntityRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) *v1.RetrieveEntityRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RetrieveEntityRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GroupsServiceClient_RetrieveEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveEntity' +type GroupsServiceClient_RetrieveEntity_Call struct { + *mock.Call +} + +// RetrieveEntity is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.RetrieveEntityReq +// - opts ...grpc.CallOption +func (_e *GroupsServiceClient_Expecter) RetrieveEntity(ctx interface{}, in interface{}, opts ...interface{}) *GroupsServiceClient_RetrieveEntity_Call { + return &GroupsServiceClient_RetrieveEntity_Call{Call: _e.mock.On("RetrieveEntity", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *GroupsServiceClient_RetrieveEntity_Call) Run(run func(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption)) *GroupsServiceClient_RetrieveEntity_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].(*v1.RetrieveEntityReq), variadicArgs...) + }) + return _c +} + +func (_c *GroupsServiceClient_RetrieveEntity_Call) Return(_a0 *v1.RetrieveEntityRes, _a1 error) *GroupsServiceClient_RetrieveEntity_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GroupsServiceClient_RetrieveEntity_Call) RunAndReturn(run func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) (*v1.RetrieveEntityRes, error)) *GroupsServiceClient_RetrieveEntity_Call { + _c.Call.Return(run) + return _c +} + +// NewGroupsServiceClient creates a new instance of GroupsServiceClient. 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 NewGroupsServiceClient(t interface { + mock.TestingT + Cleanup(func()) +}) *GroupsServiceClient { + mock := &GroupsServiceClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/groups/mocks/repository.go b/groups/mocks/repository.go new file mode 100644 index 0000000000..a4951ad168 --- /dev/null +++ b/groups/mocks/repository.go @@ -0,0 +1,764 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + groups "github.com/absmach/magistrala/groups" + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// Repository is an autogenerated mock type for the Repository type +type Repository struct { + mock.Mock +} + +// AddRoles provides a mock function with given fields: ctx, rps +func (_m *Repository) AddRoles(ctx context.Context, rps []roles.RoleProvision) ([]roles.Role, error) { + ret := _m.Called(ctx, rps) + + if len(ret) == 0 { + panic("no return value specified for AddRoles") + } + + var r0 []roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) ([]roles.Role, error)); ok { + return rf(ctx, rps) + } + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) []roles.Role); ok { + r0 = rf(ctx, rps) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []roles.RoleProvision) error); ok { + r1 = rf(ctx, rps) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AssignParentGroup provides a mock function with given fields: ctx, parentGroupID, groupIDs +func (_m *Repository) AssignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error { + ret := _m.Called(ctx, parentGroupID, groupIDs) + + if len(ret) == 0 { + panic("no return value specified for AssignParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) error); ok { + r0 = rf(ctx, parentGroupID, groupIDs...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ChangeStatus provides a mock function with given fields: ctx, group +func (_m *Repository) ChangeStatus(ctx context.Context, group groups.Group) (groups.Group, error) { + ret := _m.Called(ctx, group) + + if len(ret) == 0 { + panic("no return value specified for ChangeStatus") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, groups.Group) (groups.Group, error)); ok { + return rf(ctx, group) + } + if rf, ok := ret.Get(0).(func(context.Context, groups.Group) groups.Group); ok { + r0 = rf(ctx, group) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, groups.Group) error); ok { + r1 = rf(ctx, group) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, groupID +func (_m *Repository) Delete(ctx context.Context, groupID string) error { + ret := _m.Called(ctx, groupID) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, groupID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, members +func (_m *Repository) RemoveMemberFromAllRoles(ctx context.Context, members string) error { + ret := _m.Called(ctx, members) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRoles provides a mock function with given fields: ctx, roleIDs +func (_m *Repository) RemoveRoles(ctx context.Context, roleIDs []string) error { + ret := _m.Called(ctx, roleIDs) + + if len(ret) == 0 { + panic("no return value specified for RemoveRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok { + r0 = rf(ctx, roleIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAll provides a mock function with given fields: ctx, pm +func (_m *Repository) RetrieveAll(ctx context.Context, pm groups.PageMeta) (groups.Page, error) { + ret := _m.Called(ctx, pm) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAll") + } + + var r0 groups.Page + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, groups.PageMeta) (groups.Page, error)); ok { + return rf(ctx, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, groups.PageMeta) groups.Page); ok { + r0 = rf(ctx, pm) + } else { + r0 = ret.Get(0).(groups.Page) + } + + if rf, ok := ret.Get(1).(func(context.Context, groups.PageMeta) error); ok { + r1 = rf(ctx, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, entityID, limit, offset +func (_m *Repository) RetrieveAllRoles(ctx context.Context, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveByID provides a mock function with given fields: ctx, id +func (_m *Repository) RetrieveByID(ctx context.Context, id string) (groups.Group, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for RetrieveByID") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (groups.Group, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) groups.Group); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveByIDs provides a mock function with given fields: ctx, pm, ids +func (_m *Repository) RetrieveByIDs(ctx context.Context, pm groups.PageMeta, ids ...string) (groups.Page, error) { + ret := _m.Called(ctx, pm, ids) + + if len(ret) == 0 { + panic("no return value specified for RetrieveByIDs") + } + + var r0 groups.Page + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, groups.PageMeta, ...string) (groups.Page, error)); ok { + return rf(ctx, pm, ids...) + } + if rf, ok := ret.Get(0).(func(context.Context, groups.PageMeta, ...string) groups.Page); ok { + r0 = rf(ctx, pm, ids...) + } else { + r0 = ret.Get(0).(groups.Page) + } + + if rf, ok := ret.Get(1).(func(context.Context, groups.PageMeta, ...string) error); ok { + r1 = rf(ctx, pm, ids...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveEntitiesRolesActionsMembers provides a mock function with given fields: ctx, entityIDs +func (_m *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) { + ret := _m.Called(ctx, entityIDs) + + if len(ret) == 0 { + panic("no return value specified for RetrieveEntitiesRolesActionsMembers") + } + + var r0 []roles.EntityActionRole + var r1 []roles.EntityMemberRole + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error)); ok { + return rf(ctx, entityIDs) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []roles.EntityActionRole); ok { + r0 = rf(ctx, entityIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.EntityActionRole) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) []roles.EntityMemberRole); ok { + r1 = rf(ctx, entityIDs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]roles.EntityMemberRole) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, []string) error); ok { + r2 = rf(ctx, entityIDs) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RetrieveHierarchy provides a mock function with given fields: ctx, id, hm +func (_m *Repository) RetrieveHierarchy(ctx context.Context, id string, hm groups.HierarchyPageMeta) (groups.HierarchyPage, error) { + ret := _m.Called(ctx, id, hm) + + if len(ret) == 0 { + panic("no return value specified for RetrieveHierarchy") + } + + var r0 groups.HierarchyPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, groups.HierarchyPageMeta) (groups.HierarchyPage, error)); ok { + return rf(ctx, id, hm) + } + if rf, ok := ret.Get(0).(func(context.Context, string, groups.HierarchyPageMeta) groups.HierarchyPage); ok { + r0 = rf(ctx, id, hm) + } else { + r0 = ret.Get(0).(groups.HierarchyPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, groups.HierarchyPageMeta) error); ok { + r1 = rf(ctx, id, hm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRole provides a mock function with given fields: ctx, roleID +func (_m *Repository) RetrieveRole(ctx context.Context, roleID string) (roles.Role, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (roles.Role, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) roles.Role); ok { + r0 = rf(ctx, roleID) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRoleByEntityIDAndName provides a mock function with given fields: ctx, entityID, roleName +func (_m *Repository) RetrieveRoleByEntityIDAndName(ctx context.Context, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRoleByEntityIDAndName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (roles.Role, error)); ok { + return rf(ctx, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) roles.Role); ok { + r0 = rf(ctx, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleAddActions(ctx context.Context, role roles.Role, actions []string) ([]string, error) { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleAddMembers(ctx context.Context, role roles.Role, members []string) ([]string, error) { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, members) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, roleID, actions +func (_m *Repository) RoleCheckActionsExists(ctx context.Context, roleID string, actions []string) (bool, error) { + ret := _m.Called(ctx, roleID, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, roleID, members +func (_m *Repository) RoleCheckMembersExists(ctx context.Context, roleID string, members []string) (bool, error) { + ret := _m.Called(ctx, roleID, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, members) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, roleID +func (_m *Repository) RoleListActions(ctx context.Context, roleID string) ([]string, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, roleID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, roleID, limit, offset +func (_m *Repository) RoleListMembers(ctx context.Context, roleID string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, roleID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, roleID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, roleID, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, roleID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleRemoveActions(ctx context.Context, role roles.Role, actions []string) error { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllActions(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllMembers(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleRemoveMembers(ctx context.Context, role roles.Role, members []string) error { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Save provides a mock function with given fields: ctx, g +func (_m *Repository) Save(ctx context.Context, g groups.Group) (groups.Group, error) { + ret := _m.Called(ctx, g) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, groups.Group) (groups.Group, error)); ok { + return rf(ctx, g) + } + if rf, ok := ret.Get(0).(func(context.Context, groups.Group) groups.Group); ok { + r0 = rf(ctx, g) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, groups.Group) error); ok { + r1 = rf(ctx, g) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnassignAllChildrenGroup provides a mock function with given fields: ctx, id +func (_m *Repository) UnassignAllChildrenGroup(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for UnassignAllChildrenGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UnassignParentGroup provides a mock function with given fields: ctx, parentGroupID, groupIDs +func (_m *Repository) UnassignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error { + ret := _m.Called(ctx, parentGroupID, groupIDs) + + if len(ret) == 0 { + panic("no return value specified for UnassignParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) error); ok { + r0 = rf(ctx, parentGroupID, groupIDs...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, g +func (_m *Repository) Update(ctx context.Context, g groups.Group) (groups.Group, error) { + ret := _m.Called(ctx, g) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, groups.Group) (groups.Group, error)); ok { + return rf(ctx, g) + } + if rf, ok := ret.Get(0).(func(context.Context, groups.Group) groups.Group); ok { + r0 = rf(ctx, g) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, groups.Group) error); ok { + r1 = rf(ctx, g) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRole provides a mock function with given fields: ctx, ro +func (_m *Repository) UpdateRole(ctx context.Context, ro roles.Role) (roles.Role, error) { + ret := _m.Called(ctx, ro) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) (roles.Role, error)); ok { + return rf(ctx, ro) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) roles.Role); ok { + r0 = rf(ctx, ro) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role) error); ok { + r1 = rf(ctx, ro) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRepository creates a new instance of Repository. 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 NewRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *Repository { + mock := &Repository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/groups/mocks/service.go b/groups/mocks/service.go new file mode 100644 index 0000000000..9f679ebffc --- /dev/null +++ b/groups/mocks/service.go @@ -0,0 +1,792 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + authn "github.com/absmach/magistrala/pkg/authn" + + groups "github.com/absmach/magistrala/groups" + + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +// AddChildrenGroups provides a mock function with given fields: ctx, session, id, childrenGroupIDs +func (_m *Service) AddChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + ret := _m.Called(ctx, session, id, childrenGroupIDs) + + if len(ret) == 0 { + panic("no return value specified for AddChildrenGroups") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, []string) error); ok { + r0 = rf(ctx, session, id, childrenGroupIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AddParentGroup provides a mock function with given fields: ctx, session, id, parentID +func (_m *Service) AddParentGroup(ctx context.Context, session authn.Session, id string, parentID string) error { + ret := _m.Called(ctx, session, id, parentID) + + if len(ret) == 0 { + panic("no return value specified for AddParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, id, parentID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AddRole provides a mock function with given fields: ctx, session, entityID, roleName, optionalActions, optionalMembers +func (_m *Service) AddRole(ctx context.Context, session authn.Session, entityID string, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName, optionalActions, optionalMembers) + + if len(ret) == 0 { + panic("no return value specified for AddRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateGroup provides a mock function with given fields: ctx, session, g +func (_m *Service) CreateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { + ret := _m.Called(ctx, session, g) + + if len(ret) == 0 { + panic("no return value specified for CreateGroup") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.Group) (groups.Group, error)); ok { + return rf(ctx, session, g) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.Group) groups.Group); ok { + r0 = rf(ctx, session, g) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, groups.Group) error); ok { + r1 = rf(ctx, session, g) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteGroup provides a mock function with given fields: ctx, session, id +func (_m *Service) DeleteGroup(ctx context.Context, session authn.Session, id string) error { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DisableGroup provides a mock function with given fields: ctx, session, id +func (_m *Service) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for DisableGroup") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (groups.Group, error)); ok { + return rf(ctx, session, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) groups.Group); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, session, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EnableGroup provides a mock function with given fields: ctx, session, id +func (_m *Service) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for EnableGroup") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (groups.Group, error)); ok { + return rf(ctx, session, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) groups.Group); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, session, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListAvailableActions provides a mock function with given fields: ctx, session +func (_m *Service) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + ret := _m.Called(ctx, session) + + if len(ret) == 0 { + panic("no return value specified for ListAvailableActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) ([]string, error)); ok { + return rf(ctx, session) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) []string); ok { + r0 = rf(ctx, session) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session) error); ok { + r1 = rf(ctx, session) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListChildrenGroups provides a mock function with given fields: ctx, session, id, pm +func (_m *Service) ListChildrenGroups(ctx context.Context, session authn.Session, id string, pm groups.PageMeta) (groups.Page, error) { + ret := _m.Called(ctx, session, id, pm) + + if len(ret) == 0 { + panic("no return value specified for ListChildrenGroups") + } + + var r0 groups.Page + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, groups.PageMeta) (groups.Page, error)); ok { + return rf(ctx, session, id, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, groups.PageMeta) groups.Page); ok { + r0 = rf(ctx, session, id, pm) + } else { + r0 = ret.Get(0).(groups.Page) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, groups.PageMeta) error); ok { + r1 = rf(ctx, session, id, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListGroups provides a mock function with given fields: ctx, session, pm +func (_m *Service) ListGroups(ctx context.Context, session authn.Session, pm groups.PageMeta) (groups.Page, error) { + ret := _m.Called(ctx, session, pm) + + if len(ret) == 0 { + panic("no return value specified for ListGroups") + } + + var r0 groups.Page + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.PageMeta) (groups.Page, error)); ok { + return rf(ctx, session, pm) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.PageMeta) groups.Page); ok { + r0 = rf(ctx, session, pm) + } else { + r0 = ret.Get(0).(groups.Page) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, groups.PageMeta) error); ok { + r1 = rf(ctx, session, pm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveAllChildrenGroups provides a mock function with given fields: ctx, session, id +func (_m *Service) RemoveAllChildrenGroups(ctx context.Context, session authn.Session, id string) error { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for RemoveAllChildrenGroups") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveChildrenGroups provides a mock function with given fields: ctx, session, id, childrenGroupIDs +func (_m *Service) RemoveChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + ret := _m.Called(ctx, session, id, childrenGroupIDs) + + if len(ret) == 0 { + panic("no return value specified for RemoveChildrenGroups") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, []string) error); ok { + r0 = rf(ctx, session, id, childrenGroupIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, session, memberID +func (_m *Service) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) error { + ret := _m.Called(ctx, session, memberID) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, memberID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveParentGroup provides a mock function with given fields: ctx, session, id +func (_m *Service) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for RemoveParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RemoveRole(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RemoveRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, session, entityID, limit, offset +func (_m *Service) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, session, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, session, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, session, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveGroupHierarchy provides a mock function with given fields: ctx, session, id, hm +func (_m *Service) RetrieveGroupHierarchy(ctx context.Context, session authn.Session, id string, hm groups.HierarchyPageMeta) (groups.HierarchyPage, error) { + ret := _m.Called(ctx, session, id, hm) + + if len(ret) == 0 { + panic("no return value specified for RetrieveGroupHierarchy") + } + + var r0 groups.HierarchyPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, groups.HierarchyPageMeta) (groups.HierarchyPage, error)); ok { + return rf(ctx, session, id, hm) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, groups.HierarchyPageMeta) groups.HierarchyPage); ok { + r0 = rf(ctx, session, id, hm) + } else { + r0 = ret.Get(0).(groups.HierarchyPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, groups.HierarchyPageMeta) error); ok { + r1 = rf(ctx, session, id, hm) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RetrieveRole(ctx context.Context, session authn.Session, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleAddActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleAddMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleListActions(ctx context.Context, session authn.Session, entityID string, roleName string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) []string); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, session, entityID, roleName, limit, offset +func (_m *Service) RoleListMembers(ctx context.Context, session authn.Session, entityID string, roleName string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, session, entityID, roleName, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, session, entityID, roleName, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleRemoveActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) error { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) error { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateGroup provides a mock function with given fields: ctx, session, g +func (_m *Service) UpdateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { + ret := _m.Called(ctx, session, g) + + if len(ret) == 0 { + panic("no return value specified for UpdateGroup") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.Group) (groups.Group, error)); ok { + return rf(ctx, session, g) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.Group) groups.Group); ok { + r0 = rf(ctx, session, g) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, groups.Group) error); ok { + r1 = rf(ctx, session, g) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRoleName provides a mock function with given fields: ctx, session, entityID, oldRoleName, newRoleName +func (_m *Service) UpdateRoleName(ctx context.Context, session authn.Session, entityID string, oldRoleName string, newRoleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, oldRoleName, newRoleName) + + if len(ret) == 0 { + panic("no return value specified for UpdateRoleName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, oldRoleName, newRoleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string) error); ok { + r1 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ViewGroup provides a mock function with given fields: ctx, session, id +func (_m *Service) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for ViewGroup") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (groups.Group, error)); ok { + return rf(ctx, session, id) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) groups.Group); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { + r1 = rf(ctx, session, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewService creates a new instance of Service. 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 NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/groups/page.go b/groups/page.go new file mode 100644 index 0000000000..6226244a18 --- /dev/null +++ b/groups/page.go @@ -0,0 +1,21 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package groups + +// PageMeta contains page metadata that helps navigation. +type PageMeta struct { + Total uint64 `json:"total"` + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + Level int `json:"level,omitempty"` + Path string `json:"path,omitempty"` + DomainID string `json:"domain_id,omitempty"` + Tag string `json:"tag,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + Status Status `json:"status,omitempty"` + Permission string + ListPerms bool +} diff --git a/internal/groups/postgres/doc.go b/groups/postgres/doc.go similarity index 100% rename from internal/groups/postgres/doc.go rename to groups/postgres/doc.go diff --git a/internal/groups/postgres/groups.go b/groups/postgres/groups.go similarity index 51% rename from internal/groups/postgres/groups.go rename to groups/postgres/groups.go index 15d9b3973d..b5ec6a4f0b 100644 --- a/internal/groups/postgres/groups.go +++ b/groups/postgres/groups.go @@ -11,32 +11,49 @@ import ( "strings" "time" + mggroups "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/groups" - mggroups "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/pkg/postgres" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" "github.com/jmoiron/sqlx" ) var _ mggroups.Repository = (*groupRepository)(nil) +const ( + rolesTableNamePrefix = "groups" + entityTableName = "groups" + entityIDColumnName = "id" +) + +var ( + errParentGroupID = errors.New("parent group id is empty") + errParentGroupPath = errors.New("parent group path is empty") + errParentSuffix = errors.New("parent group path doesn't have parent id suffix") +) + type groupRepository struct { db postgres.Database + rolesPostgres.Repository } // New instantiates a PostgreSQL implementation of group // repository. func New(db postgres.Database) mggroups.Repository { + roleRepo := rolesPostgres.NewRepository(db, rolesTableNamePrefix, entityTableName, entityIDColumnName) + return &groupRepository{ - db: db, + db: db, + Repository: roleRepo, } } func (repo groupRepository) Save(ctx context.Context, g mggroups.Group) (mggroups.Group, error) { - q := `INSERT INTO groups (name, description, id, domain_id, parent_id, metadata, created_at, status) - VALUES (:name, :description, :id, :domain_id, :parent_id, :metadata, :created_at, :status) - RETURNING id, name, description, domain_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, status;` + q, err := repo.getInsertQuery(ctx, g) + if err != nil { + return mggroups.Group{}, errors.Wrap(repoerr.ErrCreateEntity, err) + } dbg, err := toDBGroup(g) if err != nil { return mggroups.Group{}, err @@ -145,24 +162,18 @@ func (repo groupRepository) RetrieveByID(ctx context.Context, id string) (mggrou return toGroup(dbg) } -func (repo groupRepository) RetrieveAll(ctx context.Context, gm mggroups.Page) (mggroups.Page, error) { +func (repo groupRepository) RetrieveAll(ctx context.Context, pm mggroups.PageMeta) (mggroups.Page, error) { var q string - query := buildQuery(gm) + query := buildQuery(pm) - if gm.ParentID != "" { - q = buildHierachy(gm) - } - if gm.ParentID == "" { - q = `SELECT DISTINCT g.id, g.domain_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, - g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g` - } - q = fmt.Sprintf("%s %s ORDER BY g.created_at LIMIT :limit OFFSET :offset;", q, query) + q = fmt.Sprintf(`SELECT DISTINCT g.id, g.domain_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, + g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g %s ORDER BY g.created_at LIMIT :limit OFFSET :offset;`, query) - dbPage, err := toDBGroupPage(gm) + dbPageMeta, err := toDBGroupPageMeta(pm) if err != nil { return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) + rows, err := repo.db.NamedQueryContext(ctx, q, dbPageMeta) if err != nil { return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } @@ -173,44 +184,39 @@ func (repo groupRepository) RetrieveAll(ctx context.Context, gm mggroups.Page) ( return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - cq := "SELECT COUNT(*) FROM groups g" - if query != "" { - cq = fmt.Sprintf(" %s %s", cq, query) - } + cq := fmt.Sprintf(` SELECT COUNT(*) AS total_count + FROM ( + SELECT DISTINCT g.id, g.domain_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, + g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g %s + ) AS subquery; + `, query) - total, err := postgres.Total(ctx, repo.db, cq, dbPage) + total, err := postgres.Total(ctx, repo.db, cq, dbPageMeta) if err != nil { return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - page := gm - page.Groups = items + page := mggroups.Page{PageMeta: pm} page.Total = total - + page.Groups = items return page, nil } -func (repo groupRepository) RetrieveByIDs(ctx context.Context, gm mggroups.Page, ids ...string) (mggroups.Page, error) { +func (repo groupRepository) RetrieveByIDs(ctx context.Context, pm mggroups.PageMeta, ids ...string) (mggroups.Page, error) { var q string - if (len(ids) == 0) && (gm.PageMeta.DomainID == "") { - return mggroups.Page{PageMeta: mggroups.PageMeta{Offset: gm.Offset, Limit: gm.Limit}}, nil + if (len(ids) == 0) && (pm.DomainID == "") { + return mggroups.Page{PageMeta: mggroups.PageMeta{Offset: pm.Offset, Limit: pm.Limit}}, nil } - query := buildQuery(gm, ids...) + query := buildQuery(pm, ids...) - if gm.ParentID != "" { - q = buildHierachy(gm) - } - if gm.ParentID == "" { - q = `SELECT DISTINCT g.id, g.domain_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, - g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g` - } - q = fmt.Sprintf("%s %s ORDER BY g.created_at LIMIT :limit OFFSET :offset;", q, query) + q = fmt.Sprintf(`SELECT DISTINCT g.id, g.domain_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, + g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g %s ORDER BY g.created_at LIMIT :limit OFFSET :offset;`, query) - dbPage, err := toDBGroupPage(gm) + dbPageMeta, err := toDBGroupPageMeta(pm) if err != nil { return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) + rows, err := repo.db.NamedQueryContext(ctx, q, dbPageMeta) if err != nil { return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } @@ -221,69 +227,274 @@ func (repo groupRepository) RetrieveByIDs(ctx context.Context, gm mggroups.Page, return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - cq := "SELECT COUNT(*) FROM groups g" - if query != "" { - cq = fmt.Sprintf(" %s %s", cq, query) - } + cq := fmt.Sprintf(` SELECT COUNT(*) AS total_count + FROM ( + SELECT DISTINCT g.id, g.domain_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, + g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g %s + ) AS subquery; + `, query) - total, err := postgres.Total(ctx, repo.db, cq, dbPage) + total, err := postgres.Total(ctx, repo.db, cq, dbPageMeta) if err != nil { return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - page := gm - page.Groups = items + page := mggroups.Page{PageMeta: pm} page.Total = total - + page.Groups = items return page, nil } -func (repo groupRepository) AssignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error { +func (repo groupRepository) RetrieveHierarchy(ctx context.Context, id string, hm mggroups.HierarchyPageMeta) (mggroups.HierarchyPage, error) { + query := "" + switch { + // ancestors + case hm.Direction >= 0: + query = ` + SELECT + g.id, + COALESCE(g.parent_id, '') AS parent_id, + g.domain_id, + g.name, + g.description, + g.metadata, + g.created_at, + g.updated_at, + g.updated_by, + g.status, + g.path, + nlevel(g.path) AS level + FROM + groups g + WHERE + g.path @> (SELECT path FROM groups WHERE id = :id LIMIT 1); + ` + // descendants + case hm.Direction < 0: + fallthrough + default: + query = ` + SELECT + g.id, + COALESCE(g.parent_id, '') AS parent_id, + g.domain_id, + g.name, + g.description, + g.metadata, + g.created_at, + g.updated_at, + g.updated_by, + g.status, + g.path, + nlevel(g.path) AS level + FROM + groups g + WHERE + g.path <@ (SELECT path FROM groups WHERE id = :id LIMIT 1); + ` + } + parameters := map[string]interface{}{ + "id": id, + "level": hm.Level, + } + rows, err := repo.db.NamedQueryContext(ctx, query, parameters) + if err != nil { + return mggroups.HierarchyPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + } + defer rows.Close() + + items, err := repo.processRows(rows) + if err != nil { + return mggroups.HierarchyPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + } + + return mggroups.HierarchyPage{HierarchyPageMeta: hm, Groups: items}, nil +} + +func (repo groupRepository) AssignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) (err error) { if len(groupIDs) == 0 { return nil } - var updateColumns []string - for _, groupID := range groupIDs { - updateColumns = append(updateColumns, fmt.Sprintf("('%s', '%s') ", groupID, parentGroupID)) + + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) } - uc := strings.Join(updateColumns, ",") - query := fmt.Sprintf(` - UPDATE groups AS g SET - parent_id = u.parent_group_id - FROM (VALUES - %s - ) AS u(id, parent_group_id) - WHERE g.id = u.id; - `, uc) - - row, err := repo.db.QueryContext(ctx, query) + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(err, errRollback) + } + } + }() + + pq := `SELECT id, path FROM groups WHERE id = $1 LIMIT 1;` + rows, err := tx.Queryx(pq, parentGroupID) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + defer rows.Close() + + pGroups, err := repo.processRows(rows) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + if len(pGroups) == 0 { + return repoerr.ErrUpdateEntity + } + pGroup := pGroups[0] + + if pGroup.ID == "" { + return errors.Wrap(repoerr.ErrViewEntity, errParentGroupID) + } + if pGroup.Path == "" { + return errors.Wrap(repoerr.ErrViewEntity, errParentGroupPath) + } + if !strings.HasSuffix(pGroup.Path, pGroup.ID) { + return errors.Wrap(repoerr.ErrViewEntity, errParentSuffix) + } + sPaths := strings.Split(pGroup.Path, ".") // 021b9f24-5337-469b-abfa-586f5813dd41.bd4a1fea-6303-4dca-9628-301cd1165a8c.c7e8f389-11e9-4849-a474-e186012ddf38 + for _, sPath := range sPaths { + for _, cgid := range groupIDs { + if sPath == cgid { + return errors.Wrap(repoerr.ErrUpdateEntity, fmt.Errorf("cyclic parent, group %s is parent of requested group %s", cgid, parentGroupID)) + } + } + } + + query := ` UPDATE groups + SET parent_id = :parent_id + WHERE id = ANY(:children_group_ids) + RETURNING id, path;` + + params := map[string]interface{}{ + "parent_id": pGroup.ID, + "children_group_ids": groupIDs, + } + + crows, err := tx.NamedQuery(query, params) if err != nil { return postgres.HandleError(repoerr.ErrUpdateEntity, err) } - defer row.Close() + defer crows.Close() + cgroups, err := repo.processRows(crows) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + + childrenPaths := []string{} + for _, cg := range cgroups { + spath := strings.Split(cg.Path, ".") + if len(spath) > 0 { + childrenPaths = append(childrenPaths, cg.Path) + } + } + + query = `UPDATE groups + SET path = text2ltree(COALESCE($1, '') || '.' || ltree2text(path)) + WHERE path <@ ANY($2::ltree[]);` + + if _, err := tx.Exec(query, pGroup.Path, childrenPaths); err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } return nil } -func (repo groupRepository) UnassignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error { +// ToDo: Query need to change to ANY +// ToDo: If parent is changed, then path of all children need to be updated https://patshaughnessy.net/2017/12/14/manipulating-trees-using-sql-and-the-postgres-ltree-extension +func (repo groupRepository) UnassignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) (err error) { if len(groupIDs) == 0 { return nil } - var updateColumns []string - for _, groupID := range groupIDs { - updateColumns = append(updateColumns, fmt.Sprintf("('%s', '%s') ", groupID, parentGroupID)) + + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(err, errRollback) + } + } + }() + pq := `SELECT id, path FROM groups WHERE id = $1 LIMIT 1;` + rows, err := tx.Queryx(pq, parentGroupID) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + defer rows.Close() + + pGroups, err := repo.processRows(rows) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + if len(pGroups) == 0 { + return repoerr.ErrUpdateEntity + } + pGroup := pGroups[0] + + if pGroup.ID == "" { + return errors.Wrap(repoerr.ErrViewEntity, errParentGroupID) + } + if pGroup.Path == "" { + return errors.Wrap(repoerr.ErrViewEntity, errParentGroupPath) } - uc := strings.Join(updateColumns, ",") - query := fmt.Sprintf(` + + query := `UPDATE groups + SET parent_id = NULL + WHERE id = ANY(:children_group_ids) AND parent_id = :parent_id + RETURNING id, path;` + + parameters := map[string]interface{}{ + "parent_id": pGroup.ID, + "children_group_ids": groupIDs, + } + crows, err := tx.NamedQuery(query, parameters) + if err != nil { + return postgres.HandleError(repoerr.ErrUpdateEntity, err) + } + defer crows.Close() + cgroups, err := repo.processRows(crows) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + + childrenPaths := []string{} + for _, cg := range cgroups { + spath := strings.Split(cg.Path, ".") + if len(spath) > 0 { + childrenPaths = append(childrenPaths, cg.Path) + } + } + + query = `UPDATE groups + SET path = text2ltree(replace(ltree2text(path), $1 || '.', '')) + WHERE path <@ ANY($2::ltree[]);` + + if _, err := tx.Exec(query, pGroup.Path, childrenPaths); err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + return nil +} + +func (repo groupRepository) UnassignAllChildrenGroup(ctx context.Context, id string) error { + + query := ` UPDATE groups AS g SET parent_id = NULL - FROM (VALUES - %s - ) AS u(id, parent_group_id) - WHERE g.id = u.id ; - `, uc) + WHERE g.parent = :parent_id ; + ` - row, err := repo.db.QueryContext(ctx, query) + row, err := repo.db.NamedQueryContext(ctx, query, dbGroup{ParentID: &id}) if err != nil { return postgres.HandleError(repoerr.ErrUpdateEntity, err) } @@ -305,27 +516,7 @@ func (repo groupRepository) Delete(ctx context.Context, groupID string) error { return nil } -func buildHierachy(gm mggroups.Page) string { - query := "" - switch { - case gm.Direction >= 0: // ancestors - query = `WITH RECURSIVE groups_cte as ( - SELECT id, COALESCE(parent_id, '') AS parent_id, domain_id, name, description, metadata, created_at, updated_at, updated_by, status, 0 as level from groups WHERE id = :parent_id - UNION SELECT x.id, COALESCE(x.parent_id, '') AS parent_id, x.domain_id, x.name, x.description, x.metadata, x.created_at, x.updated_at, x.updated_by, x.status, level - 1 from groups x - INNER JOIN groups_cte a ON a.parent_id = x.id - ) SELECT * FROM groups_cte g` - - case gm.Direction < 0: // descendants - query = `WITH RECURSIVE groups_cte as ( - SELECT id, COALESCE(parent_id, '') AS parent_id, domain_id, name, description, metadata, created_at, updated_at, updated_by, status, 0 as level, CONCAT('', '', id) as path from groups WHERE id = :parent_id - UNION SELECT x.id, COALESCE(x.parent_id, '') AS parent_id, x.domain_id, x.name, x.description, x.metadata, x.created_at, x.updated_at, x.updated_by, x.status, level + 1, CONCAT(path, '.', x.id) as path from groups x - INNER JOIN groups_cte d ON d.id = x.parent_id - ) SELECT * FROM groups_cte g` - } - return query -} - -func buildQuery(gm mggroups.Page, ids ...string) string { +func buildQuery(gm mggroups.PageMeta, ids ...string) string { queries := []string{} if len(ids) > 0 { @@ -334,7 +525,7 @@ func buildQuery(gm mggroups.Page, ids ...string) string { if gm.Name != "" { queries = append(queries, "g.name ILIKE '%' || :name || '%'") } - if gm.PageMeta.ID != "" { + if gm.ID != "" { queries = append(queries, "g.id ILIKE '%' || :id || '%'") } if gm.Status != mggroups.AllStatus { @@ -405,7 +596,7 @@ func toDBGroup(g mggroups.Group) (dbGroup, error) { } func toGroup(g dbGroup) (mggroups.Group, error) { - var metadata groups.Metadata + var metadata mggroups.Metadata if g.Metadata != nil { if err := json.Unmarshal(g.Metadata, &metadata); err != nil { return mggroups.Group{}, errors.Wrap(repoerr.ErrMalformedEntity, err) @@ -440,36 +631,29 @@ func toGroup(g dbGroup) (mggroups.Group, error) { }, nil } -func toDBGroupPage(pm mggroups.Page) (dbGroupPage, error) { - level := mggroups.MaxLevel - if pm.Level < mggroups.MaxLevel { - level = pm.Level - } +func toDBGroupPageMeta(pm mggroups.PageMeta) (dbGroupPageMeta, error) { data := []byte("{}") if len(pm.Metadata) > 0 { b, err := json.Marshal(pm.Metadata) if err != nil { - return dbGroupPage{}, errors.Wrap(errors.ErrMalformedEntity, err) + return dbGroupPageMeta{}, errors.Wrap(errors.ErrMalformedEntity, err) } data = b } - return dbGroupPage{ + return dbGroupPageMeta{ ID: pm.ID, Name: pm.Name, Metadata: data, - Path: pm.Path, - Level: level, Total: pm.Total, Offset: pm.Offset, Limit: pm.Limit, - ParentID: pm.ParentID, DomainID: pm.DomainID, Status: pm.Status, }, nil } -type dbGroupPage struct { - ClientID string `db:"client_id"` +// ToDo: check and remove field "Level" after new auth stabilize +type dbGroupPageMeta struct { ID string `db:"id"` Name string `db:"name"` ParentID string `db:"parent_id"` @@ -500,3 +684,24 @@ func (repo groupRepository) processRows(rows *sqlx.Rows) ([]mggroups.Group, erro } return items, nil } + +func (repo groupRepository) getInsertQuery(c context.Context, g mggroups.Group) (string, error) { + switch { + case g.Parent != "": + parent, err := repo.RetrieveByID(c, g.Parent) + if err != nil { + return "", err + } + path := parent.Path + "." + g.ID + if len(strings.Split(path, ".")) > mggroups.MaxPathLength { + return "", fmt.Errorf("reached max nested depth") + } + return fmt.Sprintf(`INSERT INTO groups (name, description, id, domain_id, parent_id, metadata, created_at, status, path) + VALUES (:name, :description, :id, :domain_id, :parent_id, :metadata, :created_at, :status, '%s') + RETURNING id, name, description, domain_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, status, path, nlevel(path) as level;`, path), nil + default: + return `INSERT INTO groups (name, description, id, domain_id, metadata, created_at, status, path) + VALUES (:name, :description, :id, :domain_id, :metadata, :created_at, :status, :id) + RETURNING id, name, description, domain_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, status, path, nlevel(path) as level;`, nil + } +} diff --git a/internal/groups/postgres/groups_test.go b/groups/postgres/groups_test.go similarity index 82% rename from internal/groups/postgres/groups_test.go rename to groups/postgres/groups_test.go index 7bbbee20d4..2c553111d4 100644 --- a/internal/groups/postgres/groups_test.go +++ b/groups/postgres/groups_test.go @@ -11,11 +11,11 @@ import ( "time" "github.com/0x6flab/namegenerator" - "github.com/absmach/magistrala/internal/groups/postgres" + "github.com/absmach/magistrala/groups" + "github.com/absmach/magistrala/groups/postgres" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" - mggroups "github.com/absmach/magistrala/pkg/groups" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,14 +23,14 @@ import ( var ( namegen = namegenerator.NewGenerator() invalidID = strings.Repeat("a", 37) - validGroup = mggroups.Group{ + validGroup = groups.Group{ ID: testsutil.GenerateUUID(&testing.T{}), Domain: testsutil.GenerateUUID(&testing.T{}), Name: namegen.Generate(), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, } ) @@ -44,7 +44,7 @@ func TestSave(t *testing.T) { cases := []struct { desc string - group mggroups.Group + group groups.Group err error }{ { @@ -59,72 +59,72 @@ func TestSave(t *testing.T) { }, { desc: "add group with invalid ID", - group: mggroups.Group{ + group: groups.Group{ ID: invalidID, Domain: testsutil.GenerateUUID(t), Name: namegen.Generate(), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, { desc: "add group with invalid domain", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Domain: invalidID, Name: namegen.Generate(), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, { desc: "add group with invalid parent", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Parent: invalidID, Name: namegen.Generate(), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, { desc: "add group with invalid name", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Name: strings.Repeat("a", 1025), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, { desc: "add group with invalid description", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Name: namegen.Generate(), Description: strings.Repeat("a", 1025), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, { desc: "add group with invalid metadata", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Name: namegen.Generate(), @@ -133,31 +133,31 @@ func TestSave(t *testing.T) { "key": make(chan int), }, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, { desc: "add group with empty domain", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Name: namegen.Generate(), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, { desc: "add group with empty name", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, }, err: repoerr.ErrMalformedEntity, }, @@ -187,12 +187,12 @@ func TestUpdate(t *testing.T) { cases := []struct { desc string - group mggroups.Group + group groups.Group err error }{ { desc: "update group successfully", - group: mggroups.Group{ + group: groups.Group{ ID: group.ID, Name: namegen.Generate(), Description: strings.Repeat("a", 64), @@ -204,7 +204,7 @@ func TestUpdate(t *testing.T) { }, { desc: "update group name", - group: mggroups.Group{ + group: groups.Group{ ID: group.ID, Name: namegen.Generate(), UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), @@ -214,7 +214,7 @@ func TestUpdate(t *testing.T) { }, { desc: "update group description", - group: mggroups.Group{ + group: groups.Group{ ID: group.ID, Description: strings.Repeat("a", 64), UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), @@ -224,7 +224,7 @@ func TestUpdate(t *testing.T) { }, { desc: "update group metadata", - group: mggroups.Group{ + group: groups.Group{ ID: group.ID, Metadata: map[string]interface{}{"key": "value"}, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), @@ -234,7 +234,7 @@ func TestUpdate(t *testing.T) { }, { desc: "update group with invalid ID", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), Name: namegen.Generate(), Description: strings.Repeat("a", 64), @@ -246,7 +246,7 @@ func TestUpdate(t *testing.T) { }, { desc: "update group with empty ID", - group: mggroups.Group{ + group: groups.Group{ Name: namegen.Generate(), Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"key": "value"}, @@ -283,14 +283,14 @@ func TestChangeStatus(t *testing.T) { cases := []struct { desc string - group mggroups.Group + group groups.Group err error }{ { desc: "change status group successfully", - group: mggroups.Group{ + group: groups.Group{ ID: group.ID, - Status: mggroups.DisabledStatus, + Status: groups.DisabledStatus, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), UpdatedBy: testsutil.GenerateUUID(t), }, @@ -298,9 +298,9 @@ func TestChangeStatus(t *testing.T) { }, { desc: "change status group with invalid ID", - group: mggroups.Group{ + group: groups.Group{ ID: testsutil.GenerateUUID(t), - Status: mggroups.DisabledStatus, + Status: groups.DisabledStatus, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), UpdatedBy: testsutil.GenerateUUID(t), }, @@ -308,8 +308,8 @@ func TestChangeStatus(t *testing.T) { }, { desc: "change status group with empty ID", - group: mggroups.Group{ - Status: mggroups.DisabledStatus, + group: groups.Group{ + Status: groups.DisabledStatus, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), UpdatedBy: testsutil.GenerateUUID(t), }, @@ -344,7 +344,7 @@ func TestRetrieveByID(t *testing.T) { cases := []struct { desc string id string - group mggroups.Group + group groups.Group err error }{ { @@ -356,13 +356,13 @@ func TestRetrieveByID(t *testing.T) { { desc: "retrieve group by id with invalid ID", id: invalidID, - group: mggroups.Group{}, + group: groups.Group{}, err: repoerr.ErrNotFound, }, { desc: "retrieve group by id with empty ID", id: "", - group: mggroups.Group{}, + group: groups.Group{}, err: repoerr.ErrNotFound, }, } @@ -387,11 +387,11 @@ func TestRetrieveAll(t *testing.T) { repo := postgres.New(database) num := 200 - var items []mggroups.Group + var items []groups.Group parentID := "" for i := 0; i < num; i++ { name := namegen.Generate() - group := mggroups.Group{ + group := groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Parent: parentID, @@ -399,7 +399,7 @@ func TestRetrieveAll(t *testing.T) { Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"name": name}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, } _, err := repo.Save(context.Background(), group) require.Nil(t, err, fmt.Sprintf("create invitation unexpected error: %s", err)) @@ -409,20 +409,20 @@ func TestRetrieveAll(t *testing.T) { cases := []struct { desc string - page mggroups.Page - response mggroups.Page + page groups.Page + response groups.Page err error }{ { desc: "retrieve groups successfully", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 0, Limit: 10, @@ -433,14 +433,14 @@ func TestRetrieveAll(t *testing.T) { }, { desc: "retrieve groups with offset", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 10, Limit: 10, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 10, Limit: 10, @@ -451,14 +451,14 @@ func TestRetrieveAll(t *testing.T) { }, { desc: "retrieve groups with limit", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 50, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 0, Limit: 50, @@ -469,14 +469,14 @@ func TestRetrieveAll(t *testing.T) { }, { desc: "retrieve groups with offset and limit", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 50, Limit: 50, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 50, Limit: 50, @@ -487,32 +487,32 @@ func TestRetrieveAll(t *testing.T) { }, { desc: "retrieve groups with offset out of range", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 1000, Limit: 50, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 1000, Limit: 50, }, - Groups: []mggroups.Group(nil), + Groups: []groups.Group(nil), }, err: nil, }, { desc: "retrieve groups with offset and limit out of range", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 170, Limit: 50, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 170, Limit: 50, @@ -523,14 +523,14 @@ func TestRetrieveAll(t *testing.T) { }, { desc: "retrieve groups with limit out of range", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 1000, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 0, Limit: 1000, @@ -541,78 +541,78 @@ func TestRetrieveAll(t *testing.T) { }, { desc: "retrieve groups with empty page", - page: mggroups.Page{}, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{}, + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 0, Limit: 0, }, - Groups: []mggroups.Group(nil), + Groups: []groups.Group(nil), }, err: nil, }, { desc: "retrieve groups with name", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, Name: items[0].Name, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 1, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group{items[0]}, + Groups: []groups.Group{items[0]}, }, err: nil, }, { desc: "retrieve groups with domain", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, DomainID: items[0].Domain, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 1, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group{items[0]}, + Groups: []groups.Group{items[0]}, }, err: nil, }, { desc: "retrieve groups with metadata", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, Metadata: items[0].Metadata, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 1, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group{items[0]}, + Groups: []groups.Group{items[0]}, }, err: nil, }, { desc: "retrieve groups with invalid metadata", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, Metadata: map[string]interface{}{ @@ -620,28 +620,26 @@ func TestRetrieveAll(t *testing.T) { }, }, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 0, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group(nil), + Groups: []groups.Group(nil), }, err: errors.ErrMalformedEntity, }, { desc: "retrieve parent groups", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: uint64(num), }, - ParentID: items[5].ID, - Direction: 1, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 0, Limit: uint64(num), @@ -652,16 +650,14 @@ func TestRetrieveAll(t *testing.T) { }, { desc: "retrieve children groups", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: uint64(num), }, - ParentID: items[150].ID, - Direction: -1, }, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: uint64(num), Offset: 0, Limit: uint64(num), @@ -673,7 +669,7 @@ func TestRetrieveAll(t *testing.T) { } for _, tc := range cases { - switch groups, err := repo.RetrieveAll(context.Background(), tc.page); { + switch groups, err := repo.RetrieveAll(context.Background(), tc.page.PageMeta); { case err == nil: assert.Nil(t, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) assert.Equal(t, tc.response.Total, groups.Total, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.response.Total, groups.Total)) @@ -699,11 +695,11 @@ func TestRetrieveByIDs(t *testing.T) { repo := postgres.New(database) num := 200 - var items []mggroups.Group + var items []groups.Group parentID := "" for i := 0; i < num; i++ { name := namegen.Generate() - group := mggroups.Group{ + group := groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Parent: parentID, @@ -711,7 +707,7 @@ func TestRetrieveByIDs(t *testing.T) { Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"name": name}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, } _, err := repo.Save(context.Background(), group) require.Nil(t, err, fmt.Sprintf("create invitation unexpected error: %s", err)) @@ -721,22 +717,22 @@ func TestRetrieveByIDs(t *testing.T) { cases := []struct { desc string - page mggroups.Page + page groups.Page ids []string - response mggroups.Page + response groups.Page err error }{ { desc: "retrieve groups successfully", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, }, }, ids: getIDs(items[0:3]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 3, Offset: 0, Limit: 10, @@ -747,53 +743,53 @@ func TestRetrieveByIDs(t *testing.T) { }, { desc: "retrieve groups with empty ids", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, }, }, ids: []string{}, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, }, - Groups: []mggroups.Group(nil), + Groups: []groups.Group(nil), }, err: nil, }, { desc: "retrieve groups with empty ids but with domain", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, DomainID: items[0].Domain, }, }, ids: []string{}, - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 1, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group{items[0]}, + Groups: []groups.Group{items[0]}, }, err: nil, }, { desc: "retrieve groups with offset", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 10, Limit: 10, }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 20, Offset: 10, Limit: 10, @@ -804,34 +800,34 @@ func TestRetrieveByIDs(t *testing.T) { }, { desc: "retrieve groups with offset out of range", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 1000, Limit: 50, }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 20, Offset: 1000, Limit: 50, }, - Groups: []mggroups.Group(nil), + Groups: []groups.Group(nil), }, err: nil, }, { desc: "retrieve groups with offset and limit out of range", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 15, Limit: 10, }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 20, Offset: 15, Limit: 10, @@ -842,15 +838,15 @@ func TestRetrieveByIDs(t *testing.T) { }, { desc: "retrieve groups with limit out of range", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 1000, }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 20, Offset: 0, Limit: 1000, @@ -861,68 +857,68 @@ func TestRetrieveByIDs(t *testing.T) { }, { desc: "retrieve groups with name", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, Name: items[0].Name, }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 1, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group{items[0]}, + Groups: []groups.Group{items[0]}, }, err: nil, }, { desc: "retrieve groups with domain", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, DomainID: items[0].Domain, }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 1, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group{items[0]}, + Groups: []groups.Group{items[0]}, }, err: nil, }, { desc: "retrieve groups with metadata", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, Metadata: items[0].Metadata, }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 1, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group{items[0]}, + Groups: []groups.Group{items[0]}, }, err: nil, }, { desc: "retrieve groups with invalid metadata", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: 10, Metadata: map[string]interface{}{ @@ -931,29 +927,27 @@ func TestRetrieveByIDs(t *testing.T) { }, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 0, Offset: 0, Limit: 10, }, - Groups: []mggroups.Group(nil), + Groups: []groups.Group(nil), }, err: errors.ErrMalformedEntity, }, { desc: "retrieve parent groups", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: uint64(num), }, - ParentID: items[5].ID, - Direction: 1, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 20, Offset: 0, Limit: uint64(num), @@ -964,17 +958,15 @@ func TestRetrieveByIDs(t *testing.T) { }, { desc: "retrieve children groups", - page: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + page: groups.Page{ + PageMeta: groups.PageMeta{ Offset: 0, Limit: uint64(num), }, - ParentID: items[15].ID, - Direction: -1, }, ids: getIDs(items[0:20]), - response: mggroups.Page{ - PageMeta: mggroups.PageMeta{ + response: groups.Page{ + PageMeta: groups.PageMeta{ Total: 20, Offset: 0, Limit: uint64(num), @@ -986,7 +978,7 @@ func TestRetrieveByIDs(t *testing.T) { } for _, tc := range cases { - switch groups, err := repo.RetrieveByIDs(context.Background(), tc.page, tc.ids...); { + switch groups, err := repo.RetrieveByIDs(context.Background(), tc.page.PageMeta, tc.ids...); { case err == nil: assert.Nil(t, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) assert.Equal(t, tc.response.Total, groups.Total, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.response.Total, groups.Total)) @@ -1056,11 +1048,11 @@ func TestAssignParentGroup(t *testing.T) { num := 10 - var items []mggroups.Group + var items []groups.Group parentID := "" for i := 0; i < num; i++ { name := namegen.Generate() - group := mggroups.Group{ + group := groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Parent: parentID, @@ -1068,7 +1060,7 @@ func TestAssignParentGroup(t *testing.T) { Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"name": name}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, } _, err := repo.Save(context.Background(), group) require.Nil(t, err, fmt.Sprintf("create invitation unexpected error: %s", err)) @@ -1134,11 +1126,11 @@ func TestUnassignParentGroup(t *testing.T) { num := 10 - var items []mggroups.Group + var items []groups.Group parentID := "" for i := 0; i < num; i++ { name := namegen.Generate() - group := mggroups.Group{ + group := groups.Group{ ID: testsutil.GenerateUUID(t), Domain: testsutil.GenerateUUID(t), Parent: parentID, @@ -1146,7 +1138,7 @@ func TestUnassignParentGroup(t *testing.T) { Description: strings.Repeat("a", 64), Metadata: map[string]interface{}{"name": name}, CreatedAt: time.Now().UTC().Truncate(time.Microsecond), - Status: mggroups.EnabledStatus, + Status: groups.EnabledStatus, } _, err := repo.Save(context.Background(), group) require.Nil(t, err, fmt.Sprintf("create invitation unexpected error: %s", err)) @@ -1202,7 +1194,7 @@ func TestUnassignParentGroup(t *testing.T) { } } -func getIDs(groups []mggroups.Group) []string { +func getIDs(groups []groups.Group) []string { var ids []string for _, group := range groups { ids = append(ids, group.ID) diff --git a/groups/postgres/init.go b/groups/postgres/init.go new file mode 100644 index 0000000000..f8793099a0 --- /dev/null +++ b/groups/postgres/init.go @@ -0,0 +1,63 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" + _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access + migrate "github.com/rubenv/sql-migrate" +) + +func Migration() (*migrate.MemoryMigrationSource, error) { + rolesMigration, err := rolesPostgres.Migration(rolesTableNamePrefix, entityTableName, entityIDColumnName) + if err != nil { + return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err) + } + + groupsMigration := &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: "groups_01", + Up: []string{ + `CREATE TABLE IF NOT EXISTS groups ( + id VARCHAR(36) PRIMARY KEY, + parent_id VARCHAR(36), + domain_id VARCHAR(36) NOT NULL, + name VARCHAR(1024) NOT NULL, + description VARCHAR(1024), + metadata JSONB, + created_at TIMESTAMP, + updated_at TIMESTAMP, + updated_by VARCHAR(254), + status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), + UNIQUE (domain_id, name), + FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE SET NULL, + CHECK (id != parent_id) + )`, + }, + Down: []string{ + `DROP TABLE IF EXISTS groups`, + }, + }, + { + Id: "groups_02", + Up: []string{ + `CREATE EXTENSION IF NOT EXISTS LTREE`, + `ALTER TABLE groups ADD COLUMN path LTREE`, + `CREATE INDEX path_gist_idx ON groups USING GIST (path);`, + }, + Down: []string{ + `DROP TABLE IF EXISTS groups`, + `DROP EXTENSION IF EXISTS LTREE`, + }, + }, + }, + } + + groupsMigration.Migrations = append(groupsMigration.Migrations, rolesMigration.Migrations...) + + return groupsMigration, nil +} diff --git a/internal/groups/postgres/setup_test.go b/groups/postgres/setup_test.go similarity index 90% rename from internal/groups/postgres/setup_test.go rename to groups/postgres/setup_test.go index a809a2b488..716255df8f 100644 --- a/internal/groups/postgres/setup_test.go +++ b/groups/postgres/setup_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - gpostgres "github.com/absmach/magistrala/internal/groups/postgres" + gpostgres "github.com/absmach/magistrala/groups/postgres" "github.com/absmach/magistrala/pkg/postgres" pgclient "github.com/absmach/magistrala/pkg/postgres" "github.com/jmoiron/sqlx" @@ -76,7 +76,12 @@ func TestMain(m *testing.M) { SSLRootCert: "", } - if db, err = pgclient.Setup(dbConfig, *gpostgres.Migration()); err != nil { + mig, err := gpostgres.Migration() + if err != nil { + log.Fatalf("Could not get groups migration : %s", err) + + } + if db, err = pgclient.Setup(dbConfig, *mig); err != nil { log.Fatalf("Could not setup test DB connection: %s", err) } diff --git a/groups/private/mocks/service.go b/groups/private/mocks/service.go new file mode 100644 index 0000000000..5d8a440ba4 --- /dev/null +++ b/groups/private/mocks/service.go @@ -0,0 +1,59 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + groups "github.com/absmach/magistrala/groups" + mock "github.com/stretchr/testify/mock" +) + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +// RetrieveById provides a mock function with given fields: ctx, id +func (_m *Service) RetrieveById(ctx context.Context, id string) (groups.Group, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for RetrieveById") + } + + var r0 groups.Group + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (groups.Group, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) groups.Group); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(groups.Group) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewService creates a new instance of Service. 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 NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/groups/private/service.go b/groups/private/service.go new file mode 100644 index 0000000000..65ecafbd61 --- /dev/null +++ b/groups/private/service.go @@ -0,0 +1,26 @@ +package private + +import ( + "context" + + "github.com/absmach/magistrala/groups" +) + +//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines" +type Service interface { + RetrieveById(ctx context.Context, id string) (groups.Group, error) +} + +var _ Service = (*service)(nil) + +func New(repo groups.Repository) Service { + return service{repo} +} + +type service struct { + repo groups.Repository +} + +func (svc service) RetrieveById(ctx context.Context, ids string) (groups.Group, error) { + return svc.repo.RetrieveByID(ctx, ids) +} diff --git a/groups/roleactions.go b/groups/roleactions.go new file mode 100644 index 0000000000..9812afc066 --- /dev/null +++ b/groups/roleactions.go @@ -0,0 +1,171 @@ +package groups + +import "github.com/absmach/magistrala/pkg/roles" + +const ( + Update = "update" + Read = "read" + Membership = "membership" + Delete = "delete" + SetChild = "set_child" + SetParent = "set_parent" + + ManageRole = "manage_role" + AddRoleUsers = "add_role_users" + RemoveRoleUsers = "remove_role_users" + ViewRoleUsers = "view_role_users" + + ThingCreate = "thing_create" + ChannelCreate = "channel_create" + SubgroupCreate = "subgroup_create" + SubgroupThingCreate = "subgroup_thing_create" + SubgroupChannelCreate = "subgroup_channel_create" + + ThingUpdate = "thing_update" + ThingRead = "thing_read" + ThingDelete = "thing_delete" + ThingSetParentGroup = "thing_set_parent_group" + ThingConnectToChannel = "thing_connect_to_channel" + + ThingManageRole = "thing_manage_role" + ThingAddRoleUsers = "thing_add_role_users" + ThingRemoveRoleUsers = "thing_remove_role_users" + ThingViewRoleUsers = "thing_view_role_users" + + ChannelUpdate = "channel_update" + ChannelRead = "channel_read" + ChannelDelete = "channel_delete" + ChannelSetParentGroup = "channel_set_parent_group" + ChannelConnectToThing = "channel_connect_to_thing" + ChannelPublish = "channel_publish" + ChannelSubscribe = "channel_subscribe" + + ChannelManageRole = "channel_manage_role" + ChannelAddRoleUsers = "channel_add_role_users" + ChannelRemoveRoleUsers = "channel_remove_role_users" + ChannelViewRoleUsers = "channel_view_role_users" + + SubgroupUpdate = "subgroup_update" + SubgroupRead = "subgroup_read" + SubgroupMembership = "subgroup_membership" + SubgroupDelete = "subgroup_delete" + SubgroupSetChild = "subgroup_set_child" + SubgroupSetParent = "subgroup_set_parent" + + SubgroupManageRole = "subgroup_manage_role" + SubgroupAddRoleUsers = "subgroup_add_role_users" + SubgroupRemoveRoleUsers = "subgroup_remove_role_users" + SubgroupViewRoleUsers = "subgroup_view_role_users" + + SubgroupThingUpdate = "subgroup_thing_update" + SubgroupThingRead = "subgroup_thing_read" + SubgroupThingDelete = "subgroup_thing_delete" + SubgroupThingSetParentGroup = "subgroup_thing_set_parent_group" + SubgroupThingConnectToChannel = "subgroup_thing_connect_to_channel" + + SubgroupThingManageRole = "subgroup_thing_manage_role" + SubgroupThingAddRoleUsers = "subgroup_thing_add_role_users" + SubgroupThingRemoveRoleUsers = "subgroup_thing_remove_role_users" + SubgroupThingViewRoleUsers = "subgroup_thing_view_role_users" + + SubgroupChannelUpdate = "subgroup_channel_update" + SubgroupChannelRead = "subgroup_channel_read" + SubgroupChannelDelete = "subgroup_channel_delete" + SubgroupChannelSetParentGroup = "subgroup_channel_set_parent_group" + SubgroupChannelConnectToThing = "subgroup_channel_connect_to_thing" + SubgroupChannelPublish = "subgroup_channel_publish" + SubgroupChannelSubscribe = "subgroup_channel_subscribe" + + SubgroupChannelManageRole = "subgroup_channel_manage_role" + SubgroupChannelAddRoleUsers = "subgroup_channel_add_role_users" + SubgroupChannelRemoveRoleUsers = "subgroup_channel_remove_role_users" + SubgroupChannelViewRoleUsers = "subgroup_channel_view_role_users" +) + +const ( + BuiltInRoleAdmin = "admin" + BuiltInRoleMembership = "membership" +) + +func AvailableActions() []roles.Action { + return []roles.Action{ + Update, + Read, + Membership, + Delete, + SetChild, + SetParent, + ManageRole, + AddRoleUsers, + RemoveRoleUsers, + ViewRoleUsers, + ThingCreate, + ChannelCreate, + SubgroupCreate, + SubgroupThingCreate, + SubgroupChannelCreate, + ThingUpdate, + ThingRead, + ThingDelete, + ThingSetParentGroup, + ThingConnectToChannel, + ThingManageRole, + ThingAddRoleUsers, + ThingRemoveRoleUsers, + ThingViewRoleUsers, + ChannelUpdate, + ChannelRead, + ChannelDelete, + ChannelSetParentGroup, + ChannelConnectToThing, + ChannelPublish, + ChannelSubscribe, + ChannelManageRole, + ChannelAddRoleUsers, + ChannelRemoveRoleUsers, + ChannelViewRoleUsers, + SubgroupUpdate, + SubgroupRead, + SubgroupMembership, + SubgroupDelete, + SubgroupSetChild, + SubgroupSetParent, + SubgroupManageRole, + SubgroupAddRoleUsers, + SubgroupRemoveRoleUsers, + SubgroupViewRoleUsers, + SubgroupThingUpdate, + SubgroupThingRead, + SubgroupThingDelete, + SubgroupThingSetParentGroup, + SubgroupThingConnectToChannel, + SubgroupThingManageRole, + SubgroupThingAddRoleUsers, + SubgroupThingRemoveRoleUsers, + SubgroupThingViewRoleUsers, + SubgroupChannelUpdate, + SubgroupChannelRead, + SubgroupChannelDelete, + SubgroupChannelSetParentGroup, + SubgroupChannelConnectToThing, + SubgroupChannelPublish, + SubgroupChannelSubscribe, + SubgroupChannelManageRole, + SubgroupChannelAddRoleUsers, + SubgroupChannelRemoveRoleUsers, + SubgroupChannelViewRoleUsers, + } +} + +func membershipRoleActions() []roles.Action { + return []roles.Action{ + Membership, + } +} + +func BuiltInRoles() map[roles.BuiltInRoleName][]roles.Action { + return map[roles.BuiltInRoleName][]roles.Action{ + BuiltInRoleAdmin: AvailableActions(), + BuiltInRoleMembership: membershipRoleActions(), + } +} diff --git a/groups/service.go b/groups/service.go new file mode 100644 index 0000000000..fca42f6dc6 --- /dev/null +++ b/groups/service.go @@ -0,0 +1,551 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package groups + +import ( + "context" + "fmt" + "time" + + "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + "github.com/absmach/magistrala/pkg/apiutil" + mgauthn "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/roles" + "golang.org/x/sync/errgroup" +) + +var errGroupIDs = errors.New("invalid group ids") + +type service struct { + repo Repository + policy policies.Service + idProvider magistrala.IDProvider + channels grpcChannelsV1.ChannelsServiceClient + things grpcThingsV1.ThingsServiceClient + + roles.ProvisionManageService +} + +// NewService returns a new groups service implementation. +func NewService(repo Repository, policy policies.Service, idp magistrala.IDProvider, channels grpcChannelsV1.ChannelsServiceClient, things grpcThingsV1.ThingsServiceClient, sidProvider magistrala.IDProvider) (Service, error) { + rpms, err := roles.NewProvisionManageService(policies.GroupType, repo, policy, sidProvider, AvailableActions(), BuiltInRoles()) + if err != nil { + return service{}, err + } + return service{ + repo: repo, + policy: policy, + idProvider: idp, + channels: channels, + things: things, + ProvisionManageService: rpms, + }, nil +} + +func (svc service) CreateGroup(ctx context.Context, session mgauthn.Session, g Group) (gr Group, retErr error) { + groupID, err := svc.idProvider.ID() + if err != nil { + return Group{}, err + } + if g.Status != EnabledStatus && g.Status != DisabledStatus { + return Group{}, svcerr.ErrInvalidStatus + } + + g.ID = groupID + g.CreatedAt = time.Now() + g.Domain = session.DomainID + + saved, err := svc.repo.Save(ctx, g) + if err != nil { + return Group{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + + defer func() { + if retErr != nil { + if errRollback := svc.repo.Delete(ctx, saved.ID); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + + oprs := []policies.Policy{} + + oprs = append(oprs, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.DomainType, + Subject: session.DomainID, + Relation: policies.DomainRelation, + ObjectType: policies.GroupType, + Object: saved.ID, + }) + if saved.Parent != "" { + oprs = append(oprs, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: saved.Parent, + Relation: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + Object: saved.ID, + }) + } + newBuiltInRoleMembers := map[roles.BuiltInRoleName][]roles.Member{ + BuiltInRoleAdmin: {roles.Member(session.UserID)}, + BuiltInRoleMembership: {}, + } + if _, err := svc.AddNewEntitiesRoles(ctx, session.DomainID, session.UserID, []string{saved.ID}, oprs, newBuiltInRoleMembers); err != nil { + return Group{}, errors.Wrap(svcerr.ErrAddPolicies, err) + } + + return saved, nil +} + +func (svc service) ViewGroup(ctx context.Context, session mgauthn.Session, id string) (Group, error) { + group, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { + return Group{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + + return group, nil +} + +func (svc service) ViewGroupPerms(ctx context.Context, session mgauthn.Session, id string) ([]string, error) { + return svc.listUserGroupPermission(ctx, session.DomainUserID, id) +} + +func (svc service) ListGroups(ctx context.Context, session mgauthn.Session, gm PageMeta) (Page, error) { + var ids []string + var err error + + switch session.SuperAdmin { + case true: + gm.DomainID = session.DomainID + default: + ids, err = svc.listAllGroupsOfUserID(ctx, session.DomainUserID, gm.Permission) + if err != nil { + return Page{}, err + } + } + + gp, err := svc.repo.RetrieveByIDs(ctx, gm, ids...) + if err != nil { + return Page{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + + if gm.ListPerms && len(gp.Groups) > 0 { + g, ctx := errgroup.WithContext(ctx) + + for i := range gp.Groups { + // Copying loop variable "i" to avoid "loop variable captured by func literal" + iter := i + g.Go(func() error { + return svc.retrievePermissions(ctx, session.DomainUserID, &gp.Groups[iter]) + }) + } + + if err := g.Wait(); err != nil { + return Page{}, err + } + } + return gp, nil +} + +// Experimental functions used for async calling of svc.listUserThingPermission. This might be helpful during listing of large number of entities. +func (svc service) retrievePermissions(ctx context.Context, userID string, group *Group) error { + permissions, err := svc.listUserGroupPermission(ctx, userID, group.ID) + if err != nil { + return err + } + group.Permissions = permissions + return nil +} + +func (svc service) listUserGroupPermission(ctx context.Context, userID, groupID string) ([]string, error) { + permissions, err := svc.policy.ListPermissions(ctx, policies.Policy{ + SubjectType: policies.UserType, + Subject: userID, + Object: groupID, + ObjectType: policies.GroupType, + }, []string{}) + if err != nil { + return []string{}, err + } + if len(permissions) == 0 { + return []string{}, svcerr.ErrAuthorization + } + return permissions, nil +} + +func (svc service) UpdateGroup(ctx context.Context, session mgauthn.Session, g Group) (Group, error) { + g.UpdatedAt = time.Now() + g.UpdatedBy = session.UserID + + return svc.repo.Update(ctx, g) +} + +func (svc service) EnableGroup(ctx context.Context, session mgauthn.Session, id string) (Group, error) { + + group := Group{ + ID: id, + Status: EnabledStatus, + UpdatedAt: time.Now(), + } + group, err := svc.changeGroupStatus(ctx, session, group) + if err != nil { + return Group{}, err + } + return group, nil +} + +func (svc service) DisableGroup(ctx context.Context, session mgauthn.Session, id string) (Group, error) { + group := Group{ + ID: id, + Status: DisabledStatus, + UpdatedAt: time.Now(), + } + group, err := svc.changeGroupStatus(ctx, session, group) + if err != nil { + return Group{}, err + } + return group, nil +} + +func (svc service) RetrieveGroupHierarchy(ctx context.Context, session mgauthn.Session, id string, hm HierarchyPageMeta) (HierarchyPage, error) { + hp, err := svc.repo.RetrieveHierarchy(ctx, id, hm) + if err != nil { + return HierarchyPage{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + hids := svc.getGroupIDs(hp.Groups) + ids, err := svc.filterAllowedGroupIDsOfUserID(ctx, session.DomainUserID, "read_permission", hids) + if err != nil { + return HierarchyPage{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + hp.Groups = svc.allowedGroups(hp.Groups, ids) + return hp, nil +} + +func (svc service) allowedGroups(gps []Group, ids []string) []Group { + aIDs := make(map[string]struct{}, len(ids)) + + for _, id := range ids { + aIDs[id] = struct{}{} + } + + aGroups := []Group{} + for _, g := range gps { + ag := g + if _, ok := aIDs[g.ID]; !ok { + ag = Group{ID: "xxxx-xxxx-xxxx-xxxx", Level: g.Level} + } + aGroups = append(aGroups, ag) + } + return aGroups +} +func (svc service) getGroupIDs(gps []Group) []string { + hids := []string{} + for _, g := range gps { + hids = append(hids, g.ID) + if len(g.Children) > 0 { + children := make([]Group, len(g.Children)) + for i, child := range g.Children { + children[i] = *child + } + cids := svc.getGroupIDs(children) + hids = append(hids, cids...) + } + } + return hids +} +func (svc service) AddParentGroup(ctx context.Context, session mgauthn.Session, id, parentID string) (retErr error) { + + group, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + + var pols []policies.Policy + if group.Parent != "" { + return errors.Wrap(svcerr.ErrConflict, fmt.Errorf("%s group already have parent", group.ID)) + } + pols = append(pols, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: parentID, + Relation: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + Object: group.ID, + }) + + if err := svc.policy.AddPolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrAddPolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.DeletePolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + + if err := svc.repo.AssignParentGroup(ctx, parentID, group.ID); err != nil { + return err + } + return nil +} + +func (svc service) RemoveParentGroup(ctx context.Context, session mgauthn.Session, id string) (retErr error) { + + group, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + + if group.Parent != "" { + + var pols []policies.Policy + pols = append(pols, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: group.Parent, + Relation: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + Object: group.ID, + }) + + if err := svc.policy.DeletePolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.AddPolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + + return svc.repo.UnassignParentGroup(ctx, group.Parent, group.ID) + } + + return nil +} + +func (svc service) AddChildrenGroups(ctx context.Context, session mgauthn.Session, parentGroupID string, childrenGroupIDs []string) (retErr error) { + childrenGroupsPage, err := svc.repo.RetrieveByIDs(ctx, PageMeta{Limit: 1<<63 - 1}, childrenGroupIDs...) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + if len(childrenGroupsPage.Groups) == 0 { + return errGroupIDs + } + + for _, childGroup := range childrenGroupsPage.Groups { + if childGroup.Parent != "" { + return errors.Wrap(svcerr.ErrConflict, fmt.Errorf("%s group already have parent", childGroup.ID)) + } + } + + var pols []policies.Policy + for _, childGroup := range childrenGroupsPage.Groups { + pols = append(pols, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: parentGroupID, + Relation: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + Object: childGroup.ID, + }) + } + + if err := svc.policy.AddPolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrAddPolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.DeletePolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + + return svc.repo.AssignParentGroup(ctx, parentGroupID, childrenGroupIDs...) +} + +func (svc service) RemoveChildrenGroups(ctx context.Context, session mgauthn.Session, parentGroupID string, childrenGroupIDs []string) (retErr error) { + childrenGroupsPage, err := svc.repo.RetrieveByIDs(ctx, PageMeta{Limit: 1<<63 - 1}, childrenGroupIDs...) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + if len(childrenGroupsPage.Groups) == 0 { + return errGroupIDs + } + + var pols []policies.Policy + + for _, group := range childrenGroupsPage.Groups { + if group.Parent != "" && group.Parent != parentGroupID { + return errors.Wrap(svcerr.ErrConflict, fmt.Errorf("%s group doesn't have same parent", group.ID)) + } + pols = append(pols, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: parentGroupID, + Relation: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + Object: group.ID, + }) + } + + if err := svc.policy.DeletePolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.AddPolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + + return svc.repo.UnassignParentGroup(ctx, parentGroupID, childrenGroupIDs...) +} + +func (svc service) RemoveAllChildrenGroups(ctx context.Context, session mgauthn.Session, id string) error { + pol := policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: id, + Relation: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + } + + if err := svc.policy.DeletePolicyFilter(ctx, pol); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + return svc.repo.UnassignAllChildrenGroup(ctx, id) +} + +func (svc service) ListChildrenGroups(ctx context.Context, session mgauthn.Session, id string, pm PageMeta) (Page, error) { + cids, err := svc.policy.ListAllObjects(ctx, policies.Policy{ + SubjectType: policies.GroupType, + Subject: id, + Permission: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + }) + if err != nil { + return Page{}, err + } + + ids, err := svc.filterAllowedGroupIDsOfUserID(ctx, session.DomainUserID, pm.Permission, cids.Policies) + if err != nil { + return Page{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + gp, err := svc.repo.RetrieveByIDs(ctx, pm, ids...) + if err != nil { + return Page{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return gp, nil +} + +func (svc service) DeleteGroup(ctx context.Context, session mgauthn.Session, id string) error { + if _, err := svc.channels.UnsetParentGroupFromChannels(ctx, &grpcChannelsV1.UnsetParentGroupFromChannelsReq{ParentGroupId: id}); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + if _, err := svc.things.UnsetParentGroupFromThings(ctx, &grpcThingsV1.UnsetParentGroupFromThingsReq{ParentGroupId: id}); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + g, err := svc.repo.ChangeStatus(ctx, Group{ID: id, Status: DeletedStatus}) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + filterDeletePolicies := []policies.Policy{ + { + SubjectType: policies.GroupType, + Subject: id, + }, + { + ObjectType: policies.GroupType, + Object: id, + }, + } + deletePolicies := []policies.Policy{ + { + SubjectType: policies.DomainType, + Subject: session.DomainID, + Relation: policies.DomainRelation, + ObjectType: policies.GroupType, + Object: id, + }, + } + if g.Parent != "" { + deletePolicies = append(deletePolicies, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: g.Parent, + Relation: policies.ParentGroupRelation, + ObjectType: policies.GroupType, + Object: id, + }) + } + if err := svc.RemoveEntitiesRoles(ctx, session.DomainID, session.DomainUserID, []string{id}, filterDeletePolicies, deletePolicies); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + if err := svc.repo.Delete(ctx, id); err != nil { + return err + } + + return nil +} + +func (svc service) filterAllowedGroupIDsOfUserID(ctx context.Context, userID, permission string, groupIDs []string) ([]string, error) { + var ids []string + allowedIDs, err := svc.listAllGroupsOfUserID(ctx, userID, permission) + if err != nil { + return []string{}, err + } + + for _, gid := range groupIDs { + for _, id := range allowedIDs { + if id == gid { + ids = append(ids, id) + } + } + } + return ids, nil +} + +func (svc service) listAllGroupsOfUserID(ctx context.Context, userID, permission string) ([]string, error) { + allowedIDs, err := svc.policy.ListAllObjects(ctx, policies.Policy{ + SubjectType: policies.UserType, + Subject: userID, + Permission: permission, + ObjectType: policies.GroupType, + }) + if err != nil { + return []string{}, err + } + return allowedIDs.Policies, nil +} + +func (svc service) changeGroupStatus(ctx context.Context, session mgauthn.Session, group Group) (Group, error) { + dbGroup, err := svc.repo.RetrieveByID(ctx, group.ID) + if err != nil { + return Group{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + if dbGroup.Status == group.Status { + return Group{}, errors.ErrStatusAlreadyAssigned + } + + group.UpdatedBy = session.UserID + return svc.repo.ChangeStatus(ctx, group) +} diff --git a/groups/service_test.go b/groups/service_test.go new file mode 100644 index 0000000000..365aa8f767 --- /dev/null +++ b/groups/service_test.go @@ -0,0 +1,4 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package groups_test diff --git a/internal/groups/status.go b/groups/status.go similarity index 90% rename from internal/groups/status.go rename to groups/status.go index d967dbc0b4..a33ed29f0b 100644 --- a/internal/groups/status.go +++ b/groups/status.go @@ -14,6 +14,8 @@ const ( EnabledStatus Status = iota // DisabledStatus represents disabled Group. DisabledStatus + // DeletedStatus + DeletedStatus // AllStatus is used for querying purposes to list groups irrespective // of their status - both active and inactive. It is never stored in the @@ -26,6 +28,7 @@ const ( const ( Disabled = "disabled" Enabled = "enabled" + Deleted = "deleted" All = "all" Unknown = "unknown" ) @@ -37,6 +40,8 @@ func (s Status) String() string { return Disabled case EnabledStatus: return Enabled + case DeletedStatus: + return Deleted case AllStatus: return All default: @@ -51,6 +56,8 @@ func ToStatus(status string) (Status, error) { return DisabledStatus, nil case Enabled: return EnabledStatus, nil + case Deleted: + return DeletedStatus, nil case All: return AllStatus, nil } diff --git a/internal/groups/status_test.go b/groups/status_test.go similarity index 90% rename from internal/groups/status_test.go rename to groups/status_test.go index a715ee392d..1e3a627e98 100644 --- a/internal/groups/status_test.go +++ b/groups/status_test.go @@ -6,7 +6,7 @@ package groups_test import ( "testing" - "github.com/absmach/magistrala/internal/groups" + "github.com/absmach/magistrala/groups" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/stretchr/testify/assert" ) @@ -19,6 +19,7 @@ func TestStatus_String(t *testing.T) { }{ {"Enabled", groups.EnabledStatus, "enabled"}, {"Disabled", groups.DisabledStatus, "disabled"}, + {"Deleted", groups.DeletedStatus, "deleted"}, {"All", groups.AllStatus, "all"}, {"Unknown", groups.Status(100), "unknown"}, } @@ -38,6 +39,7 @@ func TestToStatus(t *testing.T) { }{ {"Enabled", "enabled", groups.EnabledStatus, nil}, {"Disabled", "disabled", groups.DisabledStatus, nil}, + {"Deleted", "deleted", groups.DeletedStatus, nil}, {"All", "all", groups.AllStatus, nil}, {"Unknown", "unknown", groups.Status(0), svcerr.ErrInvalidStatus}, } diff --git a/internal/groups/tracing/doc.go b/groups/tracing/doc.go similarity index 100% rename from internal/groups/tracing/doc.go rename to groups/tracing/doc.go diff --git a/groups/tracing/tracing.go b/groups/tracing/tracing.go new file mode 100644 index 0000000000..929919108b --- /dev/null +++ b/groups/tracing/tracing.go @@ -0,0 +1,167 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package tracing + +import ( + "context" + "fmt" + + "github.com/absmach/magistrala/groups" + "github.com/absmach/magistrala/pkg/authn" + rmTrace "github.com/absmach/magistrala/pkg/roles/rolemanager/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var _ groups.Service = (*tracingMiddleware)(nil) + +type tracingMiddleware struct { + tracer trace.Tracer + svc groups.Service + rmTrace.RoleManagerTracing +} + +// New returns a new group service with tracing capabilities. +func New(svc groups.Service, tracer trace.Tracer) groups.Service { + return &tracingMiddleware{tracer, svc, rmTrace.NewRoleManagerTracing("group", svc, tracer)} +} + +// CreateGroup traces the "CreateGroup" operation of the wrapped groups.Service. +func (tm *tracingMiddleware) CreateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { + ctx, span := tm.tracer.Start(ctx, "svc_create_group") + defer span.End() + + return tm.svc.CreateGroup(ctx, session, g) +} + +// ViewGroup traces the "ViewGroup" operation of the wrapped groups.Service. +func (tm *tracingMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + ctx, span := tm.tracer.Start(ctx, "svc_view_group", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + + return tm.svc.ViewGroup(ctx, session, id) +} + +// ListGroups traces the "ListGroups" operation of the wrapped groups.Service. +func (tm *tracingMiddleware) ListGroups(ctx context.Context, session authn.Session, pm groups.PageMeta) (groups.Page, error) { + attr := []attribute.KeyValue{ + attribute.String("name", pm.Name), + attribute.String("tag", pm.Tag), + attribute.String("status", pm.Status.String()), + attribute.Int64("offset", int64(pm.Offset)), + attribute.Int64("limit", int64(pm.Limit)), + } + for k, v := range pm.Metadata { + attr = append(attr, attribute.String(k, fmt.Sprintf("%v", v))) + } + ctx, span := tm.tracer.Start(ctx, "svc_list_groups", trace.WithAttributes(attr...)) + defer span.End() + + return tm.svc.ListGroups(ctx, session, pm) +} + +// UpdateGroup traces the "UpdateGroup" operation of the wrapped groups.Service. +func (tm *tracingMiddleware) UpdateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { + ctx, span := tm.tracer.Start(ctx, "svc_update_group") + defer span.End() + + return tm.svc.UpdateGroup(ctx, session, g) +} + +// EnableGroup traces the "EnableGroup" operation of the wrapped groups.Service. +func (tm *tracingMiddleware) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + ctx, span := tm.tracer.Start(ctx, "svc_enable_group", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + + return tm.svc.EnableGroup(ctx, session, id) +} + +// DisableGroup traces the "DisableGroup" operation of the wrapped groups.Service. +func (tm *tracingMiddleware) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { + ctx, span := tm.tracer.Start(ctx, "svc_disable_group", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + + return tm.svc.DisableGroup(ctx, session, id) +} + +func (tm *tracingMiddleware) RetrieveGroupHierarchy(ctx context.Context, session authn.Session, id string, hm groups.HierarchyPageMeta) (groups.HierarchyPage, error) { + ctx, span := tm.tracer.Start(ctx, "svc_list_group_hierarchy", + trace.WithAttributes( + attribute.String("id", id), + attribute.Int64("level", int64(hm.Level)), + attribute.Int64("direction", hm.Direction), + attribute.Bool("tree", hm.Tree), + )) + defer span.End() + + return tm.svc.RetrieveGroupHierarchy(ctx, session, id, hm) +} + +func (tm *tracingMiddleware) AddParentGroup(ctx context.Context, session authn.Session, id, parentID string) error { + ctx, span := tm.tracer.Start(ctx, "svc_add_parent_group", + trace.WithAttributes( + attribute.String("id", id), + attribute.String("parent_id", parentID), + )) + defer span.End() + return tm.svc.AddParentGroup(ctx, session, id, parentID) +} + +func (tm *tracingMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + ctx, span := tm.tracer.Start(ctx, "svc_remove_parent_group", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + return tm.svc.RemoveParentGroup(ctx, session, id) +} + +func (tm *tracingMiddleware) AddChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + ctx, span := tm.tracer.Start(ctx, "svc_add_children_groups", + trace.WithAttributes( + attribute.String("id", id), + attribute.StringSlice("children_group_ids", childrenGroupIDs), + )) + + defer span.End() + return tm.svc.AddChildrenGroups(ctx, session, id, childrenGroupIDs) +} + +func (tm *tracingMiddleware) RemoveChildrenGroups(ctx context.Context, session authn.Session, id string, childrenGroupIDs []string) error { + ctx, span := tm.tracer.Start(ctx, "svc_remove_children_groups", + trace.WithAttributes( + attribute.String("id", id), + attribute.StringSlice("children_group_ids", childrenGroupIDs), + )) + defer span.End() + return tm.svc.RemoveChildrenGroups(ctx, session, id, childrenGroupIDs) +} + +func (tm *tracingMiddleware) RemoveAllChildrenGroups(ctx context.Context, session authn.Session, id string) error { + ctx, span := tm.tracer.Start(ctx, "svc_remove_all_children_groups", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + return tm.svc.RemoveAllChildrenGroups(ctx, session, id) +} + +func (tm *tracingMiddleware) ListChildrenGroups(ctx context.Context, session authn.Session, id string, pm groups.PageMeta) (groups.Page, error) { + attr := []attribute.KeyValue{ + attribute.String("id", id), + attribute.String("name", pm.Name), + attribute.String("tag", pm.Tag), + attribute.String("status", pm.Status.String()), + attribute.Int64("offset", int64(pm.Offset)), + attribute.Int64("limit", int64(pm.Limit)), + } + for k, v := range pm.Metadata { + attr = append(attr, attribute.String(k, fmt.Sprintf("%v", v))) + } + ctx, span := tm.tracer.Start(ctx, "svc_list_children_groups", trace.WithAttributes(attr...)) + defer span.End() + return tm.svc.ListChildrenGroups(ctx, session, id, pm) +} + +// DeleteGroup traces the "DeleteGroup" operation of the wrapped groups.Service. +func (tm *tracingMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) error { + ctx, span := tm.tracer.Start(ctx, "svc_delete_group", trace.WithAttributes(attribute.String("id", id))) + defer span.End() + + return tm.svc.DeleteGroup(ctx, session, id) +} diff --git a/http/api/endpoint_test.go b/http/api/endpoint_test.go index 6914ab838c..78602d86ff 100644 --- a/http/api/endpoint_test.go +++ b/http/api/endpoint_test.go @@ -11,11 +11,15 @@ import ( "strings" "testing" - "github.com/absmach/magistrala" + chmocks "github.com/absmach/magistrala/channels/mocks" server "github.com/absmach/magistrala/http" "github.com/absmach/magistrala/http/api" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" + mgauthn "github.com/absmach/magistrala/pkg/authn" + authnMocks "github.com/absmach/magistrala/pkg/authn/mocks" pubsub "github.com/absmach/magistrala/pkg/messaging/mocks" thmocks "github.com/absmach/magistrala/things/mocks" "github.com/absmach/mgate" @@ -30,9 +34,9 @@ const ( invalidValue = "invalid" ) -func newService(things magistrala.ThingsServiceClient) (session.Handler, *pubsub.PubSub) { +func newService(authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient) (session.Handler, *pubsub.PubSub) { pub := new(pubsub.PubSub) - return server.NewHandler(pub, mglog.NewMock(), things), pub + return server.NewHandler(pub, authn, things, channels, mglog.NewMock()), pub } func newTargetHTTPServer() *httptest.Server { @@ -82,6 +86,8 @@ func (tr testRequest) make() (*http.Response, error) { func TestPublish(t *testing.T) { things := new(thmocks.ThingsServiceClient) + authn := new(authnMocks.Authentication) + channels := new(chmocks.ChannelsServiceClient) chanID := "1" ctSenmlJSON := "application/senml+json" ctSenmlCBOR := "application/senml+cbor" @@ -91,7 +97,7 @@ func TestPublish(t *testing.T) { msg := `[{"n":"current","t":-1,"v":1.6}]` msgJSON := `{"field1":"val1","field2":"val2"}` msgCBOR := `81A3616E6763757272656E746174206176FB3FF999999999999A` - svc, pub := newService(things) + svc, pub := newService(authn, things, channels) target := newTargetHTTPServer() defer target.Close() ts, err := newProxyHTPPServer(svc, target) @@ -99,9 +105,6 @@ func TestPublish(t *testing.T) { defer ts.Close() - things.On("Authorize", mock.Anything, &magistrala.ThingsAuthzReq{ThingKey: thingKey, ChannelID: chanID, Permission: "publish"}).Return(&magistrala.ThingsAuthzRes{Authorized: true, Id: ""}, nil) - things.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.ThingsAuthzRes{Authorized: false, Id: ""}, nil) - cases := map[string]struct { chanID string msg string diff --git a/http/handler.go b/http/handler.go index b9e8827d3c..189998a973 100644 --- a/http/handler.go +++ b/http/handler.go @@ -13,8 +13,10 @@ import ( "strings" "time" - "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/pkg/apiutil" + mgauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" @@ -25,12 +27,20 @@ import ( var _ session.Handler = (*handler)(nil) -const protocol = "http" +type ctxKey string + +const ( + protocol = "http" + clientIDCtxKey ctxKey = "client_id" + clientTypeCtxKey ctxKey = "client_type" +) // Log message formats. const ( - logInfoConnected = "connected with thing_key %s" - logInfoPublished = "published with client_id %s to the topic %s" + logInfoConnected = "connected with thing_key %s" + logInfoPublished = "published with client_type %s client_id %s to the topic %s" + logInfoFailedAuthNToken = "failed to authenticate token for topic %s with error %s" + logInfoFailedAuthNThing = "failed to authenticate thing key %s for topic %s with error %s" ) // Error wrappers for MQTT errors. @@ -49,16 +59,20 @@ var channelRegExp = regexp.MustCompile(`^\/?channels\/([\w\-]+)\/messages(\/[^?] // Event implements events.Event interface. type handler struct { publisher messaging.Publisher - things magistrala.ThingsServiceClient + things grpcThingsV1.ThingsServiceClient + channels grpcChannelsV1.ChannelsServiceClient + authn mgauthn.Authentication logger *slog.Logger } // NewHandler creates new Handler entity. -func NewHandler(publisher messaging.Publisher, logger *slog.Logger, thingsClient magistrala.ThingsServiceClient) session.Handler { +func NewHandler(publisher messaging.Publisher, authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient, logger *slog.Logger) session.Handler { return &handler{ - logger: logger, publisher: publisher, - things: thingsClient, + authn: authn, + things: things, + channels: channels, + logger: logger, } } @@ -109,21 +123,39 @@ func (h *handler) Publish(ctx context.Context, topic *string, payload *[]byte) e if !ok { return errors.Wrap(errFailedPublish, errClientNotInitialized) } - h.logger.Info(fmt.Sprintf(logInfoPublished, s.ID, *topic)) - // Topics are in the format: - // channels//messages//.../ct/ - channelParts := channelRegExp.FindStringSubmatch(*topic) - if len(channelParts) < 2 { - return mgate.NewHTTPProxyError(http.StatusBadRequest, errors.Wrap(errFailedPublish, errMalformedTopic)) - } + var clientID, clientType string + switch { + case strings.HasPrefix(string(s.Password), "Thing"): + thingKey := strings.TrimPrefix(string(s.Password), apiutil.ThingPrefix) - chanID := channelParts[1] - subtopic := channelParts[2] + authnRes, err := h.things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ThingKey: thingKey}) + if err != nil { + h.logger.Info(fmt.Sprintf(logInfoFailedAuthNThing, thingKey, *topic, err)) + return mgate.NewHTTPProxyError(http.StatusUnauthorized, svcerr.ErrAuthentication) + } + if !authnRes.Authenticated { + h.logger.Info(fmt.Sprintf(logInfoFailedAuthNThing, thingKey, *topic, svcerr.ErrAuthentication)) + return mgate.NewHTTPProxyError(http.StatusUnauthorized, svcerr.ErrAuthentication) + } + clientType = policies.ThingType + clientID = authnRes.GetId() + case strings.HasPrefix(string(s.Password), apiutil.BearerPrefix): + token := strings.TrimPrefix(string(s.Password), apiutil.BearerPrefix) + authnSession, err := h.authn.Authenticate(ctx, token) + if err != nil { + h.logger.Info(fmt.Sprintf(logInfoFailedAuthNToken, *topic, err)) + return mgate.NewHTTPProxyError(http.StatusUnauthorized, svcerr.ErrAuthentication) + } + clientType = policies.UserType + clientID = authnSession.DomainUserID + default: + return mgate.NewHTTPProxyError(http.StatusUnauthorized, svcerr.ErrAuthentication) + } - subtopic, err := parseSubtopic(subtopic) + chanID, subtopic, err := parseTopic(*topic) if err != nil { - return mgate.NewHTTPProxyError(http.StatusBadRequest, errors.Wrap(errFailedParseSubtopic, err)) + return mgate.NewHTTPProxyError(http.StatusBadRequest, err) } msg := messaging.Message{ @@ -133,33 +165,31 @@ func (h *handler) Publish(ctx context.Context, topic *string, payload *[]byte) e Payload: *payload, Created: time.Now().UnixNano(), } - var tok string - switch { - case string(s.Password) == "": - return errors.Wrap(apiutil.ErrValidation, apiutil.ErrBearerKey) - case strings.HasPrefix(string(s.Password), apiutil.ThingPrefix): - tok = strings.TrimPrefix(string(s.Password), apiutil.ThingPrefix) - default: - tok = string(s.Password) - } - ar := &magistrala.ThingsAuthzReq{ - ThingKey: tok, - ChannelID: msg.Channel, + + ar := &grpcChannelsV1.AuthzReq{ + ClientId: clientID, + ClientType: clientType, + ChannelId: msg.Channel, Permission: policies.PublishPermission, } - res, err := h.things.Authorize(ctx, ar) + res, err := h.channels.Authorize(ctx, ar) if err != nil { return mgate.NewHTTPProxyError(http.StatusBadRequest, err) } if !res.GetAuthorized() { return mgate.NewHTTPProxyError(http.StatusUnauthorized, svcerr.ErrAuthorization) } - msg.Publisher = res.GetId() + + if clientType == policies.ThingType { + msg.Publisher = clientID + } if err := h.publisher.Publish(ctx, msg.Channel, &msg); err != nil { return errors.Wrap(errFailedPublishToMsgBroker, err) } + h.logger.Info(fmt.Sprintf(logInfoPublished, clientType, clientID, *topic)) + return nil } @@ -178,6 +208,25 @@ func (h *handler) Disconnect(ctx context.Context) error { return nil } +func parseTopic(topic string) (string, string, error) { + // Topics are in the format: + // channels//messages//.../ct/ + channelParts := channelRegExp.FindStringSubmatch(topic) + if len(channelParts) < 2 { + return "", "", errors.Wrap(errFailedPublish, errMalformedTopic) + } + + chanID := channelParts[1] + subtopic := channelParts[2] + + subtopic, err := parseSubtopic(subtopic) + if err != nil { + return "", "", errors.Wrap(errFailedParseSubtopic, err) + } + + return chanID, subtopic, nil +} + func parseSubtopic(subtopic string) (string, error) { if subtopic == "" { return subtopic, nil diff --git a/internal/api/common.go b/internal/api/common.go index 7c61ed26e9..ecafb57c22 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -11,7 +11,7 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/bootstrap" "github.com/absmach/magistrala/certs" - "github.com/absmach/magistrala/internal/groups" + "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" @@ -50,7 +50,7 @@ const ( EmailKey = "email" SharedByKey = "shared_by" TokenKey = "token" - DefPermission = "view" + DefPermission = "read_permission" DefTotal = uint64(100) DefOffset = 0 DefOrder = "updated_at" diff --git a/internal/groups/api/endpoint_test.go b/internal/groups/api/endpoint_test.go deleted file mode 100644 index 4a69f2fcdb..0000000000 --- a/internal/groups/api/endpoint_test.go +++ /dev/null @@ -1,1195 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - "fmt" - "net/http" - "testing" - "time" - - "github.com/absmach/magistrala/internal/api" - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/apiutil" - mgauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/groups/mocks" - "github.com/absmach/magistrala/pkg/policies" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var ( - validGroupResp = groups.Group{ - ID: testsutil.GenerateUUID(&testing.T{}), - Name: valid, - Description: valid, - Domain: testsutil.GenerateUUID(&testing.T{}), - Parent: testsutil.GenerateUUID(&testing.T{}), - Metadata: groups.Metadata{ - "name": "test", - }, - Children: []*groups.Group{}, - CreatedAt: time.Now().Add(-1 * time.Second), - UpdatedAt: time.Now(), - UpdatedBy: testsutil.GenerateUUID(&testing.T{}), - Status: groups.EnabledStatus, - } - validID = testsutil.GenerateUUID(&testing.T{}) -) - -func TestCreateGroupEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - kind string - session interface{} - req createGroupReq - svcResp groups.Group - svcErr error - resp createGroupRes - err error - }{ - { - desc: "successfully with groups kind", - kind: policies.NewGroupKind, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: createGroupReq{ - Group: groups.Group{ - Name: valid, - }, - }, - svcResp: validGroupResp, - svcErr: nil, - resp: createGroupRes{created: true, Group: validGroupResp}, - err: nil, - }, - { - desc: "successfully with channels kind", - kind: policies.NewChannelKind, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: createGroupReq{ - Group: groups.Group{ - Name: valid, - }, - }, - svcResp: validGroupResp, - svcErr: nil, - resp: createGroupRes{created: true, Group: validGroupResp}, - err: nil, - }, - { - desc: "unsuccessfully with invalid session", - kind: policies.NewGroupKind, - session: nil, - req: createGroupReq{ - Group: groups.Group{ - Name: valid, - }, - }, - resp: createGroupRes{created: false}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid request", - kind: policies.NewGroupKind, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: createGroupReq{ - Group: groups.Group{}, - }, - resp: createGroupRes{created: false}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - kind: policies.NewGroupKind, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: createGroupReq{ - Group: groups.Group{ - Name: valid, - }, - }, - svcResp: groups.Group{}, - svcErr: svcerr.ErrAuthorization, - resp: createGroupRes{created: false}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - svcCall := svc.On("CreateGroup", ctx, tc.session, tc.kind, tc.req.Group).Return(tc.svcResp, tc.svcErr) - resp, err := CreateGroupEndpoint(svc, tc.kind)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(createGroupRes) - switch err { - case nil: - assert.Equal(t, response.Code(), http.StatusCreated) - assert.Equal(t, response.Headers()["Location"], fmt.Sprintf("/groups/%s", response.ID)) - default: - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - } - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestViewGroupEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - req groupReq - session interface{} - svcResp groups.Group - svcErr error - resp viewGroupRes - err error - }{ - { - desc: "successfully", - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: groupReq{ - id: testsutil.GenerateUUID(t), - }, - svcResp: validGroupResp, - svcErr: nil, - resp: viewGroupRes{Group: validGroupResp}, - err: nil, - }, - { - desc: "unsuccessfully with invalid session", - req: groupReq{ - id: testsutil.GenerateUUID(t), - }, - svcResp: groups.Group{}, - svcErr: nil, - resp: viewGroupRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid request", - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: groupReq{}, - svcResp: groups.Group{}, - svcErr: nil, - resp: viewGroupRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: groupReq{ - id: testsutil.GenerateUUID(t), - }, - svcResp: groups.Group{}, - svcErr: svcerr.ErrAuthorization, - resp: viewGroupRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - svcCall := svc.On("ViewGroup", ctx, tc.session, tc.req.id).Return(tc.svcResp, tc.svcErr) - resp, err := ViewGroupEndpoint(svc)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(viewGroupRes) - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestViewGroupPermsEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - req groupPermsReq - session interface{} - svcResp []string - svcErr error - resp viewGroupPermsRes - err error - }{ - { - desc: "successfully", - req: groupPermsReq{ - id: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: []string{ - valid, - }, - svcErr: nil, - resp: viewGroupPermsRes{Permissions: []string{valid}}, - err: nil, - }, - { - desc: "unsuccessfully with invalid session", - req: groupPermsReq{ - id: testsutil.GenerateUUID(t), - }, - resp: viewGroupPermsRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid request", - req: groupPermsReq{}, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: viewGroupPermsRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: groupPermsReq{ - id: testsutil.GenerateUUID(t), - }, - svcResp: []string{}, - svcErr: svcerr.ErrAuthorization, - resp: viewGroupPermsRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - svcCall := svc.On("ViewGroupPerms", ctx, tc.session, tc.req.id).Return(tc.svcResp, tc.svcErr) - resp, err := ViewGroupPermsEndpoint(svc)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(viewGroupPermsRes) - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestEnableGroupEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - req changeGroupStatusReq - session interface{} - svcResp groups.Group - svcErr error - resp changeStatusRes - err error - }{ - { - desc: "successfully", - req: changeGroupStatusReq{ - id: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: validGroupResp, - svcErr: nil, - resp: changeStatusRes{Group: validGroupResp}, - err: nil, - }, - { - desc: "unsuccessfully with invalid session", - req: changeGroupStatusReq{ - id: testsutil.GenerateUUID(t), - }, - resp: changeStatusRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid request", - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: changeGroupStatusReq{}, - resp: changeStatusRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - req: changeGroupStatusReq{ - id: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Group{}, - svcErr: svcerr.ErrAuthorization, - resp: changeStatusRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - svcCall := svc.On("EnableGroup", ctx, tc.session, tc.req.id).Return(tc.svcResp, tc.svcErr) - resp, err := EnableGroupEndpoint(svc)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(changeStatusRes) - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestDisableGroupEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - req changeGroupStatusReq - session interface{} - svcResp groups.Group - svcErr error - resp changeStatusRes - err error - }{ - { - desc: "successfully", - req: changeGroupStatusReq{ - id: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: validGroupResp, - svcErr: nil, - resp: changeStatusRes{Group: validGroupResp}, - err: nil, - }, - { - desc: "unsuccessfully with invalid session", - req: changeGroupStatusReq{ - id: testsutil.GenerateUUID(t), - }, - resp: changeStatusRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid request", - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: changeGroupStatusReq{}, - resp: changeStatusRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - req: changeGroupStatusReq{ - id: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Group{}, - svcErr: svcerr.ErrAuthorization, - resp: changeStatusRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - svcCall := svc.On("DisableGroup", ctx, tc.session, tc.req.id).Return(tc.svcResp, tc.svcErr) - resp, err := DisableGroupEndpoint(svc)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(changeStatusRes) - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestDeleteGroupEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - req groupReq - session interface{} - svcErr error - resp deleteGroupRes - err error - }{ - { - desc: "successfully", - req: groupReq{ - id: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: nil, - resp: deleteGroupRes{deleted: true}, - err: nil, - }, - { - desc: "unsuccessfully with invalid session", - req: groupReq{ - id: testsutil.GenerateUUID(t), - }, - resp: deleteGroupRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid request", - req: groupReq{}, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: deleteGroupRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - req: groupReq{ - id: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: svcerr.ErrAuthorization, - resp: deleteGroupRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - svcCall := svc.On("DeleteGroup", ctx, tc.session, tc.req.id).Return(tc.svcErr) - resp, err := DeleteGroupEndpoint(svc)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(deleteGroupRes) - switch err { - case nil: - assert.Equal(t, response.Code(), http.StatusNoContent) - default: - assert.Equal(t, response.Code(), http.StatusBadRequest) - } - assert.Empty(t, response.Headers()) - assert.True(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestUpdateGroupEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - req updateGroupReq - session interface{} - svcResp groups.Group - svcErr error - resp updateGroupRes - err error - }{ - { - desc: "successfully", - req: updateGroupReq{ - id: testsutil.GenerateUUID(t), - Name: valid, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: validGroupResp, - svcErr: nil, - resp: updateGroupRes{Group: validGroupResp}, - err: nil, - }, - { - desc: "unsuccessfully with invalid session", - req: updateGroupReq{ - id: testsutil.GenerateUUID(t), - Name: valid, - }, - resp: updateGroupRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid request", - req: updateGroupReq{}, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: updateGroupRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - req: updateGroupReq{ - id: testsutil.GenerateUUID(t), - Name: valid, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Group{}, - svcErr: svcerr.ErrAuthorization, - resp: updateGroupRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - group := groups.Group{ - ID: tc.req.id, - Name: tc.req.Name, - Description: tc.req.Description, - Metadata: tc.req.Metadata, - } - svcCall := svc.On("UpdateGroup", ctx, tc.session, group).Return(tc.svcResp, tc.svcErr) - resp, err := UpdateGroupEndpoint(svc)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(updateGroupRes) - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestListGroupsEndpoint(t *testing.T) { - svc := new(mocks.Service) - childGroup := groups.Group{ - ID: testsutil.GenerateUUID(t), - Name: valid, - Description: valid, - Domain: testsutil.GenerateUUID(t), - Parent: validGroupResp.ID, - Metadata: groups.Metadata{ - "name": "test", - }, - Level: -1, - Children: []*groups.Group{}, - CreatedAt: time.Now().Add(-1 * time.Second), - UpdatedAt: time.Now(), - UpdatedBy: testsutil.GenerateUUID(t), - Status: groups.EnabledStatus, - } - parentGroup := groups.Group{ - ID: testsutil.GenerateUUID(t), - Name: valid, - Description: valid, - Domain: testsutil.GenerateUUID(t), - Metadata: groups.Metadata{ - "name": "test", - }, - Level: 1, - Children: []*groups.Group{}, - CreatedAt: time.Now().Add(-1 * time.Second), - UpdatedAt: time.Now(), - UpdatedBy: testsutil.GenerateUUID(t), - Status: groups.EnabledStatus, - } - - validGroupResp.Children = append(validGroupResp.Children, &childGroup) - parentGroup.Children = append(parentGroup.Children, &validGroupResp) - - cases := []struct { - desc string - memberKind string - req listGroupsReq - session interface{} - svcResp groups.Page - svcErr error - resp groupPageRes - err error - }{ - { - desc: "successfully", - memberKind: policies.ThingsKind, - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - memberKind: policies.ThingsKind, - memberID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Page{ - Groups: []groups.Group{validGroupResp}, - }, - svcErr: nil, - resp: groupPageRes{ - Groups: []viewGroupRes{ - { - Group: validGroupResp, - }, - }, - }, - err: nil, - }, - { - desc: "successfully with empty member kind", - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - memberKind: policies.ThingsKind, - memberID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Page{ - Groups: []groups.Group{validGroupResp}, - }, - svcErr: nil, - resp: groupPageRes{ - Groups: []viewGroupRes{ - { - Group: validGroupResp, - }, - }, - }, - err: nil, - }, - { - desc: "successfully with tree", - memberKind: policies.ThingsKind, - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - tree: true, - memberKind: policies.ThingsKind, - memberID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Page{ - Groups: []groups.Group{validGroupResp, childGroup}, - }, - svcErr: nil, - resp: groupPageRes{ - Groups: []viewGroupRes{ - { - Group: validGroupResp, - }, - }, - }, - err: nil, - }, - { - desc: "list children groups successfully without tree", - memberKind: policies.UsersKind, - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - ParentID: validGroupResp.ID, - Direction: -1, - }, - tree: false, - memberKind: policies.UsersKind, - memberID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Page{ - Groups: []groups.Group{validGroupResp, childGroup}, - }, - svcErr: nil, - resp: groupPageRes{ - Groups: []viewGroupRes{ - { - Group: childGroup, - }, - }, - }, - err: nil, - }, - { - desc: "list parent group successfully without tree", - memberKind: policies.UsersKind, - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - ParentID: validGroupResp.ID, - Direction: 1, - }, - tree: false, - memberKind: policies.UsersKind, - memberID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Page{ - Groups: []groups.Group{parentGroup, validGroupResp}, - }, - svcErr: nil, - resp: groupPageRes{ - Groups: []viewGroupRes{ - { - Group: parentGroup, - }, - }, - }, - err: nil, - }, - { - desc: "unsuccessfully with invalid request", - memberKind: policies.ThingsKind, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - req: listGroupsReq{}, - resp: groupPageRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - memberKind: policies.ThingsKind, - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - memberKind: policies.ThingsKind, - memberID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.Page{}, - svcErr: svcerr.ErrAuthorization, - resp: groupPageRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid session", - memberKind: policies.ThingsKind, - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - memberKind: policies.ThingsKind, - memberID: testsutil.GenerateUUID(t), - }, - resp: groupPageRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with empty member kind", - req: listGroupsReq{ - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - memberKind: "", - memberID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: groupPageRes{}, - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - if tc.memberKind != "" { - tc.req.memberKind = tc.memberKind - } - svcCall := svc.On("ListGroups", ctx, tc.session, tc.req.memberKind, tc.req.memberID, tc.req.Page).Return(tc.svcResp, tc.svcErr) - resp, err := ListGroupsEndpoint(svc, mock.Anything, tc.memberKind)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(groupPageRes) - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestListMembersEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - memberKind string - req listMembersReq - session interface{} - svcResp groups.MembersPage - svcErr error - resp listMembersRes - err error - }{ - { - desc: "successfully", - memberKind: policies.ThingsKind, - req: listMembersReq{ - memberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.MembersPage{ - Members: []groups.Member{ - { - ID: valid, - Type: valid, - }, - }, - }, - svcErr: nil, - resp: listMembersRes{ - Members: []groups.Member{ - { - ID: valid, - Type: valid, - }, - }, - }, - err: nil, - }, - { - desc: "successfully with empty member kind", - req: listMembersReq{ - memberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.MembersPage{ - Members: []groups.Member{ - { - ID: valid, - Type: valid, - }, - }, - }, - svcErr: nil, - resp: listMembersRes{ - Members: []groups.Member{ - { - ID: valid, - Type: valid, - }, - }, - }, - err: nil, - }, - { - desc: "unsuccessfully with invalid request", - memberKind: policies.ThingsKind, - req: listMembersReq{}, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: listMembersRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - memberKind: policies.ThingsKind, - req: listMembersReq{ - memberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcResp: groups.MembersPage{}, - svcErr: svcerr.ErrAuthorization, - resp: listMembersRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid session", - memberKind: policies.ThingsKind, - req: listMembersReq{ - memberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - }, - resp: listMembersRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - if tc.memberKind != "" { - tc.req.memberKind = tc.memberKind - } - svcCall := svc.On("ListMembers", ctx, tc.session, tc.req.groupID, tc.req.permission, tc.req.memberKind).Return(tc.svcResp, tc.svcErr) - resp, err := ListMembersEndpoint(svc, tc.memberKind)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(listMembersRes) - assert.Equal(t, response.Code(), http.StatusOK) - assert.Empty(t, response.Headers()) - assert.False(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestAssignMembersEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - relation string - session interface{} - memberKind string - req assignReq - svcErr error - resp assignRes - err error - }{ - { - desc: "successfully", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: assignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: nil, - resp: assignRes{assigned: true}, - err: nil, - }, - { - desc: "successfully with empty member kind", - relation: policies.ContributorRelation, - req: assignReq{ - groupID: testsutil.GenerateUUID(t), - MemberKind: policies.ThingsKind, - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: nil, - resp: assignRes{assigned: true}, - err: nil, - }, - { - desc: "successfully with empty relation", - memberKind: policies.ThingsKind, - req: assignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: nil, - resp: assignRes{assigned: true}, - err: nil, - }, - { - desc: "unsuccessfully with invalid request", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: assignReq{}, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: assignRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: assignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: svcerr.ErrAuthorization, - resp: assignRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid session", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: assignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - resp: assignRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - if tc.memberKind != "" { - tc.req.MemberKind = tc.memberKind - } - if tc.relation != "" { - tc.req.Relation = tc.relation - } - svcCall := svc.On("Assign", ctx, tc.session, tc.req.groupID, tc.req.Relation, tc.req.MemberKind, tc.req.Members).Return(tc.svcErr) - resp, err := AssignMembersEndpoint(svc, tc.relation, tc.memberKind)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(assignRes) - switch err { - case nil: - assert.Equal(t, response.Code(), http.StatusCreated) - default: - assert.Equal(t, response.Code(), http.StatusBadRequest) - } - assert.Empty(t, response.Headers()) - assert.True(t, response.Empty()) - svcCall.Unset() - }) - } -} - -func TestUnassignMembersEndpoint(t *testing.T) { - svc := new(mocks.Service) - cases := []struct { - desc string - relation string - memberKind string - req unassignReq - session interface{} - svcErr error - resp unassignRes - err error - }{ - { - desc: "successfully", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: unassignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: nil, - resp: unassignRes{unassigned: true}, - err: nil, - }, - { - desc: "successfully with empty member kind", - relation: policies.ContributorRelation, - req: unassignReq{ - groupID: testsutil.GenerateUUID(t), - MemberKind: policies.ThingsKind, - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: nil, - resp: unassignRes{unassigned: true}, - err: nil, - }, - { - desc: "successfully with empty relation", - memberKind: policies.ThingsKind, - req: unassignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - svcErr: nil, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: unassignRes{unassigned: true}, - err: nil, - }, - { - desc: "unsuccessfully with invalid request", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: unassignReq{}, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - resp: unassignRes{}, - err: apiutil.ErrValidation, - }, - { - desc: "unsuccessfully with repo error", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: unassignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - session: mgauthn.Session{DomainUserID: validID + "_" + validID, UserID: validID, DomainID: validID}, - svcErr: svcerr.ErrAuthorization, - resp: unassignRes{}, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with invalid session", - relation: policies.ContributorRelation, - memberKind: policies.ThingsKind, - req: unassignReq{ - MemberKind: policies.ThingsKind, - groupID: testsutil.GenerateUUID(t), - Members: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - resp: unassignRes{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - ctx := context.WithValue(context.Background(), api.SessionKey, tc.session) - if tc.memberKind != "" { - tc.req.MemberKind = tc.memberKind - } - if tc.relation != "" { - tc.req.Relation = tc.relation - } - svcCall := svc.On("Unassign", ctx, tc.session, tc.req.groupID, tc.req.Relation, tc.req.MemberKind, tc.req.Members).Return(tc.svcErr) - resp, err := UnassignMembersEndpoint(svc, tc.relation, tc.memberKind)(ctx, tc.req) - assert.Equal(t, tc.resp, resp, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, resp)) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - response := resp.(unassignRes) - switch err { - case nil: - assert.Equal(t, response.Code(), http.StatusCreated) - default: - assert.Equal(t, response.Code(), http.StatusBadRequest) - } - assert.Empty(t, response.Headers()) - assert.True(t, response.Empty()) - svcCall.Unset() - }) - } -} diff --git a/internal/groups/api/requests.go b/internal/groups/api/requests.go deleted file mode 100644 index 7144ef2361..0000000000 --- a/internal/groups/api/requests.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "github.com/absmach/magistrala/internal/api" - "github.com/absmach/magistrala/pkg/apiutil" - mggroups "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/policies" -) - -type createGroupReq struct { - mggroups.Group -} - -func (req createGroupReq) validate() error { - if len(req.Name) > api.MaxNameSize || req.Name == "" { - return apiutil.ErrNameSize - } - - return nil -} - -type updateGroupReq struct { - id string - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} - -func (req updateGroupReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - if len(req.Name) > api.MaxNameSize { - return apiutil.ErrNameSize - } - return nil -} - -type listGroupsReq struct { - mggroups.Page - memberKind string - memberID string - // - `true` - result is JSON tree representing groups hierarchy, - // - `false` - result is JSON array of groups. - tree bool -} - -func (req listGroupsReq) validate() error { - if req.memberKind == "" { - return apiutil.ErrMissingMemberKind - } - if req.memberKind == policies.ThingsKind && req.memberID == "" { - return apiutil.ErrMissingID - } - if req.Level > mggroups.MaxLevel { - return apiutil.ErrInvalidLevel - } - if req.Limit > api.MaxLimitSize || req.Limit < 1 { - return apiutil.ErrLimitSize - } - - return nil -} - -type groupReq struct { - id string -} - -func (req groupReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type groupPermsReq struct { - id string -} - -func (req groupPermsReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - - return nil -} - -type changeGroupStatusReq struct { - id string -} - -func (req changeGroupStatusReq) validate() error { - if req.id == "" { - return apiutil.ErrMissingID - } - return nil -} - -type assignReq struct { - groupID string - Relation string `json:"relation,omitempty"` - MemberKind string `json:"member_kind,omitempty"` - Members []string `json:"members"` -} - -func (req assignReq) validate() error { - if req.MemberKind == "" { - return apiutil.ErrMissingMemberKind - } - - if req.groupID == "" { - return apiutil.ErrMissingID - } - - if len(req.Members) == 0 { - return apiutil.ErrEmptyList - } - - return nil -} - -type unassignReq struct { - groupID string - Relation string `json:"relation,omitempty"` - MemberKind string `json:"member_kind,omitempty"` - Members []string `json:"members"` -} - -func (req unassignReq) validate() error { - if req.MemberKind == "" { - return apiutil.ErrMissingMemberKind - } - - if req.groupID == "" { - return apiutil.ErrMissingID - } - - if len(req.Members) == 0 { - return apiutil.ErrEmptyList - } - - return nil -} - -type listMembersReq struct { - groupID string - permission string - memberKind string -} - -func (req listMembersReq) validate() error { - if req.memberKind == "" { - return apiutil.ErrMissingMemberKind - } - - if req.groupID == "" { - return apiutil.ErrMissingID - } - return nil -} diff --git a/internal/groups/api/requests_test.go b/internal/groups/api/requests_test.go deleted file mode 100644 index ed9fa15ac5..0000000000 --- a/internal/groups/api/requests_test.go +++ /dev/null @@ -1,404 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "fmt" - "strings" - "testing" - - "github.com/absmach/magistrala/internal/api" - "github.com/absmach/magistrala/pkg/apiutil" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/policies" - "github.com/stretchr/testify/assert" -) - -var valid = "valid" - -func TestCreateGroupReqValidation(t *testing.T) { - cases := []struct { - desc string - req createGroupReq - err error - }{ - { - desc: "valid request", - req: createGroupReq{ - Group: groups.Group{ - Name: valid, - }, - }, - err: nil, - }, - { - desc: "long name", - req: createGroupReq{ - Group: groups.Group{ - Name: strings.Repeat("a", api.MaxNameSize+1), - }, - }, - err: apiutil.ErrNameSize, - }, - { - desc: "empty name", - req: createGroupReq{ - Group: groups.Group{}, - }, - err: apiutil.ErrNameSize, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestUpdateGroupReqValidation(t *testing.T) { - cases := []struct { - desc string - req updateGroupReq - err error - }{ - { - desc: "valid request", - req: updateGroupReq{ - id: valid, - Name: valid, - }, - err: nil, - }, - { - desc: "long name", - req: updateGroupReq{ - id: valid, - Name: strings.Repeat("a", api.MaxNameSize+1), - }, - err: apiutil.ErrNameSize, - }, - { - desc: "empty id", - req: updateGroupReq{ - Name: valid, - }, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestListGroupReqValidation(t *testing.T) { - cases := []struct { - desc string - req listGroupsReq - err error - }{ - { - desc: "valid request", - req: listGroupsReq{ - memberKind: policies.ThingsKind, - memberID: valid, - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - }, - err: nil, - }, - { - desc: "empty memberkind", - req: listGroupsReq{ - memberID: valid, - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - }, - err: apiutil.ErrMissingMemberKind, - }, - { - desc: "empty member id", - req: listGroupsReq{ - memberKind: policies.ThingsKind, - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - }, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "invalid upper level", - req: listGroupsReq{ - memberKind: policies.ThingsKind, - memberID: valid, - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 10, - }, - Level: groups.MaxLevel + 1, - }, - }, - err: apiutil.ErrInvalidLevel, - }, - { - desc: "invalid lower limit", - req: listGroupsReq{ - memberKind: policies.ThingsKind, - memberID: valid, - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: 0, - }, - }, - }, - err: apiutil.ErrLimitSize, - }, - { - desc: "invalid upper limit", - req: listGroupsReq{ - memberKind: policies.ThingsKind, - memberID: valid, - Page: groups.Page{ - PageMeta: groups.PageMeta{ - Limit: api.MaxLimitSize + 1, - }, - }, - }, - err: apiutil.ErrLimitSize, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestGroupReqValidation(t *testing.T) { - cases := []struct { - desc string - req groupReq - err error - }{ - { - desc: "valid request", - req: groupReq{ - id: valid, - }, - err: nil, - }, - { - desc: "empty id", - req: groupReq{}, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestGroupPermsReqValidation(t *testing.T) { - cases := []struct { - desc string - req groupPermsReq - err error - }{ - { - desc: "valid request", - req: groupPermsReq{ - id: valid, - }, - err: nil, - }, - { - desc: "empty id", - req: groupPermsReq{}, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestChangeGroupStatusReqValidation(t *testing.T) { - cases := []struct { - desc string - req changeGroupStatusReq - err error - }{ - { - desc: "valid request", - req: changeGroupStatusReq{ - id: valid, - }, - err: nil, - }, - { - desc: "empty id", - req: changeGroupStatusReq{}, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestAssignReqValidation(t *testing.T) { - cases := []struct { - desc string - req assignReq - err error - }{ - { - desc: "valid request", - req: assignReq{ - groupID: valid, - Relation: policies.ContributorRelation, - MemberKind: policies.ThingsKind, - Members: []string{valid}, - }, - err: nil, - }, - { - desc: "empty member kind", - req: assignReq{ - groupID: valid, - Relation: policies.ContributorRelation, - Members: []string{valid}, - }, - err: apiutil.ErrMissingMemberKind, - }, - { - desc: "empty groupID", - req: assignReq{ - Relation: policies.ContributorRelation, - MemberKind: policies.ThingsKind, - Members: []string{valid}, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "empty Members", - req: assignReq{ - groupID: valid, - Relation: policies.ContributorRelation, - MemberKind: policies.ThingsKind, - }, - err: apiutil.ErrEmptyList, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestUnAssignReqValidation(t *testing.T) { - cases := []struct { - desc string - req unassignReq - err error - }{ - { - desc: "valid request", - req: unassignReq{ - groupID: valid, - Relation: policies.ContributorRelation, - MemberKind: policies.ThingsKind, - Members: []string{valid}, - }, - err: nil, - }, - { - desc: "empty member kind", - req: unassignReq{ - groupID: valid, - Relation: policies.ContributorRelation, - Members: []string{valid}, - }, - err: apiutil.ErrMissingMemberKind, - }, - { - desc: "empty groupID", - req: unassignReq{ - Relation: policies.ContributorRelation, - MemberKind: policies.ThingsKind, - Members: []string{valid}, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "empty Members", - req: unassignReq{ - groupID: valid, - Relation: policies.ContributorRelation, - MemberKind: policies.ThingsKind, - }, - err: apiutil.ErrEmptyList, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} - -func TestListMembersReqValidation(t *testing.T) { - cases := []struct { - desc string - req listMembersReq - err error - }{ - { - desc: "valid request", - req: listMembersReq{ - groupID: valid, - permission: policies.ViewPermission, - memberKind: policies.ThingsKind, - }, - err: nil, - }, - { - desc: "empty member kind", - req: listMembersReq{ - groupID: valid, - permission: policies.ViewPermission, - }, - err: apiutil.ErrMissingMemberKind, - }, - { - desc: "empty groupID", - req: listMembersReq{ - permission: policies.ViewPermission, - memberKind: policies.ThingsKind, - }, - err: apiutil.ErrMissingID, - }, - } - - for _, tc := range cases { - err := tc.req.validate() - assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } -} diff --git a/internal/groups/middleware/authorization.go b/internal/groups/middleware/authorization.go deleted file mode 100644 index d6a2e0acff..0000000000 --- a/internal/groups/middleware/authorization.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package middleware - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/authz" - mgauthz "github.com/absmach/magistrala/pkg/authz" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/policies" -) - -var _ groups.Service = (*authorizationMiddleware)(nil) - -type authorizationMiddleware struct { - svc groups.Service - authz mgauthz.Authorization -} - -// AuthorizationMiddleware adds authorization to the clients service. -func AuthorizationMiddleware(svc groups.Service, authz mgauthz.Authorization) groups.Service { - return &authorizationMiddleware{ - svc: svc, - authz: authz, - } -} - -func (am *authorizationMiddleware) CreateGroup(ctx context.Context, session authn.Session, kind string, g groups.Group) (groups.Group, error) { - if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.CreatePermission, policies.DomainType, session.DomainID); err != nil { - return groups.Group{}, err - } - if g.Parent != "" { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.GroupType, g.Parent); err != nil { - return groups.Group{}, err - } - } - - return am.svc.CreateGroup(ctx, session, kind, g) -} - -func (am *authorizationMiddleware) UpdateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.GroupType, g.ID); err != nil { - return groups.Group{}, err - } - - return am.svc.UpdateGroup(ctx, session, g) -} - -func (am *authorizationMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.ViewPermission, policies.GroupType, id); err != nil { - return groups.Group{}, err - } - - return am.svc.ViewGroup(ctx, session, id) -} - -func (am *authorizationMiddleware) ViewGroupPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - return am.svc.ViewGroupPerms(ctx, session, id) -} - -func (am *authorizationMiddleware) ListGroups(ctx context.Context, session authn.Session, memberKind, memberID string, gm groups.Page) (groups.Page, error) { - switch memberKind { - case policies.ThingsKind: - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.ViewPermission, policies.ThingType, memberID); err != nil { - return groups.Page{}, err - } - case policies.GroupsKind: - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, gm.Permission, policies.GroupType, memberID); err != nil { - return groups.Page{}, err - } - case policies.ChannelsKind: - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.ViewPermission, policies.GroupType, memberID); err != nil { - return groups.Page{}, err - } - case policies.UsersKind: - switch { - case memberID != "" && session.UserID != memberID: - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err != nil { - return groups.Page{}, err - } - default: - err := am.checkSuperAdmin(ctx, session.UserID) - switch { - case err == nil: - session.SuperAdmin = true - default: - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID); err != nil { - return groups.Page{}, err - } - } - } - default: - return groups.Page{}, svcerr.ErrAuthorization - } - - return am.svc.ListGroups(ctx, session, memberKind, memberID, gm) -} - -func (am *authorizationMiddleware) ListMembers(ctx context.Context, session authn.Session, groupID, permission, memberKind string) (groups.MembersPage, error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.ViewPermission, policies.GroupType, groupID); err != nil { - return groups.MembersPage{}, err - } - - return am.svc.ListMembers(ctx, session, groupID, permission, memberKind) -} - -func (am *authorizationMiddleware) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.GroupType, id); err != nil { - return groups.Group{}, err - } - - return am.svc.EnableGroup(ctx, session, id) -} - -func (am *authorizationMiddleware) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.GroupType, id); err != nil { - return groups.Group{}, err - } - - return am.svc.DisableGroup(ctx, session, id) -} - -func (am *authorizationMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) error { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.GroupType, id); err != nil { - return err - } - - return am.svc.DeleteGroup(ctx, session, id) -} - -func (am *authorizationMiddleware) Assign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.GroupType, groupID); err != nil { - return err - } - - return am.svc.Assign(ctx, session, groupID, relation, memberKind, memberIDs...) -} - -func (am *authorizationMiddleware) Unassign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.GroupType, groupID); err != nil { - return err - } - - return am.svc.Unassign(ctx, session, groupID, relation, memberKind, memberIDs...) -} - -func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error { - if err := am.authz.Authorize(ctx, authz.PolicyReq{ - SubjectType: policies.UserType, - Subject: adminID, - Permission: policies.AdminPermission, - ObjectType: policies.PlatformType, - Object: policies.MagistralaObject, - }); err != nil { - return err - } - return nil -} - -func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error { - req := authz.PolicyReq{ - Domain: domain, - SubjectType: subjType, - SubjectKind: subjKind, - Subject: subj, - Permission: perm, - ObjectType: objType, - Object: obj, - } - if err := am.authz.Authorize(ctx, req); err != nil { - return err - } - - return nil -} diff --git a/internal/groups/postgres/init.go b/internal/groups/postgres/init.go deleted file mode 100644 index 0b799c46cb..0000000000 --- a/internal/groups/postgres/init.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access - migrate "github.com/rubenv/sql-migrate" -) - -func Migration() *migrate.MemoryMigrationSource { - return &migrate.MemoryMigrationSource{ - Migrations: []*migrate.Migration{ - { - Id: "groups_01", - Up: []string{ - `CREATE TABLE IF NOT EXISTS groups ( - id VARCHAR(36) PRIMARY KEY, - parent_id VARCHAR(36), - domain_id VARCHAR(36) NOT NULL, - name VARCHAR(1024) NOT NULL, - description VARCHAR(1024), - metadata JSONB, - created_at TIMESTAMP, - updated_at TIMESTAMP, - updated_by VARCHAR(254), - status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), - UNIQUE (domain_id, name), - FOREIGN KEY (parent_id) REFERENCES groups (id) ON DELETE SET NULL - )`, - }, - Down: []string{ - `DROP TABLE IF EXISTS groups`, - }, - }, - }, - } -} diff --git a/internal/groups/service.go b/internal/groups/service.go deleted file mode 100644 index 807a91772b..0000000000 --- a/internal/groups/service.go +++ /dev/null @@ -1,586 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package groups - -import ( - "context" - "fmt" - "time" - - "github.com/absmach/magistrala" - mgauth "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/pkg/apiutil" - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/policies" - "golang.org/x/sync/errgroup" -) - -var ( - errMemberKind = errors.New("invalid member kind") - errGroupIDs = errors.New("invalid group ids") -) - -type service struct { - groups groups.Repository - policies policies.Service - idProvider magistrala.IDProvider -} - -// NewService returns a new Clients service implementation. -func NewService(g groups.Repository, idp magistrala.IDProvider, policyService policies.Service) groups.Service { - return service{ - groups: g, - idProvider: idp, - policies: policyService, - } -} - -func (svc service) CreateGroup(ctx context.Context, session authn.Session, kind string, g groups.Group) (gr groups.Group, err error) { - groupID, err := svc.idProvider.ID() - if err != nil { - return groups.Group{}, err - } - if g.Status != groups.EnabledStatus && g.Status != groups.DisabledStatus { - return groups.Group{}, svcerr.ErrInvalidStatus - } - - g.ID = groupID - g.CreatedAt = time.Now() - g.Domain = session.DomainID - - policyList, err := svc.addGroupPolicy(ctx, session.DomainUserID, session.DomainID, g.ID, g.Parent, kind) - if err != nil { - return groups.Group{}, err - } - - defer func() { - if err != nil { - if errRollback := svc.policies.DeletePolicies(ctx, policyList); errRollback != nil { - err = errors.Wrap(errors.Wrap(errors.ErrRollbackTx, errRollback), err) - } - } - }() - - saved, err := svc.groups.Save(ctx, g) - if err != nil { - return groups.Group{}, errors.Wrap(svcerr.ErrCreateEntity, err) - } - - return saved, nil -} - -func (svc service) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - group, err := svc.groups.RetrieveByID(ctx, id) - if err != nil { - return groups.Group{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - return group, nil -} - -func (svc service) ViewGroupPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - return svc.listUserGroupPermission(ctx, session.DomainUserID, id) -} - -func (svc service) ListGroups(ctx context.Context, session authn.Session, memberKind, memberID string, gm groups.Page) (groups.Page, error) { - var ids []string - var err error - - switch memberKind { - case policies.ThingsKind: - cids, err := svc.policies.ListAllSubjects(ctx, policies.Policy{ - SubjectType: policies.GroupType, - Permission: policies.GroupRelation, - ObjectType: policies.ThingType, - Object: memberID, - }) - if err != nil { - return groups.Page{}, err - } - ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, session.DomainUserID, gm.Permission, cids.Policies) - if err != nil { - return groups.Page{}, err - } - case policies.GroupsKind: - gids, err := svc.policies.ListAllObjects(ctx, policies.Policy{ - SubjectType: policies.GroupType, - Subject: memberID, - Permission: policies.ParentGroupRelation, - ObjectType: policies.GroupType, - }) - if err != nil { - return groups.Page{}, err - } - ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, session.DomainUserID, gm.Permission, gids.Policies) - if err != nil { - return groups.Page{}, err - } - case policies.ChannelsKind: - gids, err := svc.policies.ListAllSubjects(ctx, policies.Policy{ - SubjectType: policies.GroupType, - Permission: policies.ParentGroupRelation, - ObjectType: policies.GroupType, - Object: memberID, - }) - if err != nil { - return groups.Page{}, err - } - - ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, session.DomainUserID, gm.Permission, gids.Policies) - if err != nil { - return groups.Page{}, err - } - case policies.UsersKind: - switch { - case memberID != "" && session.UserID != memberID: - gids, err := svc.policies.ListAllObjects(ctx, policies.Policy{ - SubjectType: policies.UserType, - Subject: mgauth.EncodeDomainUserID(session.DomainID, memberID), - Permission: gm.Permission, - ObjectType: policies.GroupType, - }) - if err != nil { - return groups.Page{}, err - } - ids, err = svc.filterAllowedGroupIDsOfUserID(ctx, session.DomainUserID, gm.Permission, gids.Policies) - if err != nil { - return groups.Page{}, err - } - default: - switch session.SuperAdmin { - case true: - gm.PageMeta.DomainID = session.DomainID - default: - ids, err = svc.listAllGroupsOfUserID(ctx, session.DomainUserID, gm.Permission) - if err != nil { - return groups.Page{}, err - } - } - } - default: - return groups.Page{}, errMemberKind - } - gp, err := svc.groups.RetrieveByIDs(ctx, gm, ids...) - if err != nil { - return groups.Page{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - - if gm.ListPerms && len(gp.Groups) > 0 { - g, ctx := errgroup.WithContext(ctx) - - for i := range gp.Groups { - // Copying loop variable "i" to avoid "loop variable captured by func literal" - iter := i - g.Go(func() error { - return svc.retrievePermissions(ctx, session.DomainUserID, &gp.Groups[iter]) - }) - } - - if err := g.Wait(); err != nil { - return groups.Page{}, err - } - } - return gp, nil -} - -// Experimental functions used for async calling of svc.listUserThingPermission. This might be helpful during listing of large number of entities. -func (svc service) retrievePermissions(ctx context.Context, userID string, group *groups.Group) error { - permissions, err := svc.listUserGroupPermission(ctx, userID, group.ID) - if err != nil { - return err - } - group.Permissions = permissions - return nil -} - -func (svc service) listUserGroupPermission(ctx context.Context, userID, groupID string) ([]string, error) { - permissions, err := svc.policies.ListPermissions(ctx, policies.Policy{ - SubjectType: policies.UserType, - Subject: userID, - Object: groupID, - ObjectType: policies.GroupType, - }, []string{}) - if err != nil { - return []string{}, err - } - if len(permissions) == 0 { - return []string{}, svcerr.ErrAuthorization - } - return permissions, nil -} - -// IMPROVEMENT NOTE: remove this function and all its related auxiliary function, ListMembers are moved to respective service. -func (svc service) ListMembers(ctx context.Context, session authn.Session, groupID, permission, memberKind string) (groups.MembersPage, error) { - switch memberKind { - case policies.ThingsKind: - tids, err := svc.policies.ListAllObjects(ctx, policies.Policy{ - SubjectType: policies.GroupType, - Subject: groupID, - Relation: policies.GroupRelation, - ObjectType: policies.ThingType, - }) - if err != nil { - return groups.MembersPage{}, err - } - - members := []groups.Member{} - - for _, id := range tids.Policies { - members = append(members, groups.Member{ - ID: id, - Type: policies.ThingType, - }) - } - return groups.MembersPage{ - Total: uint64(len(members)), - Offset: 0, - Limit: uint64(len(members)), - Members: members, - }, nil - case policies.UsersKind: - uids, err := svc.policies.ListAllSubjects(ctx, policies.Policy{ - SubjectType: policies.UserType, - Permission: permission, - Object: groupID, - ObjectType: policies.GroupType, - }) - if err != nil { - return groups.MembersPage{}, err - } - - members := []groups.Member{} - - for _, id := range uids.Policies { - members = append(members, groups.Member{ - ID: id, - Type: policies.UserType, - }) - } - return groups.MembersPage{ - Total: uint64(len(members)), - Offset: 0, - Limit: uint64(len(members)), - Members: members, - }, nil - default: - return groups.MembersPage{}, errMemberKind - } -} - -func (svc service) UpdateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { - g.UpdatedAt = time.Now() - g.UpdatedBy = session.UserID - - return svc.groups.Update(ctx, g) -} - -func (svc service) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - group := groups.Group{ - ID: id, - Status: groups.EnabledStatus, - UpdatedAt: time.Now(), - } - group, err := svc.changeGroupStatus(ctx, session, group) - if err != nil { - return groups.Group{}, err - } - return group, nil -} - -func (svc service) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - group := groups.Group{ - ID: id, - Status: groups.DisabledStatus, - UpdatedAt: time.Now(), - } - group, err := svc.changeGroupStatus(ctx, session, group) - if err != nil { - return groups.Group{}, err - } - return group, nil -} - -func (svc service) Assign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) error { - policyList := []policies.Policy{} - switch memberKind { - case policies.ThingsKind: - for _, memberID := range memberIDs { - policyList = append(policyList, policies.Policy{ - Domain: session.DomainID, - SubjectType: policies.GroupType, - SubjectKind: policies.ChannelsKind, - Subject: groupID, - Relation: relation, - ObjectType: policies.ThingType, - Object: memberID, - }) - } - case policies.ChannelsKind: - for _, memberID := range memberIDs { - policyList = append(policyList, policies.Policy{ - Domain: session.DomainID, - SubjectType: policies.GroupType, - Subject: memberID, - Relation: relation, - ObjectType: policies.GroupType, - Object: groupID, - }) - } - case policies.GroupsKind: - return svc.assignParentGroup(ctx, session.DomainID, groupID, memberIDs) - - case policies.UsersKind: - for _, memberID := range memberIDs { - policyList = append(policyList, policies.Policy{ - Domain: session.DomainID, - SubjectType: policies.UserType, - Subject: mgauth.EncodeDomainUserID(session.DomainID, memberID), - Relation: relation, - ObjectType: policies.GroupType, - Object: groupID, - }) - } - default: - return errMemberKind - } - - if err := svc.policies.AddPolicies(ctx, policyList); err != nil { - return errors.Wrap(svcerr.ErrAddPolicies, err) - } - - return nil -} - -func (svc service) assignParentGroup(ctx context.Context, domain, parentGroupID string, groupIDs []string) (err error) { - groupsPage, err := svc.groups.RetrieveByIDs(ctx, groups.Page{PageMeta: groups.PageMeta{Limit: 1<<63 - 1}}, groupIDs...) - if err != nil { - return errors.Wrap(svcerr.ErrViewEntity, err) - } - if len(groupsPage.Groups) == 0 { - return errGroupIDs - } - - policyList := []policies.Policy{} - for _, group := range groupsPage.Groups { - if group.Parent != "" { - return errors.Wrap(svcerr.ErrConflict, fmt.Errorf("%s group already have parent", group.ID)) - } - policyList = append(policyList, policies.Policy{ - Domain: domain, - SubjectType: policies.GroupType, - Subject: parentGroupID, - Relation: policies.ParentGroupRelation, - ObjectType: policies.GroupType, - Object: group.ID, - }) - } - - if err := svc.policies.AddPolicies(ctx, policyList); err != nil { - return errors.Wrap(svcerr.ErrAddPolicies, err) - } - defer func() { - if err != nil { - if errRollback := svc.policies.DeletePolicies(ctx, policyList); errRollback != nil { - err = errors.Wrap(err, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) - } - } - }() - - return svc.groups.AssignParentGroup(ctx, parentGroupID, groupIDs...) -} - -func (svc service) unassignParentGroup(ctx context.Context, domain, parentGroupID string, groupIDs []string) (err error) { - groupsPage, err := svc.groups.RetrieveByIDs(ctx, groups.Page{PageMeta: groups.PageMeta{Limit: 1<<63 - 1}}, groupIDs...) - if err != nil { - return errors.Wrap(svcerr.ErrViewEntity, err) - } - if len(groupsPage.Groups) == 0 { - return errGroupIDs - } - - policyList := []policies.Policy{} - for _, group := range groupsPage.Groups { - if group.Parent != "" && group.Parent != parentGroupID { - return errors.Wrap(svcerr.ErrConflict, fmt.Errorf("%s group doesn't have same parent", group.ID)) - } - policyList = append(policyList, policies.Policy{ - Domain: domain, - SubjectType: policies.GroupType, - Subject: parentGroupID, - Relation: policies.ParentGroupRelation, - ObjectType: policies.GroupType, - Object: group.ID, - }) - } - - if err := svc.policies.DeletePolicies(ctx, policyList); err != nil { - return errors.Wrap(svcerr.ErrDeletePolicies, err) - } - defer func() { - if err != nil { - if errRollback := svc.policies.AddPolicies(ctx, policyList); errRollback != nil { - err = errors.Wrap(err, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) - } - } - }() - - return svc.groups.UnassignParentGroup(ctx, parentGroupID, groupIDs...) -} - -func (svc service) Unassign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) error { - policyList := []policies.Policy{} - switch memberKind { - case policies.ThingsKind: - for _, memberID := range memberIDs { - policyList = append(policyList, policies.Policy{ - Domain: session.DomainID, - SubjectType: policies.GroupType, - SubjectKind: policies.ChannelsKind, - Subject: groupID, - Relation: relation, - ObjectType: policies.ThingType, - Object: memberID, - }) - } - case policies.ChannelsKind: - for _, memberID := range memberIDs { - policyList = append(policyList, policies.Policy{ - Domain: session.DomainID, - SubjectType: policies.GroupType, - Subject: memberID, - Relation: relation, - ObjectType: policies.GroupType, - Object: groupID, - }) - } - case policies.GroupsKind: - return svc.unassignParentGroup(ctx, session.DomainID, groupID, memberIDs) - case policies.UsersKind: - for _, memberID := range memberIDs { - policyList = append(policyList, policies.Policy{ - Domain: session.DomainID, - SubjectType: policies.UserType, - Subject: mgauth.EncodeDomainUserID(session.DomainID, memberID), - Relation: relation, - ObjectType: policies.GroupType, - Object: groupID, - }) - } - default: - return errMemberKind - } - - if err := svc.policies.DeletePolicies(ctx, policyList); err != nil { - return errors.Wrap(svcerr.ErrDeletePolicies, err) - } - return nil -} - -func (svc service) DeleteGroup(ctx context.Context, session authn.Session, id string) error { - req := policies.Policy{ - SubjectType: policies.GroupType, - Subject: id, - } - if err := svc.policies.DeletePolicyFilter(ctx, req); err != nil { - return errors.Wrap(svcerr.ErrDeletePolicies, err) - } - - req = policies.Policy{ - Object: id, - ObjectType: policies.GroupType, - } - - if err := svc.policies.DeletePolicyFilter(ctx, req); err != nil { - return errors.Wrap(svcerr.ErrDeletePolicies, err) - } - - if err := svc.groups.Delete(ctx, id); err != nil { - return err - } - - return nil -} - -func (svc service) filterAllowedGroupIDsOfUserID(ctx context.Context, userID, permission string, groupIDs []string) ([]string, error) { - var ids []string - allowedIDs, err := svc.listAllGroupsOfUserID(ctx, userID, permission) - if err != nil { - return []string{}, err - } - - for _, gid := range groupIDs { - for _, id := range allowedIDs { - if id == gid { - ids = append(ids, id) - } - } - } - return ids, nil -} - -func (svc service) listAllGroupsOfUserID(ctx context.Context, userID, permission string) ([]string, error) { - allowedIDs, err := svc.policies.ListAllObjects(ctx, policies.Policy{ - SubjectType: policies.UserType, - Subject: userID, - Permission: permission, - ObjectType: policies.GroupType, - }) - if err != nil { - return []string{}, err - } - return allowedIDs.Policies, nil -} - -func (svc service) changeGroupStatus(ctx context.Context, session authn.Session, group groups.Group) (groups.Group, error) { - dbGroup, err := svc.groups.RetrieveByID(ctx, group.ID) - if err != nil { - return groups.Group{}, errors.Wrap(svcerr.ErrViewEntity, err) - } - if dbGroup.Status == group.Status { - return groups.Group{}, errors.ErrStatusAlreadyAssigned - } - - group.UpdatedBy = session.UserID - return svc.groups.ChangeStatus(ctx, group) -} - -func (svc service) addGroupPolicy(ctx context.Context, userID, domainID, id, parentID, kind string) ([]policies.Policy, error) { - policyList := []policies.Policy{} - policyList = append(policyList, policies.Policy{ - Domain: domainID, - SubjectType: policies.UserType, - Subject: userID, - Relation: policies.AdministratorRelation, - ObjectKind: kind, - ObjectType: policies.GroupType, - Object: id, - }) - policyList = append(policyList, policies.Policy{ - Domain: domainID, - SubjectType: policies.DomainType, - Subject: domainID, - Relation: policies.DomainRelation, - ObjectType: policies.GroupType, - Object: id, - }) - if parentID != "" { - policyList = append(policyList, policies.Policy{ - Domain: domainID, - SubjectType: policies.GroupType, - Subject: parentID, - Relation: policies.ParentGroupRelation, - ObjectKind: kind, - ObjectType: policies.GroupType, - Object: id, - }) - } - if err := svc.policies.AddPolicies(ctx, policyList); err != nil { - return policyList, errors.Wrap(svcerr.ErrAddPolicies, err) - } - - return []policies.Policy{}, nil -} diff --git a/internal/groups/service_test.go b/internal/groups/service_test.go deleted file mode 100644 index 799a03f91f..0000000000 --- a/internal/groups/service_test.go +++ /dev/null @@ -1,1460 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package groups_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/0x6flab/namegenerator" - mgauth "github.com/absmach/magistrala/auth" - "github.com/absmach/magistrala/internal/groups" - "github.com/absmach/magistrala/internal/testsutil" - "github.com/absmach/magistrala/pkg/apiutil" - "github.com/absmach/magistrala/pkg/authn" - mgauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - repoerr "github.com/absmach/magistrala/pkg/errors/repository" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - mggroups "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/groups/mocks" - policysvc "github.com/absmach/magistrala/pkg/policies" - policymocks "github.com/absmach/magistrala/pkg/policies/mocks" - "github.com/absmach/magistrala/pkg/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -var ( - idProvider = uuid.New() - namegen = namegenerator.NewGenerator() - validGroup = mggroups.Group{ - Name: namegen.Generate(), - Description: namegen.Generate(), - Metadata: map[string]interface{}{ - "key": "value", - }, - Status: mggroups.EnabledStatus, - } - allowedIDs = []string{ - testsutil.GenerateUUID(&testing.T{}), - testsutil.GenerateUUID(&testing.T{}), - testsutil.GenerateUUID(&testing.T{}), - } - validID = testsutil.GenerateUUID(&testing.T{}) -) - -func TestCreateGroup(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - kind string - group mggroups.Group - repoResp mggroups.Group - repoErr error - addPolErr error - deletePolErr error - err error - }{ - { - desc: "successfully", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - kind: policysvc.NewGroupKind, - group: validGroup, - repoResp: mggroups.Group{ - ID: testsutil.GenerateUUID(t), - CreatedAt: time.Now(), - Domain: testsutil.GenerateUUID(t), - }, - err: nil, - }, - { - desc: "with invalid status", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - kind: policysvc.NewGroupKind, - group: mggroups.Group{ - Name: namegen.Generate(), - Description: namegen.Generate(), - Status: mggroups.Status(100), - }, - err: svcerr.ErrInvalidStatus, - }, - { - desc: "successfully with parent", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - kind: policysvc.NewGroupKind, - group: mggroups.Group{ - Name: namegen.Generate(), - Description: namegen.Generate(), - Status: mggroups.EnabledStatus, - Parent: testsutil.GenerateUUID(t), - }, - repoResp: mggroups.Group{ - ID: testsutil.GenerateUUID(t), - CreatedAt: time.Now(), - Domain: testsutil.GenerateUUID(t), - Parent: testsutil.GenerateUUID(t), - }, - }, - { - desc: "with repo error", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - kind: policysvc.NewGroupKind, - group: validGroup, - repoResp: mggroups.Group{}, - repoErr: errors.ErrMalformedEntity, - err: errors.ErrMalformedEntity, - }, - { - desc: "with failed to add policies", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - kind: policysvc.NewGroupKind, - group: validGroup, - repoResp: mggroups.Group{ - ID: testsutil.GenerateUUID(t), - }, - addPolErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "with failed to delete policies response", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - kind: policysvc.NewGroupKind, - group: mggroups.Group{ - Name: namegen.Generate(), - Description: namegen.Generate(), - Status: mggroups.EnabledStatus, - Parent: testsutil.GenerateUUID(t), - }, - repoErr: errors.ErrMalformedEntity, - deletePolErr: svcerr.ErrAuthorization, - err: errors.ErrMalformedEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("Save", context.Background(), mock.Anything).Return(tc.repoResp, tc.repoErr) - policyCall := policies.On("AddPolicies", context.Background(), mock.Anything).Return(tc.addPolErr) - policyCall1 := policies.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePolErr) - got, err := svc.CreateGroup(context.Background(), tc.session, tc.kind, tc.group) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.NotEmpty(t, got.ID) - assert.NotEmpty(t, got.CreatedAt) - assert.NotEmpty(t, got.Domain) - assert.WithinDuration(t, time.Now(), got.CreatedAt, 2*time.Second) - ok := repoCall.Parent.AssertCalled(t, "Save", context.Background(), mock.Anything) - assert.True(t, ok, fmt.Sprintf("Save was not called on %s", tc.desc)) - } - repoCall.Unset() - policyCall.Unset() - policyCall1.Unset() - }) - } -} - -func TestViewGroup(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - id string - repoResp mggroups.Group - repoErr error - err error - }{ - { - desc: "successfully", - id: testsutil.GenerateUUID(t), - repoResp: validGroup, - }, - { - desc: "with repo error", - id: testsutil.GenerateUUID(t), - repoErr: repoerr.ErrNotFound, - err: svcerr.ErrViewEntity, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("RetrieveByID", context.Background(), tc.id).Return(tc.repoResp, tc.repoErr) - got, err := svc.ViewGroup(context.Background(), mgauthn.Session{}, tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.Equal(t, tc.repoResp, got) - ok := repo.AssertCalled(t, "RetrieveByID", context.Background(), tc.id) - assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) - } - repoCall.Unset() - }) - } -} - -func TestViewGroupPerms(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - id string - listResp policysvc.Permissions - listErr error - err error - }{ - { - desc: "successfully", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - listResp: []string{ - policysvc.ViewPermission, - policysvc.EditPermission, - }, - }, - { - desc: "with failed to list permissions", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - listErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "with empty permissions", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - listResp: []string{}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - policyCall := policies.On("ListPermissions", context.Background(), policysvc.Policy{ - SubjectType: policysvc.UserType, - Subject: validID, - Object: tc.id, - ObjectType: policysvc.GroupType, - }, []string{}).Return(tc.listResp, tc.listErr) - got, err := svc.ViewGroupPerms(context.Background(), tc.session, tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.ElementsMatch(t, tc.listResp, got) - } - policyCall.Unset() - }) - } -} - -func TestUpdateGroup(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - group mggroups.Group - repoResp mggroups.Group - repoErr error - err error - }{ - { - desc: "successfully", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - group: mggroups.Group{ - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - }, - repoResp: validGroup, - }, - { - desc: " with repo error", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - group: mggroups.Group{ - ID: testsutil.GenerateUUID(t), - Name: namegen.Generate(), - }, - repoErr: repoerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("Update", context.Background(), mock.Anything).Return(tc.repoResp, tc.repoErr) - got, err := svc.UpdateGroup(context.Background(), tc.session, tc.group) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.Equal(t, tc.repoResp, got) - ok := repo.AssertCalled(t, "Update", context.Background(), mock.Anything) - assert.True(t, ok, fmt.Sprintf("Update was not called on %s", tc.desc)) - } - repoCall.Unset() - }) - } -} - -func TestEnableGroup(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - id string - retrieveResp mggroups.Group - retrieveErr error - changeResp mggroups.Group - changeErr error - err error - }{ - { - desc: "successfully", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - retrieveResp: mggroups.Group{ - Status: mggroups.DisabledStatus, - }, - changeResp: validGroup, - }, - { - desc: "with enabled group", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - retrieveResp: mggroups.Group{ - Status: mggroups.EnabledStatus, - }, - err: errors.ErrStatusAlreadyAssigned, - }, - { - desc: "with retrieve error", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - retrieveResp: mggroups.Group{}, - retrieveErr: repoerr.ErrNotFound, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("RetrieveByID", context.Background(), tc.id).Return(tc.retrieveResp, tc.retrieveErr) - repoCall1 := repo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeResp, tc.changeErr) - got, err := svc.EnableGroup(context.Background(), tc.session, tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.Equal(t, tc.changeResp, got) - ok := repo.AssertCalled(t, "RetrieveByID", context.Background(), tc.id) - assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) - } - repoCall.Unset() - repoCall1.Unset() - }) - } -} - -func TestDisableGroup(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - id string - retrieveResp mggroups.Group - retrieveErr error - changeResp mggroups.Group - changeErr error - err error - }{ - { - desc: "successfully", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - retrieveResp: mggroups.Group{ - Status: mggroups.EnabledStatus, - }, - changeResp: validGroup, - }, - { - desc: "with enabled group", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - retrieveResp: mggroups.Group{ - Status: mggroups.DisabledStatus, - }, - err: errors.ErrStatusAlreadyAssigned, - }, - { - desc: "with retrieve error", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - id: testsutil.GenerateUUID(t), - retrieveResp: mggroups.Group{}, - retrieveErr: repoerr.ErrNotFound, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := repo.On("RetrieveByID", context.Background(), tc.id).Return(tc.retrieveResp, tc.retrieveErr) - repoCall1 := repo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeResp, tc.changeErr) - got, err := svc.DisableGroup(context.Background(), tc.session, tc.id) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.Equal(t, tc.changeResp, got) - ok := repo.AssertCalled(t, "RetrieveByID", context.Background(), tc.id) - assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) - } - repoCall.Unset() - repoCall1.Unset() - }) - } -} - -func TestListMembers(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - groupID string - permission string - memberKind string - listSubjectResp policysvc.PolicyPage - listSubjectErr error - listObjectResp policysvc.PolicyPage - listObjectErr error - err error - }{ - { - desc: "successfully with things kind", - groupID: testsutil.GenerateUUID(t), - memberKind: policysvc.ThingsKind, - listObjectResp: policysvc.PolicyPage{ - Policies: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - }, - { - desc: "successfully with users kind", - groupID: testsutil.GenerateUUID(t), - memberKind: policysvc.UsersKind, - permission: policysvc.ViewPermission, - listSubjectResp: policysvc.PolicyPage{ - Policies: []string{ - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - testsutil.GenerateUUID(t), - }, - }, - }, - { - desc: "with invalid kind", - groupID: testsutil.GenerateUUID(t), - memberKind: policysvc.GroupsKind, - permission: policysvc.ViewPermission, - err: errors.New("invalid member kind"), - }, - { - desc: "failed to list objects with things kind", - groupID: testsutil.GenerateUUID(t), - memberKind: policysvc.ThingsKind, - listObjectResp: policysvc.PolicyPage{}, - listObjectErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "failed to list subjects with users kind", - groupID: testsutil.GenerateUUID(t), - memberKind: policysvc.UsersKind, - permission: policysvc.ViewPermission, - listSubjectResp: policysvc.PolicyPage{}, - listSubjectErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - policyCall := policies.On("ListAllObjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.GroupType, - Subject: tc.groupID, - Relation: policysvc.GroupRelation, - ObjectType: policysvc.ThingType, - }).Return(tc.listObjectResp, tc.listObjectErr) - policyCall1 := policies.On("ListAllSubjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.UserType, - Permission: tc.permission, - Object: tc.groupID, - ObjectType: policysvc.GroupType, - }).Return(tc.listSubjectResp, tc.listSubjectErr) - got, err := svc.ListMembers(context.Background(), mgauthn.Session{}, tc.groupID, tc.permission, tc.memberKind) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.NotEmpty(t, got) - } - policyCall.Unset() - policyCall1.Unset() - }) - } -} - -func TestListGroups(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - memberKind string - memberID string - page mggroups.Page - listSubjectResp policysvc.PolicyPage - listSubjectErr error - listObjectResp policysvc.PolicyPage - listObjectErr error - listObjectFilterResp policysvc.PolicyPage - listObjectFilterErr error - repoResp mggroups.Page - repoErr error - listPermResp policysvc.Permissions - listPermErr error - err error - }{ - { - desc: "successfully with things kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ThingsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - listPermResp: []string{ - policysvc.ViewPermission, - policysvc.EditPermission, - }, - }, - { - desc: "successfully with groups kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.GroupsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - listPermResp: []string{ - policysvc.ViewPermission, - policysvc.EditPermission, - }, - }, - { - desc: "successfully with channels kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ChannelsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - listPermResp: []string{ - policysvc.ViewPermission, - policysvc.EditPermission, - }, - }, - { - desc: "successfully with users kind non admin", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.UsersKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - listPermResp: []string{ - policysvc.ViewPermission, - policysvc.EditPermission, - }, - }, - { - desc: "successfully with users kind admin", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberKind: policysvc.UsersKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - listPermResp: []string{ - policysvc.ViewPermission, - policysvc.EditPermission, - }, - }, - { - desc: "unsuccessfully with things kind due to failed to list subjects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ThingsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{}, - listSubjectErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with things kind due to failed to list filtered objects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ThingsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{}, - listObjectFilterErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with groups kind due to failed to list subjects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.GroupsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{}, - listObjectErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with groups kind due to failed to list filtered objects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.GroupsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{}, - listObjectFilterErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with channels kind due to failed to list subjects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ChannelsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{}, - listSubjectErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with channels kind due to failed to list filtered objects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ChannelsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{}, - listObjectFilterErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with users kind due to failed to list subjects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.UsersKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{}, - listObjectErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with users kind due to failed to list filtered objects", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.UsersKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{}, - listObjectFilterErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "successfully with users kind admin", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberKind: policysvc.UsersKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listObjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - listPermResp: []string{ - policysvc.ViewPermission, - policysvc.EditPermission, - }, - }, - { - desc: "unsuccessfully with invalid kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: "invalid", - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - err: errors.New("invalid member kind"), - }, - { - desc: "unsuccessfully with things kind due to repo error", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ThingsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{}, - repoErr: repoerr.ErrViewEntity, - err: repoerr.ErrViewEntity, - }, - { - desc: "unsuccessfully with things kind due to failed to list permissions", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - memberID: testsutil.GenerateUUID(t), - memberKind: policysvc.ThingsKind, - page: mggroups.Page{ - Permission: policysvc.ViewPermission, - ListPerms: true, - }, - listSubjectResp: policysvc.PolicyPage{Policies: allowedIDs}, - listObjectFilterResp: policysvc.PolicyPage{Policies: allowedIDs}, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - listPermResp: []string{}, - listPermErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - policyCall := &mock.Call{} - policyCall1 := &mock.Call{} - switch tc.memberKind { - case policysvc.ThingsKind: - policyCall = policies.On("ListAllSubjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.GroupType, - Permission: policysvc.GroupRelation, - ObjectType: policysvc.ThingType, - Object: tc.memberID, - }).Return(tc.listSubjectResp, tc.listSubjectErr) - policyCall1 = policies.On("ListAllObjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.UserType, - Subject: validID, - Permission: tc.page.Permission, - ObjectType: policysvc.GroupType, - }).Return(tc.listObjectFilterResp, tc.listObjectFilterErr) - case policysvc.GroupsKind: - policyCall = policies.On("ListAllObjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.GroupType, - Subject: tc.memberID, - Permission: policysvc.ParentGroupRelation, - ObjectType: policysvc.GroupType, - }).Return(tc.listObjectResp, tc.listObjectErr) - policyCall1 = policies.On("ListAllObjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.UserType, - Subject: validID, - Permission: tc.page.Permission, - ObjectType: policysvc.GroupType, - }).Return(tc.listObjectFilterResp, tc.listObjectFilterErr) - case policysvc.ChannelsKind: - policyCall = policies.On("ListAllSubjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.GroupType, - Permission: policysvc.ParentGroupRelation, - ObjectType: policysvc.GroupType, - Object: tc.memberID, - }).Return(tc.listSubjectResp, tc.listSubjectErr) - policyCall1 = policies.On("ListAllObjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.UserType, - Subject: validID, - Permission: tc.page.Permission, - ObjectType: policysvc.GroupType, - }).Return(tc.listObjectFilterResp, tc.listObjectFilterErr) - case policysvc.UsersKind: - policyCall = policies.On("ListAllObjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.UserType, - Subject: mgauth.EncodeDomainUserID(validID, tc.memberID), - Permission: tc.page.Permission, - ObjectType: policysvc.GroupType, - }).Return(tc.listObjectResp, tc.listObjectErr) - policyCall1 = policies.On("ListAllObjects", context.Background(), policysvc.Policy{ - SubjectType: policysvc.UserType, - Subject: validID, - Permission: tc.page.Permission, - ObjectType: policysvc.GroupType, - }).Return(tc.listObjectFilterResp, tc.listObjectFilterErr) - } - repoCall := repo.On("RetrieveByIDs", context.Background(), mock.Anything, mock.Anything).Return(tc.repoResp, tc.repoErr) - policyCall2 := policies.On("ListPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tc.listPermResp, tc.listPermErr) - got, err := svc.ListGroups(context.Background(), tc.session, tc.memberKind, tc.memberID, tc.page) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - if err == nil { - assert.NotEmpty(t, got) - } - repoCall.Unset() - switch tc.memberKind { - case policysvc.ThingsKind, policysvc.GroupsKind, policysvc.ChannelsKind, policysvc.UsersKind: - policyCall.Unset() - policyCall1.Unset() - policyCall2.Unset() - } - }) - } -} - -func TestAssign(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - groupID string - relation string - memberKind string - memberIDs []string - addPoliciesErr error - repoResp mggroups.Page - repoErr error - addParentPoliciesErr error - deleteParentPoliciesErr error - repoParentGroupErr error - err error - }{ - { - desc: "successfully with things kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.ThingsKind, - memberIDs: allowedIDs, - err: nil, - }, - { - desc: "successfully with channels kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.ChannelsKind, - memberIDs: allowedIDs, - err: nil, - }, - { - desc: "successfully with groups kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - repoParentGroupErr: nil, - }, - { - desc: "successfully with users kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.UsersKind, - memberIDs: allowedIDs, - err: nil, - }, - { - desc: "unsuccessfully with groups kind due to repo err", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{}, - repoErr: repoerr.ErrViewEntity, - err: repoerr.ErrViewEntity, - }, - { - desc: "unsuccessfully with groups kind due to empty page", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{}, - }, - err: errors.New("invalid group ids"), - }, - { - desc: "unsuccessfully with groups kind due to non empty parent", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - { - ID: testsutil.GenerateUUID(t), - Parent: testsutil.GenerateUUID(t), - }, - }, - }, - err: repoerr.ErrConflict, - }, - { - desc: "unsuccessfully with groups kind due to failed to add policies", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - addPoliciesErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with groups kind due to failed to assign parent", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - repoParentGroupErr: repoerr.ErrConflict, - err: repoerr.ErrConflict, - }, - { - desc: "unsuccessfully with groups kind due to failed to assign parent and delete policies", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - deleteParentPoliciesErr: svcerr.ErrAuthorization, - repoParentGroupErr: repoerr.ErrConflict, - err: apiutil.ErrRollbackTx, - }, - { - desc: "unsuccessfully with invalid kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: "invalid", - memberIDs: allowedIDs, - err: errors.New("invalid member kind"), - }, - { - desc: "unsuccessfully with failed to add policies", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.ThingsKind, - memberIDs: allowedIDs, - addPoliciesErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - retrieveByIDsCall := &mock.Call{} - deletePoliciesCall := &mock.Call{} - assignParentCall := &mock.Call{} - policyList := []policysvc.Policy{} - switch tc.memberKind { - case policysvc.ThingsKind: - for _, memberID := range tc.memberIDs { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.GroupType, - SubjectKind: policysvc.ChannelsKind, - Subject: tc.groupID, - Relation: tc.relation, - ObjectType: policysvc.ThingType, - Object: memberID, - }) - } - case policysvc.GroupsKind: - retrieveByIDsCall = repo.On("RetrieveByIDs", context.Background(), mggroups.Page{PageMeta: mggroups.PageMeta{Limit: 1<<63 - 1}}, mock.Anything).Return(tc.repoResp, tc.repoErr) - for _, group := range tc.repoResp.Groups { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.GroupType, - Subject: tc.groupID, - Relation: policysvc.ParentGroupRelation, - ObjectType: policysvc.GroupType, - Object: group.ID, - }) - } - deletePoliciesCall = policies.On("DeletePolicies", context.Background(), policyList).Return(tc.deleteParentPoliciesErr) - assignParentCall = repo.On("AssignParentGroup", context.Background(), tc.groupID, tc.memberIDs).Return(tc.repoParentGroupErr) - case policysvc.ChannelsKind: - for _, memberID := range tc.memberIDs { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.GroupType, - Subject: memberID, - Relation: tc.relation, - ObjectType: policysvc.GroupType, - Object: tc.groupID, - }) - } - case policysvc.UsersKind: - for _, memberID := range tc.memberIDs { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.UserType, - Subject: mgauth.EncodeDomainUserID(validID, memberID), - Relation: tc.relation, - ObjectType: policysvc.GroupType, - Object: tc.groupID, - }) - } - } - policyCall := policies.On("AddPolicies", context.Background(), policyList).Return(tc.addPoliciesErr) - err := svc.Assign(context.Background(), tc.session, tc.groupID, tc.relation, tc.memberKind, tc.memberIDs...) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - policyCall.Unset() - if tc.memberKind == policysvc.GroupsKind { - retrieveByIDsCall.Unset() - deletePoliciesCall.Unset() - assignParentCall.Unset() - } - }) - } -} - -func TestUnassign(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - session authn.Session - groupID string - relation string - memberKind string - memberIDs []string - deletePoliciesErr error - repoResp mggroups.Page - repoErr error - addParentPoliciesErr error - deleteParentPoliciesErr error - repoParentGroupErr error - err error - }{ - { - desc: "successfully with things kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.ThingsKind, - memberIDs: allowedIDs, - err: nil, - }, - { - desc: "successfully with channels kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.ChannelsKind, - memberIDs: allowedIDs, - err: nil, - }, - { - desc: "successfully with groups kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - repoParentGroupErr: nil, - }, - { - desc: "successfully with users kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.UsersKind, - memberIDs: allowedIDs, - err: nil, - }, - { - desc: "unsuccessfully with groups kind due to repo err", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{}, - repoErr: repoerr.ErrViewEntity, - err: repoerr.ErrViewEntity, - }, - { - desc: "unsuccessfully with groups kind due to empty page", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{}, - }, - err: errors.New("invalid group ids"), - }, - { - desc: "unsuccessfully with groups kind due to non empty parent", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - { - ID: testsutil.GenerateUUID(t), - Parent: testsutil.GenerateUUID(t), - }, - }, - }, - err: repoerr.ErrConflict, - }, - { - desc: "unsuccessfully with groups kind due to failed to add policies", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - deletePoliciesErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with groups kind due to failed to unassign parent", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - repoParentGroupErr: repoerr.ErrConflict, - err: repoerr.ErrConflict, - }, - { - desc: "unsuccessfully with groups kind due to failed to unassign parent and add policies", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.GroupsKind, - memberIDs: allowedIDs, - repoResp: mggroups.Page{ - Groups: []mggroups.Group{ - validGroup, - validGroup, - validGroup, - }, - }, - repoParentGroupErr: repoerr.ErrConflict, - addParentPoliciesErr: svcerr.ErrAuthorization, - err: repoerr.ErrConflict, - }, - { - desc: "unsuccessfully with invalid kind", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: "invalid", - memberIDs: allowedIDs, - err: errors.New("invalid member kind"), - }, - { - desc: "unsuccessfully with failed to add policies", - session: authn.Session{UserID: validID, DomainID: validID, DomainUserID: validID}, - groupID: testsutil.GenerateUUID(t), - relation: policysvc.ContributorRelation, - memberKind: policysvc.ThingsKind, - memberIDs: allowedIDs, - deletePoliciesErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - retrieveByIDsCall := &mock.Call{} - addPoliciesCall := &mock.Call{} - assignParentCall := &mock.Call{} - policyList := []policysvc.Policy{} - switch tc.memberKind { - case policysvc.ThingsKind: - for _, memberID := range tc.memberIDs { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.GroupType, - SubjectKind: policysvc.ChannelsKind, - Subject: tc.groupID, - Relation: tc.relation, - ObjectType: policysvc.ThingType, - Object: memberID, - }) - } - case policysvc.GroupsKind: - retrieveByIDsCall = repo.On("RetrieveByIDs", context.Background(), mggroups.Page{PageMeta: mggroups.PageMeta{Limit: 1<<63 - 1}}, mock.Anything).Return(tc.repoResp, tc.repoErr) - for _, group := range tc.repoResp.Groups { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.GroupType, - Subject: tc.groupID, - Relation: policysvc.ParentGroupRelation, - ObjectType: policysvc.GroupType, - Object: group.ID, - }) - } - addPoliciesCall = policies.On("AddPolicies", context.Background(), policyList).Return(tc.addParentPoliciesErr) - assignParentCall = repo.On("UnassignParentGroup", context.Background(), tc.groupID, tc.memberIDs).Return(tc.repoParentGroupErr) - case policysvc.ChannelsKind: - for _, memberID := range tc.memberIDs { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.GroupType, - Subject: memberID, - Relation: tc.relation, - ObjectType: policysvc.GroupType, - Object: tc.groupID, - }) - } - case policysvc.UsersKind: - for _, memberID := range tc.memberIDs { - policyList = append(policyList, policysvc.Policy{ - Domain: validID, - SubjectType: policysvc.UserType, - Subject: mgauth.EncodeDomainUserID(validID, memberID), - Relation: tc.relation, - ObjectType: policysvc.GroupType, - Object: tc.groupID, - }) - } - } - policyCall := policies.On("DeletePolicies", context.Background(), policyList).Return(tc.deletePoliciesErr) - err := svc.Unassign(context.Background(), tc.session, tc.groupID, tc.relation, tc.memberKind, tc.memberIDs...) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - policyCall.Unset() - if tc.memberKind == policysvc.GroupsKind { - retrieveByIDsCall.Unset() - addPoliciesCall.Unset() - assignParentCall.Unset() - } - }) - } -} - -func TestDeleteGroup(t *testing.T) { - repo := new(mocks.Repository) - policies := new(policymocks.Service) - svc := groups.NewService(repo, idProvider, policies) - - cases := []struct { - desc string - groupID string - deleteSubjectPoliciesErr error - deleteObjectPoliciesErr error - repoErr error - err error - }{ - { - desc: "successfully", - groupID: testsutil.GenerateUUID(t), - err: nil, - }, - { - desc: "unsuccessfully with failed to remove subject policies", - groupID: testsutil.GenerateUUID(t), - deleteSubjectPoliciesErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with failed to remove object policies", - groupID: testsutil.GenerateUUID(t), - deleteObjectPoliciesErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "unsuccessfully with repo err", - groupID: testsutil.GenerateUUID(t), - repoErr: repoerr.ErrNotFound, - err: repoerr.ErrNotFound, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - policyCall := policies.On("DeletePolicyFilter", context.Background(), policysvc.Policy{ - SubjectType: policysvc.GroupType, - Subject: tc.groupID, - }).Return(tc.deleteSubjectPoliciesErr) - policyCall2 := policies.On("DeletePolicyFilter", context.Background(), policysvc.Policy{ - ObjectType: policysvc.GroupType, - Object: tc.groupID, - }).Return(tc.deleteObjectPoliciesErr) - repoCall := repo.On("Delete", context.Background(), tc.groupID).Return(tc.repoErr) - err := svc.DeleteGroup(context.Background(), mgauthn.Session{}, tc.groupID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected error %v to contain %v", err, tc.err)) - policyCall.Unset() - policyCall2.Unset() - repoCall.Unset() - }) - } -} diff --git a/internal/groups/tracing/tracing.go b/internal/groups/tracing/tracing.go deleted file mode 100644 index 190188668f..0000000000 --- a/internal/groups/tracing/tracing.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package tracing - -import ( - "context" - - "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/groups" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -var _ groups.Service = (*tracingMiddleware)(nil) - -type tracingMiddleware struct { - tracer trace.Tracer - gsvc groups.Service -} - -// New returns a new group service with tracing capabilities. -func New(gsvc groups.Service, tracer trace.Tracer) groups.Service { - return &tracingMiddleware{tracer, gsvc} -} - -// CreateGroup traces the "CreateGroup" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) CreateGroup(ctx context.Context, session authn.Session, kind string, g groups.Group) (groups.Group, error) { - ctx, span := tm.tracer.Start(ctx, "svc_create_group") - defer span.End() - - return tm.gsvc.CreateGroup(ctx, session, kind, g) -} - -// ViewGroup traces the "ViewGroup" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - ctx, span := tm.tracer.Start(ctx, "svc_view_group", trace.WithAttributes(attribute.String("id", id))) - defer span.End() - - return tm.gsvc.ViewGroup(ctx, session, id) -} - -// ViewGroupPerms traces the "ViewGroupPerms" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) ViewGroupPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - ctx, span := tm.tracer.Start(ctx, "svc_view_group", trace.WithAttributes(attribute.String("id", id))) - defer span.End() - - return tm.gsvc.ViewGroupPerms(ctx, session, id) -} - -// ListGroups traces the "ListGroups" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) ListGroups(ctx context.Context, session authn.Session, memberKind, memberID string, gm groups.Page) (groups.Page, error) { - ctx, span := tm.tracer.Start(ctx, "svc_list_groups") - defer span.End() - - return tm.gsvc.ListGroups(ctx, session, memberKind, memberID, gm) -} - -// ListMembers traces the "ListMembers" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) ListMembers(ctx context.Context, session authn.Session, groupID, permission, memberKind string) (groups.MembersPage, error) { - ctx, span := tm.tracer.Start(ctx, "svc_list_members", trace.WithAttributes(attribute.String("groupID", groupID))) - defer span.End() - - return tm.gsvc.ListMembers(ctx, session, groupID, permission, memberKind) -} - -// UpdateGroup traces the "UpdateGroup" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) UpdateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { - ctx, span := tm.tracer.Start(ctx, "svc_update_group") - defer span.End() - - return tm.gsvc.UpdateGroup(ctx, session, g) -} - -// EnableGroup traces the "EnableGroup" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - ctx, span := tm.tracer.Start(ctx, "svc_enable_group", trace.WithAttributes(attribute.String("id", id))) - defer span.End() - - return tm.gsvc.EnableGroup(ctx, session, id) -} - -// DisableGroup traces the "DisableGroup" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - ctx, span := tm.tracer.Start(ctx, "svc_disable_group", trace.WithAttributes(attribute.String("id", id))) - defer span.End() - - return tm.gsvc.DisableGroup(ctx, session, id) -} - -// Assign traces the "Assign" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) Assign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) error { - ctx, span := tm.tracer.Start(ctx, "svc_assign", trace.WithAttributes(attribute.String("id", groupID))) - defer span.End() - - return tm.gsvc.Assign(ctx, session, groupID, relation, memberKind, memberIDs...) -} - -// Unassign traces the "Unassign" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) Unassign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) error { - ctx, span := tm.tracer.Start(ctx, "svc_unassign", trace.WithAttributes(attribute.String("id", groupID))) - defer span.End() - - return tm.gsvc.Unassign(ctx, session, groupID, relation, memberKind, memberIDs...) -} - -// DeleteGroup traces the "DeleteGroup" operation of the wrapped groups.Service. -func (tm *tracingMiddleware) DeleteGroup(ctx context.Context, session authn.Session, id string) error { - ctx, span := tm.tracer.Start(ctx, "svc_delete_group", trace.WithAttributes(attribute.String("id", id))) - defer span.End() - - return tm.gsvc.DeleteGroup(ctx, session, id) -} diff --git a/internal/grpc/auth/v1/auth.pb.go b/internal/grpc/auth/v1/auth.pb.go new file mode 100644 index 0000000000..71bf566f95 --- /dev/null +++ b/internal/grpc/auth/v1/auth.pb.go @@ -0,0 +1,396 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: auth/v1/auth.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AuthNReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *AuthNReq) Reset() { + *x = AuthNReq{} + mi := &file_auth_v1_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthNReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthNReq) ProtoMessage() {} + +func (x *AuthNReq) ProtoReflect() protoreflect.Message { + mi := &file_auth_v1_auth_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthNReq.ProtoReflect.Descriptor instead. +func (*AuthNReq) Descriptor() ([]byte, []int) { + return file_auth_v1_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthNReq) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type AuthNRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // IMPROVEMENT NOTE: change name from "id" to "subject" , sub in jwt = user id + domain id // + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // user id + DomainId string `protobuf:"bytes,3,opt,name=domain_id,json=domainId,proto3" json:"domain_id,omitempty"` // domain id +} + +func (x *AuthNRes) Reset() { + *x = AuthNRes{} + mi := &file_auth_v1_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthNRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthNRes) ProtoMessage() {} + +func (x *AuthNRes) ProtoReflect() protoreflect.Message { + mi := &file_auth_v1_auth_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthNRes.ProtoReflect.Descriptor instead. +func (*AuthNRes) Descriptor() ([]byte, []int) { + return file_auth_v1_auth_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthNRes) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AuthNRes) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *AuthNRes) GetDomainId() string { + if x != nil { + return x.DomainId + } + return "" +} + +type AuthZReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` // Domain + SubjectType string `protobuf:"bytes,2,opt,name=subject_type,json=subjectType,proto3" json:"subject_type,omitempty"` // Thing or User + SubjectKind string `protobuf:"bytes,3,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` // ID or Token + SubjectRelation string `protobuf:"bytes,4,opt,name=subject_relation,json=subjectRelation,proto3" json:"subject_relation,omitempty"` // Subject relation + Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"` // Subject value (id or token, depending on kind) + Relation string `protobuf:"bytes,6,opt,name=relation,proto3" json:"relation,omitempty"` // Relation to filter + Permission string `protobuf:"bytes,7,opt,name=permission,proto3" json:"permission,omitempty"` // Action + Object string `protobuf:"bytes,8,opt,name=object,proto3" json:"object,omitempty"` // Object ID + ObjectType string `protobuf:"bytes,9,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"` // Thing, User, Group +} + +func (x *AuthZReq) Reset() { + *x = AuthZReq{} + mi := &file_auth_v1_auth_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthZReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthZReq) ProtoMessage() {} + +func (x *AuthZReq) ProtoReflect() protoreflect.Message { + mi := &file_auth_v1_auth_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthZReq.ProtoReflect.Descriptor instead. +func (*AuthZReq) Descriptor() ([]byte, []int) { + return file_auth_v1_auth_proto_rawDescGZIP(), []int{2} +} + +func (x *AuthZReq) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +func (x *AuthZReq) GetSubjectType() string { + if x != nil { + return x.SubjectType + } + return "" +} + +func (x *AuthZReq) GetSubjectKind() string { + if x != nil { + return x.SubjectKind + } + return "" +} + +func (x *AuthZReq) GetSubjectRelation() string { + if x != nil { + return x.SubjectRelation + } + return "" +} + +func (x *AuthZReq) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *AuthZReq) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *AuthZReq) GetPermission() string { + if x != nil { + return x.Permission + } + return "" +} + +func (x *AuthZReq) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *AuthZReq) GetObjectType() string { + if x != nil { + return x.ObjectType + } + return "" +} + +type AuthZRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *AuthZRes) Reset() { + *x = AuthZRes{} + mi := &file_auth_v1_auth_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthZRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthZRes) ProtoMessage() {} + +func (x *AuthZRes) ProtoReflect() protoreflect.Message { + mi := &file_auth_v1_auth_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthZRes.ProtoReflect.Descriptor instead. +func (*AuthZRes) Descriptor() ([]byte, []int) { + return file_auth_v1_auth_proto_rawDescGZIP(), []int{3} +} + +func (x *AuthZRes) GetAuthorized() bool { + if x != nil { + return x.Authorized + } + return false +} + +func (x *AuthZRes) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_auth_v1_auth_proto protoreflect.FileDescriptor + +var file_auth_v1_auth_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x22, 0x20, 0x0a, + 0x08, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, + 0x50, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x49, + 0x64, 0x22, 0xa2, 0x02, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x5a, 0x52, 0x65, 0x71, 0x12, 0x16, + 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x29, 0x0a, 0x10, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, + 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x5a, 0x52, + 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x32, 0x7a, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x33, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x12, 0x11, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x5a, 0x52, 0x65, + 0x71, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, + 0x5a, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x76, 0x31, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4e, 0x52, 0x65, 0x73, 0x22, 0x00, 0x42, 0x35, + 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 0x73, + 0x6d, 0x61, 0x63, 0x68, 0x2f, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x61, 0x75, + 0x74, 0x68, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_auth_v1_auth_proto_rawDescOnce sync.Once + file_auth_v1_auth_proto_rawDescData = file_auth_v1_auth_proto_rawDesc +) + +func file_auth_v1_auth_proto_rawDescGZIP() []byte { + file_auth_v1_auth_proto_rawDescOnce.Do(func() { + file_auth_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_auth_v1_auth_proto_rawDescData) + }) + return file_auth_v1_auth_proto_rawDescData +} + +var file_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_auth_v1_auth_proto_goTypes = []any{ + (*AuthNReq)(nil), // 0: auth.v1.AuthNReq + (*AuthNRes)(nil), // 1: auth.v1.AuthNRes + (*AuthZReq)(nil), // 2: auth.v1.AuthZReq + (*AuthZRes)(nil), // 3: auth.v1.AuthZRes +} +var file_auth_v1_auth_proto_depIdxs = []int32{ + 2, // 0: auth.v1.AuthService.Authorize:input_type -> auth.v1.AuthZReq + 0, // 1: auth.v1.AuthService.Authenticate:input_type -> auth.v1.AuthNReq + 3, // 2: auth.v1.AuthService.Authorize:output_type -> auth.v1.AuthZRes + 1, // 3: auth.v1.AuthService.Authenticate:output_type -> auth.v1.AuthNRes + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_auth_v1_auth_proto_init() } +func file_auth_v1_auth_proto_init() { + if File_auth_v1_auth_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_auth_v1_auth_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_auth_v1_auth_proto_goTypes, + DependencyIndexes: file_auth_v1_auth_proto_depIdxs, + MessageInfos: file_auth_v1_auth_proto_msgTypes, + }.Build() + File_auth_v1_auth_proto = out.File + file_auth_v1_auth_proto_rawDesc = nil + file_auth_v1_auth_proto_goTypes = nil + file_auth_v1_auth_proto_depIdxs = nil +} diff --git a/internal/grpc/auth/v1/auth_grpc.pb.go b/internal/grpc/auth/v1/auth_grpc.pb.go new file mode 100644 index 0000000000..5aae19082d --- /dev/null +++ b/internal/grpc/auth/v1/auth_grpc.pb.go @@ -0,0 +1,168 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.2 +// source: auth/v1/auth.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AuthService_Authorize_FullMethodName = "/auth.v1.AuthService/Authorize" + AuthService_Authenticate_FullMethodName = "/auth.v1.AuthService/Authenticate" +) + +// AuthServiceClient is the client API for AuthService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// AuthService is a service that provides authentication and authorization +// functionalities for magistrala services. +type AuthServiceClient interface { + Authorize(ctx context.Context, in *AuthZReq, opts ...grpc.CallOption) (*AuthZRes, error) + Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) +} + +type authServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { + return &authServiceClient{cc} +} + +func (c *authServiceClient) Authorize(ctx context.Context, in *AuthZReq, opts ...grpc.CallOption) (*AuthZRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthZRes) + err := c.cc.Invoke(ctx, AuthService_Authorize_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthNRes) + err := c.cc.Invoke(ctx, AuthService_Authenticate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServiceServer is the server API for AuthService service. +// All implementations must embed UnimplementedAuthServiceServer +// for forward compatibility. +// +// AuthService is a service that provides authentication and authorization +// functionalities for magistrala services. +type AuthServiceServer interface { + Authorize(context.Context, *AuthZReq) (*AuthZRes, error) + Authenticate(context.Context, *AuthNReq) (*AuthNRes, error) + mustEmbedUnimplementedAuthServiceServer() +} + +// UnimplementedAuthServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAuthServiceServer struct{} + +func (UnimplementedAuthServiceServer) Authorize(context.Context, *AuthZReq) (*AuthZRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented") +} +func (UnimplementedAuthServiceServer) Authenticate(context.Context, *AuthNReq) (*AuthNRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented") +} +func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} +func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} + +// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthServiceServer will +// result in compilation errors. +type UnsafeAuthServiceServer interface { + mustEmbedUnimplementedAuthServiceServer() +} + +func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { + // If the following call pancis, it indicates UnimplementedAuthServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AuthService_ServiceDesc, srv) +} + +func _AuthService_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthZReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).Authorize(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_Authorize_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).Authorize(ctx, req.(*AuthZReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthNReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).Authenticate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_Authenticate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).Authenticate(ctx, req.(*AuthNReq)) + } + return interceptor(ctx, in, info, handler) +} + +// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AuthService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "auth.v1.AuthService", + HandlerType: (*AuthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Authorize", + Handler: _AuthService_Authorize_Handler, + }, + { + MethodName: "Authenticate", + Handler: _AuthService_Authenticate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "auth/v1/auth.proto", +} diff --git a/internal/grpc/channels/v1/channels.pb.go b/internal/grpc/channels/v1/channels.pb.go new file mode 100644 index 0000000000..9dab7701c9 --- /dev/null +++ b/internal/grpc/channels/v1/channels.pb.go @@ -0,0 +1,425 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: channels/v1/channels.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RemoveThingConnectionsReq 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"` +} + +func (x *RemoveThingConnectionsReq) Reset() { + *x = RemoveThingConnectionsReq{} + mi := &file_channels_v1_channels_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveThingConnectionsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveThingConnectionsReq) ProtoMessage() {} + +func (x *RemoveThingConnectionsReq) ProtoReflect() protoreflect.Message { + mi := &file_channels_v1_channels_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveThingConnectionsReq.ProtoReflect.Descriptor instead. +func (*RemoveThingConnectionsReq) Descriptor() ([]byte, []int) { + return file_channels_v1_channels_proto_rawDescGZIP(), []int{0} +} + +func (x *RemoveThingConnectionsReq) GetThingId() string { + if x != nil { + return x.ThingId + } + return "" +} + +type RemoveThingConnectionsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RemoveThingConnectionsRes) Reset() { + *x = RemoveThingConnectionsRes{} + mi := &file_channels_v1_channels_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveThingConnectionsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveThingConnectionsRes) ProtoMessage() {} + +func (x *RemoveThingConnectionsRes) ProtoReflect() protoreflect.Message { + mi := &file_channels_v1_channels_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveThingConnectionsRes.ProtoReflect.Descriptor instead. +func (*RemoveThingConnectionsRes) Descriptor() ([]byte, []int) { + return file_channels_v1_channels_proto_rawDescGZIP(), []int{1} +} + +type UnsetParentGroupFromChannelsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ParentGroupId string `protobuf:"bytes,1,opt,name=parent_group_id,json=parentGroupId,proto3" json:"parent_group_id,omitempty"` +} + +func (x *UnsetParentGroupFromChannelsReq) Reset() { + *x = UnsetParentGroupFromChannelsReq{} + mi := &file_channels_v1_channels_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetParentGroupFromChannelsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetParentGroupFromChannelsReq) ProtoMessage() {} + +func (x *UnsetParentGroupFromChannelsReq) ProtoReflect() protoreflect.Message { + mi := &file_channels_v1_channels_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnsetParentGroupFromChannelsReq.ProtoReflect.Descriptor instead. +func (*UnsetParentGroupFromChannelsReq) Descriptor() ([]byte, []int) { + return file_channels_v1_channels_proto_rawDescGZIP(), []int{2} +} + +func (x *UnsetParentGroupFromChannelsReq) GetParentGroupId() string { + if x != nil { + return x.ParentGroupId + } + return "" +} + +type UnsetParentGroupFromChannelsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *UnsetParentGroupFromChannelsRes) Reset() { + *x = UnsetParentGroupFromChannelsRes{} + mi := &file_channels_v1_channels_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetParentGroupFromChannelsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetParentGroupFromChannelsRes) ProtoMessage() {} + +func (x *UnsetParentGroupFromChannelsRes) ProtoReflect() protoreflect.Message { + mi := &file_channels_v1_channels_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnsetParentGroupFromChannelsRes.ProtoReflect.Descriptor instead. +func (*UnsetParentGroupFromChannelsRes) Descriptor() ([]byte, []int) { + return file_channels_v1_channels_proto_rawDescGZIP(), []int{3} +} + +type AuthzReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DomainId string `protobuf:"bytes,1,opt,name=domain_id,json=domainId,proto3" json:"domain_id,omitempty"` + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientType string `protobuf:"bytes,3,opt,name=client_type,json=clientType,proto3" json:"client_type,omitempty"` + ChannelId string `protobuf:"bytes,4,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + Permission string `protobuf:"bytes,5,opt,name=permission,proto3" json:"permission,omitempty"` +} + +func (x *AuthzReq) Reset() { + *x = AuthzReq{} + mi := &file_channels_v1_channels_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthzReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthzReq) ProtoMessage() {} + +func (x *AuthzReq) ProtoReflect() protoreflect.Message { + mi := &file_channels_v1_channels_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthzReq.ProtoReflect.Descriptor instead. +func (*AuthzReq) Descriptor() ([]byte, []int) { + return file_channels_v1_channels_proto_rawDescGZIP(), []int{4} +} + +func (x *AuthzReq) GetDomainId() string { + if x != nil { + return x.DomainId + } + return "" +} + +func (x *AuthzReq) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *AuthzReq) GetClientType() string { + if x != nil { + return x.ClientType + } + return "" +} + +func (x *AuthzReq) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +func (x *AuthzReq) GetPermission() string { + if x != nil { + return x.Permission + } + return "" +} + +type AuthzRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` +} + +func (x *AuthzRes) Reset() { + *x = AuthzRes{} + mi := &file_channels_v1_channels_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthzRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthzRes) ProtoMessage() {} + +func (x *AuthzRes) ProtoReflect() protoreflect.Message { + mi := &file_channels_v1_channels_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthzRes.ProtoReflect.Descriptor instead. +func (*AuthzRes) Descriptor() ([]byte, []int) { + return file_channels_v1_channels_proto_rawDescGZIP(), []int{5} +} + +func (x *AuthzRes) GetAuthorized() bool { + if x != nil { + return x.Authorized + } + return false +} + +var File_channels_v1_channels_proto protoreflect.FileDescriptor + +var file_channels_v1_channels_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x36, 0x0a, 0x19, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 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, 0x22, 0x1b, 0x0a, 0x19, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x68, 0x69, 0x6e, 0x67, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x22, 0x49, + 0x0a, 0x1f, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, + 0x71, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x21, 0x0a, 0x1f, 0x55, 0x6e, 0x73, + 0x65, 0x74, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x72, 0x6f, + 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x22, 0xa4, 0x01, 0x0a, + 0x08, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, + 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x22, 0x2a, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x73, 0x12, + 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x32, + 0xb8, 0x02, 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x3b, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, + 0x12, 0x15, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x73, 0x22, 0x00, + 0x12, 0x6a, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, + 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x1a, 0x26, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x7c, 0x0a, 0x1c, + 0x55, 0x6e, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x2c, 0x2e, 0x63, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x74, + 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x72, 0x6f, 0x6d, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x2c, 0x2e, 0x63, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x50, 0x61, + 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 0x73, 0x6d, 0x61, 0x63, 0x68, + 0x2f, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x73, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_channels_v1_channels_proto_rawDescOnce sync.Once + file_channels_v1_channels_proto_rawDescData = file_channels_v1_channels_proto_rawDesc +) + +func file_channels_v1_channels_proto_rawDescGZIP() []byte { + file_channels_v1_channels_proto_rawDescOnce.Do(func() { + file_channels_v1_channels_proto_rawDescData = protoimpl.X.CompressGZIP(file_channels_v1_channels_proto_rawDescData) + }) + return file_channels_v1_channels_proto_rawDescData +} + +var file_channels_v1_channels_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_channels_v1_channels_proto_goTypes = []any{ + (*RemoveThingConnectionsReq)(nil), // 0: channels.v1.RemoveThingConnectionsReq + (*RemoveThingConnectionsRes)(nil), // 1: channels.v1.RemoveThingConnectionsRes + (*UnsetParentGroupFromChannelsReq)(nil), // 2: channels.v1.UnsetParentGroupFromChannelsReq + (*UnsetParentGroupFromChannelsRes)(nil), // 3: channels.v1.UnsetParentGroupFromChannelsRes + (*AuthzReq)(nil), // 4: channels.v1.AuthzReq + (*AuthzRes)(nil), // 5: channels.v1.AuthzRes +} +var file_channels_v1_channels_proto_depIdxs = []int32{ + 4, // 0: channels.v1.ChannelsService.Authorize:input_type -> channels.v1.AuthzReq + 0, // 1: channels.v1.ChannelsService.RemoveThingConnections:input_type -> channels.v1.RemoveThingConnectionsReq + 2, // 2: channels.v1.ChannelsService.UnsetParentGroupFromChannels:input_type -> channels.v1.UnsetParentGroupFromChannelsReq + 5, // 3: channels.v1.ChannelsService.Authorize:output_type -> channels.v1.AuthzRes + 1, // 4: channels.v1.ChannelsService.RemoveThingConnections:output_type -> channels.v1.RemoveThingConnectionsRes + 3, // 5: channels.v1.ChannelsService.UnsetParentGroupFromChannels:output_type -> channels.v1.UnsetParentGroupFromChannelsRes + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_channels_v1_channels_proto_init() } +func file_channels_v1_channels_proto_init() { + if File_channels_v1_channels_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_channels_v1_channels_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_channels_v1_channels_proto_goTypes, + DependencyIndexes: file_channels_v1_channels_proto_depIdxs, + MessageInfos: file_channels_v1_channels_proto_msgTypes, + }.Build() + File_channels_v1_channels_proto = out.File + file_channels_v1_channels_proto_rawDesc = nil + file_channels_v1_channels_proto_goTypes = nil + file_channels_v1_channels_proto_depIdxs = nil +} diff --git a/internal/grpc/channels/v1/channels_grpc.pb.go b/internal/grpc/channels/v1/channels_grpc.pb.go new file mode 100644 index 0000000000..6225256a18 --- /dev/null +++ b/internal/grpc/channels/v1/channels_grpc.pb.go @@ -0,0 +1,200 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.2 +// source: channels/v1/channels.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ChannelsService_Authorize_FullMethodName = "/channels.v1.ChannelsService/Authorize" + ChannelsService_RemoveThingConnections_FullMethodName = "/channels.v1.ChannelsService/RemoveThingConnections" + ChannelsService_UnsetParentGroupFromChannels_FullMethodName = "/channels.v1.ChannelsService/UnsetParentGroupFromChannels" +) + +// ChannelsServiceClient is the client API for ChannelsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ChannelsServiceClient interface { + Authorize(ctx context.Context, in *AuthzReq, opts ...grpc.CallOption) (*AuthzRes, error) + RemoveThingConnections(ctx context.Context, in *RemoveThingConnectionsReq, opts ...grpc.CallOption) (*RemoveThingConnectionsRes, error) + UnsetParentGroupFromChannels(ctx context.Context, in *UnsetParentGroupFromChannelsReq, opts ...grpc.CallOption) (*UnsetParentGroupFromChannelsRes, error) +} + +type channelsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewChannelsServiceClient(cc grpc.ClientConnInterface) ChannelsServiceClient { + return &channelsServiceClient{cc} +} + +func (c *channelsServiceClient) Authorize(ctx context.Context, in *AuthzReq, opts ...grpc.CallOption) (*AuthzRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthzRes) + err := c.cc.Invoke(ctx, ChannelsService_Authorize_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelsServiceClient) RemoveThingConnections(ctx context.Context, in *RemoveThingConnectionsReq, opts ...grpc.CallOption) (*RemoveThingConnectionsRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveThingConnectionsRes) + err := c.cc.Invoke(ctx, ChannelsService_RemoveThingConnections_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelsServiceClient) UnsetParentGroupFromChannels(ctx context.Context, in *UnsetParentGroupFromChannelsReq, opts ...grpc.CallOption) (*UnsetParentGroupFromChannelsRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UnsetParentGroupFromChannelsRes) + err := c.cc.Invoke(ctx, ChannelsService_UnsetParentGroupFromChannels_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ChannelsServiceServer is the server API for ChannelsService service. +// All implementations must embed UnimplementedChannelsServiceServer +// for forward compatibility. +type ChannelsServiceServer interface { + Authorize(context.Context, *AuthzReq) (*AuthzRes, error) + RemoveThingConnections(context.Context, *RemoveThingConnectionsReq) (*RemoveThingConnectionsRes, error) + UnsetParentGroupFromChannels(context.Context, *UnsetParentGroupFromChannelsReq) (*UnsetParentGroupFromChannelsRes, error) + mustEmbedUnimplementedChannelsServiceServer() +} + +// UnimplementedChannelsServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedChannelsServiceServer struct{} + +func (UnimplementedChannelsServiceServer) Authorize(context.Context, *AuthzReq) (*AuthzRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented") +} +func (UnimplementedChannelsServiceServer) RemoveThingConnections(context.Context, *RemoveThingConnectionsReq) (*RemoveThingConnectionsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveThingConnections not implemented") +} +func (UnimplementedChannelsServiceServer) UnsetParentGroupFromChannels(context.Context, *UnsetParentGroupFromChannelsReq) (*UnsetParentGroupFromChannelsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnsetParentGroupFromChannels not implemented") +} +func (UnimplementedChannelsServiceServer) mustEmbedUnimplementedChannelsServiceServer() {} +func (UnimplementedChannelsServiceServer) testEmbeddedByValue() {} + +// UnsafeChannelsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ChannelsServiceServer will +// result in compilation errors. +type UnsafeChannelsServiceServer interface { + mustEmbedUnimplementedChannelsServiceServer() +} + +func RegisterChannelsServiceServer(s grpc.ServiceRegistrar, srv ChannelsServiceServer) { + // If the following call pancis, it indicates UnimplementedChannelsServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ChannelsService_ServiceDesc, srv) +} + +func _ChannelsService_Authorize_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthzReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelsServiceServer).Authorize(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelsService_Authorize_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelsServiceServer).Authorize(ctx, req.(*AuthzReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelsService_RemoveThingConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveThingConnectionsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelsServiceServer).RemoveThingConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelsService_RemoveThingConnections_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelsServiceServer).RemoveThingConnections(ctx, req.(*RemoveThingConnectionsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelsService_UnsetParentGroupFromChannels_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnsetParentGroupFromChannelsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelsServiceServer).UnsetParentGroupFromChannels(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelsService_UnsetParentGroupFromChannels_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelsServiceServer).UnsetParentGroupFromChannels(ctx, req.(*UnsetParentGroupFromChannelsReq)) + } + return interceptor(ctx, in, info, handler) +} + +// ChannelsService_ServiceDesc is the grpc.ServiceDesc for ChannelsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ChannelsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "channels.v1.ChannelsService", + HandlerType: (*ChannelsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Authorize", + Handler: _ChannelsService_Authorize_Handler, + }, + { + MethodName: "RemoveThingConnections", + Handler: _ChannelsService_RemoveThingConnections_Handler, + }, + { + MethodName: "UnsetParentGroupFromChannels", + Handler: _ChannelsService_UnsetParentGroupFromChannels_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "channels/v1/channels.proto", +} diff --git a/internal/grpc/common/v1/common.pb.go b/internal/grpc/common/v1/common.pb.go new file mode 100644 index 0000000000..fb7ae92a47 --- /dev/null +++ b/internal/grpc/common/v1/common.pb.go @@ -0,0 +1,648 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: common/v1/common.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RetrieveEntitiesReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"` +} + +func (x *RetrieveEntitiesReq) Reset() { + *x = RetrieveEntitiesReq{} + mi := &file_common_v1_common_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetrieveEntitiesReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetrieveEntitiesReq) ProtoMessage() {} + +func (x *RetrieveEntitiesReq) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetrieveEntitiesReq.ProtoReflect.Descriptor instead. +func (*RetrieveEntitiesReq) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{0} +} + +func (x *RetrieveEntitiesReq) GetIds() []string { + if x != nil { + return x.Ids + } + return nil +} + +type RetrieveEntitiesRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"` + Limit uint64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint64 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + Entities []*EntityBasic `protobuf:"bytes,4,rep,name=entities,proto3" json:"entities,omitempty"` +} + +func (x *RetrieveEntitiesRes) Reset() { + *x = RetrieveEntitiesRes{} + mi := &file_common_v1_common_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetrieveEntitiesRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetrieveEntitiesRes) ProtoMessage() {} + +func (x *RetrieveEntitiesRes) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetrieveEntitiesRes.ProtoReflect.Descriptor instead. +func (*RetrieveEntitiesRes) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{1} +} + +func (x *RetrieveEntitiesRes) GetTotal() uint64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *RetrieveEntitiesRes) GetLimit() uint64 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *RetrieveEntitiesRes) GetOffset() uint64 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *RetrieveEntitiesRes) GetEntities() []*EntityBasic { + if x != nil { + return x.Entities + } + return nil +} + +type RetrieveEntityReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *RetrieveEntityReq) Reset() { + *x = RetrieveEntityReq{} + mi := &file_common_v1_common_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetrieveEntityReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetrieveEntityReq) ProtoMessage() {} + +func (x *RetrieveEntityReq) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetrieveEntityReq.ProtoReflect.Descriptor instead. +func (*RetrieveEntityReq) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{2} +} + +func (x *RetrieveEntityReq) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type RetrieveEntityRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Entity *EntityBasic `protobuf:"bytes,1,opt,name=entity,proto3" json:"entity,omitempty"` +} + +func (x *RetrieveEntityRes) Reset() { + *x = RetrieveEntityRes{} + mi := &file_common_v1_common_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetrieveEntityRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetrieveEntityRes) ProtoMessage() {} + +func (x *RetrieveEntityRes) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetrieveEntityRes.ProtoReflect.Descriptor instead. +func (*RetrieveEntityRes) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{3} +} + +func (x *RetrieveEntityRes) GetEntity() *EntityBasic { + if x != nil { + return x.Entity + } + return nil +} + +type EntityBasic struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + DomainId string `protobuf:"bytes,2,opt,name=domain_id,json=domainId,proto3" json:"domain_id,omitempty"` + Status uint32 `protobuf:"varint,3,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *EntityBasic) Reset() { + *x = EntityBasic{} + mi := &file_common_v1_common_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EntityBasic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntityBasic) ProtoMessage() {} + +func (x *EntityBasic) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntityBasic.ProtoReflect.Descriptor instead. +func (*EntityBasic) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{4} +} + +func (x *EntityBasic) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *EntityBasic) GetDomainId() string { + if x != nil { + return x.DomainId + } + return "" +} + +func (x *EntityBasic) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +type AddConnectionsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Connections []*Connection `protobuf:"bytes,1,rep,name=connections,proto3" json:"connections,omitempty"` +} + +func (x *AddConnectionsReq) Reset() { + *x = AddConnectionsReq{} + mi := &file_common_v1_common_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddConnectionsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddConnectionsReq) ProtoMessage() {} + +func (x *AddConnectionsReq) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddConnectionsReq.ProtoReflect.Descriptor instead. +func (*AddConnectionsReq) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{5} +} + +func (x *AddConnectionsReq) GetConnections() []*Connection { + if x != nil { + return x.Connections + } + return nil +} + +type AddConnectionsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` +} + +func (x *AddConnectionsRes) Reset() { + *x = AddConnectionsRes{} + mi := &file_common_v1_common_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddConnectionsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddConnectionsRes) ProtoMessage() {} + +func (x *AddConnectionsRes) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddConnectionsRes.ProtoReflect.Descriptor instead. +func (*AddConnectionsRes) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{6} +} + +func (x *AddConnectionsRes) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +type RemoveConnectionsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Connections []*Connection `protobuf:"bytes,1,rep,name=connections,proto3" json:"connections,omitempty"` +} + +func (x *RemoveConnectionsReq) Reset() { + *x = RemoveConnectionsReq{} + mi := &file_common_v1_common_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveConnectionsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveConnectionsReq) ProtoMessage() {} + +func (x *RemoveConnectionsReq) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveConnectionsReq.ProtoReflect.Descriptor instead. +func (*RemoveConnectionsReq) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{7} +} + +func (x *RemoveConnectionsReq) GetConnections() []*Connection { + if x != nil { + return x.Connections + } + return nil +} + +type RemoveConnectionsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` +} + +func (x *RemoveConnectionsRes) Reset() { + *x = RemoveConnectionsRes{} + mi := &file_common_v1_common_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveConnectionsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveConnectionsRes) ProtoMessage() {} + +func (x *RemoveConnectionsRes) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveConnectionsRes.ProtoReflect.Descriptor instead. +func (*RemoveConnectionsRes) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{8} +} + +func (x *RemoveConnectionsRes) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +type Connection 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"` + DomainId string `protobuf:"bytes,3,opt,name=domain_id,json=domainId,proto3" json:"domain_id,omitempty"` +} + +func (x *Connection) Reset() { + *x = Connection{} + mi := &file_common_v1_common_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Connection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Connection) ProtoMessage() {} + +func (x *Connection) ProtoReflect() protoreflect.Message { + mi := &file_common_v1_common_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Connection.ProtoReflect.Descriptor instead. +func (*Connection) Descriptor() ([]byte, []int) { + return file_common_v1_common_proto_rawDescGZIP(), []int{9} +} + +func (x *Connection) GetThingId() string { + if x != nil { + return x.ThingId + } + return "" +} + +func (x *Connection) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +func (x *Connection) GetDomainId() string { + if x != nil { + return x.DomainId + } + return "" +} + +var File_common_v1_common_proto protoreflect.FileDescriptor + +var file_common_v1_common_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x76, 0x31, 0x22, 0x27, 0x0a, 0x13, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x8d, 0x01, 0x0a, + 0x13, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x32, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x42, 0x61, 0x73, + 0x69, 0x63, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x23, 0x0a, 0x11, + 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, + 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x22, 0x43, 0x0a, 0x11, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x42, 0x61, 0x73, 0x69, 0x63, 0x52, 0x06, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x52, 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x42, 0x61, 0x73, 0x69, 0x63, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4c, 0x0a, 0x11, 0x41, 0x64, + 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, + 0x37, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x23, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x0e, 0x0a, + 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x22, 0x4f, 0x0a, + 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x37, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x26, + 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x22, 0x63, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 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, 0x1b, + 0x0a, 0x09, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x42, 0x37, 0x5a, 0x35, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 0x73, 0x6d, 0x61, 0x63, + 0x68, 0x2f, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_v1_common_proto_rawDescOnce sync.Once + file_common_v1_common_proto_rawDescData = file_common_v1_common_proto_rawDesc +) + +func file_common_v1_common_proto_rawDescGZIP() []byte { + file_common_v1_common_proto_rawDescOnce.Do(func() { + file_common_v1_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_v1_common_proto_rawDescData) + }) + return file_common_v1_common_proto_rawDescData +} + +var file_common_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_common_v1_common_proto_goTypes = []any{ + (*RetrieveEntitiesReq)(nil), // 0: common.v1.RetrieveEntitiesReq + (*RetrieveEntitiesRes)(nil), // 1: common.v1.RetrieveEntitiesRes + (*RetrieveEntityReq)(nil), // 2: common.v1.RetrieveEntityReq + (*RetrieveEntityRes)(nil), // 3: common.v1.RetrieveEntityRes + (*EntityBasic)(nil), // 4: common.v1.EntityBasic + (*AddConnectionsReq)(nil), // 5: common.v1.AddConnectionsReq + (*AddConnectionsRes)(nil), // 6: common.v1.AddConnectionsRes + (*RemoveConnectionsReq)(nil), // 7: common.v1.RemoveConnectionsReq + (*RemoveConnectionsRes)(nil), // 8: common.v1.RemoveConnectionsRes + (*Connection)(nil), // 9: common.v1.Connection +} +var file_common_v1_common_proto_depIdxs = []int32{ + 4, // 0: common.v1.RetrieveEntitiesRes.entities:type_name -> common.v1.EntityBasic + 4, // 1: common.v1.RetrieveEntityRes.entity:type_name -> common.v1.EntityBasic + 9, // 2: common.v1.AddConnectionsReq.connections:type_name -> common.v1.Connection + 9, // 3: common.v1.RemoveConnectionsReq.connections:type_name -> common.v1.Connection + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_common_v1_common_proto_init() } +func file_common_v1_common_proto_init() { + if File_common_v1_common_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_common_v1_common_proto_rawDesc, + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_v1_common_proto_goTypes, + DependencyIndexes: file_common_v1_common_proto_depIdxs, + MessageInfos: file_common_v1_common_proto_msgTypes, + }.Build() + File_common_v1_common_proto = out.File + file_common_v1_common_proto_rawDesc = nil + file_common_v1_common_proto_goTypes = nil + file_common_v1_common_proto_depIdxs = nil +} diff --git a/internal/grpc/domains/v1/domains.pb.go b/internal/grpc/domains/v1/domains.pb.go new file mode 100644 index 0000000000..3ec787d6b8 --- /dev/null +++ b/internal/grpc/domains/v1/domains.pb.go @@ -0,0 +1,189 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: domains/v1/domains.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DeleteUserRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` +} + +func (x *DeleteUserRes) Reset() { + *x = DeleteUserRes{} + mi := &file_domains_v1_domains_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserRes) ProtoMessage() {} + +func (x *DeleteUserRes) ProtoReflect() protoreflect.Message { + mi := &file_domains_v1_domains_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserRes.ProtoReflect.Descriptor instead. +func (*DeleteUserRes) Descriptor() ([]byte, []int) { + return file_domains_v1_domains_proto_rawDescGZIP(), []int{0} +} + +func (x *DeleteUserRes) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +type DeleteUserReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *DeleteUserReq) Reset() { + *x = DeleteUserReq{} + mi := &file_domains_v1_domains_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserReq) ProtoMessage() {} + +func (x *DeleteUserReq) ProtoReflect() protoreflect.Message { + mi := &file_domains_v1_domains_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserReq.ProtoReflect.Descriptor instead. +func (*DeleteUserReq) Descriptor() ([]byte, []int) { + return file_domains_v1_domains_proto_rawDescGZIP(), []int{1} +} + +func (x *DeleteUserReq) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_domains_v1_domains_proto protoreflect.FileDescriptor + +var file_domains_v1_domains_proto_rawDesc = []byte{ + 0x0a, 0x18, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x29, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x22, 0x1f, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x32, 0x61, 0x0a, 0x0e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x4f, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, + 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x19, 0x2e, + 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x19, 0x2e, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x22, 0x00, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 0x73, 0x6d, 0x61, 0x63, 0x68, 0x2f, 0x6d, 0x61, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_domains_v1_domains_proto_rawDescOnce sync.Once + file_domains_v1_domains_proto_rawDescData = file_domains_v1_domains_proto_rawDesc +) + +func file_domains_v1_domains_proto_rawDescGZIP() []byte { + file_domains_v1_domains_proto_rawDescOnce.Do(func() { + file_domains_v1_domains_proto_rawDescData = protoimpl.X.CompressGZIP(file_domains_v1_domains_proto_rawDescData) + }) + return file_domains_v1_domains_proto_rawDescData +} + +var file_domains_v1_domains_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_domains_v1_domains_proto_goTypes = []any{ + (*DeleteUserRes)(nil), // 0: domains.v1.DeleteUserRes + (*DeleteUserReq)(nil), // 1: domains.v1.DeleteUserReq +} +var file_domains_v1_domains_proto_depIdxs = []int32{ + 1, // 0: domains.v1.DomainsService.DeleteUserFromDomains:input_type -> domains.v1.DeleteUserReq + 0, // 1: domains.v1.DomainsService.DeleteUserFromDomains:output_type -> domains.v1.DeleteUserRes + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_domains_v1_domains_proto_init() } +func file_domains_v1_domains_proto_init() { + if File_domains_v1_domains_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_domains_v1_domains_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_domains_v1_domains_proto_goTypes, + DependencyIndexes: file_domains_v1_domains_proto_depIdxs, + MessageInfos: file_domains_v1_domains_proto_msgTypes, + }.Build() + File_domains_v1_domains_proto = out.File + file_domains_v1_domains_proto_rawDesc = nil + file_domains_v1_domains_proto_goTypes = nil + file_domains_v1_domains_proto_depIdxs = nil +} diff --git a/internal/grpc/domains/v1/domains_grpc.pb.go b/internal/grpc/domains/v1/domains_grpc.pb.go new file mode 100644 index 0000000000..8ad9a28f36 --- /dev/null +++ b/internal/grpc/domains/v1/domains_grpc.pb.go @@ -0,0 +1,130 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.2 +// source: domains/v1/domains.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DomainsService_DeleteUserFromDomains_FullMethodName = "/domains.v1.DomainsService/DeleteUserFromDomains" +) + +// DomainsServiceClient is the client API for DomainsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// DomainsService is a service that provides access to domains +// functionalities for magistrala services. +type DomainsServiceClient interface { + DeleteUserFromDomains(ctx context.Context, in *DeleteUserReq, opts ...grpc.CallOption) (*DeleteUserRes, error) +} + +type domainsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDomainsServiceClient(cc grpc.ClientConnInterface) DomainsServiceClient { + return &domainsServiceClient{cc} +} + +func (c *domainsServiceClient) DeleteUserFromDomains(ctx context.Context, in *DeleteUserReq, opts ...grpc.CallOption) (*DeleteUserRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteUserRes) + err := c.cc.Invoke(ctx, DomainsService_DeleteUserFromDomains_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DomainsServiceServer is the server API for DomainsService service. +// All implementations must embed UnimplementedDomainsServiceServer +// for forward compatibility. +// +// DomainsService is a service that provides access to domains +// functionalities for magistrala services. +type DomainsServiceServer interface { + DeleteUserFromDomains(context.Context, *DeleteUserReq) (*DeleteUserRes, error) + mustEmbedUnimplementedDomainsServiceServer() +} + +// UnimplementedDomainsServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDomainsServiceServer struct{} + +func (UnimplementedDomainsServiceServer) DeleteUserFromDomains(context.Context, *DeleteUserReq) (*DeleteUserRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteUserFromDomains not implemented") +} +func (UnimplementedDomainsServiceServer) mustEmbedUnimplementedDomainsServiceServer() {} +func (UnimplementedDomainsServiceServer) testEmbeddedByValue() {} + +// UnsafeDomainsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DomainsServiceServer will +// result in compilation errors. +type UnsafeDomainsServiceServer interface { + mustEmbedUnimplementedDomainsServiceServer() +} + +func RegisterDomainsServiceServer(s grpc.ServiceRegistrar, srv DomainsServiceServer) { + // If the following call pancis, it indicates UnimplementedDomainsServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DomainsService_ServiceDesc, srv) +} + +func _DomainsService_DeleteUserFromDomains_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteUserReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DomainsServiceServer).DeleteUserFromDomains(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DomainsService_DeleteUserFromDomains_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DomainsServiceServer).DeleteUserFromDomains(ctx, req.(*DeleteUserReq)) + } + return interceptor(ctx, in, info, handler) +} + +// DomainsService_ServiceDesc is the grpc.ServiceDesc for DomainsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DomainsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "domains.v1.DomainsService", + HandlerType: (*DomainsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "DeleteUserFromDomains", + Handler: _DomainsService_DeleteUserFromDomains_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "domains/v1/domains.proto", +} diff --git a/internal/grpc/groups/v1/groups.pb.go b/internal/grpc/groups/v1/groups.pb.go new file mode 100644 index 0000000000..736e4e24ab --- /dev/null +++ b/internal/grpc/groups/v1/groups.pb.go @@ -0,0 +1,81 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: groups/v1/groups.proto + +package v1 + +import ( + v1 "github.com/absmach/magistrala/internal/grpc/common/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +var File_groups_v1_groups_proto protoreflect.FileDescriptor + +var file_groups_v1_groups_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x2e, 0x76, 0x31, 0x1a, 0x16, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0x5f, 0x0a, 0x0d, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4e, 0x0a, 0x0e, + 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1c, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x69, + 0x65, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x1c, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, + 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x42, 0x37, 0x5a, 0x35, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 0x73, 0x6d, 0x61, + 0x63, 0x68, 0x2f, 0x6d, 0x61, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var file_groups_v1_groups_proto_goTypes = []any{ + (*v1.RetrieveEntityReq)(nil), // 0: common.v1.RetrieveEntityReq + (*v1.RetrieveEntityRes)(nil), // 1: common.v1.RetrieveEntityRes +} +var file_groups_v1_groups_proto_depIdxs = []int32{ + 0, // 0: groups.v1.GroupsService.RetrieveEntity:input_type -> common.v1.RetrieveEntityReq + 1, // 1: groups.v1.GroupsService.RetrieveEntity:output_type -> common.v1.RetrieveEntityRes + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_groups_v1_groups_proto_init() } +func file_groups_v1_groups_proto_init() { + if File_groups_v1_groups_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_groups_v1_groups_proto_rawDesc, + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_groups_v1_groups_proto_goTypes, + DependencyIndexes: file_groups_v1_groups_proto_depIdxs, + }.Build() + File_groups_v1_groups_proto = out.File + file_groups_v1_groups_proto_rawDesc = nil + file_groups_v1_groups_proto_goTypes = nil + file_groups_v1_groups_proto_depIdxs = nil +} diff --git a/internal/grpc/groups/v1/groups_grpc.pb.go b/internal/grpc/groups/v1/groups_grpc.pb.go new file mode 100644 index 0000000000..0c47283a70 --- /dev/null +++ b/internal/grpc/groups/v1/groups_grpc.pb.go @@ -0,0 +1,131 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.2 +// source: groups/v1/groups.proto + +package v1 + +import ( + context "context" + v1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GroupsService_RetrieveEntity_FullMethodName = "/groups.v1.GroupsService/RetrieveEntity" +) + +// GroupsServiceClient is the client API for GroupsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// GroupssService is a service that provides groups functionalities +// for magistrala services. +type GroupsServiceClient interface { + RetrieveEntity(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption) (*v1.RetrieveEntityRes, error) +} + +type groupsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGroupsServiceClient(cc grpc.ClientConnInterface) GroupsServiceClient { + return &groupsServiceClient{cc} +} + +func (c *groupsServiceClient) RetrieveEntity(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption) (*v1.RetrieveEntityRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.RetrieveEntityRes) + err := c.cc.Invoke(ctx, GroupsService_RetrieveEntity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GroupsServiceServer is the server API for GroupsService service. +// All implementations must embed UnimplementedGroupsServiceServer +// for forward compatibility. +// +// GroupssService is a service that provides groups functionalities +// for magistrala services. +type GroupsServiceServer interface { + RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) + mustEmbedUnimplementedGroupsServiceServer() +} + +// UnimplementedGroupsServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGroupsServiceServer struct{} + +func (UnimplementedGroupsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented") +} +func (UnimplementedGroupsServiceServer) mustEmbedUnimplementedGroupsServiceServer() {} +func (UnimplementedGroupsServiceServer) testEmbeddedByValue() {} + +// UnsafeGroupsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GroupsServiceServer will +// result in compilation errors. +type UnsafeGroupsServiceServer interface { + mustEmbedUnimplementedGroupsServiceServer() +} + +func RegisterGroupsServiceServer(s grpc.ServiceRegistrar, srv GroupsServiceServer) { + // If the following call pancis, it indicates UnimplementedGroupsServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GroupsService_ServiceDesc, srv) +} + +func _GroupsService_RetrieveEntity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.RetrieveEntityReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupsServiceServer).RetrieveEntity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupsService_RetrieveEntity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupsServiceServer).RetrieveEntity(ctx, req.(*v1.RetrieveEntityReq)) + } + return interceptor(ctx, in, info, handler) +} + +// GroupsService_ServiceDesc is the grpc.ServiceDesc for GroupsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GroupsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "groups.v1.GroupsService", + HandlerType: (*GroupsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RetrieveEntity", + Handler: _GroupsService_RetrieveEntity_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "groups/v1/groups.proto", +} diff --git a/internal/grpc/things/v1/things.pb.go b/internal/grpc/things/v1/things.pb.go new file mode 100644 index 0000000000..f2a3379450 --- /dev/null +++ b/internal/grpc/things/v1/things.pb.go @@ -0,0 +1,443 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: things/v1/things.proto + +package v1 + +import ( + v1 "github.com/absmach/magistrala/internal/grpc/common/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AuthnReq 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"` + ThingKey string `protobuf:"bytes,2,opt,name=thing_key,json=thingKey,proto3" json:"thing_key,omitempty"` +} + +func (x *AuthnReq) Reset() { + *x = AuthnReq{} + mi := &file_things_v1_things_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthnReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthnReq) ProtoMessage() {} + +func (x *AuthnReq) ProtoReflect() protoreflect.Message { + mi := &file_things_v1_things_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthnReq.ProtoReflect.Descriptor instead. +func (*AuthnReq) Descriptor() ([]byte, []int) { + return file_things_v1_things_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthnReq) GetThingId() string { + if x != nil { + return x.ThingId + } + return "" +} + +func (x *AuthnReq) GetThingKey() string { + if x != nil { + return x.ThingKey + } + return "" +} + +type AuthnRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Authenticated bool `protobuf:"varint,1,opt,name=authenticated,proto3" json:"authenticated,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *AuthnRes) Reset() { + *x = AuthnRes{} + mi := &file_things_v1_things_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthnRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthnRes) ProtoMessage() {} + +func (x *AuthnRes) ProtoReflect() protoreflect.Message { + mi := &file_things_v1_things_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthnRes.ProtoReflect.Descriptor instead. +func (*AuthnRes) Descriptor() ([]byte, []int) { + return file_things_v1_things_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthnRes) GetAuthenticated() bool { + if x != nil { + return x.Authenticated + } + return false +} + +func (x *AuthnRes) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type RemoveChannelConnectionsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` +} + +func (x *RemoveChannelConnectionsReq) Reset() { + *x = RemoveChannelConnectionsReq{} + mi := &file_things_v1_things_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveChannelConnectionsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveChannelConnectionsReq) ProtoMessage() {} + +func (x *RemoveChannelConnectionsReq) ProtoReflect() protoreflect.Message { + mi := &file_things_v1_things_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveChannelConnectionsReq.ProtoReflect.Descriptor instead. +func (*RemoveChannelConnectionsReq) Descriptor() ([]byte, []int) { + return file_things_v1_things_proto_rawDescGZIP(), []int{2} +} + +func (x *RemoveChannelConnectionsReq) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +type RemoveChannelConnectionsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RemoveChannelConnectionsRes) Reset() { + *x = RemoveChannelConnectionsRes{} + mi := &file_things_v1_things_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveChannelConnectionsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveChannelConnectionsRes) ProtoMessage() {} + +func (x *RemoveChannelConnectionsRes) ProtoReflect() protoreflect.Message { + mi := &file_things_v1_things_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveChannelConnectionsRes.ProtoReflect.Descriptor instead. +func (*RemoveChannelConnectionsRes) Descriptor() ([]byte, []int) { + return file_things_v1_things_proto_rawDescGZIP(), []int{3} +} + +type UnsetParentGroupFromThingsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ParentGroupId string `protobuf:"bytes,1,opt,name=parent_group_id,json=parentGroupId,proto3" json:"parent_group_id,omitempty"` +} + +func (x *UnsetParentGroupFromThingsReq) Reset() { + *x = UnsetParentGroupFromThingsReq{} + mi := &file_things_v1_things_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetParentGroupFromThingsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetParentGroupFromThingsReq) ProtoMessage() {} + +func (x *UnsetParentGroupFromThingsReq) ProtoReflect() protoreflect.Message { + mi := &file_things_v1_things_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnsetParentGroupFromThingsReq.ProtoReflect.Descriptor instead. +func (*UnsetParentGroupFromThingsReq) Descriptor() ([]byte, []int) { + return file_things_v1_things_proto_rawDescGZIP(), []int{4} +} + +func (x *UnsetParentGroupFromThingsReq) GetParentGroupId() string { + if x != nil { + return x.ParentGroupId + } + return "" +} + +type UnsetParentGroupFromThingsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *UnsetParentGroupFromThingsRes) Reset() { + *x = UnsetParentGroupFromThingsRes{} + mi := &file_things_v1_things_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetParentGroupFromThingsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetParentGroupFromThingsRes) ProtoMessage() {} + +func (x *UnsetParentGroupFromThingsRes) ProtoReflect() protoreflect.Message { + mi := &file_things_v1_things_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnsetParentGroupFromThingsRes.ProtoReflect.Descriptor instead. +func (*UnsetParentGroupFromThingsRes) Descriptor() ([]byte, []int) { + return file_things_v1_things_proto_rawDescGZIP(), []int{5} +} + +var File_things_v1_things_proto protoreflect.FileDescriptor + +var file_things_v1_things_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x68, 0x69, 0x6e, + 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, + 0x2e, 0x76, 0x31, 0x1a, 0x16, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x42, 0x0a, 0x08, 0x41, + 0x75, 0x74, 0x68, 0x6e, 0x52, 0x65, 0x71, 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, 0x1b, 0x0a, 0x09, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x22, + 0x40, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x52, 0x65, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x61, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x22, 0x3c, 0x0a, 0x1b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x22, + 0x1d, 0x0a, 0x1b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x22, 0x47, + 0x0a, 0x1d, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x46, 0x72, 0x6f, 0x6d, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x12, + 0x26, 0x0a, 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x55, 0x6e, 0x73, 0x65, 0x74, + 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x72, 0x6f, 0x6d, 0x54, + 0x68, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x32, 0xfc, 0x04, 0x0a, 0x0d, 0x54, 0x68, 0x69, + 0x6e, 0x67, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x41, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x13, 0x2e, 0x74, 0x68, 0x69, + 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6e, 0x52, 0x65, 0x71, 0x1a, + 0x13, 0x2e, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, + 0x6e, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x0e, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, + 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x1c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x54, 0x0a, 0x10, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, + 0x76, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1e, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x4e, 0x0a, 0x0e, + 0x41, 0x64, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1c, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x11, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x1a, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x73, 0x22, 0x00, 0x12, 0x6c, 0x0a, 0x18, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x26, 0x2e, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x26, 0x2e, 0x74, 0x68, 0x69, 0x6e, + 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x22, 0x00, 0x12, 0x72, 0x0a, 0x1a, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x72, 0x6f, 0x6d, 0x54, 0x68, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x28, 0x2e, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, + 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x72, + 0x6f, 0x6d, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x28, 0x2e, 0x74, 0x68, + 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x72, 0x6f, 0x6d, 0x54, 0x68, 0x69, 0x6e, + 0x67, 0x73, 0x52, 0x65, 0x73, 0x22, 0x00, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 0x73, 0x6d, 0x61, 0x63, 0x68, 0x2f, 0x6d, 0x61, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2f, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_things_v1_things_proto_rawDescOnce sync.Once + file_things_v1_things_proto_rawDescData = file_things_v1_things_proto_rawDesc +) + +func file_things_v1_things_proto_rawDescGZIP() []byte { + file_things_v1_things_proto_rawDescOnce.Do(func() { + file_things_v1_things_proto_rawDescData = protoimpl.X.CompressGZIP(file_things_v1_things_proto_rawDescData) + }) + return file_things_v1_things_proto_rawDescData +} + +var file_things_v1_things_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_things_v1_things_proto_goTypes = []any{ + (*AuthnReq)(nil), // 0: things.v1.AuthnReq + (*AuthnRes)(nil), // 1: things.v1.AuthnRes + (*RemoveChannelConnectionsReq)(nil), // 2: things.v1.RemoveChannelConnectionsReq + (*RemoveChannelConnectionsRes)(nil), // 3: things.v1.RemoveChannelConnectionsRes + (*UnsetParentGroupFromThingsReq)(nil), // 4: things.v1.UnsetParentGroupFromThingsReq + (*UnsetParentGroupFromThingsRes)(nil), // 5: things.v1.UnsetParentGroupFromThingsRes + (*v1.RetrieveEntityReq)(nil), // 6: common.v1.RetrieveEntityReq + (*v1.RetrieveEntitiesReq)(nil), // 7: common.v1.RetrieveEntitiesReq + (*v1.AddConnectionsReq)(nil), // 8: common.v1.AddConnectionsReq + (*v1.RemoveConnectionsReq)(nil), // 9: common.v1.RemoveConnectionsReq + (*v1.RetrieveEntityRes)(nil), // 10: common.v1.RetrieveEntityRes + (*v1.RetrieveEntitiesRes)(nil), // 11: common.v1.RetrieveEntitiesRes + (*v1.AddConnectionsRes)(nil), // 12: common.v1.AddConnectionsRes + (*v1.RemoveConnectionsRes)(nil), // 13: common.v1.RemoveConnectionsRes +} +var file_things_v1_things_proto_depIdxs = []int32{ + 0, // 0: things.v1.ThingsService.Authenticate:input_type -> things.v1.AuthnReq + 6, // 1: things.v1.ThingsService.RetrieveEntity:input_type -> common.v1.RetrieveEntityReq + 7, // 2: things.v1.ThingsService.RetrieveEntities:input_type -> common.v1.RetrieveEntitiesReq + 8, // 3: things.v1.ThingsService.AddConnections:input_type -> common.v1.AddConnectionsReq + 9, // 4: things.v1.ThingsService.RemoveConnections:input_type -> common.v1.RemoveConnectionsReq + 2, // 5: things.v1.ThingsService.RemoveChannelConnections:input_type -> things.v1.RemoveChannelConnectionsReq + 4, // 6: things.v1.ThingsService.UnsetParentGroupFromThings:input_type -> things.v1.UnsetParentGroupFromThingsReq + 1, // 7: things.v1.ThingsService.Authenticate:output_type -> things.v1.AuthnRes + 10, // 8: things.v1.ThingsService.RetrieveEntity:output_type -> common.v1.RetrieveEntityRes + 11, // 9: things.v1.ThingsService.RetrieveEntities:output_type -> common.v1.RetrieveEntitiesRes + 12, // 10: things.v1.ThingsService.AddConnections:output_type -> common.v1.AddConnectionsRes + 13, // 11: things.v1.ThingsService.RemoveConnections:output_type -> common.v1.RemoveConnectionsRes + 3, // 12: things.v1.ThingsService.RemoveChannelConnections:output_type -> things.v1.RemoveChannelConnectionsRes + 5, // 13: things.v1.ThingsService.UnsetParentGroupFromThings:output_type -> things.v1.UnsetParentGroupFromThingsRes + 7, // [7:14] is the sub-list for method output_type + 0, // [0:7] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_things_v1_things_proto_init() } +func file_things_v1_things_proto_init() { + if File_things_v1_things_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_things_v1_things_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_things_v1_things_proto_goTypes, + DependencyIndexes: file_things_v1_things_proto_depIdxs, + MessageInfos: file_things_v1_things_proto_msgTypes, + }.Build() + File_things_v1_things_proto = out.File + file_things_v1_things_proto_rawDesc = nil + file_things_v1_things_proto_goTypes = nil + file_things_v1_things_proto_depIdxs = nil +} diff --git a/internal/grpc/things/v1/things_grpc.pb.go b/internal/grpc/things/v1/things_grpc.pb.go new file mode 100644 index 0000000000..ae8be865a3 --- /dev/null +++ b/internal/grpc/things/v1/things_grpc.pb.go @@ -0,0 +1,361 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.2 +// source: things/v1/things.proto + +package v1 + +import ( + context "context" + v1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ThingsService_Authenticate_FullMethodName = "/things.v1.ThingsService/Authenticate" + ThingsService_RetrieveEntity_FullMethodName = "/things.v1.ThingsService/RetrieveEntity" + ThingsService_RetrieveEntities_FullMethodName = "/things.v1.ThingsService/RetrieveEntities" + ThingsService_AddConnections_FullMethodName = "/things.v1.ThingsService/AddConnections" + ThingsService_RemoveConnections_FullMethodName = "/things.v1.ThingsService/RemoveConnections" + ThingsService_RemoveChannelConnections_FullMethodName = "/things.v1.ThingsService/RemoveChannelConnections" + ThingsService_UnsetParentGroupFromThings_FullMethodName = "/things.v1.ThingsService/UnsetParentGroupFromThings" +) + +// ThingsServiceClient is the client API for ThingsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// ThingsService is a service that provides things authorization functionalities +// for magistrala services. +type ThingsServiceClient interface { + // Authorize checks if the thing is authorized to perform + Authenticate(ctx context.Context, in *AuthnReq, opts ...grpc.CallOption) (*AuthnRes, error) + RetrieveEntity(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption) (*v1.RetrieveEntityRes, error) + RetrieveEntities(ctx context.Context, in *v1.RetrieveEntitiesReq, opts ...grpc.CallOption) (*v1.RetrieveEntitiesRes, error) + AddConnections(ctx context.Context, in *v1.AddConnectionsReq, opts ...grpc.CallOption) (*v1.AddConnectionsRes, error) + RemoveConnections(ctx context.Context, in *v1.RemoveConnectionsReq, opts ...grpc.CallOption) (*v1.RemoveConnectionsRes, error) + RemoveChannelConnections(ctx context.Context, in *RemoveChannelConnectionsReq, opts ...grpc.CallOption) (*RemoveChannelConnectionsRes, error) + UnsetParentGroupFromThings(ctx context.Context, in *UnsetParentGroupFromThingsReq, opts ...grpc.CallOption) (*UnsetParentGroupFromThingsRes, error) +} + +type thingsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewThingsServiceClient(cc grpc.ClientConnInterface) ThingsServiceClient { + return &thingsServiceClient{cc} +} + +func (c *thingsServiceClient) Authenticate(ctx context.Context, in *AuthnReq, opts ...grpc.CallOption) (*AuthnRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthnRes) + err := c.cc.Invoke(ctx, ThingsService_Authenticate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *thingsServiceClient) RetrieveEntity(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption) (*v1.RetrieveEntityRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.RetrieveEntityRes) + err := c.cc.Invoke(ctx, ThingsService_RetrieveEntity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *thingsServiceClient) RetrieveEntities(ctx context.Context, in *v1.RetrieveEntitiesReq, opts ...grpc.CallOption) (*v1.RetrieveEntitiesRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.RetrieveEntitiesRes) + err := c.cc.Invoke(ctx, ThingsService_RetrieveEntities_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *thingsServiceClient) AddConnections(ctx context.Context, in *v1.AddConnectionsReq, opts ...grpc.CallOption) (*v1.AddConnectionsRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.AddConnectionsRes) + err := c.cc.Invoke(ctx, ThingsService_AddConnections_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *thingsServiceClient) RemoveConnections(ctx context.Context, in *v1.RemoveConnectionsReq, opts ...grpc.CallOption) (*v1.RemoveConnectionsRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.RemoveConnectionsRes) + err := c.cc.Invoke(ctx, ThingsService_RemoveConnections_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *thingsServiceClient) RemoveChannelConnections(ctx context.Context, in *RemoveChannelConnectionsReq, opts ...grpc.CallOption) (*RemoveChannelConnectionsRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveChannelConnectionsRes) + err := c.cc.Invoke(ctx, ThingsService_RemoveChannelConnections_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *thingsServiceClient) UnsetParentGroupFromThings(ctx context.Context, in *UnsetParentGroupFromThingsReq, opts ...grpc.CallOption) (*UnsetParentGroupFromThingsRes, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UnsetParentGroupFromThingsRes) + err := c.cc.Invoke(ctx, ThingsService_UnsetParentGroupFromThings_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ThingsServiceServer is the server API for ThingsService service. +// All implementations must embed UnimplementedThingsServiceServer +// for forward compatibility. +// +// ThingsService is a service that provides things authorization functionalities +// for magistrala services. +type ThingsServiceServer interface { + // Authorize checks if the thing is authorized to perform + Authenticate(context.Context, *AuthnReq) (*AuthnRes, error) + RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) + RetrieveEntities(context.Context, *v1.RetrieveEntitiesReq) (*v1.RetrieveEntitiesRes, error) + AddConnections(context.Context, *v1.AddConnectionsReq) (*v1.AddConnectionsRes, error) + RemoveConnections(context.Context, *v1.RemoveConnectionsReq) (*v1.RemoveConnectionsRes, error) + RemoveChannelConnections(context.Context, *RemoveChannelConnectionsReq) (*RemoveChannelConnectionsRes, error) + UnsetParentGroupFromThings(context.Context, *UnsetParentGroupFromThingsReq) (*UnsetParentGroupFromThingsRes, error) + mustEmbedUnimplementedThingsServiceServer() +} + +// UnimplementedThingsServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedThingsServiceServer struct{} + +func (UnimplementedThingsServiceServer) Authenticate(context.Context, *AuthnReq) (*AuthnRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented") +} +func (UnimplementedThingsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented") +} +func (UnimplementedThingsServiceServer) RetrieveEntities(context.Context, *v1.RetrieveEntitiesReq) (*v1.RetrieveEntitiesRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntities not implemented") +} +func (UnimplementedThingsServiceServer) AddConnections(context.Context, *v1.AddConnectionsReq) (*v1.AddConnectionsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddConnections not implemented") +} +func (UnimplementedThingsServiceServer) RemoveConnections(context.Context, *v1.RemoveConnectionsReq) (*v1.RemoveConnectionsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveConnections not implemented") +} +func (UnimplementedThingsServiceServer) RemoveChannelConnections(context.Context, *RemoveChannelConnectionsReq) (*RemoveChannelConnectionsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveChannelConnections not implemented") +} +func (UnimplementedThingsServiceServer) UnsetParentGroupFromThings(context.Context, *UnsetParentGroupFromThingsReq) (*UnsetParentGroupFromThingsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnsetParentGroupFromThings not implemented") +} +func (UnimplementedThingsServiceServer) mustEmbedUnimplementedThingsServiceServer() {} +func (UnimplementedThingsServiceServer) testEmbeddedByValue() {} + +// UnsafeThingsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ThingsServiceServer will +// result in compilation errors. +type UnsafeThingsServiceServer interface { + mustEmbedUnimplementedThingsServiceServer() +} + +func RegisterThingsServiceServer(s grpc.ServiceRegistrar, srv ThingsServiceServer) { + // If the following call pancis, it indicates UnimplementedThingsServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ThingsService_ServiceDesc, srv) +} + +func _ThingsService_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthnReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThingsServiceServer).Authenticate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThingsService_Authenticate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThingsServiceServer).Authenticate(ctx, req.(*AuthnReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ThingsService_RetrieveEntity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.RetrieveEntityReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThingsServiceServer).RetrieveEntity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThingsService_RetrieveEntity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThingsServiceServer).RetrieveEntity(ctx, req.(*v1.RetrieveEntityReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ThingsService_RetrieveEntities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.RetrieveEntitiesReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThingsServiceServer).RetrieveEntities(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThingsService_RetrieveEntities_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThingsServiceServer).RetrieveEntities(ctx, req.(*v1.RetrieveEntitiesReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ThingsService_AddConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.AddConnectionsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThingsServiceServer).AddConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThingsService_AddConnections_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThingsServiceServer).AddConnections(ctx, req.(*v1.AddConnectionsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ThingsService_RemoveConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.RemoveConnectionsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThingsServiceServer).RemoveConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThingsService_RemoveConnections_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThingsServiceServer).RemoveConnections(ctx, req.(*v1.RemoveConnectionsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ThingsService_RemoveChannelConnections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveChannelConnectionsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThingsServiceServer).RemoveChannelConnections(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThingsService_RemoveChannelConnections_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThingsServiceServer).RemoveChannelConnections(ctx, req.(*RemoveChannelConnectionsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _ThingsService_UnsetParentGroupFromThings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnsetParentGroupFromThingsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ThingsServiceServer).UnsetParentGroupFromThings(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ThingsService_UnsetParentGroupFromThings_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ThingsServiceServer).UnsetParentGroupFromThings(ctx, req.(*UnsetParentGroupFromThingsReq)) + } + return interceptor(ctx, in, info, handler) +} + +// ThingsService_ServiceDesc is the grpc.ServiceDesc for ThingsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ThingsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "things.v1.ThingsService", + HandlerType: (*ThingsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Authenticate", + Handler: _ThingsService_Authenticate_Handler, + }, + { + MethodName: "RetrieveEntity", + Handler: _ThingsService_RetrieveEntity_Handler, + }, + { + MethodName: "RetrieveEntities", + Handler: _ThingsService_RetrieveEntities_Handler, + }, + { + MethodName: "AddConnections", + Handler: _ThingsService_AddConnections_Handler, + }, + { + MethodName: "RemoveConnections", + Handler: _ThingsService_RemoveConnections_Handler, + }, + { + MethodName: "RemoveChannelConnections", + Handler: _ThingsService_RemoveChannelConnections_Handler, + }, + { + MethodName: "UnsetParentGroupFromThings", + Handler: _ThingsService_UnsetParentGroupFromThings_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "things/v1/things.proto", +} diff --git a/internal/grpc/token/v1/token.pb.go b/internal/grpc/token/v1/token.pb.go new file mode 100644 index 0000000000..3b70547057 --- /dev/null +++ b/internal/grpc/token/v1/token.pb.go @@ -0,0 +1,276 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: token/v1/token.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type IssueReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Type uint32 `protobuf:"varint,3,opt,name=type,proto3" json:"type,omitempty"` +} + +func (x *IssueReq) Reset() { + *x = IssueReq{} + mi := &file_token_v1_token_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IssueReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IssueReq) ProtoMessage() {} + +func (x *IssueReq) ProtoReflect() protoreflect.Message { + mi := &file_token_v1_token_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IssueReq.ProtoReflect.Descriptor instead. +func (*IssueReq) Descriptor() ([]byte, []int) { + return file_token_v1_token_proto_rawDescGZIP(), []int{0} +} + +func (x *IssueReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *IssueReq) GetType() uint32 { + if x != nil { + return x.Type + } + return 0 +} + +type RefreshReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RefreshToken string `protobuf:"bytes,1,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` +} + +func (x *RefreshReq) Reset() { + *x = RefreshReq{} + mi := &file_token_v1_token_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshReq) ProtoMessage() {} + +func (x *RefreshReq) ProtoReflect() protoreflect.Message { + mi := &file_token_v1_token_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RefreshReq.ProtoReflect.Descriptor instead. +func (*RefreshReq) Descriptor() ([]byte, []int) { + return file_token_v1_token_proto_rawDescGZIP(), []int{1} +} + +func (x *RefreshReq) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" +} + +// If a token is not carrying any information itself, the type +// field can be used to determine how to validate the token. +// Also, different tokens can be encoded in different ways. +type Token struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AccessToken string `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"` + RefreshToken *string `protobuf:"bytes,2,opt,name=refreshToken,proto3,oneof" json:"refreshToken,omitempty"` + AccessType string `protobuf:"bytes,3,opt,name=accessType,proto3" json:"accessType,omitempty"` +} + +func (x *Token) Reset() { + *x = Token{} + mi := &file_token_v1_token_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Token) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Token) ProtoMessage() {} + +func (x *Token) ProtoReflect() protoreflect.Message { + mi := &file_token_v1_token_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Token.ProtoReflect.Descriptor instead. +func (*Token) Descriptor() ([]byte, []int) { + return file_token_v1_token_proto_rawDescGZIP(), []int{2} +} + +func (x *Token) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *Token) GetRefreshToken() string { + if x != nil && x.RefreshToken != nil { + return *x.RefreshToken + } + return "" +} + +func (x *Token) GetAccessType() string { + if x != nil { + return x.AccessType + } + return "" +} + +var File_token_v1_token_proto protoreflect.FileDescriptor + +var file_token_v1_token_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x76, 0x31, + 0x22, 0x37, 0x0a, 0x08, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x31, 0x0a, 0x0a, 0x52, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, + 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x83, 0x01, 0x0a, + 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x27, 0x0a, 0x0c, 0x72, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x88, 0x01, + 0x01, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, + 0x65, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x32, 0x72, 0x0a, 0x0c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x49, 0x73, 0x73, 0x75, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x71, 0x1a, + 0x0f, 0x2e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x22, 0x00, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x14, 0x2e, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, + 0x52, 0x65, 0x71, 0x1a, 0x0f, 0x2e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x62, 0x73, 0x6d, 0x61, 0x63, 0x68, 0x2f, 0x6d, 0x61, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x6c, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2f, 0x76, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_token_v1_token_proto_rawDescOnce sync.Once + file_token_v1_token_proto_rawDescData = file_token_v1_token_proto_rawDesc +) + +func file_token_v1_token_proto_rawDescGZIP() []byte { + file_token_v1_token_proto_rawDescOnce.Do(func() { + file_token_v1_token_proto_rawDescData = protoimpl.X.CompressGZIP(file_token_v1_token_proto_rawDescData) + }) + return file_token_v1_token_proto_rawDescData +} + +var file_token_v1_token_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_token_v1_token_proto_goTypes = []any{ + (*IssueReq)(nil), // 0: token.v1.IssueReq + (*RefreshReq)(nil), // 1: token.v1.RefreshReq + (*Token)(nil), // 2: token.v1.Token +} +var file_token_v1_token_proto_depIdxs = []int32{ + 0, // 0: token.v1.TokenService.Issue:input_type -> token.v1.IssueReq + 1, // 1: token.v1.TokenService.Refresh:input_type -> token.v1.RefreshReq + 2, // 2: token.v1.TokenService.Issue:output_type -> token.v1.Token + 2, // 3: token.v1.TokenService.Refresh:output_type -> token.v1.Token + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_token_v1_token_proto_init() } +func file_token_v1_token_proto_init() { + if File_token_v1_token_proto != nil { + return + } + file_token_v1_token_proto_msgTypes[2].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_token_v1_token_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_token_v1_token_proto_goTypes, + DependencyIndexes: file_token_v1_token_proto_depIdxs, + MessageInfos: file_token_v1_token_proto_msgTypes, + }.Build() + File_token_v1_token_proto = out.File + file_token_v1_token_proto_rawDesc = nil + file_token_v1_token_proto_goTypes = nil + file_token_v1_token_proto_depIdxs = nil +} diff --git a/internal/grpc/token/v1/token_grpc.pb.go b/internal/grpc/token/v1/token_grpc.pb.go new file mode 100644 index 0000000000..777d124dd8 --- /dev/null +++ b/internal/grpc/token/v1/token_grpc.pb.go @@ -0,0 +1,162 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.2 +// source: token/v1/token.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + TokenService_Issue_FullMethodName = "/token.v1.TokenService/Issue" + TokenService_Refresh_FullMethodName = "/token.v1.TokenService/Refresh" +) + +// TokenServiceClient is the client API for TokenService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type TokenServiceClient interface { + Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error) + Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*Token, error) +} + +type tokenServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewTokenServiceClient(cc grpc.ClientConnInterface) TokenServiceClient { + return &tokenServiceClient{cc} +} + +func (c *tokenServiceClient) Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Token) + err := c.cc.Invoke(ctx, TokenService_Issue_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tokenServiceClient) Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*Token, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Token) + err := c.cc.Invoke(ctx, TokenService_Refresh_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TokenServiceServer is the server API for TokenService service. +// All implementations must embed UnimplementedTokenServiceServer +// for forward compatibility. +type TokenServiceServer interface { + Issue(context.Context, *IssueReq) (*Token, error) + Refresh(context.Context, *RefreshReq) (*Token, error) + mustEmbedUnimplementedTokenServiceServer() +} + +// UnimplementedTokenServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTokenServiceServer struct{} + +func (UnimplementedTokenServiceServer) Issue(context.Context, *IssueReq) (*Token, error) { + return nil, status.Errorf(codes.Unimplemented, "method Issue not implemented") +} +func (UnimplementedTokenServiceServer) Refresh(context.Context, *RefreshReq) (*Token, error) { + return nil, status.Errorf(codes.Unimplemented, "method Refresh not implemented") +} +func (UnimplementedTokenServiceServer) mustEmbedUnimplementedTokenServiceServer() {} +func (UnimplementedTokenServiceServer) testEmbeddedByValue() {} + +// UnsafeTokenServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TokenServiceServer will +// result in compilation errors. +type UnsafeTokenServiceServer interface { + mustEmbedUnimplementedTokenServiceServer() +} + +func RegisterTokenServiceServer(s grpc.ServiceRegistrar, srv TokenServiceServer) { + // If the following call pancis, it indicates UnimplementedTokenServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&TokenService_ServiceDesc, srv) +} + +func _TokenService_Issue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IssueReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TokenServiceServer).Issue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TokenService_Issue_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TokenServiceServer).Issue(ctx, req.(*IssueReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _TokenService_Refresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RefreshReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TokenServiceServer).Refresh(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TokenService_Refresh_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TokenServiceServer).Refresh(ctx, req.(*RefreshReq)) + } + return interceptor(ctx, in, info, handler) +} + +// TokenService_ServiceDesc is the grpc.ServiceDesc for TokenService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TokenService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "token.v1.TokenService", + HandlerType: (*TokenServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Issue", + Handler: _TokenService_Issue_Handler, + }, + { + MethodName: "Refresh", + Handler: _TokenService_Refresh_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "token/v1/token.proto", +} diff --git a/internal/proto/auth/v1/auth.proto b/internal/proto/auth/v1/auth.proto new file mode 100644 index 0000000000..74bb04fcc0 --- /dev/null +++ b/internal/proto/auth/v1/auth.proto @@ -0,0 +1,42 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package auth.v1; +option go_package = "github.com/absmach/magistrala/internal/grpc/auth/v1"; + +// AuthService is a service that provides authentication and authorization +// functionalities for magistrala services. +service AuthService { + rpc Authorize(AuthZReq) returns (AuthZRes) {} + rpc Authenticate(AuthNReq) returns (AuthNRes) {} +} + + +message AuthNReq { + string token = 1; +} + +message AuthNRes { + string id = 1; // IMPROVEMENT NOTE: change name from "id" to "subject" , sub in jwt = user id + domain id // + string user_id = 2; // user id + string domain_id = 3; // domain id +} + +message AuthZReq { + string domain = 1; // Domain + string subject_type = 2; // Thing or User + string subject_kind = 3; // ID or Token + string subject_relation = 4; // Subject relation + string subject = 5; // Subject value (id or token, depending on kind) + string relation = 6; // Relation to filter + string permission = 7; // Action + string object = 8; // Object ID + string object_type = 9; // Thing, User, Group +} + +message AuthZRes { + bool authorized = 1; + string id = 2; +} diff --git a/internal/proto/channels/v1/channels.proto b/internal/proto/channels/v1/channels.proto new file mode 100644 index 0000000000..398c70f82c --- /dev/null +++ b/internal/proto/channels/v1/channels.proto @@ -0,0 +1,38 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package channels.v1; +option go_package = "github.com/absmach/magistrala/internal/grpc/channels/v1"; + + +service ChannelsService { + rpc Authorize(AuthzReq)returns(AuthzRes) {} + rpc RemoveThingConnections(RemoveThingConnectionsReq)returns(RemoveThingConnectionsRes) {} + rpc UnsetParentGroupFromChannels(UnsetParentGroupFromChannelsReq)returns(UnsetParentGroupFromChannelsRes){} +} + +message RemoveThingConnectionsReq { + string thing_id = 1; +} + +message RemoveThingConnectionsRes {} + +message UnsetParentGroupFromChannelsReq { + string parent_group_id = 1; +} + +message UnsetParentGroupFromChannelsRes {} + +message AuthzReq { + string domain_id = 1; + string client_id = 2; + string client_type = 3; + string channel_id = 4; + string permission = 5; +} + +message AuthzRes { + bool authorized = 1; +} diff --git a/internal/proto/common/v1/common.proto b/internal/proto/common/v1/common.proto new file mode 100644 index 0000000000..5e7b993542 --- /dev/null +++ b/internal/proto/common/v1/common.proto @@ -0,0 +1,55 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package common.v1; +option go_package = "github.com/absmach/magistrala/internal/grpc/common/v1"; + + +message RetrieveEntitiesReq { + repeated string ids = 1; +} + +message RetrieveEntitiesRes { + uint64 total = 1; + uint64 limit = 2; + uint64 offset =3; + repeated EntityBasic entities = 4; +} + +message RetrieveEntityReq{ + string id = 1; +} + +message RetrieveEntityRes { + EntityBasic entity = 1; +} + +message EntityBasic { + string id = 1; + string domain_id = 2; + uint32 status = 3; +} + +message AddConnectionsReq { + repeated Connection connections = 1; +} + +message AddConnectionsRes { + bool ok = 1; +} + +message RemoveConnectionsReq { + repeated Connection connections = 1; +} + +message RemoveConnectionsRes { + bool ok = 1; +} + +message Connection { + string thing_id = 1; + string channel_id = 2; + string domain_id = 3; +} diff --git a/internal/proto/domains/v1/domains.proto b/internal/proto/domains/v1/domains.proto new file mode 100644 index 0000000000..716bfd2849 --- /dev/null +++ b/internal/proto/domains/v1/domains.proto @@ -0,0 +1,20 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package domains.v1; +option go_package = "github.com/absmach/magistrala/internal/grpc/domains/v1"; + + +// DomainsService is a service that provides access to domains +// functionalities for magistrala services. +service DomainsService { + rpc DeleteUserFromDomains(DeleteUserReq) returns (DeleteUserRes) {} +} + +message DeleteUserRes { bool deleted = 1; } + +message DeleteUserReq{ + string id = 1; +} diff --git a/internal/proto/groups/v1/groups.proto b/internal/proto/groups/v1/groups.proto new file mode 100644 index 0000000000..d703de76d1 --- /dev/null +++ b/internal/proto/groups/v1/groups.proto @@ -0,0 +1,15 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package groups.v1; +option go_package = "github.com/absmach/magistrala/internal/grpc/groups/v1"; + +import "common/v1/common.proto"; + +// GroupssService is a service that provides groups functionalities +// for magistrala services. +service GroupsService { + rpc RetrieveEntity(common.v1.RetrieveEntityReq) returns (common.v1.RetrieveEntityRes) {} +} diff --git a/internal/proto/things/v1/things.proto b/internal/proto/things/v1/things.proto new file mode 100644 index 0000000000..fbc4b61b7c --- /dev/null +++ b/internal/proto/things/v1/things.proto @@ -0,0 +1,45 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package things.v1; +option go_package = "github.com/absmach/magistrala/internal/grpc/things/v1"; + +import "common/v1/common.proto"; + +// ThingsService is a service that provides things authorization functionalities +// for magistrala services. +service ThingsService { + // Authorize checks if the thing is authorized to perform + rpc Authenticate(AuthnReq) returns (AuthnRes) {} + rpc RetrieveEntity(common.v1.RetrieveEntityReq) returns (common.v1.RetrieveEntityRes) {} + rpc RetrieveEntities(common.v1.RetrieveEntitiesReq) returns (common.v1.RetrieveEntitiesRes) {} + rpc AddConnections(common.v1.AddConnectionsReq)returns(common.v1.AddConnectionsRes) {} + rpc RemoveConnections(common.v1.RemoveConnectionsReq)returns(common.v1.RemoveConnectionsRes) {} + rpc RemoveChannelConnections(RemoveChannelConnectionsReq)returns(RemoveChannelConnectionsRes) {} + rpc UnsetParentGroupFromThings(UnsetParentGroupFromThingsReq)returns(UnsetParentGroupFromThingsRes){} +} + + +message AuthnReq { + string thing_id = 1; + string thing_key = 2; +} + +message AuthnRes { + bool authenticated = 1; + string id = 2; +} + +message RemoveChannelConnectionsReq { + string channel_id = 1; +} + +message RemoveChannelConnectionsRes {} + +message UnsetParentGroupFromThingsReq { + string parent_group_id = 1; +} + +message UnsetParentGroupFromThingsRes {} diff --git a/internal/proto/token/v1/token.proto b/internal/proto/token/v1/token.proto new file mode 100644 index 0000000000..d1fbcc9fa2 --- /dev/null +++ b/internal/proto/token/v1/token.proto @@ -0,0 +1,30 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package token.v1; +option go_package = "github.com/absmach/magistrala/internal/grpc/token/v1"; + +service TokenService { + rpc Issue(IssueReq) returns (Token) {} + rpc Refresh(RefreshReq) returns (Token) {} +} + +message IssueReq { + string user_id = 1; + uint32 type = 3; +} + +message RefreshReq { + string refresh_token = 1; +} + +// If a token is not carrying any information itself, the type +// field can be used to determine how to validate the token. +// Also, different tokens can be encoded in different ways. +message Token { + string accessToken = 1; + optional string refreshToken = 2; + string accessType = 3; +} diff --git a/invitations/service.go b/invitations/service.go index 5b81d7ea68..89c4e00836 100644 --- a/invitations/service.go +++ b/invitations/service.go @@ -7,20 +7,20 @@ import ( "context" "time" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/authn" svcerr "github.com/absmach/magistrala/pkg/errors/service" mgsdk "github.com/absmach/magistrala/pkg/sdk/go" ) type service struct { - token magistrala.TokenServiceClient + token grpcTokenV1.TokenServiceClient repo Repository sdk mgsdk.SDK } -func NewService(token magistrala.TokenServiceClient, repo Repository, sdk mgsdk.SDK) Service { +func NewService(token grpcTokenV1.TokenServiceClient, repo Repository, sdk mgsdk.SDK) Service { return &service{ token: token, repo: repo, @@ -35,7 +35,7 @@ func (svc *service) SendInvitation(ctx context.Context, session authn.Session, i invitation.InvitedBy = session.UserID - joinToken, err := svc.token.Issue(ctx, &magistrala.IssueReq{UserId: session.UserID, Type: uint32(auth.InvitationKey)}) + joinToken, err := svc.token.Issue(ctx, &grpcTokenV1.IssueReq{UserId: session.UserID, Type: uint32(auth.InvitationKey)}) if err != nil { return err } diff --git a/invitations/service_test.go b/invitations/service_test.go index 92538652c3..6c6104b548 100644 --- a/invitations/service_test.go +++ b/invitations/service_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/absmach/magistrala" authmocks "github.com/absmach/magistrala/auth/mocks" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/invitations" "github.com/absmach/magistrala/invitations/mocks" @@ -108,17 +108,15 @@ func TestSendInvitation(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repocall1 := token.On("Issue", context.Background(), mock.Anything).Return(&magistrala.Token{AccessToken: tc.req.Token}, tc.issueErr) - repocall2 := repo.On("Create", context.Background(), mock.Anything).Return(tc.repoErr) - if tc.req.Resend { - repocall2 = repo.On("UpdateToken", context.Background(), mock.Anything).Return(tc.repoErr) - } - err := svc.SendInvitation(context.Background(), tc.session, tc.req) - assert.Equal(t, tc.err, err, tc.desc) - repocall1.Unset() - repocall2.Unset() - }) + repocall1 := token.On("Issue", context.Background(), mock.Anything).Return(&grpcTokenV1.Token{AccessToken: tc.req.Token}, tc.issueErr) + repocall2 := repo.On("Create", context.Background(), mock.Anything).Return(tc.repoErr) + if tc.req.Resend { + repocall2 = repo.On("UpdateToken", context.Background(), mock.Anything).Return(tc.repoErr) + } + err := svc.SendInvitation(context.Background(), tc.session, tc.req) + assert.Equal(t, tc.err, err, tc.desc) + repocall1.Unset() + repocall2.Unset() } } diff --git a/mqtt/handler.go b/mqtt/handler.go index fe6c007ad8..6985322398 100644 --- a/mqtt/handler.go +++ b/mqtt/handler.go @@ -12,7 +12,8 @@ import ( "strings" "time" - "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/mqtt/events" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" @@ -53,23 +54,26 @@ var ( ErrFailedPublishToMsgBroker = errors.New("failed to publish to magistrala message broker") ) +var errInvalidUserId = errors.New("invalid user id") var channelRegExp = regexp.MustCompile(`^\/?channels\/([\w\-]+)\/messages(\/[^?]*)?(\?.*)?$`) // Event implements events.Event interface. type handler struct { publisher messaging.Publisher - things magistrala.ThingsServiceClient + things grpcThingsV1.ThingsServiceClient + channels grpcChannelsV1.ChannelsServiceClient logger *slog.Logger es events.EventStore } // NewHandler creates new Handler entity. -func NewHandler(publisher messaging.Publisher, es events.EventStore, logger *slog.Logger, thingsClient magistrala.ThingsServiceClient) session.Handler { +func NewHandler(publisher messaging.Publisher, es events.EventStore, logger *slog.Logger, thingsClient grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient) session.Handler { return &handler{ es: es, logger: logger, publisher: publisher, things: thingsClient, + channels: channels, } } @@ -87,6 +91,18 @@ func (h *handler) AuthConnect(ctx context.Context) error { pwd := string(s.Password) + res, err := h.things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ThingKey: pwd}) + if err != nil { + return errors.Wrap(svcerr.ErrAuthentication, err) + } + if !res.GetAuthenticated() { + return svcerr.ErrAuthentication + } + + if s.Username != "" && res.GetId() != s.Username { + return errInvalidUserId + } + if err := h.es.Connect(ctx, pwd); err != nil { h.logger.Error(errors.Wrap(ErrFailedPublishConnectEvent, err).Error()) } @@ -105,7 +121,7 @@ func (h *handler) AuthPublish(ctx context.Context, topic *string, payload *[]byt return ErrClientNotInitialized } - return h.authAccess(ctx, string(s.Password), *topic, policies.PublishPermission) + return h.authAccess(ctx, string(s.Username), *topic, policies.PublishPermission) } // AuthSubscribe is called on device subscribe, @@ -119,8 +135,8 @@ func (h *handler) AuthSubscribe(ctx context.Context, topics *[]string) error { return ErrMissingTopicSub } - for _, v := range *topics { - if err := h.authAccess(ctx, string(s.Password), v, policies.SubscribePermission); err != nil { + for _, topic := range *topics { + if err := h.authAccess(ctx, string(s.Username), topic, policies.SubscribePermission); err != nil { return err } } @@ -210,7 +226,7 @@ func (h *handler) Disconnect(ctx context.Context) error { return nil } -func (h *handler) authAccess(ctx context.Context, password, topic, action string) error { +func (h *handler) authAccess(ctx context.Context, thingID, topic, action string) error { // Topics are in the format: // channels//messages//.../ct/ if !channelRegExp.MatchString(topic) { @@ -224,12 +240,13 @@ func (h *handler) authAccess(ctx context.Context, password, topic, action string chanID := channelParts[1] - ar := &magistrala.ThingsAuthzReq{ + ar := &grpcChannelsV1.AuthzReq{ Permission: action, - ThingKey: password, - ChannelID: chanID, + ClientId: thingID, + ClientType: policies.ThingType, + ChannelId: chanID, } - res, err := h.things.Authorize(ctx, ar) + res, err := h.channels.Authorize(ctx, ar) if err != nil { return err } diff --git a/mqtt/handler_test.go b/mqtt/handler_test.go index 8f0ff9543e..636d658fa7 100644 --- a/mqtt/handler_test.go +++ b/mqtt/handler_test.go @@ -10,8 +10,7 @@ import ( "log" "testing" - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/internal/testsutil" + chmocks "github.com/absmach/magistrala/channels/mocks" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/mqtt" "github.com/absmach/magistrala/mqtt/mocks" @@ -65,7 +64,7 @@ var ( ) func TestAuthConnect(t *testing.T) { - handler, _, eventStore := newHandler() + handler, _, _, eventStore := newHandler() cases := []struct { desc string @@ -121,7 +120,7 @@ func TestAuthConnect(t *testing.T) { } func TestAuthPublish(t *testing.T) { - handler, things, _ := newHandler() + handler, _, _, _ := newHandler() cases := []struct { desc string @@ -161,19 +160,17 @@ func TestAuthPublish(t *testing.T) { } for _, tc := range cases { - repocall := things.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.ThingsAuthzRes{Authorized: true, Id: testsutil.GenerateUUID(t)}, tc.err) ctx := context.TODO() if tc.session != nil { ctx = session.NewContext(ctx, tc.session) } err := handler.AuthPublish(ctx, tc.topic, &tc.payload) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repocall.Unset() } } func TestAuthSubscribe(t *testing.T) { - handler, things, _ := newHandler() + handler, _, _, _ := newHandler() cases := []struct { desc string @@ -214,19 +211,17 @@ func TestAuthSubscribe(t *testing.T) { } for _, tc := range cases { - repocall := things.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.ThingsAuthzRes{Authorized: true, Id: testsutil.GenerateUUID(t)}, tc.err) ctx := context.TODO() if tc.session != nil { ctx = session.NewContext(ctx, tc.session) } err := handler.AuthSubscribe(ctx, tc.topic) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - repocall.Unset() } } func TestConnect(t *testing.T) { - handler, _, _ := newHandler() + handler, _, _, _ := newHandler() logBuffer.Reset() cases := []struct { @@ -260,7 +255,7 @@ func TestConnect(t *testing.T) { } func TestPublish(t *testing.T) { - handler, _, _ := newHandler() + handler, _, _, _ := newHandler() logBuffer.Reset() malformedSubtopics := topic + "/" + subtopic + "%" @@ -339,7 +334,7 @@ func TestPublish(t *testing.T) { } func TestSubscribe(t *testing.T) { - handler, _, _ := newHandler() + handler, _, _, _ := newHandler() logBuffer.Reset() cases := []struct { @@ -375,7 +370,7 @@ func TestSubscribe(t *testing.T) { } func TestUnsubscribe(t *testing.T) { - handler, _, _ := newHandler() + handler, _, _, _ := newHandler() logBuffer.Reset() cases := []struct { @@ -411,7 +406,7 @@ func TestUnsubscribe(t *testing.T) { } func TestDisconnect(t *testing.T) { - handler, _, eventStore := newHandler() + handler, _, _, eventStore := newHandler() logBuffer.Reset() cases := []struct { @@ -450,12 +445,13 @@ func TestDisconnect(t *testing.T) { } } -func newHandler() (session.Handler, *thmocks.ThingsServiceClient, *mocks.EventStore) { +func newHandler() (session.Handler, *thmocks.ThingsServiceClient, *chmocks.ChannelsServiceClient, *mocks.EventStore) { logger, err := mglog.New(&logBuffer, "debug") if err != nil { log.Fatalf("failed to create logger: %s", err) } things := new(thmocks.ThingsServiceClient) + channels := new(chmocks.ChannelsServiceClient) eventStore := new(mocks.EventStore) - return mqtt.NewHandler(mocks.NewPublisher(), eventStore, logger, things), things, eventStore + return mqtt.NewHandler(mocks.NewPublisher(), eventStore, logger, things, channels), things, channels, eventStore } diff --git a/pkg/apiutil/errors.go b/pkg/apiutil/errors.go index 2b53375122..8a72ff5f7e 100644 --- a/pkg/apiutil/errors.go +++ b/pkg/apiutil/errors.go @@ -21,6 +21,18 @@ var ( // ErrMissingID indicates missing entity ID. ErrMissingID = errors.New("missing entity id") + // ErrMissingParentGroupID indicates missing parent group ID. + ErrMissingParentGroupID = errors.New("missing parent group id") + + // ErrMissingChildrenGroupIDs indicates missing children group IDs. + ErrMissingChildrenGroupIDs = errors.New("missing children group ids") + + // ErrSelfParentingNotAllowed indicates child id is same as parent id. + ErrSelfParentingNotAllowed = errors.New("self parenting not allowed") + + // ErrInvalidChildGroupID indicates invalid child group ID. + ErrInvalidChildGroupID = errors.New("invalid child group id") + // ErrInvalidAuthKey indicates invalid auth key. ErrInvalidAuthKey = errors.New("invalid auth key") @@ -39,6 +51,9 @@ var ( // ErrLimitSize indicates that an invalid limit. ErrLimitSize = errors.New("invalid limit size") + // ErrLevel indicates that an invalid level. + ErrLevel = errors.New("invalid level") + // ErrOffsetSize indicates an invalid offset. ErrOffsetSize = errors.New("invalid offset size") @@ -54,6 +69,15 @@ var ( // ErrEmptyList indicates that entity data is empty. ErrEmptyList = errors.New("empty list provided") + // ErrMissingRoleName indicates that role name are empty + ErrMissingRoleName = errors.New("empty role name") + + // ErrMissingRoleOperations indicates that role operations are empty + ErrMissingRoleOperations = errors.New("empty role operations") + + // ErrMissingRoleMembers indicates that role members are empty + ErrMissingRoleMembers = errors.New("empty role members") + // ErrMalformedPolicy indicates that policies are malformed. ErrMalformedPolicy = errors.New("malformed policy") diff --git a/pkg/authn/authsvc/authn.go b/pkg/authn/authsvc/authn.go index 88b44c518c..b9a2821e0a 100644 --- a/pkg/authn/authsvc/authn.go +++ b/pkg/authn/authsvc/authn.go @@ -6,8 +6,8 @@ package authsvc import ( "context" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth/api/grpc/auth" + grpcAuthV1 "github.com/absmach/magistrala/internal/grpc/auth/v1" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" "github.com/absmach/magistrala/pkg/grpcclient" @@ -15,7 +15,7 @@ import ( ) type authentication struct { - authSvcClient magistrala.AuthServiceClient + authSvcClient grpcAuthV1.AuthServiceClient } var _ authn.Authentication = (*authentication)(nil) @@ -38,7 +38,7 @@ func NewAuthentication(ctx context.Context, cfg grpcclient.Config) (authn.Authen } func (a authentication) Authenticate(ctx context.Context, token string) (authn.Session, error) { - res, err := a.authSvcClient.Authenticate(ctx, &magistrala.AuthNReq{Token: token}) + res, err := a.authSvcClient.Authenticate(ctx, &grpcAuthV1.AuthNReq{Token: token}) if err != nil { return authn.Session{}, errors.Wrap(errors.ErrAuthentication, err) } diff --git a/pkg/authz/authsvc/authz.go b/pkg/authz/authsvc/authz.go index 47db088e2f..07f15ebe06 100644 --- a/pkg/authz/authsvc/authz.go +++ b/pkg/authz/authsvc/authz.go @@ -6,8 +6,8 @@ package authsvc import ( "context" - "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth/api/grpc/auth" + grpcAuthV1 "github.com/absmach/magistrala/internal/grpc/auth/v1" "github.com/absmach/magistrala/pkg/authz" "github.com/absmach/magistrala/pkg/errors" "github.com/absmach/magistrala/pkg/grpcclient" @@ -15,7 +15,7 @@ import ( ) type authorization struct { - authSvcClient magistrala.AuthServiceClient + authSvcClient grpcAuthV1.AuthServiceClient } var _ authz.Authorization = (*authorization)(nil) @@ -38,7 +38,7 @@ func NewAuthorization(ctx context.Context, cfg grpcclient.Config) (authz.Authori } func (a authorization) Authorize(ctx context.Context, pr authz.PolicyReq) error { - req := magistrala.AuthZReq{ + req := grpcAuthV1.AuthZReq{ Domain: pr.Domain, SubjectType: pr.SubjectType, SubjectKind: pr.SubjectKind, @@ -53,7 +53,7 @@ func (a authorization) Authorize(ctx context.Context, pr authz.PolicyReq) error if err != nil { return errors.Wrap(errors.ErrAuthorization, err) } - if !res.Authorized { + if !res.GetAuthorized() { return errors.ErrAuthorization } return nil diff --git a/pkg/errors/repository/types.go b/pkg/errors/repository/types.go index a189ae9e6f..d67f79079d 100644 --- a/pkg/errors/repository/types.go +++ b/pkg/errors/repository/types.go @@ -34,6 +34,8 @@ var ( // ErrFailedToRetrieveAllGroups failed to retrieve groups. ErrFailedToRetrieveAllGroups = errors.New("failed to retrieve all groups") + ErrRoleMigration = errors.New("role migration initialization failed") + // ErrMissingNames indicates missing first and last names. ErrMissingNames = errors.New("missing first or last name") ) diff --git a/pkg/groups/groups.go b/pkg/groups/groups.go deleted file mode 100644 index 8719424cf8..0000000000 --- a/pkg/groups/groups.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package groups - -import ( - "context" - "time" - - "github.com/absmach/magistrala/pkg/authn" -) - -// MaxLevel represents the maximum group hierarchy level. -const MaxLevel = uint64(5) - -// Group represents the group of Clients. -// Indicates a level in tree hierarchy. Root node is level 1. -// Path in a tree consisting of group IDs -// Paths are unique per domain. -type Group struct { - ID string `json:"id"` - Domain string `json:"domain_id,omitempty"` - Parent string `json:"parent_id,omitempty"` - Name string `json:"name"` - Description string `json:"description,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` - Level int `json:"level,omitempty"` - Path string `json:"path,omitempty"` - Children []*Group `json:"children,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at,omitempty"` - UpdatedBy string `json:"updated_by,omitempty"` - Status Status `json:"status"` - Permissions []string `json:"permissions,omitempty"` -} - -type Member struct { - ID string `json:"id"` - Type string `json:"type"` -} - -// Memberships contains page related metadata as well as list of memberships that -// belong to this page. -type MembersPage struct { - Total uint64 `json:"total"` - Offset uint64 `json:"offset"` - Limit uint64 `json:"limit"` - Members []Member `json:"members"` -} - -// Page contains page related metadata as well as list -// of Groups that belong to the page. -type Page struct { - PageMeta - Path string - Level uint64 - ParentID string - Permission string - ListPerms bool - Direction int64 // ancestors (+1) or descendants (-1) - Groups []Group -} - -// Metadata represents arbitrary JSON. -type Metadata map[string]interface{} - -// Repository specifies a group persistence API. -// -//go:generate mockery --name Repository --output=./mocks --filename repository.go --quiet --note "Copyright (c) Abstract Machines" --unroll-variadic=false -type Repository interface { - // Save group. - Save(ctx context.Context, g Group) (Group, error) - - // Update a group. - Update(ctx context.Context, g Group) (Group, error) - - // RetrieveByID retrieves group by its id. - RetrieveByID(ctx context.Context, id string) (Group, error) - - // RetrieveAll retrieves all groups. - RetrieveAll(ctx context.Context, gm Page) (Page, error) - - // RetrieveByIDs retrieves group by ids and query. - RetrieveByIDs(ctx context.Context, gm Page, ids ...string) (Page, error) - - // ChangeStatus changes groups status to active or inactive - ChangeStatus(ctx context.Context, group Group) (Group, error) - - // AssignParentGroup assigns parent group id to a given group id - AssignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error - - // UnassignParentGroup unassign parent group id fr given group id - UnassignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error - - // Delete a group - Delete(ctx context.Context, groupID string) error -} - -//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines" --unroll-variadic=false -type Service interface { - // CreateGroup creates new group. - CreateGroup(ctx context.Context, session authn.Session, kind string, g Group) (Group, error) - - // UpdateGroup updates the group identified by the provided ID. - UpdateGroup(ctx context.Context, session authn.Session, g Group) (Group, error) - - // ViewGroup retrieves data about the group identified by ID. - ViewGroup(ctx context.Context, session authn.Session, id string) (Group, error) - - // ViewGroupPerms retrieves permissions on the group id for the given authorized token. - ViewGroupPerms(ctx context.Context, session authn.Session, id string) ([]string, error) - - // ListGroups retrieves a list of groups basesd on entity type and entity id. - ListGroups(ctx context.Context, session authn.Session, memberKind, memberID string, gm Page) (Page, error) - - // ListMembers retrieves everything that is assigned to a group identified by groupID. - ListMembers(ctx context.Context, session authn.Session, groupID, permission, memberKind string) (MembersPage, error) - - // EnableGroup logically enables the group identified with the provided ID. - EnableGroup(ctx context.Context, session authn.Session, id string) (Group, error) - - // DisableGroup logically disables the group identified with the provided ID. - DisableGroup(ctx context.Context, session authn.Session, id string) (Group, error) - - // DeleteGroup delete the given group id - DeleteGroup(ctx context.Context, session authn.Session, id string) error - - // Assign member to group - Assign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) - - // Unassign member from group - Unassign(ctx context.Context, session authn.Session, groupID, relation, memberKind string, memberIDs ...string) (err error) -} diff --git a/pkg/groups/mocks/repository.go b/pkg/groups/mocks/repository.go deleted file mode 100644 index 918b852cb4..0000000000 --- a/pkg/groups/mocks/repository.go +++ /dev/null @@ -1,253 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -// Copyright (c) Abstract Machines - -package mocks - -import ( - context "context" - - groups "github.com/absmach/magistrala/pkg/groups" - mock "github.com/stretchr/testify/mock" -) - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -// AssignParentGroup provides a mock function with given fields: ctx, parentGroupID, groupIDs -func (_m *Repository) AssignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error { - ret := _m.Called(ctx, parentGroupID, groupIDs) - - if len(ret) == 0 { - panic("no return value specified for AssignParentGroup") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, ...string) error); ok { - r0 = rf(ctx, parentGroupID, groupIDs...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ChangeStatus provides a mock function with given fields: ctx, group -func (_m *Repository) ChangeStatus(ctx context.Context, group groups.Group) (groups.Group, error) { - ret := _m.Called(ctx, group) - - if len(ret) == 0 { - panic("no return value specified for ChangeStatus") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, groups.Group) (groups.Group, error)); ok { - return rf(ctx, group) - } - if rf, ok := ret.Get(0).(func(context.Context, groups.Group) groups.Group); ok { - r0 = rf(ctx, group) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, groups.Group) error); ok { - r1 = rf(ctx, group) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Delete provides a mock function with given fields: ctx, groupID -func (_m *Repository) Delete(ctx context.Context, groupID string) error { - ret := _m.Called(ctx, groupID) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, groupID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// RetrieveAll provides a mock function with given fields: ctx, gm -func (_m *Repository) RetrieveAll(ctx context.Context, gm groups.Page) (groups.Page, error) { - ret := _m.Called(ctx, gm) - - if len(ret) == 0 { - panic("no return value specified for RetrieveAll") - } - - var r0 groups.Page - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, groups.Page) (groups.Page, error)); ok { - return rf(ctx, gm) - } - if rf, ok := ret.Get(0).(func(context.Context, groups.Page) groups.Page); ok { - r0 = rf(ctx, gm) - } else { - r0 = ret.Get(0).(groups.Page) - } - - if rf, ok := ret.Get(1).(func(context.Context, groups.Page) error); ok { - r1 = rf(ctx, gm) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RetrieveByID provides a mock function with given fields: ctx, id -func (_m *Repository) RetrieveByID(ctx context.Context, id string) (groups.Group, error) { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for RetrieveByID") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (groups.Group, error)); ok { - return rf(ctx, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string) groups.Group); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RetrieveByIDs provides a mock function with given fields: ctx, gm, ids -func (_m *Repository) RetrieveByIDs(ctx context.Context, gm groups.Page, ids ...string) (groups.Page, error) { - ret := _m.Called(ctx, gm, ids) - - if len(ret) == 0 { - panic("no return value specified for RetrieveByIDs") - } - - var r0 groups.Page - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, groups.Page, ...string) (groups.Page, error)); ok { - return rf(ctx, gm, ids...) - } - if rf, ok := ret.Get(0).(func(context.Context, groups.Page, ...string) groups.Page); ok { - r0 = rf(ctx, gm, ids...) - } else { - r0 = ret.Get(0).(groups.Page) - } - - if rf, ok := ret.Get(1).(func(context.Context, groups.Page, ...string) error); ok { - r1 = rf(ctx, gm, ids...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Save provides a mock function with given fields: ctx, g -func (_m *Repository) Save(ctx context.Context, g groups.Group) (groups.Group, error) { - ret := _m.Called(ctx, g) - - if len(ret) == 0 { - panic("no return value specified for Save") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, groups.Group) (groups.Group, error)); ok { - return rf(ctx, g) - } - if rf, ok := ret.Get(0).(func(context.Context, groups.Group) groups.Group); ok { - r0 = rf(ctx, g) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, groups.Group) error); ok { - r1 = rf(ctx, g) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UnassignParentGroup provides a mock function with given fields: ctx, parentGroupID, groupIDs -func (_m *Repository) UnassignParentGroup(ctx context.Context, parentGroupID string, groupIDs ...string) error { - ret := _m.Called(ctx, parentGroupID, groupIDs) - - if len(ret) == 0 { - panic("no return value specified for UnassignParentGroup") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, ...string) error); ok { - r0 = rf(ctx, parentGroupID, groupIDs...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Update provides a mock function with given fields: ctx, g -func (_m *Repository) Update(ctx context.Context, g groups.Group) (groups.Group, error) { - ret := _m.Called(ctx, g) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, groups.Group) (groups.Group, error)); ok { - return rf(ctx, g) - } - if rf, ok := ret.Get(0).(func(context.Context, groups.Group) groups.Group); ok { - r0 = rf(ctx, g) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, groups.Group) error); ok { - r1 = rf(ctx, g) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewRepository creates a new instance of Repository. 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 NewRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *Repository { - mock := &Repository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/groups/mocks/service.go b/pkg/groups/mocks/service.go deleted file mode 100644 index 9fd1418911..0000000000 --- a/pkg/groups/mocks/service.go +++ /dev/null @@ -1,314 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -// Copyright (c) Abstract Machines - -package mocks - -import ( - context "context" - - authn "github.com/absmach/magistrala/pkg/authn" - - groups "github.com/absmach/magistrala/pkg/groups" - - mock "github.com/stretchr/testify/mock" -) - -// Service is an autogenerated mock type for the Service type -type Service struct { - mock.Mock -} - -// Assign provides a mock function with given fields: ctx, session, groupID, relation, memberKind, memberIDs -func (_m *Service) Assign(ctx context.Context, session authn.Session, groupID string, relation string, memberKind string, memberIDs ...string) error { - ret := _m.Called(ctx, session, groupID, relation, memberKind, memberIDs) - - if len(ret) == 0 { - panic("no return value specified for Assign") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string, ...string) error); ok { - r0 = rf(ctx, session, groupID, relation, memberKind, memberIDs...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// CreateGroup provides a mock function with given fields: ctx, session, kind, g -func (_m *Service) CreateGroup(ctx context.Context, session authn.Session, kind string, g groups.Group) (groups.Group, error) { - ret := _m.Called(ctx, session, kind, g) - - if len(ret) == 0 { - panic("no return value specified for CreateGroup") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, groups.Group) (groups.Group, error)); ok { - return rf(ctx, session, kind, g) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, groups.Group) groups.Group); ok { - r0 = rf(ctx, session, kind, g) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, groups.Group) error); ok { - r1 = rf(ctx, session, kind, g) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// DeleteGroup provides a mock function with given fields: ctx, session, id -func (_m *Service) DeleteGroup(ctx context.Context, session authn.Session, id string) error { - ret := _m.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for DeleteGroup") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { - r0 = rf(ctx, session, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// DisableGroup provides a mock function with given fields: ctx, session, id -func (_m *Service) DisableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - ret := _m.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for DisableGroup") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (groups.Group, error)); ok { - return rf(ctx, session, id) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) groups.Group); ok { - r0 = rf(ctx, session, id) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = rf(ctx, session, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EnableGroup provides a mock function with given fields: ctx, session, id -func (_m *Service) EnableGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - ret := _m.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for EnableGroup") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (groups.Group, error)); ok { - return rf(ctx, session, id) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) groups.Group); ok { - r0 = rf(ctx, session, id) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = rf(ctx, session, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ListGroups provides a mock function with given fields: ctx, session, memberKind, memberID, gm -func (_m *Service) ListGroups(ctx context.Context, session authn.Session, memberKind string, memberID string, gm groups.Page) (groups.Page, error) { - ret := _m.Called(ctx, session, memberKind, memberID, gm) - - if len(ret) == 0 { - panic("no return value specified for ListGroups") - } - - var r0 groups.Page - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, groups.Page) (groups.Page, error)); ok { - return rf(ctx, session, memberKind, memberID, gm) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, groups.Page) groups.Page); ok { - r0 = rf(ctx, session, memberKind, memberID, gm) - } else { - r0 = ret.Get(0).(groups.Page) - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, groups.Page) error); ok { - r1 = rf(ctx, session, memberKind, memberID, gm) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ListMembers provides a mock function with given fields: ctx, session, groupID, permission, memberKind -func (_m *Service) ListMembers(ctx context.Context, session authn.Session, groupID string, permission string, memberKind string) (groups.MembersPage, error) { - ret := _m.Called(ctx, session, groupID, permission, memberKind) - - if len(ret) == 0 { - panic("no return value specified for ListMembers") - } - - var r0 groups.MembersPage - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) (groups.MembersPage, error)); ok { - return rf(ctx, session, groupID, permission, memberKind) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) groups.MembersPage); ok { - r0 = rf(ctx, session, groupID, permission, memberKind) - } else { - r0 = ret.Get(0).(groups.MembersPage) - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string) error); ok { - r1 = rf(ctx, session, groupID, permission, memberKind) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Unassign provides a mock function with given fields: ctx, session, groupID, relation, memberKind, memberIDs -func (_m *Service) Unassign(ctx context.Context, session authn.Session, groupID string, relation string, memberKind string, memberIDs ...string) error { - ret := _m.Called(ctx, session, groupID, relation, memberKind, memberIDs) - - if len(ret) == 0 { - panic("no return value specified for Unassign") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string, ...string) error); ok { - r0 = rf(ctx, session, groupID, relation, memberKind, memberIDs...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UpdateGroup provides a mock function with given fields: ctx, session, g -func (_m *Service) UpdateGroup(ctx context.Context, session authn.Session, g groups.Group) (groups.Group, error) { - ret := _m.Called(ctx, session, g) - - if len(ret) == 0 { - panic("no return value specified for UpdateGroup") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.Group) (groups.Group, error)); ok { - return rf(ctx, session, g) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, groups.Group) groups.Group); ok { - r0 = rf(ctx, session, g) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, groups.Group) error); ok { - r1 = rf(ctx, session, g) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ViewGroup provides a mock function with given fields: ctx, session, id -func (_m *Service) ViewGroup(ctx context.Context, session authn.Session, id string) (groups.Group, error) { - ret := _m.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for ViewGroup") - } - - var r0 groups.Group - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (groups.Group, error)); ok { - return rf(ctx, session, id) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) groups.Group); ok { - r0 = rf(ctx, session, id) - } else { - r0 = ret.Get(0).(groups.Group) - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = rf(ctx, session, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ViewGroupPerms provides a mock function with given fields: ctx, session, id -func (_m *Service) ViewGroupPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - ret := _m.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for ViewGroupPerms") - } - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) ([]string, error)); ok { - return rf(ctx, session, id) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) []string); ok { - r0 = rf(ctx, session, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = rf(ctx, session, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewService creates a new instance of Service. 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 NewService(t interface { - mock.TestingT - Cleanup(func()) -}) *Service { - mock := &Service{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/groups/page.go b/pkg/groups/page.go deleted file mode 100644 index e49ec6690e..0000000000 --- a/pkg/groups/page.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package groups - -// PageMeta contains page metadata that helps navigation. -type PageMeta struct { - Total uint64 `json:"total"` - Offset uint64 `json:"offset"` - Limit uint64 `json:"limit"` - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` - DomainID string `json:"domain_id,omitempty"` - Tag string `json:"tag,omitempty"` - Metadata Metadata `json:"metadata,omitempty"` - Status Status `json:"status,omitempty"` -} diff --git a/pkg/grpcclient/client.go b/pkg/grpcclient/client.go index 5c29571115..a8ac6d52c2 100644 --- a/pkg/grpcclient/client.go +++ b/pkg/grpcclient/client.go @@ -6,9 +6,15 @@ package grpcclient import ( "context" - "github.com/absmach/magistrala" - domainsgrpc "github.com/absmach/magistrala/auth/api/grpc/domains" tokengrpc "github.com/absmach/magistrala/auth/api/grpc/token" + channelsgrpc "github.com/absmach/magistrala/channels/api/grpc" + domainsgrpc "github.com/absmach/magistrala/domains/api/grpc" + groupsgrpc "github.com/absmach/magistrala/groups/api/grpc" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" thingsauth "github.com/absmach/magistrala/things/api/grpc" grpchealth "google.golang.org/grpc/health/grpc_health_v1" ) @@ -18,7 +24,7 @@ import ( // For example: // // tokenClient, tokenHandler, err := grpcclient.SetupTokenClient(ctx, grpcclient.Config{}). -func SetupTokenClient(ctx context.Context, cfg Config) (magistrala.TokenServiceClient, Handler, error) { +func SetupTokenClient(ctx context.Context, cfg Config) (grpcTokenV1.TokenServiceClient, Handler, error) { client, err := NewHandler(cfg) if err != nil { return nil, nil, err @@ -26,6 +32,7 @@ func SetupTokenClient(ctx context.Context, cfg Config) (magistrala.TokenServiceC health := grpchealth.NewHealthClient(client.Connection()) resp, err := health.Check(ctx, &grpchealth.HealthCheckRequest{ + // Health Service name is the svcName provided during gRPC server creation `grpcserver.NewServer(ctx, cancel, svcName, grpcServerConfig, registerAuthServiceServer, logger)` Service: "auth", }) if err != nil || resp.GetStatus() != grpchealth.HealthCheckResponse_SERVING { @@ -40,20 +47,12 @@ func SetupTokenClient(ctx context.Context, cfg Config) (magistrala.TokenServiceC // For example: // // domainsClient, domainsHandler, err := grpcclient.SetupDomainsClient(ctx, grpcclient.Config{}). -func SetupDomainsClient(ctx context.Context, cfg Config) (magistrala.DomainsServiceClient, Handler, error) { +func SetupDomainsClient(ctx context.Context, cfg Config) (grpcDomainsV1.DomainsServiceClient, Handler, error) { client, err := NewHandler(cfg) if err != nil { return nil, nil, err } - health := grpchealth.NewHealthClient(client.Connection()) - resp, err := health.Check(ctx, &grpchealth.HealthCheckRequest{ - Service: "auth", - }) - if err != nil || resp.GetStatus() != grpchealth.HealthCheckResponse_SERVING { - return nil, nil, ErrSvcNotServing - } - return domainsgrpc.NewDomainsClient(client.Connection(), cfg.Timeout), client, nil } @@ -62,19 +61,39 @@ func SetupDomainsClient(ctx context.Context, cfg Config) (magistrala.DomainsServ // For example: // // thingClient, thingHandler, err := grpcclient.SetupThings(ctx, grpcclient.Config{}). -func SetupThingsClient(ctx context.Context, cfg Config) (magistrala.ThingsServiceClient, Handler, error) { +func SetupThingsClient(ctx context.Context, cfg Config) (grpcThingsV1.ThingsServiceClient, Handler, error) { client, err := NewHandler(cfg) if err != nil { return nil, nil, err } - health := grpchealth.NewHealthClient(client.Connection()) - resp, err := health.Check(ctx, &grpchealth.HealthCheckRequest{ - Service: "things", - }) - if err != nil || resp.GetStatus() != grpchealth.HealthCheckResponse_SERVING { - return nil, nil, ErrSvcNotServing + return thingsauth.NewClient(client.Connection(), cfg.Timeout), client, nil +} + +// SetupChannelsClient loads channels gRPC configuration and creates new channels gRPC client. +// +// For example: +// +// channelClient, channelHandler, err := grpcclient.SetupChannelsClient(ctx, grpcclient.Config{}). +func SetupChannelsClient(ctx context.Context, cfg Config) (grpcChannelsV1.ChannelsServiceClient, Handler, error) { + client, err := NewHandler(cfg) + if err != nil { + return nil, nil, err } - return thingsauth.NewClient(client.Connection(), cfg.Timeout), client, nil + return channelsgrpc.NewClient(client.Connection(), cfg.Timeout), client, nil +} + +// SetupGroupsClient loads groups gRPC configuration and creates new groups gRPC client. +// +// For example: +// +// groupClient, groupHandler, err := grpcclient.SetupGroupsClient(ctx, grpcclient.Config{}). +func SetupGroupsClient(ctx context.Context, cfg Config) (grpcGroupsV1.GroupsServiceClient, Handler, error) { + client, err := NewHandler(cfg) + if err != nil { + return nil, nil, err + } + + return groupsgrpc.NewClient(client.Connection(), cfg.Timeout), client, nil } diff --git a/pkg/grpcclient/client_test.go b/pkg/grpcclient/client_test.go index acc0ebbe31..0c962dbdc3 100644 --- a/pkg/grpcclient/client_test.go +++ b/pkg/grpcclient/client_test.go @@ -9,17 +9,20 @@ import ( "testing" "time" - "github.com/absmach/magistrala" - domainsgrpcapi "github.com/absmach/magistrala/auth/api/grpc/domains" tokengrpcapi "github.com/absmach/magistrala/auth/api/grpc/token" "github.com/absmach/magistrala/auth/mocks" + domainsgrpcapi "github.com/absmach/magistrala/domains/api/grpc" + domainsMocks "github.com/absmach/magistrala/domains/mocks" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/errors" "github.com/absmach/magistrala/pkg/grpcclient" "github.com/absmach/magistrala/pkg/server" grpcserver "github.com/absmach/magistrala/pkg/server/grpc" thingsgrpcapi "github.com/absmach/magistrala/things/api/grpc" - thmocks "github.com/absmach/magistrala/things/mocks" + thmocks "github.com/absmach/magistrala/things/private/mocks" "github.com/stretchr/testify/assert" "google.golang.org/grpc" ) @@ -28,7 +31,7 @@ func TestSetupToken(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() registerAuthServiceServer := func(srv *grpc.Server) { - magistrala.RegisterTokenServiceServer(srv, tokengrpcapi.NewTokenServer(new(mocks.Service))) + grpcTokenV1.RegisterTokenServiceServer(srv, tokengrpcapi.NewTokenServer(new(mocks.Service))) } gs := grpcserver.NewServer(ctx, cancel, "auth", server.Config{Port: "12345"}, registerAuthServiceServer, mglog.NewMock()) go func() { @@ -80,7 +83,7 @@ func TestSetupThingsClient(t *testing.T) { defer cancel() registerThingsServiceServer := func(srv *grpc.Server) { - magistrala.RegisterThingsServiceServer(srv, thingsgrpcapi.NewServer(new(thmocks.Service))) + grpcThingsV1.RegisterThingsServiceServer(srv, thingsgrpcapi.NewServer(new(thmocks.Service))) } gs := grpcserver.NewServer(ctx, cancel, "things", server.Config{Port: "12345"}, registerThingsServiceServer, mglog.NewMock()) go func() { @@ -131,7 +134,7 @@ func TestSetupDomainsClient(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() registerDomainsServiceServer := func(srv *grpc.Server) { - magistrala.RegisterDomainsServiceServer(srv, domainsgrpcapi.NewDomainsServer(new(mocks.Service))) + grpcDomainsV1.RegisterDomainsServiceServer(srv, domainsgrpcapi.NewDomainsServer(new(domainsMocks.Service))) } gs := grpcserver.NewServer(ctx, cancel, "auth", server.Config{Port: "12345"}, registerDomainsServiceServer, mglog.NewMock()) go func() { diff --git a/pkg/grpcclient/connect.go b/pkg/grpcclient/connect.go index e8678ed1b5..12c41cbf12 100644 --- a/pkg/grpcclient/connect.go +++ b/pkg/grpcclient/connect.go @@ -33,11 +33,12 @@ var ( ) type Config struct { - URL string `env:"URL" envDefault:""` - Timeout time.Duration `env:"TIMEOUT" envDefault:"1s"` - ClientCert string `env:"CLIENT_CERT" envDefault:""` - ClientKey string `env:"CLIENT_KEY" envDefault:""` - ServerCAFile string `env:"SERVER_CA_CERTS" envDefault:""` + URL string `env:"URL" envDefault:""` + Timeout time.Duration `env:"TIMEOUT" envDefault:"1s"` + ClientCert string `env:"CLIENT_CERT" envDefault:""` + ClientKey string `env:"CLIENT_KEY" envDefault:""` + ServerCAFile string `env:"SERVER_CA_CERTS" envDefault:""` + BypassHealthCheck bool } // Handler is used to handle gRPC connection. diff --git a/pkg/messaging/message.pb.go b/pkg/messaging/message.pb.go index 804b02e7de..70868c1d48 100644 --- a/pkg/messaging/message.pb.go +++ b/pkg/messaging/message.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.1 +// protoc-gen-go v1.35.1 +// protoc v5.28.2 // source: pkg/messaging/message.proto package messaging @@ -39,11 +39,9 @@ type Message struct { func (x *Message) Reset() { *x = Message{} - if protoimpl.UnsafeEnabled { - mi := &file_pkg_messaging_message_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_pkg_messaging_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Message) String() string { @@ -54,7 +52,7 @@ func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { mi := &file_pkg_messaging_message_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -160,20 +158,6 @@ func file_pkg_messaging_message_proto_init() { if File_pkg_messaging_message_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_pkg_messaging_message_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*Message); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/pkg/policies/evaluator.go b/pkg/policies/evaluator.go index c6288697c4..d775421809 100644 --- a/pkg/policies/evaluator.go +++ b/pkg/policies/evaluator.go @@ -21,8 +21,10 @@ const ( ) const ( + RoleType = "role" GroupType = "group" ThingType = "thing" + ChannelType = "channel" UserType = "user" DomainType = "domain" PlatformType = "platform" diff --git a/pkg/policies/service.go b/pkg/policies/service.go index 446926c137..ee3837ab26 100644 --- a/pkg/policies/service.go +++ b/pkg/policies/service.go @@ -26,6 +26,9 @@ type Policy struct { // SubjectRelation contains subject relations. SubjectRelation string `json:"subject_relation,omitempty"` + // ObjectPrefix contains the Optional Object Prefix which is used for delete with filter. + ObjectPrefix string `json:"object_prefix"` + // Object contains the object ID. Object string `json:"object"` @@ -102,3 +105,10 @@ type Service interface { // ListPermissions lists permission betweeen given subject and object . ListPermissions(ctx context.Context, pr Policy, permissionsFilter []string) (Permissions, error) } + +func EncodeDomainUserID(domainID, userID string) string { + if domainID == "" || userID == "" { + return "" + } + return domainID + "_" + userID +} diff --git a/pkg/policies/spicedb/service.go b/pkg/policies/spicedb/service.go index 6abbf59651..bb2922dfb9 100644 --- a/pkg/policies/spicedb/service.go +++ b/pkg/policies/spicedb/service.go @@ -142,8 +142,9 @@ func (ps *policyService) AddPolicies(ctx context.Context, prs []policies.Policy) func (ps *policyService) DeletePolicyFilter(ctx context.Context, pr policies.Policy) error { req := &v1.DeleteRelationshipsRequest{ RelationshipFilter: &v1.RelationshipFilter{ - ResourceType: pr.ObjectType, - OptionalResourceId: pr.Object, + ResourceType: pr.ObjectType, + OptionalResourceId: pr.Object, + OptionalResourceIdPrefix: pr.ObjectPrefix, }, } @@ -359,8 +360,8 @@ func (ps *policyService) addPolicyPreCondition(ctx context.Context, pr policies. // - GROUP (channel) with DOMAIN RELATION to DOMAIN // - NO GROUP should not have PARENT_GROUP RELATION with GROUP (channel) // - THING with DOMAIN RELATION to DOMAIN - case pr.SubjectType == policies.GroupType && pr.ObjectType == policies.ThingType: - return channelThingPreCondition(pr) + // case pr.SubjectType == policies.GroupType && pr.ObjectType == policies.ThingType: + // return channelThingPreCondition(pr) // 5.) user -> domain // Checks : diff --git a/pkg/roles/mocks/provisioner.go b/pkg/roles/mocks/provisioner.go new file mode 100644 index 0000000000..70cda3aa5e --- /dev/null +++ b/pkg/roles/mocks/provisioner.go @@ -0,0 +1,81 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + policies "github.com/absmach/magistrala/pkg/policies" + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// Provisioner is an autogenerated mock type for the Provisioner type +type Provisioner struct { + mock.Mock +} + +// AddNewEntitiesRoles provides a mock function with given fields: ctx, domainID, userID, entityIDs, optionalEntityPolicies, newBuiltInRoleMembers +func (_m *Provisioner) AddNewEntitiesRoles(ctx context.Context, domainID string, userID string, entityIDs []string, optionalEntityPolicies []policies.Policy, newBuiltInRoleMembers map[roles.BuiltInRoleName][]roles.Member) ([]roles.RoleProvision, error) { + ret := _m.Called(ctx, domainID, userID, entityIDs, optionalEntityPolicies, newBuiltInRoleMembers) + + if len(ret) == 0 { + panic("no return value specified for AddNewEntitiesRoles") + } + + var r0 []roles.RoleProvision + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []string, []policies.Policy, map[roles.BuiltInRoleName][]roles.Member) ([]roles.RoleProvision, error)); ok { + return rf(ctx, domainID, userID, entityIDs, optionalEntityPolicies, newBuiltInRoleMembers) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, []string, []policies.Policy, map[roles.BuiltInRoleName][]roles.Member) []roles.RoleProvision); ok { + r0 = rf(ctx, domainID, userID, entityIDs, optionalEntityPolicies, newBuiltInRoleMembers) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.RoleProvision) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, []string, []policies.Policy, map[roles.BuiltInRoleName][]roles.Member) error); ok { + r1 = rf(ctx, domainID, userID, entityIDs, optionalEntityPolicies, newBuiltInRoleMembers) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveEntitiesRoles provides a mock function with given fields: ctx, domainID, userID, entityIDs, optionalFilterDeletePolicies, optionalDeletePolicies +func (_m *Provisioner) RemoveEntitiesRoles(ctx context.Context, domainID string, userID string, entityIDs []string, optionalFilterDeletePolicies []policies.Policy, optionalDeletePolicies []policies.Policy) error { + ret := _m.Called(ctx, domainID, userID, entityIDs, optionalFilterDeletePolicies, optionalDeletePolicies) + + if len(ret) == 0 { + panic("no return value specified for RemoveEntitiesRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []string, []policies.Policy, []policies.Policy) error); ok { + r0 = rf(ctx, domainID, userID, entityIDs, optionalFilterDeletePolicies, optionalDeletePolicies) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewProvisioner creates a new instance of Provisioner. 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 NewProvisioner(t interface { + mock.TestingT + Cleanup(func()) +}) *Provisioner { + mock := &Provisioner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/roles/mocks/rolemanager.go b/pkg/roles/mocks/rolemanager.go new file mode 100644 index 0000000000..2abad325d7 --- /dev/null +++ b/pkg/roles/mocks/rolemanager.go @@ -0,0 +1,458 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + authn "github.com/absmach/magistrala/pkg/authn" + + mock "github.com/stretchr/testify/mock" + + roles "github.com/absmach/magistrala/pkg/roles" +) + +// RoleManager is an autogenerated mock type for the RoleManager type +type RoleManager struct { + mock.Mock +} + +// AddRole provides a mock function with given fields: ctx, session, entityID, roleName, optionalActions, optionalMembers +func (_m *RoleManager) AddRole(ctx context.Context, session authn.Session, entityID string, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName, optionalActions, optionalMembers) + + if len(ret) == 0 { + panic("no return value specified for AddRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListAvailableActions provides a mock function with given fields: ctx, session +func (_m *RoleManager) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + ret := _m.Called(ctx, session) + + if len(ret) == 0 { + panic("no return value specified for ListAvailableActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) ([]string, error)); ok { + return rf(ctx, session) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) []string); ok { + r0 = rf(ctx, session) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session) error); ok { + r1 = rf(ctx, session) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, session, memberID +func (_m *RoleManager) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) error { + ret := _m.Called(ctx, session, memberID) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, memberID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *RoleManager) RemoveRole(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RemoveRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, session, entityID, limit, offset +func (_m *RoleManager) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, session, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, session, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, session, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *RoleManager) RetrieveRole(ctx context.Context, session authn.Session, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *RoleManager) RoleAddActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *RoleManager) RoleAddMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *RoleManager) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *RoleManager) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *RoleManager) RoleListActions(ctx context.Context, session authn.Session, entityID string, roleName string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) []string); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, session, entityID, roleName, limit, offset +func (_m *RoleManager) RoleListMembers(ctx context.Context, session authn.Session, entityID string, roleName string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, session, entityID, roleName, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, session, entityID, roleName, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *RoleManager) RoleRemoveActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) error { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *RoleManager) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *RoleManager) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *RoleManager) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) error { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRoleName provides a mock function with given fields: ctx, session, entityID, oldRoleName, newRoleName +func (_m *RoleManager) UpdateRoleName(ctx context.Context, session authn.Session, entityID string, oldRoleName string, newRoleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, oldRoleName, newRoleName) + + if len(ret) == 0 { + panic("no return value specified for UpdateRoleName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, oldRoleName, newRoleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string) error); ok { + r1 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRoleManager creates a new instance of RoleManager. 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 NewRoleManager(t interface { + mock.TestingT + Cleanup(func()) +}) *RoleManager { + mock := &RoleManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/roles/mocks/rolesRepo.go b/pkg/roles/mocks/rolesRepo.go new file mode 100644 index 0000000000..2e34d05d85 --- /dev/null +++ b/pkg/roles/mocks/rolesRepo.go @@ -0,0 +1,494 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + roles "github.com/absmach/magistrala/pkg/roles" + mock "github.com/stretchr/testify/mock" +) + +// Repository is an autogenerated mock type for the Repository type +type Repository struct { + mock.Mock +} + +// AddRoles provides a mock function with given fields: ctx, rps +func (_m *Repository) AddRoles(ctx context.Context, rps []roles.RoleProvision) ([]roles.Role, error) { + ret := _m.Called(ctx, rps) + + if len(ret) == 0 { + panic("no return value specified for AddRoles") + } + + var r0 []roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) ([]roles.Role, error)); ok { + return rf(ctx, rps) + } + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) []roles.Role); ok { + r0 = rf(ctx, rps) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []roles.RoleProvision) error); ok { + r1 = rf(ctx, rps) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, members +func (_m *Repository) RemoveMemberFromAllRoles(ctx context.Context, members string) error { + ret := _m.Called(ctx, members) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRoles provides a mock function with given fields: ctx, roleIDs +func (_m *Repository) RemoveRoles(ctx context.Context, roleIDs []string) error { + ret := _m.Called(ctx, roleIDs) + + if len(ret) == 0 { + panic("no return value specified for RemoveRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok { + r0 = rf(ctx, roleIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, entityID, limit, offset +func (_m *Repository) RetrieveAllRoles(ctx context.Context, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveEntitiesRolesActionsMembers provides a mock function with given fields: ctx, entityIDs +func (_m *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) { + ret := _m.Called(ctx, entityIDs) + + if len(ret) == 0 { + panic("no return value specified for RetrieveEntitiesRolesActionsMembers") + } + + var r0 []roles.EntityActionRole + var r1 []roles.EntityMemberRole + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error)); ok { + return rf(ctx, entityIDs) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []roles.EntityActionRole); ok { + r0 = rf(ctx, entityIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.EntityActionRole) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) []roles.EntityMemberRole); ok { + r1 = rf(ctx, entityIDs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]roles.EntityMemberRole) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, []string) error); ok { + r2 = rf(ctx, entityIDs) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RetrieveRole provides a mock function with given fields: ctx, roleID +func (_m *Repository) RetrieveRole(ctx context.Context, roleID string) (roles.Role, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (roles.Role, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) roles.Role); ok { + r0 = rf(ctx, roleID) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRoleByEntityIDAndName provides a mock function with given fields: ctx, entityID, roleName +func (_m *Repository) RetrieveRoleByEntityIDAndName(ctx context.Context, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRoleByEntityIDAndName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (roles.Role, error)); ok { + return rf(ctx, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) roles.Role); ok { + r0 = rf(ctx, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleAddActions(ctx context.Context, role roles.Role, actions []string) ([]string, error) { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleAddMembers(ctx context.Context, role roles.Role, members []string) ([]string, error) { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, members) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, roleID, actions +func (_m *Repository) RoleCheckActionsExists(ctx context.Context, roleID string, actions []string) (bool, error) { + ret := _m.Called(ctx, roleID, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, roleID, members +func (_m *Repository) RoleCheckMembersExists(ctx context.Context, roleID string, members []string) (bool, error) { + ret := _m.Called(ctx, roleID, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, members) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, roleID +func (_m *Repository) RoleListActions(ctx context.Context, roleID string) ([]string, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, roleID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, roleID, limit, offset +func (_m *Repository) RoleListMembers(ctx context.Context, roleID string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, roleID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, roleID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, roleID, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, roleID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleRemoveActions(ctx context.Context, role roles.Role, actions []string) error { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllActions(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllMembers(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleRemoveMembers(ctx context.Context, role roles.Role, members []string) error { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRole provides a mock function with given fields: ctx, ro +func (_m *Repository) UpdateRole(ctx context.Context, ro roles.Role) (roles.Role, error) { + ret := _m.Called(ctx, ro) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) (roles.Role, error)); ok { + return rf(ctx, ro) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) roles.Role); ok { + r0 = rf(ctx, ro) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role) error); ok { + r1 = rf(ctx, ro) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRepository creates a new instance of Repository. 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 NewRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *Repository { + mock := &Repository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/roles/provisionmanage.go b/pkg/roles/provisionmanage.go new file mode 100644 index 0000000000..ee19fca4b4 --- /dev/null +++ b/pkg/roles/provisionmanage.go @@ -0,0 +1,630 @@ +package roles + +import ( + "context" + "fmt" + "time" + + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" +) + +var ( + errRemoveOptionalDeletePolicies = errors.New("failed to delete the additional requested policies") + errRemoveOptionalFilterDeletePolicies = errors.New("failed to filter delete the additional requested policies") + errRollbackRoles = errors.New("failed to rollback roles") +) + +type roleProvisionerManger interface { + RoleManager + Provisioner +} + +var _ roleProvisionerManger = (*ProvisionManageService)(nil) + +type ProvisionManageService struct { + entityType string + repo Repository + sidProvider magistrala.IDProvider + policy policies.Service + actions []Action + builtInRoles map[BuiltInRoleName][]Action +} + +func NewProvisionManageService(entityType string, repo Repository, policy policies.Service, sidProvider magistrala.IDProvider, actions []Action, builtInRoles map[BuiltInRoleName][]Action) (ProvisionManageService, error) { + rm := ProvisionManageService{ + entityType: entityType, + repo: repo, + sidProvider: sidProvider, + policy: policy, + actions: actions, + builtInRoles: builtInRoles, + } + return rm, nil +} + +func toRolesActions(actions []string) []Action { + roActions := []Action{} + for _, action := range actions { + roActions = append(roActions, Action(action)) + } + return roActions +} + +func roleActionsToString(roActions []Action) []string { + actions := []string{} + for _, roAction := range roActions { + actions = append(actions, roAction.String()) + } + return actions +} + +func roleMembersToString(roMems []Member) []string { + mems := []string{} + for _, roMem := range roMems { + mems = append(mems, roMem.String()) + } + return mems +} + +func (r ProvisionManageService) isActionAllowed(action Action) bool { + for _, cap := range r.actions { + if cap == action { + return true + } + } + return false +} +func (r ProvisionManageService) validateActions(actions []Action) error { + for _, ac := range actions { + action := Action(ac) + if !r.isActionAllowed(action) { + return errors.Wrap(svcerr.ErrMalformedEntity, fmt.Errorf("invalid action %s ", action)) + } + } + return nil +} + +func (r ProvisionManageService) RemoveEntitiesRoles(ctx context.Context, domainID, userID string, entityIDs []string, optionalFilterDeletePolicies []policies.Policy, optionalDeletePolicies []policies.Policy) error { + ears, emrs, err := r.repo.RetrieveEntitiesRolesActionsMembers(ctx, entityIDs) + if err != nil { + return err + } + + deletePolicies := []policies.Policy{} + for _, ear := range ears { + deletePolicies = append(deletePolicies, policies.Policy{ + Subject: ear.RoleID, + SubjectRelation: policies.MemberRelation, + SubjectType: policies.RoleType, + Relation: ear.Action, + ObjectType: r.entityType, + Object: ear.EntityID, + }) + } + for _, emr := range emrs { + deletePolicies = append(deletePolicies, policies.Policy{ + Subject: policies.EncodeDomainUserID(domainID, emr.MemberID), + SubjectType: policies.UserType, + Relation: policies.MemberRelation, + ObjectType: policies.RoleType, + Object: emr.RoleID, + }) + } + + if err := r.policy.DeletePolicies(ctx, deletePolicies); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + if len(optionalDeletePolicies) > 1 { + if err := r.policy.DeletePolicies(ctx, optionalDeletePolicies); err != nil { + return errors.Wrap(errRemoveOptionalDeletePolicies, err) + } + } + + for _, optionalFilterDeletePolicy := range optionalFilterDeletePolicies { + if err := r.policy.DeletePolicyFilter(ctx, optionalFilterDeletePolicy); err != nil { + return errors.Wrap(errRemoveOptionalFilterDeletePolicies, err) + } + } + return nil +} + +func (r ProvisionManageService) AddNewEntitiesRoles(ctx context.Context, domainID, userID string, entityIDs []string, optionalEntityPolicies []policies.Policy, newBuiltInRoleMembers map[BuiltInRoleName][]Member) (retRolesProvision []RoleProvision, retErr error) { + var newRolesProvision []RoleProvision + prs := []policies.Policy{} + + for _, entityID := range entityIDs { + for defaultRole, defaultRoleMembers := range newBuiltInRoleMembers { + actions, ok := r.builtInRoles[defaultRole] + if !ok { + return []RoleProvision{}, fmt.Errorf("default role %s not found in in-built roles", defaultRole) + } + + // There an option to have id as entityID_roleName where in roleName all space are removed with _ and starts with letter and supports only alphanumeric, space and hyphen + sid, err := r.sidProvider.ID() + if err != nil { + return []RoleProvision{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + + id := r.entityType + "_" + sid + if err := r.validateActions(actions); err != nil { + return []RoleProvision{}, errors.Wrap(svcerr.ErrMalformedEntity, err) + } + + members := roleMembersToString(defaultRoleMembers) + caps := roleActionsToString(actions) + + newRolesProvision = append(newRolesProvision, RoleProvision{ + Role: Role{ + ID: id, + Name: defaultRole.String(), + EntityID: entityID, + CreatedAt: time.Now(), + CreatedBy: userID, + }, + OptionalActions: caps, + OptionalMembers: members, + }) + + for _, cap := range caps { + prs = append(prs, policies.Policy{ + SubjectType: policies.RoleType, + SubjectRelation: policies.MemberRelation, + Subject: id, + Relation: cap, + Object: entityID, + ObjectType: r.entityType, + }) + } + + for _, member := range members { + prs = append(prs, policies.Policy{ + SubjectType: policies.UserType, + Subject: policies.EncodeDomainUserID(domainID, member), + Relation: policies.MemberRelation, + Object: id, + ObjectType: policies.RoleType, + }) + } + } + } + prs = append(prs, optionalEntityPolicies...) + + if len(prs) > 0 { + if err := r.policy.AddPolicies(ctx, prs); err != nil { + return []RoleProvision{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + defer func() { + if retErr != nil { + if errRollBack := r.policy.DeletePolicies(ctx, prs); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errRollbackRoles, errRollBack)) + } + } + }() + } + + if _, err := r.repo.AddRoles(ctx, newRolesProvision); err != nil { + return []RoleProvision{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + + return newRolesProvision, nil +} + +func (r ProvisionManageService) AddRole(ctx context.Context, session authn.Session, entityID string, roleName string, optionalActions []string, optionalMembers []string) (retRole Role, retErr error) { + + // ToDo: Research: Discuss: There an option to have id as eentityType_entityID_roleName where in roleName all space are removed with _ and starts with letter and supports only alphanumeric, space and hyphen + sid, err := r.sidProvider.ID() + if err != nil { + return Role{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + + id := r.entityType + "_" + sid + + if err := r.validateActions(toRolesActions(optionalActions)); err != nil { + return Role{}, errors.Wrap(svcerr.ErrMalformedEntity, err) + } + + newRoleProvisions := []RoleProvision{ + { + Role: Role{ + ID: id, + Name: roleName, + EntityID: entityID, + CreatedAt: time.Now(), + CreatedBy: session.UserID, + }, + OptionalActions: optionalActions, + OptionalMembers: optionalMembers, + }, + } + prs := []policies.Policy{} + + for _, cap := range optionalActions { + prs = append(prs, policies.Policy{ + SubjectType: policies.RoleType, + SubjectRelation: policies.MemberRelation, + Subject: id, + Relation: cap, + Object: entityID, + ObjectType: r.entityType, + }) + } + + for _, member := range optionalMembers { + prs = append(prs, policies.Policy{ + SubjectType: policies.UserType, + Subject: policies.EncodeDomainUserID(session.DomainID, member), + Relation: policies.MemberRelation, + Object: id, + ObjectType: policies.RoleType, + }) + } + + if len(prs) > 0 { + if err := r.policy.AddPolicies(ctx, prs); err != nil { + return Role{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + + defer func() { + if retErr != nil { + if errRollBack := r.policy.DeletePolicies(ctx, prs); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errRollbackRoles, errRollBack)) + } + } + }() + } + + newRoles, err := r.repo.AddRoles(context.Background(), newRoleProvisions) + if err != nil { + return Role{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + + if len(newRoles) == 0 { + return Role{}, svcerr.ErrCreateEntity + } + + return newRoles[0], nil +} + +func (r ProvisionManageService) RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) error { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + req := policies.Policy{ + SubjectType: policies.RoleType, + Subject: ro.ID, + } + // ToDo: Add Role as Object in DeletePolicyFilter + if err := r.policy.DeletePolicyFilter(ctx, req); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + if err := r.repo.RemoveRoles(ctx, []string{ro.ID}); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + return nil +} + +func (r ProvisionManageService) UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (Role, error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, oldRoleName) + if err != nil { + return Role{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + ro, err = r.repo.UpdateRole(ctx, Role{ + ID: ro.ID, + EntityID: entityID, + Name: newRoleName, + UpdatedBy: session.UserID, + UpdatedAt: time.Now(), + }) + if err != nil { + return Role{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return ro, nil +} + +func (r ProvisionManageService) RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (Role, error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return Role{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return ro, nil +} + +func (r ProvisionManageService) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (RolePage, error) { + ros, err := r.repo.RetrieveAllRoles(ctx, entityID, limit, offset) + if err != nil { + return RolePage{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return ros, nil +} + +func (r ProvisionManageService) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + acts := []string{} + for _, a := range r.actions { + acts = append(acts, string(a)) + } + return acts, nil +} + +func (r ProvisionManageService) RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (retActs []string, retErr error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return []string{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + + if len(actions) == 0 { + return []string{}, svcerr.ErrMalformedEntity + } + + if err := r.validateActions(toRolesActions(actions)); err != nil { + return []string{}, errors.Wrap(svcerr.ErrMalformedEntity, err) + } + + prs := []policies.Policy{} + for _, cap := range actions { + prs = append(prs, policies.Policy{ + SubjectType: policies.RoleType, + SubjectRelation: policies.MemberRelation, + Subject: ro.ID, + Relation: cap, + Object: entityID, + ObjectType: r.entityType, + }) + } + + if err := r.policy.AddPolicies(ctx, prs); err != nil { + return []string{}, errors.Wrap(svcerr.ErrAddPolicies, err) + } + + defer func() { + if retErr != nil { + if errRollBack := r.policy.DeletePolicies(ctx, prs); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errRollbackRoles, errRollBack)) + } + } + }() + + ro.UpdatedAt = time.Now() + ro.UpdatedBy = session.UserID + + resActs, err := r.repo.RoleAddActions(ctx, ro, actions) + if err != nil { + return []string{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + return resActs, nil +} + +func (r ProvisionManageService) RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) ([]string, error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return []string{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + + acts, err := r.repo.RoleListActions(ctx, ro.ID) + if err != nil { + return []string{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return acts, nil + +} + +func (r ProvisionManageService) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return false, errors.Wrap(svcerr.ErrViewEntity, err) + } + + result, err := r.repo.RoleCheckActionsExists(ctx, ro.ID, actions) + if err != nil { + return true, errors.Wrap(svcerr.ErrViewEntity, err) + } + return result, nil +} + +func (r ProvisionManageService) RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + if len(actions) == 0 { + return svcerr.ErrMalformedEntity + } + + prs := []policies.Policy{} + for _, op := range actions { + prs = append(prs, policies.Policy{ + SubjectType: policies.RoleType, + SubjectRelation: policies.MemberRelation, + Subject: ro.ID, + Relation: op, + Object: entityID, + ObjectType: r.entityType, + }) + } + + if err := r.policy.DeletePolicies(ctx, prs); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + ro.UpdatedAt = time.Now() + ro.UpdatedBy = session.UserID + if err := r.repo.RoleRemoveActions(ctx, ro, actions); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + return nil +} + +func (r ProvisionManageService) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) error { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + prs := policies.Policy{ + SubjectType: policies.RoleType, + Subject: ro.ID, + } + + if err := r.policy.DeletePolicyFilter(ctx, prs); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + ro.UpdatedAt = time.Now() + ro.UpdatedBy = session.UserID + + if err := r.repo.RoleRemoveAllActions(ctx, ro); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + return nil +} + +func (r ProvisionManageService) RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (retMems []string, retErr error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return []string{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + + if len(members) == 0 { + return []string{}, svcerr.ErrMalformedEntity + } + + prs := []policies.Policy{} + for _, mem := range members { + prs = append(prs, policies.Policy{ + SubjectType: policies.UserType, + Subject: policies.EncodeDomainUserID(session.DomainID, mem), + Relation: policies.MemberRelation, + Object: ro.ID, + ObjectType: policies.RoleType, + }) + } + + if err := r.policy.AddPolicies(ctx, prs); err != nil { + return []string{}, errors.Wrap(svcerr.ErrAddPolicies, err) + } + + defer func() { + if retErr != nil { + if errRollBack := r.policy.DeletePolicies(ctx, prs); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errRollbackRoles, errRollBack)) + } + } + }() + + ro.UpdatedAt = time.Now() + ro.UpdatedBy = session.UserID + + mems, err := r.repo.RoleAddMembers(ctx, ro, members) + if err != nil { + return []string{}, errors.Wrap(svcerr.ErrCreateEntity, err) + } + return mems, nil +} + +func (r ProvisionManageService) RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (MembersPage, error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return MembersPage{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + + mp, err := r.repo.RoleListMembers(ctx, ro.ID, limit, offset) + if err != nil { + return MembersPage{}, errors.Wrap(svcerr.ErrViewEntity, err) + } + return mp, nil +} + +func (r ProvisionManageService) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return false, errors.Wrap(svcerr.ErrViewEntity, err) + } + + result, err := r.repo.RoleCheckMembersExists(ctx, ro.ID, members) + if err != nil { + return true, errors.Wrap(svcerr.ErrViewEntity, err) + } + return result, nil +} + +func (r ProvisionManageService) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + if len(members) == 0 { + return svcerr.ErrMalformedEntity + } + + prs := []policies.Policy{} + for _, mem := range members { + prs = append(prs, policies.Policy{ + SubjectType: policies.UserType, + Subject: policies.EncodeDomainUserID(session.DomainID, mem), + Relation: policies.MemberRelation, + Object: ro.ID, + ObjectType: policies.RoleType, + }) + } + + if err := r.policy.DeletePolicies(ctx, prs); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + ro.UpdatedAt = time.Now() + // ro.UpdatedBy = userID + if err := r.repo.RoleRemoveMembers(ctx, ro, members); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + return nil +} + +func (r ProvisionManageService) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + ro, err := r.repo.RetrieveRoleByEntityIDAndName(ctx, entityID, roleName) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + prs := policies.Policy{ + ObjectType: policies.RoleType, + Object: ro.ID, + SubjectType: policies.UserType, + } + + if err := r.policy.DeletePolicyFilter(ctx, prs); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + ro.UpdatedAt = time.Now() + ro.UpdatedBy = session.UserID + + if err := r.repo.RoleRemoveAllMembers(ctx, ro); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + return nil +} + +func (r ProvisionManageService) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, member string) (err error) { + if err := r.repo.RemoveMemberFromAllRoles(ctx, member); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + prs := policies.Policy{ + ObjectType: policies.RoleType, + ObjectPrefix: r.entityType + "_", + SubjectType: policies.UserType, + } + + if err := r.policy.DeletePolicyFilter(ctx, prs); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + return fmt.Errorf("not implemented") +} diff --git a/pkg/roles/repo/doc.go b/pkg/roles/repo/doc.go new file mode 100644 index 0000000000..13812d96ca --- /dev/null +++ b/pkg/roles/repo/doc.go @@ -0,0 +1,4 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package repo diff --git a/pkg/roles/repo/postgres/doc.go b/pkg/roles/repo/postgres/doc.go new file mode 100644 index 0000000000..2112922075 --- /dev/null +++ b/pkg/roles/repo/postgres/doc.go @@ -0,0 +1,4 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres diff --git a/pkg/roles/repo/postgres/init.go b/pkg/roles/repo/postgres/init.go new file mode 100644 index 0000000000..8c1727b299 --- /dev/null +++ b/pkg/roles/repo/postgres/init.go @@ -0,0 +1,63 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "fmt" + + _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access + migrate "github.com/rubenv/sql-migrate" +) + +// Migration of Auth service. +func Migration(rolesTableNamePrefix, entityTableName, entityIDColumnName string) (*migrate.MemoryMigrationSource, error) { + + // ToDo: need to add check in database to check table exists and column exits as primary key. For this Migration function need database. + // ToDo: Add entity name in all table prefix. This helps when all entities uses same database + // ToDo: Add table name prefix option in service and repo. So each entity can have its own tables in same database + if entityTableName == "" || entityIDColumnName == "" { + return nil, fmt.Errorf("invalid entity Table Name or column name") + } + + return &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: fmt.Sprintf("%s_roles_1", rolesTableNamePrefix), + Up: []string{ + fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s_roles ( + id VARCHAR(254) NOT NULL PRIMARY KEY, + name varchar(200) NOT NULL, + entity_id VARCHAR(36) NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + updated_by VARCHAR(254), + created_by VARCHAR(254), + CONSTRAINT unique_role_name_entity_id_constraint UNIQUE ( name, entity_id), + CONSTRAINT fk_entity_id FOREIGN KEY(entity_id) REFERENCES %s(%s) ON DELETE CASCADE + );`, rolesTableNamePrefix, entityTableName, entityIDColumnName), + + fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s_role_actions ( + role_id VARCHAR(254) NOT NULL, + action VARCHAR(254) NOT NULL, + CONSTRAINT unique_domain_role_action_constraint UNIQUE ( role_id, action), + CONSTRAINT fk_%s_roles_id FOREIGN KEY(role_id) REFERENCES %s_roles(id) ON DELETE CASCADE + + );`, rolesTableNamePrefix, rolesTableNamePrefix, rolesTableNamePrefix), + + fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s_role_members ( + role_id VARCHAR(254) NOT NULL, + member_id VARCHAR(254) NOT NULL, + CONSTRAINT unique_role_member_constraint UNIQUE (role_id, member_id), + CONSTRAINT fk_%s_roles_id FOREIGN KEY(role_id) REFERENCES %s_roles(id) ON DELETE CASCADE + );`, rolesTableNamePrefix, rolesTableNamePrefix, rolesTableNamePrefix), + }, + Down: []string{ + fmt.Sprintf(`DROP TABLE IF EXISTS %s_roles`, rolesTableNamePrefix), + fmt.Sprintf(`DROP TABLE IF EXISTS %s_roles_actions`, rolesTableNamePrefix), + fmt.Sprintf(`DROP TABLE IF EXISTS %s_roles_members`, rolesTableNamePrefix), + }, + }, + }, + }, nil +} diff --git a/pkg/roles/repo/postgres/roles.go b/pkg/roles/repo/postgres/roles.go new file mode 100644 index 0000000000..9b9ed2e8eb --- /dev/null +++ b/pkg/roles/repo/postgres/roles.go @@ -0,0 +1,772 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package postgres + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + "github.com/absmach/magistrala/pkg/postgres" + "github.com/absmach/magistrala/pkg/roles" +) + +var _ roles.Repository = (*Repository)(nil) + +type Repository struct { + db postgres.Database + tableNamePrefix string + entityTableName string + entityIDColumnName string +} + +// NewRepository instantiates a PostgreSQL +// implementation of Roles repository. +func NewRepository(db postgres.Database, tableNamePrefix, entityTableName, entityIDColumnName string) Repository { + return Repository{ + db: db, + tableNamePrefix: tableNamePrefix, + entityTableName: entityTableName, + entityIDColumnName: entityIDColumnName, + } +} + +type dbPage struct { + ID string `db:"id"` + Name string `db:"name"` + EntityID string `db:"entity_id"` + RoleID string `db:"role_id"` + Limit uint64 `db:"limit"` + Offset uint64 `db:"offset"` +} +type dbRole struct { + ID string `db:"id"` + Name string `db:"name"` + EntityID string `db:"entity_id"` + CreatedBy *string `db:"created_by"` + CreatedAt sql.NullTime `db:"created_at"` + UpdatedBy *string `db:"updated_by"` + UpdatedAt sql.NullTime `db:"updated_at"` +} + +type dbEntityActionRole struct { + EntityID string `db:"entity_id"` + Action string `db:"action"` + RoleID string `db:"role_id"` +} +type dbEntityMemberRole struct { + EntityID string `db:"entity_id"` + MemberID string `db:"member_id"` + RoleID string `db:"role_id"` +} + +func dbToEntityActionRole(dbs []dbEntityActionRole) []roles.EntityActionRole { + var r []roles.EntityActionRole + for _, d := range dbs { + r = append(r, roles.EntityActionRole{ + EntityID: d.EntityID, + Action: d.Action, + RoleID: d.RoleID, + }) + } + return r +} + +func dbToEntityMemberRole(dbs []dbEntityMemberRole) []roles.EntityMemberRole { + var r []roles.EntityMemberRole + for _, d := range dbs { + r = append(r, roles.EntityMemberRole{ + EntityID: d.EntityID, + MemberID: d.MemberID, + RoleID: d.RoleID, + }) + } + return r +} + +type dbRoleAction struct { + RoleID string `db:"role_id"` + Action string `db:"action"` +} + +type dbRoleMember struct { + RoleID string `db:"role_id"` + MemberID string `db:"member_id"` +} + +func toDBRoles(role roles.Role) dbRole { + var createdBy *string + if role.CreatedBy != "" { + createdBy = &role.UpdatedBy + } + var createdAt sql.NullTime + if role.CreatedAt != (time.Time{}) && !role.CreatedAt.IsZero() { + createdAt = sql.NullTime{Time: role.CreatedAt, Valid: true} + } + + var updatedBy *string + if role.UpdatedBy != "" { + updatedBy = &role.UpdatedBy + } + var updatedAt sql.NullTime + if role.UpdatedAt != (time.Time{}) && !role.UpdatedAt.IsZero() { + updatedAt = sql.NullTime{Time: role.UpdatedAt, Valid: true} + } + + return dbRole{ + ID: role.ID, + Name: role.Name, + EntityID: role.EntityID, + CreatedBy: createdBy, + CreatedAt: createdAt, + UpdatedBy: updatedBy, + UpdatedAt: updatedAt, + } +} + +func toRole(r dbRole) roles.Role { + + var createdBy string + if r.CreatedBy != nil { + createdBy = *r.CreatedBy + } + var createdAt time.Time + if r.CreatedAt.Valid { + createdAt = r.CreatedAt.Time + } + + var updatedBy string + if r.UpdatedBy != nil { + updatedBy = *r.UpdatedBy + } + var updatedAt time.Time + if r.UpdatedAt.Valid { + updatedAt = r.UpdatedAt.Time + } + + return roles.Role{ + ID: r.ID, + Name: r.Name, + EntityID: r.EntityID, + CreatedBy: createdBy, + CreatedAt: createdAt, + UpdatedBy: updatedBy, + UpdatedAt: updatedAt, + } + +} +func (repo *Repository) AddRoles(ctx context.Context, rps []roles.RoleProvision) ([]roles.Role, error) { + + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return []roles.Role{}, errors.Wrap(repoerr.ErrCreateEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) + } + } + }() + + var retRoles []roles.Role + + for _, rp := range rps { + + q := fmt.Sprintf(`INSERT INTO %s_roles (id, name, entity_id, created_by, created_at, updated_by, updated_at) + VALUES (:id, :name, :entity_id, :created_by, :created_at, :updated_by, :updated_at);`, repo.tableNamePrefix) + + if _, err := tx.NamedExec(q, toDBRoles(rp.Role)); err != nil { + return []roles.Role{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + retRoles = append(retRoles, rp.Role) + + if len(rp.OptionalActions) > 0 { + capq := fmt.Sprintf(`INSERT INTO %s_role_actions (role_id, action) + VALUES (:role_id, :action) + RETURNING role_id, action`, repo.tableNamePrefix) + + rCaps := []dbRoleAction{} + for _, cap := range rp.OptionalActions { + rCaps = append(rCaps, dbRoleAction{ + RoleID: rp.ID, + Action: string(cap), + }) + } + if _, err := tx.NamedExec(capq, rCaps); err != nil { + return []roles.Role{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + } + + if len(rp.OptionalMembers) > 0 { + mq := fmt.Sprintf(`INSERT INTO %s_role_members (role_id, member_id) + VALUES (:role_id, :member_id) + RETURNING role_id, member_id`, repo.tableNamePrefix) + + rMems := []dbRoleMember{} + for _, m := range rp.OptionalMembers { + rMems = append(rMems, dbRoleMember{ + RoleID: rp.ID, + MemberID: m, + }) + } + if _, err := tx.NamedExec(mq, rMems); err != nil { + return []roles.Role{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + } + } + + if err := tx.Commit(); err != nil { + return []roles.Role{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + return retRoles, nil +} + +func (repo *Repository) RemoveRoles(ctx context.Context, roleIDs []string) error { + q := fmt.Sprintf("DELETE FROM %s_roles WHERE id = ANY(:role_ids) ;", repo.tableNamePrefix) + + params := map[string]interface{}{ + "role_ids": roleIDs, + } + result, err := repo.db.NamedExecContext(ctx, q, params) + if err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + if rows, _ := result.RowsAffected(); rows == 0 { + return repoerr.ErrNotFound + } + + return nil +} + +// Update only role name, don't update ID +func (repo *Repository) UpdateRole(ctx context.Context, role roles.Role) (roles.Role, error) { + var query []string + var upq string + if role.Name != "" { + query = append(query, "name = :name,") + } + + if len(query) > 0 { + upq = strings.Join(query, " ") + } + + q := fmt.Sprintf(`UPDATE %s_roles SET %s updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id + RETURNING id, name, entity_id, created_by, created_at, updated_by, updated_at`, + repo.tableNamePrefix, upq) + + row, err := repo.db.NamedQueryContext(ctx, q, toDBRoles(role)) + + if err != nil { + return roles.Role{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) + } + defer row.Close() + + dbr := dbRole{} + if row.Next() { + if err := row.StructScan(&dbr); err != nil { + return roles.Role{}, errors.Wrap(repoerr.ErrUpdateEntity, err) + } + return toRole(dbr), nil + } + + return roles.Role{}, repoerr.ErrNotFound +} + +func (repo *Repository) RetrieveRole(ctx context.Context, roleID string) (roles.Role, error) { + q := fmt.Sprintf(`SELECT id, name, entity_id, created_by, created_at, updated_by, updated_at + FROM %s_roles WHERE id = :id`, repo.tableNamePrefix) + + dbr := dbRole{ + ID: roleID, + } + + rows, err := repo.db.NamedQueryContext(ctx, q, dbr) + if err != nil { + return roles.Role{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + dbr = dbRole{} + if rows.Next() { + if err = rows.StructScan(&dbr); err != nil { + return roles.Role{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + + return toRole(dbr), nil + } + + return roles.Role{}, repoerr.ErrNotFound +} + +func (repo *Repository) RetrieveRoleByEntityIDAndName(ctx context.Context, entityID, roleName string) (roles.Role, error) { + q := fmt.Sprintf(`SELECT id, name, entity_id, created_by, created_at, updated_by, updated_at + FROM %s_roles WHERE entity_id = :entity_id and name = :name`, repo.tableNamePrefix) + + dbr := dbRole{ + EntityID: entityID, + Name: roleName, + } + + rows, err := repo.db.NamedQueryContext(ctx, q, dbr) + if err != nil { + return roles.Role{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + dbr = dbRole{} + if rows.Next() { + if err = rows.StructScan(&dbr); err != nil { + return roles.Role{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + + return toRole(dbr), nil + } + + return roles.Role{}, repoerr.ErrNotFound +} +func (repo *Repository) RetrieveAllRoles(ctx context.Context, entityID string, limit, offset uint64) (roles.RolePage, error) { + q := fmt.Sprintf(`SELECT id, name, entity_id, created_by, created_at, updated_by, updated_at + FROM %s_roles WHERE entity_id = :entity_id ORDER BY created_at LIMIT :limit OFFSET :offset;`, repo.tableNamePrefix) + + dbp := dbPage{ + EntityID: entityID, + Limit: limit, + Offset: offset, + } + + rows, err := repo.db.NamedQueryContext(ctx, q, dbp) + if err != nil { + return roles.RolePage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + items := []roles.Role{} + for rows.Next() { + dbr := dbRole{} + if err := rows.StructScan(&dbr); err != nil { + return roles.RolePage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + items = append(items, toRole(dbr)) + } + cq := fmt.Sprintf(`SELECT COUNT(*) FROM %s_roles WHERE entity_id = :entity_id`, repo.tableNamePrefix) + + total, err := postgres.Total(ctx, repo.db, cq, dbp) + if err != nil { + return roles.RolePage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + page := roles.RolePage{ + Roles: items, + Total: total, + Offset: offset, + Limit: limit, + } + + return page, nil +} + +func (repo *Repository) RoleAddActions(ctx context.Context, role roles.Role, actions []string) (caps []string, err error) { + + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return []string{}, errors.Wrap(repoerr.ErrCreateEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) + } + } + }() + + capq := fmt.Sprintf(`INSERT INTO %s_role_actions (role_id, action) + VALUES (:role_id, :action) + RETURNING role_id, action`, repo.tableNamePrefix) + + rCaps := []dbRoleAction{} + for _, cap := range actions { + rCaps = append(rCaps, dbRoleAction{ + RoleID: role.ID, + Action: string(cap), + }) + } + if _, err := tx.NamedExecContext(ctx, capq, rCaps); err != nil { + return []string{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + upq := fmt.Sprintf(`UPDATE %s_roles SET updated_at = :updated_at, updated_by = :updated_by WHERE id = :id;`, repo.tableNamePrefix) + if _, err := tx.NamedExecContext(ctx, upq, toDBRoles(role)); err != nil { + return []string{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + if err := tx.Commit(); err != nil { + return []string{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + return repo.RoleListActions(ctx, role.ID) +} + +func (repo *Repository) RoleListActions(ctx context.Context, roleID string) ([]string, error) { + q := fmt.Sprintf(`SELECT role_id, action FROM %s_role_actions WHERE role_id = :role_id ;`, repo.tableNamePrefix) + + dbrcap := dbRoleAction{ + RoleID: roleID, + } + + rows, err := repo.db.NamedQueryContext(ctx, q, dbrcap) + if err != nil { + return []string{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + items := []string{} + for rows.Next() { + dbrcap = dbRoleAction{} + if err := rows.StructScan(&dbrcap); err != nil { + return []string{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + items = append(items, dbrcap.Action) + } + return items, nil +} + +func (repo *Repository) RoleCheckActionsExists(ctx context.Context, roleID string, actions []string) (bool, error) { + q := fmt.Sprintf(`SELECT COUNT(*) FROM %s_role_actions WHERE role_id = :role_id AND action IN (:actions)`, repo.tableNamePrefix) + + params := map[string]interface{}{ + "role_id": roleID, + "actions": actions, + } + var count int + query, err := repo.db.NamedQueryContext(ctx, q, params) + if err != nil { + return false, errors.Wrap(repoerr.ErrViewEntity, err) + } + + defer query.Close() + + if query.Next() { + if err := query.Scan(&count); err != nil { + return false, errors.Wrap(repoerr.ErrViewEntity, err) + } + } + + // Check if the count matches the number of actions provided + if count != len(actions) { + return false, nil + } + + return true, nil +} + +func (repo *Repository) RoleRemoveActions(ctx context.Context, role roles.Role, actions []string) (err error) { + + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) + } + } + }() + + q := fmt.Sprintf(`DELETE FROM %s_role_actions WHERE role_id = :role_id AND action = ANY(:actions)`, repo.tableNamePrefix) + + params := map[string]interface{}{ + "role_id": role.ID, + "actions": actions, + } + + if _, err := tx.NamedExec(q, params); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + + upq := fmt.Sprintf(`UPDATE %s_roles SET updated_at = :updated_at, updated_by = :updated_by WHERE id = :id;`, repo.tableNamePrefix) + if _, err := tx.NamedExec(upq, toDBRoles(role)); err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + + return nil +} + +func (repo *Repository) RoleRemoveAllActions(ctx context.Context, role roles.Role) error { + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) + } + } + }() + + q := fmt.Sprintf(`DELETE FROM %s_role_actions WHERE role_id = :role_id `, repo.tableNamePrefix) + + dbrcap := dbRoleAction{RoleID: role.ID} + + if _, err := tx.NamedExec(q, dbrcap); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + + upq := fmt.Sprintf(`UPDATE %s_roles SET updated_at = :updated_at, updated_by = :updated_by WHERE id = :id;`, repo.tableNamePrefix) + if _, err := tx.NamedExec(upq, toDBRoles(role)); err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + + return nil +} + +func (repo *Repository) RoleAddMembers(ctx context.Context, role roles.Role, members []string) ([]string, error) { + mq := fmt.Sprintf(`INSERT INTO %s_role_members (role_id, member_id) + VALUES (:role_id, :member_id) + RETURNING role_id, member_id`, repo.tableNamePrefix) + + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return []string{}, errors.Wrap(repoerr.ErrCreateEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) + } + } + }() + + rMems := []dbRoleMember{} + for _, m := range members { + rMems = append(rMems, dbRoleMember{ + RoleID: role.ID, + MemberID: m, + }) + } + if _, err := tx.NamedExec(mq, rMems); err != nil { + return []string{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + upq := fmt.Sprintf(`UPDATE %s_roles SET updated_at = :updated_at, updated_by = :updated_by WHERE id = :id;`, repo.tableNamePrefix) + if _, err := tx.NamedExec(upq, toDBRoles(role)); err != nil { + return []string{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + if err := tx.Commit(); err != nil { + return []string{}, postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + return members, nil +} + +func (repo *Repository) RoleListMembers(ctx context.Context, roleID string, limit, offset uint64) (roles.MembersPage, error) { + q := fmt.Sprintf(`SELECT role_id, member_id FROM %s_role_members WHERE role_id = :role_id LIMIT :limit OFFSET :offset;`, repo.tableNamePrefix) + + dbp := dbPage{ + RoleID: roleID, + Limit: limit, + Offset: offset, + } + + rows, err := repo.db.NamedQueryContext(ctx, q, dbp) + if err != nil { + return roles.MembersPage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + items := []string{} + for rows.Next() { + dbrmems := dbRoleMember{} + if err := rows.StructScan(&dbrmems); err != nil { + return roles.MembersPage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + items = append(items, dbrmems.MemberID) + } + + cq := fmt.Sprintf(`SELECT COUNT(*) FROM %s_role_members WHERE role_id = :role_id`, repo.tableNamePrefix) + + total, err := postgres.Total(ctx, repo.db, cq, dbp) + if err != nil { + return roles.MembersPage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + return roles.MembersPage{ + Members: items, + Total: total, + Offset: offset, + Limit: limit, + }, nil + +} + +func (repo *Repository) RoleCheckMembersExists(ctx context.Context, roleID string, members []string) (bool, error) { + q := fmt.Sprintf(`SELECT COUNT(*) FROM %s_role_members WHERE role_id = :role_id AND action IN (:members)`, repo.tableNamePrefix) + + params := map[string]interface{}{ + "role_id": roleID, + "members": members, + } + var count int + query, err := repo.db.NamedQueryContext(ctx, q, params) + if err != nil { + return false, errors.Wrap(repoerr.ErrViewEntity, err) + } + + defer query.Close() + + if query.Next() { + if err := query.Scan(&count); err != nil { + return false, errors.Wrap(repoerr.ErrViewEntity, err) + } + } + + if count != len(members) { + return false, nil + } + + return true, nil +} + +func (repo *Repository) RoleRemoveMembers(ctx context.Context, role roles.Role, members []string) (err error) { + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) + } + } + }() + + q := fmt.Sprintf(`DELETE FROM %s_role_members WHERE role_id = :role_id AND member_id = ANY(:member_ids)`, repo.tableNamePrefix) + + params := map[string]interface{}{ + "role_id": role.ID, + "member_ids": members, + } + + if _, err := tx.NamedExec(q, params); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + + upq := fmt.Sprintf(`UPDATE %s_roles SET updated_at = :updated_at, updated_by = :updated_by WHERE id = :id;`, repo.tableNamePrefix) + if _, err := tx.NamedExec(upq, toDBRoles(role)); err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (repo *Repository) RoleRemoveAllMembers(ctx context.Context, role roles.Role) (err error) { + tx, err := repo.db.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + defer func() { + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) + } + } + }() + q := fmt.Sprintf(`DELETE FROM %s_role_members WHERE role_id = :role_id `, repo.tableNamePrefix) + + dbrcap := dbRoleAction{RoleID: role.ID} + + if _, err := repo.db.NamedExecContext(ctx, q, dbrcap); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + + upq := fmt.Sprintf(`UPDATE %s_roles SET updated_at = :updated_at, updated_by = :updated_by WHERE id = :id;`, repo.tableNamePrefix) + if _, err := tx.NamedExec(upq, toDBRoles(role)); err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (repo *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) { + params := map[string]interface{}{ + "entity_ids": entityIDs, + } + + thingsActionsRolesQuery := fmt.Sprintf(`SELECT e.%s AS entity_id , era."action" AS "action", er.id AS role_id + FROM %s e + JOIN %s_roles er ON er.entity_id = e.%s + JOIN %s_role_actions era ON era.role_id = er.id + WHERE e.%s = ANY(:entity_ids); + `, repo.entityIDColumnName, repo.entityTableName, repo.tableNamePrefix, repo.entityIDColumnName, repo.tableNamePrefix, repo.entityIDColumnName) + rows, err := repo.db.NamedQueryContext(ctx, thingsActionsRolesQuery, params) + if err != nil { + return []roles.EntityActionRole{}, []roles.EntityMemberRole{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + + defer rows.Close() + dbears := []dbEntityActionRole{} + for rows.Next() { + dbear := dbEntityActionRole{} + if err = rows.StructScan(&dbear); err != nil { + return []roles.EntityActionRole{}, []roles.EntityMemberRole{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + + dbears = append(dbears, dbear) + } + thingsMembersRolesQuery := fmt.Sprintf(`SELECT e.%s AS entity_id , erm.member_id AS member_id, er.id AS role_id + FROM %s e + JOIN %s_roles er ON er.entity_id = e.%s + JOIN %s_role_members erm ON erm.role_id = er.id + WHERE e.%s = ANY(:entity_ids); + `, repo.entityIDColumnName, repo.entityTableName, repo.tableNamePrefix, repo.entityIDColumnName, repo.tableNamePrefix, repo.entityIDColumnName) + + rows, err = repo.db.NamedQueryContext(ctx, thingsMembersRolesQuery, params) + if err != nil { + return []roles.EntityActionRole{}, []roles.EntityMemberRole{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + + defer rows.Close() + dbemrs := []dbEntityMemberRole{} + for rows.Next() { + dbemr := dbEntityMemberRole{} + if err = rows.StructScan(&dbemr); err != nil { + return []roles.EntityActionRole{}, []roles.EntityMemberRole{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + + dbemrs = append(dbemrs, dbemr) + } + return dbToEntityActionRole(dbears), dbToEntityMemberRole(dbemrs), nil +} +func (repo *Repository) RemoveMemberFromAllRoles(ctx context.Context, memberID string) (err error) { + + return nil +} diff --git a/pkg/roles/rolemanager/api/decoders.go b/pkg/roles/rolemanager/api/decoders.go new file mode 100644 index 0000000000..fb73797df8 --- /dev/null +++ b/pkg/roles/rolemanager/api/decoders.go @@ -0,0 +1,200 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + "encoding/json" + "net/http" + "strings" + + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/errors" + "github.com/go-chi/chi/v5" +) + +type Decoder struct { + entityIDTemplate string +} + +func NewDecoder(entityIDTemplate string) Decoder { + return Decoder{entityIDTemplate} +} +func (d Decoder) DecodeCreateRole(_ 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 := createRoleReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + } + 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 (d Decoder) DecodeListRoles(_ context.Context, r *http.Request) (interface{}, error) { + o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + req := listRolesReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + limit: l, + offset: o, + } + return req, nil +} + +func (d Decoder) DecodeViewRole(_ context.Context, r *http.Request) (interface{}, error) { + req := viewRoleReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + return req, nil +} + +func (d Decoder) DecodeUpdateRole(_ 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 := updateRoleReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + 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 (d Decoder) DecodeDeleteRole(_ context.Context, r *http.Request) (interface{}, error) { + req := deleteRoleReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + return req, nil +} +func (d Decoder) DecodeListAvailableActions(_ context.Context, r *http.Request) (interface{}, error) { + req := listAvailableActionsReq{ + token: apiutil.ExtractBearerToken(r), + } + return req, nil +} + +func (d Decoder) DecodeAddRoleActions(_ 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 := addRoleActionsReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + 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 (d Decoder) DecodeListRoleActions(_ context.Context, r *http.Request) (interface{}, error) { + req := listRoleActionsReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + return req, nil +} + +func (d Decoder) DecodeDeleteRoleActions(_ 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 := deleteRoleActionsReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + 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 (d Decoder) DecodeDeleteAllRoleActions(_ context.Context, r *http.Request) (interface{}, error) { + req := deleteAllRoleActionsReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + return req, nil +} + +func (d Decoder) DecodeAddRoleMembers(_ 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 := addRoleMembersReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + 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 (d Decoder) DecodeListRoleMembers(_ context.Context, r *http.Request) (interface{}, error) { + o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + req := listRoleMembersReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + limit: l, + offset: o, + } + return req, nil +} + +func (d Decoder) DecodeDeleteRoleMembers(_ 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 := deleteRoleMembersReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + 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 (d Decoder) DecodeDeleteAllRoleMembers(_ context.Context, r *http.Request) (interface{}, error) { + req := deleteAllRoleMembersReq{ + token: apiutil.ExtractBearerToken(r), + entityID: chi.URLParam(r, d.entityIDTemplate), + roleName: chi.URLParam(r, "roleName"), + } + return req, nil +} diff --git a/pkg/roles/rolemanager/api/endpoints.go b/pkg/roles/rolemanager/api/endpoints.go new file mode 100644 index 0000000000..52efe71e08 --- /dev/null +++ b/pkg/roles/rolemanager/api/endpoints.go @@ -0,0 +1,278 @@ +package http + +import ( + "context" + + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/roles" + "github.com/go-kit/kit/endpoint" +) + +func CreateRoleEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(createRoleReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ro, err := svc.AddRole(ctx, session, req.entityID, req.RoleName, req.OptionalMembers, req.OptionalMembers) + if err != nil { + return nil, err + } + return createRoleRes{Role: ro}, nil + } +} + +func ListRolesEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listRolesReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ros, err := svc.RetrieveAllRoles(ctx, session, req.entityID, req.limit, req.offset) + if err != nil { + return nil, err + } + return listRolesRes{RolePage: ros}, nil + } +} + +func ViewRoleEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(viewRoleReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ro, err := svc.RetrieveRole(ctx, session, req.entityID, req.roleName) + if err != nil { + return nil, err + } + return viewRoleRes{Role: ro}, nil + } +} + +func UpdateRoleEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(updateRoleReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + ro, err := svc.UpdateRoleName(ctx, session, req.entityID, req.roleName, req.Name) + if err != nil { + return nil, err + } + return updateRoleRes{Role: ro}, nil + } +} +func DeleteRoleEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteRoleReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.RemoveRole(ctx, session, req.entityID, req.roleName); err != nil { + return nil, err + } + return deleteRoleRes{}, nil + } +} +func ListAvailableActionsEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listAvailableActionsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + acts, err := svc.ListAvailableActions(ctx, session) + if err != nil { + return nil, err + } + return listAvailableActionsRes{acts}, nil + } +} +func AddRoleActionsEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(addRoleActionsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + caps, err := svc.RoleAddActions(ctx, session, req.entityID, req.roleName, req.Actions) + if err != nil { + return nil, err + } + return addRoleActionsRes{Actions: caps}, nil + } +} +func ListRoleActionsEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listRoleActionsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + caps, err := svc.RoleListActions(ctx, session, req.entityID, req.roleName) + if err != nil { + return nil, err + } + return listRoleActionsRes{Actions: caps}, nil + } +} +func DeleteRoleActionsEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteRoleActionsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.RoleRemoveActions(ctx, session, req.entityID, req.roleName, req.Actions); err != nil { + return nil, err + } + return deleteRoleActionsRes{}, nil + } +} +func DeleteAllRoleActionsEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteAllRoleActionsReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.RoleRemoveAllActions(ctx, session, req.entityID, req.roleName); err != nil { + return nil, err + } + return deleteAllRoleActionsRes{}, nil + } +} +func AddRoleMembersEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(addRoleMembersReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + members, err := svc.RoleAddMembers(ctx, session, req.entityID, req.roleName, req.Members) + if err != nil { + return nil, err + } + return addRoleMembersRes{members}, nil + } +} +func ListRoleMembersEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listRoleMembersReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + mp, err := svc.RoleListMembers(ctx, session, req.entityID, req.roleName, req.limit, req.offset) + if err != nil { + return nil, err + } + return listRoleMembersRes{mp}, nil + } +} +func DeleteRoleMembersEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteRoleMembersReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.RoleRemoveMembers(ctx, session, req.entityID, req.roleName, req.Members); err != nil { + return nil, err + } + return deleteRoleMembersRes{}, nil + } +} +func DeleteAllRoleMembersEndpoint(svc roles.RoleManager) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteAllRoleMembersReq) + if err := req.validate(); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + session, ok := ctx.Value(api.SessionKey).(authn.Session) + if !ok { + return nil, svcerr.ErrAuthentication + } + + if err := svc.RoleRemoveAllMembers(ctx, session, req.entityID, req.roleName); err != nil { + return nil, err + } + return deleteAllRoleMemberRes{}, nil + } +} diff --git a/pkg/roles/rolemanager/api/requests.go b/pkg/roles/rolemanager/api/requests.go new file mode 100644 index 0000000000..314a71985f --- /dev/null +++ b/pkg/roles/rolemanager/api/requests.go @@ -0,0 +1,298 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" +) + +type createRoleReq struct { + token string + entityID string + RoleName string `json:"role_name"` + OptionalActions []string `json:"optional_actions"` + OptionalMembers []string `json:"optional_members"` +} + +func (req createRoleReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if err := api.ValidateUUID(req.entityID); err != nil { + return err + } + if len(req.RoleName) == 0 { + return apiutil.ErrMissingRoleName + } + if len(req.RoleName) > 200 { + return apiutil.ErrNameSize + } + + return nil +} + +type listRolesReq struct { + token string + entityID string + limit uint64 + offset uint64 +} + +func (req listRolesReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.limit > api.MaxLimitSize || req.limit < 1 { + return apiutil.ErrLimitSize + } + return nil +} + +type viewRoleReq struct { + token string + entityID string + roleName string +} + +func (req viewRoleReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + return nil +} + +type updateRoleReq struct { + token string + entityID string + roleName string + Name string `json:"name"` +} + +func (req updateRoleReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" || req.Name == "" { + return apiutil.ErrMissingRoleName + } + return nil +} + +type deleteRoleReq struct { + token string + entityID string + roleName string +} + +func (req deleteRoleReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + return nil +} + +type listAvailableActionsReq struct { + token string +} + +func (req listAvailableActionsReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + return nil +} + +type addRoleActionsReq struct { + token string + entityID string + roleName string + Actions []string `json:"actions"` +} + +func (req addRoleActionsReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + + if len(req.Actions) == 0 { + return apiutil.ErrMissingPolicyEntityType + } + return nil +} + +type listRoleActionsReq struct { + token string + entityID string + roleName string +} + +func (req listRoleActionsReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + return nil +} + +type deleteRoleActionsReq struct { + token string + entityID string + roleName string + Actions []string `json:"actions"` +} + +func (req deleteRoleActionsReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + + if len(req.Actions) == 0 { + return apiutil.ErrMissingPolicyEntityType + } + return nil +} + +type deleteAllRoleActionsReq struct { + token string + entityID string + roleName string +} + +func (req deleteAllRoleActionsReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + return nil +} + +type addRoleMembersReq struct { + token string + entityID string + roleName string + Members []string `json:"members"` +} + +func (req addRoleMembersReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + if len(req.Members) == 0 { + return apiutil.ErrMissingRoleMembers + } + return nil +} + +type listRoleMembersReq struct { + token string + entityID string + roleName string + limit uint64 + offset uint64 +} + +func (req listRoleMembersReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + if req.limit > api.MaxLimitSize || req.limit < 1 { + return apiutil.ErrLimitSize + } + return nil +} + +type deleteRoleMembersReq struct { + token string + entityID string + roleName string + Members []string `json:"members"` +} + +func (req deleteRoleMembersReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + if len(req.Members) == 0 { + return apiutil.ErrMissingRoleMembers + } + return nil +} + +type deleteAllRoleMembersReq struct { + token string + entityID string + roleName string +} + +func (req deleteAllRoleMembersReq) validate() error { + if req.token == "" { + return apiutil.ErrBearerToken + } + if req.entityID == "" { + return apiutil.ErrMissingID + } + if req.roleName == "" { + return apiutil.ErrMissingRoleName + } + return nil +} diff --git a/pkg/roles/rolemanager/api/responses.go b/pkg/roles/rolemanager/api/responses.go new file mode 100644 index 0000000000..2633edb129 --- /dev/null +++ b/pkg/roles/rolemanager/api/responses.go @@ -0,0 +1,243 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "net/http" + + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/pkg/roles" +) + +var ( + _ magistrala.Response = (*createRoleRes)(nil) + _ magistrala.Response = (*listRolesRes)(nil) + _ magistrala.Response = (*viewRoleRes)(nil) + _ magistrala.Response = (*updateRoleRes)(nil) + _ magistrala.Response = (*deleteRoleRes)(nil) + _ magistrala.Response = (*listAvailableActionsRes)(nil) + _ magistrala.Response = (*addRoleActionsRes)(nil) + _ magistrala.Response = (*listRoleActionsRes)(nil) + _ magistrala.Response = (*deleteRoleActionsRes)(nil) + _ magistrala.Response = (*deleteAllRoleActionsRes)(nil) + _ magistrala.Response = (*addRoleMembersRes)(nil) + _ magistrala.Response = (*listRoleMembersRes)(nil) + _ magistrala.Response = (*deleteRoleMembersRes)(nil) + _ magistrala.Response = (*deleteAllRoleMemberRes)(nil) +) + +type createRoleRes struct { + roles.Role +} + +func (res createRoleRes) Code() int { + return http.StatusCreated +} + +func (res createRoleRes) Headers() map[string]string { + return map[string]string{} +} + +func (res createRoleRes) Empty() bool { + return false +} + +type listRolesRes struct { + roles.RolePage +} + +func (res listRolesRes) Code() int { + return http.StatusOK +} + +func (res listRolesRes) Headers() map[string]string { + return map[string]string{} +} + +func (res listRolesRes) Empty() bool { + return false +} + +type viewRoleRes struct { + roles.Role +} + +func (res viewRoleRes) Code() int { + return http.StatusOK +} + +func (res viewRoleRes) Headers() map[string]string { + return map[string]string{} +} + +func (res viewRoleRes) Empty() bool { + return false +} + +type updateRoleRes struct { + roles.Role +} + +func (res updateRoleRes) Code() int { + return http.StatusOK +} + +func (res updateRoleRes) Headers() map[string]string { + return map[string]string{} +} + +func (res updateRoleRes) Empty() bool { + return false +} + +type deleteRoleRes struct { +} + +func (res deleteRoleRes) Code() int { + return http.StatusNoContent +} + +func (res deleteRoleRes) Headers() map[string]string { + return map[string]string{} +} + +func (res deleteRoleRes) Empty() bool { + return true +} + +type listAvailableActionsRes struct { + AvailableActions []string `json:"available_actions"` +} + +func (res listAvailableActionsRes) Code() int { + return http.StatusOK +} + +func (res listAvailableActionsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res listAvailableActionsRes) Empty() bool { + return false +} + +type addRoleActionsRes struct { + Actions []string `json:"actions"` +} + +func (res addRoleActionsRes) Code() int { + return http.StatusOK +} + +func (res addRoleActionsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res addRoleActionsRes) Empty() bool { + return false +} + +type listRoleActionsRes struct { + Actions []string `json:"actions"` +} + +func (res listRoleActionsRes) Code() int { + return http.StatusOK +} + +func (res listRoleActionsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res listRoleActionsRes) Empty() bool { + return false +} + +type deleteRoleActionsRes struct{} + +func (res deleteRoleActionsRes) Code() int { + return http.StatusNoContent +} + +func (res deleteRoleActionsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res deleteRoleActionsRes) Empty() bool { + return true +} + +type deleteAllRoleActionsRes struct{} + +func (res deleteAllRoleActionsRes) Code() int { + return http.StatusNoContent +} + +func (res deleteAllRoleActionsRes) Headers() map[string]string { + return map[string]string{} +} + +func (res deleteAllRoleActionsRes) Empty() bool { + return true +} + +type addRoleMembersRes struct { + Members []string `json:"members"` +} + +func (res addRoleMembersRes) Code() int { + return http.StatusOK +} + +func (res addRoleMembersRes) Headers() map[string]string { + return map[string]string{} +} + +func (res addRoleMembersRes) Empty() bool { + return false +} + +type listRoleMembersRes struct { + roles.MembersPage +} + +func (res listRoleMembersRes) Code() int { + return http.StatusOK +} + +func (res listRoleMembersRes) Headers() map[string]string { + return map[string]string{} +} + +func (res listRoleMembersRes) Empty() bool { + return false +} + +type deleteRoleMembersRes struct{} + +func (res deleteRoleMembersRes) Code() int { + return http.StatusNoContent +} + +func (res deleteRoleMembersRes) Headers() map[string]string { + return map[string]string{} +} + +func (res deleteRoleMembersRes) Empty() bool { + return true +} + +type deleteAllRoleMemberRes struct{} + +func (res deleteAllRoleMemberRes) Code() int { + return http.StatusNoContent +} + +func (res deleteAllRoleMemberRes) Headers() map[string]string { + return map[string]string{} +} + +func (res deleteAllRoleMemberRes) Empty() bool { + return true +} diff --git a/pkg/roles/rolemanager/api/router.go b/pkg/roles/rolemanager/api/router.go new file mode 100644 index 0000000000..4b427df516 --- /dev/null +++ b/pkg/roles/rolemanager/api/router.go @@ -0,0 +1,128 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/roles" + "github.com/go-chi/chi/v5" + kithttp "github.com/go-kit/kit/transport/http" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +func EntityRoleMangerRouter(svc roles.RoleManager, d Decoder, r chi.Router, opts []kithttp.ServerOption) chi.Router { + r.Route("/roles", func(r chi.Router) { + + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + CreateRoleEndpoint(svc), + d.DecodeCreateRole, + api.EncodeResponse, + opts..., + ), "create_role").ServeHTTP) + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + ListRolesEndpoint(svc), + d.DecodeListRoles, + api.EncodeResponse, + opts..., + ), "list_roles").ServeHTTP) + + r.Route("/{roleName}", func(r chi.Router) { + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + ViewRoleEndpoint(svc), + d.DecodeViewRole, + api.EncodeResponse, + opts..., + ), "view_role").ServeHTTP) + + r.Put("/", otelhttp.NewHandler(kithttp.NewServer( + UpdateRoleEndpoint(svc), + d.DecodeUpdateRole, + api.EncodeResponse, + opts..., + ), "update_role").ServeHTTP) + + r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( + DeleteRoleEndpoint(svc), + d.DecodeDeleteRole, + api.EncodeResponse, + opts..., + ), "delete_role").ServeHTTP) + + r.Route("/actions", func(r chi.Router) { + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + AddRoleActionsEndpoint(svc), + d.DecodeAddRoleActions, + api.EncodeResponse, + opts..., + ), "add_role_actions").ServeHTTP) + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + ListRoleActionsEndpoint(svc), + d.DecodeListRoleActions, + api.EncodeResponse, + opts..., + ), "list_role_actions").ServeHTTP) + + r.Post("/delete", otelhttp.NewHandler(kithttp.NewServer( + DeleteRoleActionsEndpoint(svc), + d.DecodeDeleteRoleActions, + api.EncodeResponse, + opts..., + ), "delete_role_actions").ServeHTTP) + + r.Post("/delete-all", otelhttp.NewHandler(kithttp.NewServer( + DeleteAllRoleActionsEndpoint(svc), + d.DecodeDeleteAllRoleActions, + api.EncodeResponse, + opts..., + ), "delete_all_role_actions").ServeHTTP) + }) + + r.Route("/members", func(r chi.Router) { + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + AddRoleMembersEndpoint(svc), + d.DecodeAddRoleMembers, + api.EncodeResponse, + opts..., + ), "add_role_members").ServeHTTP) + + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + ListRoleMembersEndpoint(svc), + d.DecodeListRoleMembers, + api.EncodeResponse, + opts..., + ), "list_role_members").ServeHTTP) + + r.Post("/delete", otelhttp.NewHandler(kithttp.NewServer( + DeleteRoleMembersEndpoint(svc), + d.DecodeDeleteRoleMembers, + api.EncodeResponse, + opts..., + ), "delete_role_members").ServeHTTP) + + r.Post("/delete-all", otelhttp.NewHandler(kithttp.NewServer( + DeleteAllRoleMembersEndpoint(svc), + d.DecodeDeleteAllRoleMembers, + api.EncodeResponse, + opts..., + ), "delete_all_role_members").ServeHTTP) + }) + }) + + }) + + return r +} + +func EntityAvailableActionsRouter(svc roles.RoleManager, d Decoder, r chi.Router, opts []kithttp.ServerOption) chi.Router { + r.Get("/roles/available-actions", otelhttp.NewHandler(kithttp.NewServer( + ListAvailableActionsEndpoint(svc), + d.DecodeListAvailableActions, + api.EncodeResponse, + opts..., + ), "list_available_actions").ServeHTTP) + + return r +} diff --git a/pkg/roles/rolemanager/doc.go b/pkg/roles/rolemanager/doc.go new file mode 100644 index 0000000000..e3d9fc1aec --- /dev/null +++ b/pkg/roles/rolemanager/doc.go @@ -0,0 +1,4 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package rolemanager diff --git a/pkg/roles/rolemanager/events/doc.go b/pkg/roles/rolemanager/events/doc.go new file mode 100644 index 0000000000..a115b5f927 --- /dev/null +++ b/pkg/roles/rolemanager/events/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package events provides the domain concept definitions needed to +// support Magistrala auth service functionality. +package events diff --git a/pkg/roles/rolemanager/events/events.go b/pkg/roles/rolemanager/events/events.go new file mode 100644 index 0000000000..80daff19cb --- /dev/null +++ b/pkg/roles/rolemanager/events/events.go @@ -0,0 +1,4 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events diff --git a/pkg/roles/rolemanager/events/streams.go b/pkg/roles/rolemanager/events/streams.go new file mode 100644 index 0000000000..12a39c220c --- /dev/null +++ b/pkg/roles/rolemanager/events/streams.go @@ -0,0 +1,82 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/events" + "github.com/absmach/magistrala/pkg/roles" +) + +var _ roles.RoleManager = (*RoleManagerEventStore)(nil) + +type RoleManagerEventStore struct { + events.Publisher + svc roles.RoleManager + svcName string +} + +// NewEventStoreMiddleware returns wrapper around auth service that sends +// events to event store. +func NewRoleManagerEventStore(svcName string, svc roles.RoleManager, publisher events.Publisher) RoleManagerEventStore { + return RoleManagerEventStore{ + svcName: svcName, + svc: svc, + Publisher: publisher, + } +} + +func (res *RoleManagerEventStore) AddRole(ctx context.Context, session authn.Session, entityID, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + return res.svc.AddRole(ctx, session, entityID, roleName, optionalActions, optionalMembers) +} +func (res *RoleManagerEventStore) RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) error { + return res.svc.RemoveRole(ctx, session, entityID, roleName) +} +func (res *RoleManagerEventStore) UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (roles.Role, error) { + return res.svc.UpdateRoleName(ctx, session, entityID, oldRoleName, newRoleName) +} +func (res *RoleManagerEventStore) RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (roles.Role, error) { + return res.svc.RetrieveRole(ctx, session, entityID, roleName) +} +func (res *RoleManagerEventStore) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (roles.RolePage, error) { + return res.svc.RetrieveAllRoles(ctx, session, entityID, limit, offset) +} +func (res *RoleManagerEventStore) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + return res.svc.ListAvailableActions(ctx, session) +} +func (res *RoleManagerEventStore) RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (ops []string, err error) { + return res.svc.RoleAddActions(ctx, session, entityID, roleName, actions) +} +func (res *RoleManagerEventStore) RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) ([]string, error) { + return res.svc.RoleListActions(ctx, session, entityID, roleName) +} +func (res *RoleManagerEventStore) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) { + return res.svc.RoleCheckActionsExists(ctx, session, entityID, roleName, actions) +} +func (res *RoleManagerEventStore) RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) { + return res.svc.RoleRemoveActions(ctx, session, entityID, roleName, actions) +} +func (res *RoleManagerEventStore) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) error { + return res.svc.RoleRemoveAllActions(ctx, session, entityID, roleName) +} +func (res *RoleManagerEventStore) RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) ([]string, error) { + return res.svc.RoleAddMembers(ctx, session, entityID, roleName, members) +} +func (res *RoleManagerEventStore) RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (roles.MembersPage, error) { + return res.svc.RoleListMembers(ctx, session, entityID, roleName, limit, offset) +} +func (res *RoleManagerEventStore) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) { + return res.svc.RoleCheckMembersExists(ctx, session, entityID, roleName, members) +} +func (res *RoleManagerEventStore) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) { + return res.svc.RoleRemoveMembers(ctx, session, entityID, roleName, members) +} +func (res *RoleManagerEventStore) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + return res.svc.RoleRemoveAllMembers(ctx, session, entityID, roleName) +} +func (res *RoleManagerEventStore) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, membersID string) (err error) { + return res.svc.RemoveMemberFromAllRoles(ctx, session, membersID) +} diff --git a/pkg/roles/rolemanager/middleware/authoirzation.go b/pkg/roles/rolemanager/middleware/authoirzation.go new file mode 100644 index 0000000000..92c6e16134 --- /dev/null +++ b/pkg/roles/rolemanager/middleware/authoirzation.go @@ -0,0 +1,273 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "context" + + "github.com/absmach/magistrala/pkg/authn" + mgauthz "github.com/absmach/magistrala/pkg/authz" + "github.com/absmach/magistrala/pkg/errors" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/roles" + "github.com/absmach/magistrala/pkg/svcutil" +) + +var _ roles.RoleManager = (*RoleManagerAuthorizationMiddleware)(nil) + +type RoleManagerAuthorizationMiddleware struct { + entityType string + svc roles.RoleManager + authz mgauthz.Authorization + opp svcutil.OperationPerm +} + +// AuthorizationMiddleware adds authorization to the clients service. +func NewRoleManagerAuthorizationMiddleware(entityType string, svc roles.RoleManager, authz mgauthz.Authorization, opPerm map[svcutil.Operation]svcutil.Permission) (RoleManagerAuthorizationMiddleware, error) { + opp := roles.NewOperationPerm() + if err := opp.AddOperationPermissionMap(opPerm); err != nil { + return RoleManagerAuthorizationMiddleware{}, err + } + if err := opp.Validate(); err != nil { + return RoleManagerAuthorizationMiddleware{}, err + } + + ram := RoleManagerAuthorizationMiddleware{ + entityType: entityType, + svc: svc, + authz: authz, + opp: opp, + } + if err := ram.validate(); err != nil { + return RoleManagerAuthorizationMiddleware{}, err + } + return ram, nil +} + +func (ram RoleManagerAuthorizationMiddleware) validate() error { + if err := ram.opp.Validate(); err != nil { + return err + } + return nil +} + +func (ram RoleManagerAuthorizationMiddleware) AddRole(ctx context.Context, session authn.Session, entityID, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + if err := ram.authorize(ctx, roles.OpAddRole, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return roles.Role{}, err + } + return ram.svc.AddRole(ctx, session, entityID, roleName, optionalActions, optionalMembers) +} +func (ram RoleManagerAuthorizationMiddleware) RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) error { + if err := ram.authorize(ctx, roles.OpRemoveRole, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return err + } + return ram.svc.RemoveRole(ctx, session, entityID, roleName) +} +func (ram RoleManagerAuthorizationMiddleware) UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (roles.Role, error) { + if err := ram.authorize(ctx, roles.OpUpdateRoleName, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return roles.Role{}, err + } + return ram.svc.UpdateRoleName(ctx, session, entityID, oldRoleName, newRoleName) +} +func (ram RoleManagerAuthorizationMiddleware) RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (roles.Role, error) { + if err := ram.authorize(ctx, roles.OpRetrieveRole, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return roles.Role{}, err + } + return ram.svc.RetrieveRole(ctx, session, entityID, roleName) +} +func (ram RoleManagerAuthorizationMiddleware) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (roles.RolePage, error) { + if err := ram.authorize(ctx, roles.OpRetrieveAllRoles, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return roles.RolePage{}, err + } + return ram.svc.RetrieveAllRoles(ctx, session, entityID, limit, offset) +} +func (ram RoleManagerAuthorizationMiddleware) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + return ram.svc.ListAvailableActions(ctx, session) +} +func (ram RoleManagerAuthorizationMiddleware) RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (ops []string, err error) { + if err := ram.authorize(ctx, roles.OpRoleAddActions, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return []string{}, err + } + + return ram.svc.RoleAddActions(ctx, session, entityID, roleName, actions) +} +func (ram RoleManagerAuthorizationMiddleware) RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) ([]string, error) { + if err := ram.authorize(ctx, roles.OpRoleListActions, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return []string{}, err + } + + return ram.svc.RoleListActions(ctx, session, entityID, roleName) +} +func (ram RoleManagerAuthorizationMiddleware) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) { + if err := ram.authorize(ctx, roles.OpRoleCheckActionsExists, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return false, err + } + return ram.svc.RoleCheckActionsExists(ctx, session, entityID, roleName, actions) +} +func (ram RoleManagerAuthorizationMiddleware) RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) { + if err := ram.authorize(ctx, roles.OpRoleRemoveActions, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return err + } + return ram.svc.RoleRemoveActions(ctx, session, entityID, roleName, actions) +} +func (ram RoleManagerAuthorizationMiddleware) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) error { + if err := ram.authorize(ctx, roles.OpRoleRemoveAllActions, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return err + } + return ram.svc.RoleRemoveAllActions(ctx, session, entityID, roleName) +} +func (ram RoleManagerAuthorizationMiddleware) RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) ([]string, error) { + if err := ram.authorize(ctx, roles.OpRoleAddMembers, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return []string{}, err + } + return ram.svc.RoleAddMembers(ctx, session, entityID, roleName, members) +} +func (ram RoleManagerAuthorizationMiddleware) RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (roles.MembersPage, error) { + if err := ram.authorize(ctx, roles.OpRoleListMembers, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return roles.MembersPage{}, err + } + return ram.svc.RoleListMembers(ctx, session, entityID, roleName, limit, offset) +} +func (ram RoleManagerAuthorizationMiddleware) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) { + if err := ram.authorize(ctx, roles.OpRoleCheckMembersExists, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return false, err + } + return ram.svc.RoleCheckMembersExists(ctx, session, entityID, roleName, members) +} +func (ram RoleManagerAuthorizationMiddleware) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) { + if err := ram.authorize(ctx, roles.OpRoleRemoveMembers, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return err + } + return ram.svc.RoleRemoveMembers(ctx, session, entityID, roleName, members) +} +func (ram RoleManagerAuthorizationMiddleware) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + if err := ram.authorize(ctx, roles.OpRoleRemoveAllMembers, mgauthz.PolicyReq{ + Domain: session.DomainID, + Subject: session.DomainUserID, + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: entityID, + ObjectType: ram.entityType, + }); err != nil { + return err + } + return ram.svc.RoleRemoveAllMembers(ctx, session, entityID, roleName) +} + +func (ram RoleManagerAuthorizationMiddleware) authorize(ctx context.Context, op svcutil.Operation, pr mgauthz.PolicyReq) error { + perm, err := ram.opp.GetPermission(op) + if err != nil { + return err + } + + pr.Permission = perm.String() + + if err := ram.authz.Authorize(ctx, pr); err != nil { + return errors.Wrap(errors.ErrAuthorization, err) + } + + return nil +} + +func (ram RoleManagerAuthorizationMiddleware) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) (err error) { + return ram.svc.RemoveMemberFromAllRoles(ctx, session, memberID) +} diff --git a/pkg/roles/rolemanager/middleware/logging.go b/pkg/roles/rolemanager/middleware/logging.go new file mode 100644 index 0000000000..f8149a4f3b --- /dev/null +++ b/pkg/roles/rolemanager/middleware/logging.go @@ -0,0 +1,346 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +//go:build !test + +package middleware + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/roles" +) + +var _ roles.RoleManager = (*RoleManagerLoggingMiddleware)(nil) + +type RoleManagerLoggingMiddleware struct { + svcName string + svc roles.RoleManager + logger *slog.Logger +} + +func NewRoleManagerLoggingMiddleware(svcName string, svc roles.RoleManager, logger *slog.Logger) RoleManagerLoggingMiddleware { + return RoleManagerLoggingMiddleware{ + svcName: svcName, + svc: svc, + logger: logger, + } +} + +func (lm *RoleManagerLoggingMiddleware) AddRole(ctx context.Context, session authn.Session, entityID, roleName string, optionalActions []string, optionalMembers []string) (ro roles.Role, err error) { + prefix := fmt.Sprintf("Add %s roles", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_add_role", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + slog.Any("optional_actions", optionalActions), + slog.Any("optional_members", optionalMembers), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.AddRole(ctx, session, entityID, roleName, optionalActions, optionalMembers) +} + +func (lm *RoleManagerLoggingMiddleware) RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + prefix := fmt.Sprintf("Delete %s role", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_delete_role", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RemoveRole(ctx, session, entityID, roleName) +} + +func (lm *RoleManagerLoggingMiddleware) UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (ro roles.Role, err error) { + prefix := fmt.Sprintf("Update %s role name", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_update_role_name", + slog.String("entity_id", entityID), + slog.String("old_role_name", oldRoleName), + slog.String("new_role_name", newRoleName), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.UpdateRoleName(ctx, session, entityID, oldRoleName, newRoleName) +} + +func (lm *RoleManagerLoggingMiddleware) RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (ro roles.Role, err error) { + prefix := fmt.Sprintf("Retrieve %s role", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_update_role_name", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RetrieveRole(ctx, session, entityID, roleName) +} + +func (lm *RoleManagerLoggingMiddleware) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (rp roles.RolePage, err error) { + prefix := fmt.Sprintf("List %s roles", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_roles_retrieve_all", + slog.String("entity_id", entityID), + slog.Uint64("limit", limit), + slog.Uint64("offset", offset), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RetrieveAllRoles(ctx, session, entityID, limit, offset) +} + +func (lm *RoleManagerLoggingMiddleware) ListAvailableActions(ctx context.Context, session authn.Session) (acts []string, err error) { + prefix := fmt.Sprintf("List %s available actions", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName + "_list_available_actions"), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.ListAvailableActions(ctx, session) +} + +func (lm *RoleManagerLoggingMiddleware) RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (caps []string, err error) { + prefix := fmt.Sprintf("%s role add actions", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_role_add_actions", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + slog.Any("actions", actions), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleAddActions(ctx, session, entityID, roleName, actions) +} + +func (lm *RoleManagerLoggingMiddleware) RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) (roOps []string, err error) { + prefix := fmt.Sprintf("%s role list actions", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_list_role_actions", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleListActions(ctx, session, entityID, roleName) +} + +func (lm *RoleManagerLoggingMiddleware) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) { + return lm.svc.RoleCheckActionsExists(ctx, session, entityID, roleName, actions) +} + +func (lm *RoleManagerLoggingMiddleware) RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) { + prefix := fmt.Sprintf("%s role remove actions", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_role_remove_actions", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + slog.Any("actions", actions), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleRemoveActions(ctx, session, entityID, roleName, actions) +} + +func (lm *RoleManagerLoggingMiddleware) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + prefix := fmt.Sprintf("%s role remove all actions", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_role_remove_all_actions", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleRemoveAllActions(ctx, session, entityID, roleName) +} + +func (lm *RoleManagerLoggingMiddleware) RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (mems []string, err error) { + prefix := fmt.Sprintf("%s role add members", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_role_add_members", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + slog.Any("members", members), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleAddMembers(ctx, session, entityID, roleName, members) +} + +func (lm *RoleManagerLoggingMiddleware) RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (mp roles.MembersPage, err error) { + prefix := fmt.Sprintf("%s role list members", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_role_add_members", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + slog.Uint64("limit", limit), + slog.Uint64("offset", offset), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleListMembers(ctx, session, entityID, roleName, limit, offset) +} + +func (lm *RoleManagerLoggingMiddleware) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) { + return lm.svc.RoleCheckMembersExists(ctx, session, entityID, roleName, members) +} + +func (lm *RoleManagerLoggingMiddleware) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) { + prefix := fmt.Sprintf("%s role remove members", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_role_remove_members", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + slog.Any("members", members), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleRemoveMembers(ctx, session, entityID, roleName, members) +} + +func (lm *RoleManagerLoggingMiddleware) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + prefix := fmt.Sprintf("%s role remove all members", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_role_remove_all_members", + slog.String("entity_id", entityID), + slog.String("role_name", roleName), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RoleRemoveAllMembers(ctx, session, entityID, roleName) +} + +func (lm *RoleManagerLoggingMiddleware) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) (err error) { + prefix := fmt.Sprintf("%s remove members from all roles", lm.svcName) + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group(lm.svcName+"_remove_members_from_all_roles", + slog.Any("member_id", memberID), + ), + } + if err != nil { + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn(prefix+" failed", args...) + return + } + lm.logger.Info(prefix+" completed successfully", args...) + }(time.Now()) + return lm.svc.RemoveMemberFromAllRoles(ctx, session, memberID) +} diff --git a/pkg/roles/rolemanager/middleware/meterics.go b/pkg/roles/rolemanager/middleware/meterics.go new file mode 100644 index 0000000000..48228a7214 --- /dev/null +++ b/pkg/roles/rolemanager/middleware/meterics.go @@ -0,0 +1,84 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +//go:build !test + +package middleware + +import ( + "context" + + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/roles" + "github.com/go-kit/kit/metrics" +) + +var _ roles.RoleManager = (*RoleManagerMetricsMiddleware)(nil) + +type RoleManagerMetricsMiddleware struct { + svcName string + svc roles.RoleManager + counter metrics.Counter + latency metrics.Histogram +} + +func NewRoleManagerMetricsMiddleware(svcName string, svc roles.RoleManager, counter metrics.Counter, latency metrics.Histogram) RoleManagerMetricsMiddleware { + return RoleManagerMetricsMiddleware{ + svcName: svcName, + svc: svc, + counter: counter, + latency: latency, + } +} + +func (rmm *RoleManagerMetricsMiddleware) AddRole(ctx context.Context, session authn.Session, entityID, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + return rmm.svc.AddRole(ctx, session, entityID, roleName, optionalActions, optionalMembers) +} +func (rmm *RoleManagerMetricsMiddleware) RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) error { + return rmm.svc.RemoveRole(ctx, session, entityID, roleName) +} +func (rmm *RoleManagerMetricsMiddleware) UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (roles.Role, error) { + return rmm.svc.UpdateRoleName(ctx, session, entityID, oldRoleName, newRoleName) +} +func (rmm *RoleManagerMetricsMiddleware) RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (roles.Role, error) { + return rmm.svc.RetrieveRole(ctx, session, entityID, roleName) +} +func (rmm *RoleManagerMetricsMiddleware) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (roles.RolePage, error) { + return rmm.svc.RetrieveAllRoles(ctx, session, entityID, limit, offset) +} +func (rmm *RoleManagerMetricsMiddleware) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + return rmm.svc.ListAvailableActions(ctx, session) +} +func (rmm *RoleManagerMetricsMiddleware) RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (caps []string, err error) { + return rmm.svc.RoleAddActions(ctx, session, entityID, roleName, actions) +} +func (rmm *RoleManagerMetricsMiddleware) RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) ([]string, error) { + return rmm.svc.RoleListActions(ctx, session, entityID, roleName) +} +func (rmm *RoleManagerMetricsMiddleware) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) { + return rmm.svc.RoleCheckActionsExists(ctx, session, entityID, roleName, actions) +} +func (rmm *RoleManagerMetricsMiddleware) RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) { + return rmm.svc.RoleRemoveActions(ctx, session, entityID, roleName, actions) +} +func (rmm *RoleManagerMetricsMiddleware) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) error { + return rmm.svc.RoleRemoveAllActions(ctx, session, entityID, roleName) +} +func (rmm *RoleManagerMetricsMiddleware) RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) ([]string, error) { + return rmm.svc.RoleAddMembers(ctx, session, entityID, roleName, members) +} +func (rmm *RoleManagerMetricsMiddleware) RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (roles.MembersPage, error) { + return rmm.svc.RoleListMembers(ctx, session, entityID, roleName, limit, offset) +} +func (rmm *RoleManagerMetricsMiddleware) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) { + return rmm.svc.RoleCheckMembersExists(ctx, session, entityID, roleName, members) +} +func (rmm *RoleManagerMetricsMiddleware) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) { + return rmm.svc.RoleRemoveMembers(ctx, session, entityID, roleName, members) +} +func (rmm *RoleManagerMetricsMiddleware) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + return rmm.svc.RoleRemoveAllMembers(ctx, session, entityID, roleName) +} +func (rmm *RoleManagerMetricsMiddleware) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) (err error) { + return rmm.svc.RemoveMemberFromAllRoles(ctx, session, memberID) +} diff --git a/pkg/roles/rolemanager/tracing/tracing.go b/pkg/roles/rolemanager/tracing/tracing.go new file mode 100644 index 0000000000..2db8a92fde --- /dev/null +++ b/pkg/roles/rolemanager/tracing/tracing.go @@ -0,0 +1,73 @@ +package tracing + +import ( + "context" + + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/roles" + "go.opentelemetry.io/otel/trace" +) + +var _ roles.RoleManager = (*RoleManagerTracing)(nil) + +type RoleManagerTracing struct { + svcName string + roles roles.RoleManager + tracer trace.Tracer +} + +func NewRoleManagerTracing(svcName string, svc roles.RoleManager, tracer trace.Tracer) RoleManagerTracing { + return RoleManagerTracing{svcName, svc, tracer} +} + +func (rtm *RoleManagerTracing) AddRole(ctx context.Context, session authn.Session, entityID, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + return rtm.roles.AddRole(ctx, session, entityID, roleName, optionalActions, optionalMembers) +} +func (rtm *RoleManagerTracing) RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) error { + return rtm.roles.RemoveRole(ctx, session, entityID, roleName) +} +func (rtm *RoleManagerTracing) UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (roles.Role, error) { + return rtm.roles.UpdateRoleName(ctx, session, entityID, oldRoleName, newRoleName) +} +func (rtm *RoleManagerTracing) RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (roles.Role, error) { + return rtm.roles.RetrieveRole(ctx, session, entityID, roleName) +} +func (rtm *RoleManagerTracing) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (roles.RolePage, error) { + return rtm.roles.RetrieveAllRoles(ctx, session, entityID, limit, offset) +} +func (rtm *RoleManagerTracing) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + return rtm.roles.ListAvailableActions(ctx, session) +} +func (rtm *RoleManagerTracing) RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (ops []string, err error) { + return rtm.roles.RoleAddActions(ctx, session, entityID, roleName, actions) +} +func (rtm *RoleManagerTracing) RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) ([]string, error) { + return rtm.roles.RoleListActions(ctx, session, entityID, roleName) +} +func (rtm *RoleManagerTracing) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) { + return rtm.roles.RoleCheckActionsExists(ctx, session, entityID, roleName, actions) +} +func (rtm *RoleManagerTracing) RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) { + return rtm.roles.RoleRemoveActions(ctx, session, entityID, roleName, actions) +} +func (rtm *RoleManagerTracing) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) error { + return rtm.roles.RoleRemoveAllActions(ctx, session, entityID, roleName) +} +func (rtm *RoleManagerTracing) RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) ([]string, error) { + return rtm.roles.RoleAddMembers(ctx, session, entityID, roleName, members) +} +func (rtm *RoleManagerTracing) RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (roles.MembersPage, error) { + return rtm.roles.RoleListMembers(ctx, session, entityID, roleName, limit, offset) +} +func (rtm *RoleManagerTracing) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) { + return rtm.roles.RoleCheckMembersExists(ctx, session, entityID, roleName, members) +} +func (rtm *RoleManagerTracing) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) { + return rtm.roles.RoleRemoveMembers(ctx, session, entityID, roleName, members) +} +func (rtm *RoleManagerTracing) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) { + return rtm.roles.RoleRemoveAllMembers(ctx, session, entityID, roleName) +} +func (rtm *RoleManagerTracing) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) (err error) { + return rtm.roles.RemoveMemberFromAllRoles(ctx, session, memberID) +} diff --git a/pkg/roles/roles.go b/pkg/roles/roles.go new file mode 100644 index 0000000000..ffe2c87804 --- /dev/null +++ b/pkg/roles/roles.go @@ -0,0 +1,255 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package roles + +import ( + "context" + "time" + + "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/svcutil" +) + +type Action string + +func (ac Action) String() string { + return string(ac) +} + +type Member string + +func (mem Member) String() string { + return string(mem) +} + +type RoleName string + +func (r RoleName) String() string { + return string(r) +} + +type BuiltInRoleName RoleName + +func (b BuiltInRoleName) ToRoleName() RoleName { + return RoleName(b) +} + +func (b BuiltInRoleName) String() string { + return string(b) +} + +type Role struct { + ID string `json:"id"` + Name string `json:"name"` + EntityID string `json:"entity_id"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedBy string `json:"updated_by"` + UpdatedAt time.Time `json:"updated_at"` +} + +type RoleProvision struct { + Role + OptionalActions []string `json:"-"` + OptionalMembers []string `json:"-"` +} + +type RolePage struct { + Total uint64 `json:"total"` + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Roles []Role `json:"roles"` +} + +type MembersPage struct { + Total uint64 `json:"total"` + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Members []string `json:"members"` +} + +type EntityActionRole struct { + EntityID string `json:"entity_id"` + Action string `json:"action"` + RoleID string `json:"role_id"` +} +type EntityMemberRole struct { + EntityID string `json:"entity_id"` + MemberID string `json:"member_id"` + RoleID string `json:"role_id"` +} + +//go:generate mockery --name Provisioner --output=./mocks --filename provisioner.go --quiet --note "Copyright (c) Abstract Machines" +type Provisioner interface { + AddNewEntitiesRoles(ctx context.Context, domainID, userID string, entityIDs []string, optionalEntityPolicies []policies.Policy, newBuiltInRoleMembers map[BuiltInRoleName][]Member) ([]RoleProvision, error) + RemoveEntitiesRoles(ctx context.Context, domainID, userID string, entityIDs []string, optionalFilterDeletePolicies []policies.Policy, optionalDeletePolicies []policies.Policy) error +} + +//go:generate mockery --name RoleManager --output=./mocks --filename rolemanager.go --quiet --note "Copyright (c) Abstract Machines" +type RoleManager interface { + + // Add New role to entity + AddRole(ctx context.Context, session authn.Session, entityID, roleName string, optionalActions []string, optionalMembers []string) (Role, error) + + // Remove removes the roles of entity. + RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) error + + // UpdateName update the name of the entity role. + UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (Role, error) + + RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (Role, error) + + RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (RolePage, error) + + ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) + + RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (ops []string, err error) + + RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) ([]string, error) + + RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) + + RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) + + RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) error + + RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) ([]string, error) + + RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (MembersPage, error) + + RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) + + RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) + + RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) + + RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) (err error) +} + +//go:generate mockery --name Repository --output=./mocks --filename rolesRepo.go --quiet --note "Copyright (c) Abstract Machines" +type Repository interface { + AddRoles(ctx context.Context, rps []RoleProvision) ([]Role, error) + RemoveRoles(ctx context.Context, roleIDs []string) error + UpdateRole(ctx context.Context, ro Role) (Role, error) + RetrieveRole(ctx context.Context, roleID string) (Role, error) + RetrieveRoleByEntityIDAndName(ctx context.Context, entityID, roleName string) (Role, error) + RetrieveAllRoles(ctx context.Context, entityID string, limit, offset uint64) (RolePage, error) + RoleAddActions(ctx context.Context, role Role, actions []string) (ops []string, err error) + RoleListActions(ctx context.Context, roleID string) ([]string, error) + RoleCheckActionsExists(ctx context.Context, roleID string, actions []string) (bool, error) + RoleRemoveActions(ctx context.Context, role Role, actions []string) (err error) + RoleRemoveAllActions(ctx context.Context, role Role) error + RoleAddMembers(ctx context.Context, role Role, members []string) ([]string, error) + RoleListMembers(ctx context.Context, roleID string, limit, offset uint64) (MembersPage, error) + RoleCheckMembersExists(ctx context.Context, roleID string, members []string) (bool, error) + RoleRemoveMembers(ctx context.Context, role Role, members []string) (err error) + RoleRemoveAllMembers(ctx context.Context, role Role) (err error) + RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]EntityActionRole, []EntityMemberRole, error) + RemoveMemberFromAllRoles(ctx context.Context, members string) (err error) +} + +type Roles interface { + + // Add New role to entity + AddRole(ctx context.Context, session authn.Session, entityID, roleName string, optionalActions []string, optionalMembers []string) (Role, error) + + // Remove removes the roles of entity. + RemoveRole(ctx context.Context, session authn.Session, entityID, roleName string) error + + // UpdateName update the name of the entity role. + UpdateRoleName(ctx context.Context, session authn.Session, entityID, oldRoleName, newRoleName string) (Role, error) + + RetrieveRole(ctx context.Context, session authn.Session, entityID, roleName string) (Role, error) + + RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit, offset uint64) (RolePage, error) + + ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) + + RoleAddActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (ops []string, err error) + + RoleListActions(ctx context.Context, session authn.Session, entityID, roleName string) ([]string, error) + + RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (bool, error) + + RoleRemoveActions(ctx context.Context, session authn.Session, entityID, roleName string, actions []string) (err error) + + RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID, roleName string) error + + RoleAddMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) ([]string, error) + + RoleListMembers(ctx context.Context, session authn.Session, entityID, roleName string, limit, offset uint64) (MembersPage, error) + + RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (bool, error) + + RoleRemoveMembers(ctx context.Context, session authn.Session, entityID, roleName string, members []string) (err error) + + RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID, roleName string) (err error) + + RemoveMembersFromAllRoles(ctx context.Context, session authn.Session, members []string) (err error) + + RemoveMembersFromRoles(ctx context.Context, session authn.Session, members []string, roleNames []string) (err error) + + RemoveActionsFromAllRoles(ctx context.Context, session authn.Session, actions []string) (err error) + + RemoveActionsFromRoles(ctx context.Context, session authn.Session, actions []string, roleNames []string) (err error) +} + +const ( + OpAddRole svcutil.Operation = iota + OpRemoveRole + OpUpdateRoleName + OpRetrieveRole + OpRetrieveAllRoles + OpRoleAddActions + OpRoleListActions + OpRoleCheckActionsExists + OpRoleRemoveActions + OpRoleRemoveAllActions + OpRoleAddMembers + OpRoleListMembers + OpRoleCheckMembersExists + OpRoleRemoveMembers + OpRoleRemoveAllMembers +) + +var expectedOperations = []svcutil.Operation{ + OpAddRole, + OpRemoveRole, + OpUpdateRoleName, + OpRetrieveRole, + OpRetrieveAllRoles, + OpRoleAddActions, + OpRoleListActions, + OpRoleCheckActionsExists, + OpRoleRemoveActions, + OpRoleRemoveAllActions, + OpRoleAddMembers, + OpRoleListMembers, + OpRoleCheckMembersExists, + OpRoleRemoveMembers, + OpRoleRemoveAllMembers, +} + +var operationNames = []string{ + "OpAddRole", + "OpRemoveRole", + "OpUpdateRoleName", + "OpRetrieveRole", + "OpRetrieveAllRoles", + "OpRoleAddActions", + "OpRoleListActions", + "OpRoleCheckActionsExists", + "OpRoleRemoveActions", + "OpRoleRemoveAllActions", + "OpRoleAddMembers", + "OpRoleListMembers", + "OpRoleCheckMembersExists", + "OpRoleRemoveMembers", + "OpRoleRemoveAllMembers", +} + +func NewOperationPerm() svcutil.OperationPerm { + return svcutil.NewOperationPerm(expectedOperations, operationNames) +} diff --git a/pkg/sdk/go/channels_test.go b/pkg/sdk/go/channels_test.go index d4b02dc686..e07020fff4 100644 --- a/pkg/sdk/go/channels_test.go +++ b/pkg/sdk/go/channels_test.go @@ -12,6 +12,8 @@ import ( "time" authmocks "github.com/absmach/magistrala/auth/mocks" + "github.com/absmach/magistrala/groups" + gmocks "github.com/absmach/magistrala/groups/mocks" "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" @@ -19,8 +21,6 @@ import ( authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" - gmocks "github.com/absmach/magistrala/pkg/groups/mocks" oauth2mocks "github.com/absmach/magistrala/pkg/oauth2/mocks" policies "github.com/absmach/magistrala/pkg/policies" sdk "github.com/absmach/magistrala/pkg/sdk/go" @@ -52,7 +52,7 @@ func setupChannels() (*httptest.Server, *gmocks.Service, *authnmocks.Authenticat mux := chi.NewRouter() - thapi.MakeHandler(tsvc, gsvc, authn, mux, logger, "") + thapi.MakeHandler(tsvc, authn, mux, logger, "") usapi.MakeHandler(usvc, authn, token, true, gsvc, mux, logger, "", passRegex, provider) return httptest.NewServer(mux), gsvc, authn } diff --git a/pkg/sdk/go/domains_test.go b/pkg/sdk/go/domains_test.go index ea1c484ecd..b9bf0c162a 100644 --- a/pkg/sdk/go/domains_test.go +++ b/pkg/sdk/go/domains_test.go @@ -11,8 +11,8 @@ import ( "time" "github.com/absmach/magistrala/auth" - httpapi "github.com/absmach/magistrala/auth/api/http/domains" authmocks "github.com/absmach/magistrala/auth/mocks" + httpapi "github.com/absmach/magistrala/domains/api/http" internalapi "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" diff --git a/pkg/sdk/go/groups_test.go b/pkg/sdk/go/groups_test.go index 82271465e1..0cf3fac032 100644 --- a/pkg/sdk/go/groups_test.go +++ b/pkg/sdk/go/groups_test.go @@ -12,6 +12,8 @@ import ( "time" authmocks "github.com/absmach/magistrala/auth/mocks" + "github.com/absmach/magistrala/groups" + "github.com/absmach/magistrala/groups/mocks" "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" @@ -19,8 +21,6 @@ import ( authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/groups/mocks" oauth2mocks "github.com/absmach/magistrala/pkg/oauth2/mocks" policies "github.com/absmach/magistrala/pkg/policies" sdk "github.com/absmach/magistrala/pkg/sdk/go" diff --git a/pkg/sid/README.md b/pkg/sid/README.md new file mode 100644 index 0000000000..1e95d1045b --- /dev/null +++ b/pkg/sid/README.md @@ -0,0 +1,2 @@ +# Short identity provider + diff --git a/pkg/sid/doc.go b/pkg/sid/doc.go new file mode 100644 index 0000000000..a3b28fdfde --- /dev/null +++ b/pkg/sid/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package ulid contains ULID generator. +package sid diff --git a/pkg/sid/mock.go b/pkg/sid/mock.go new file mode 100644 index 0000000000..5211b34606 --- /dev/null +++ b/pkg/sid/mock.go @@ -0,0 +1,35 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package sid + +import ( + "fmt" + "sync" + + "github.com/absmach/magistrala" +) + +// Prefix represents the prefix used to generate UUID mocks. +const Prefix = "123e4567-e89b-12d3-a456-" + +var _ magistrala.IDProvider = (*sidProviderMock)(nil) + +type sidProviderMock struct { + mu sync.Mutex + counter int +} + +func (up *sidProviderMock) ID() (string, error) { + up.mu.Lock() + defer up.mu.Unlock() + + up.counter++ + return fmt.Sprintf("%s%012d", Prefix, up.counter), nil +} + +// NewMock creates "mirror" uuid provider, i.e. generated +// token will hold value provided by the caller. +func NewMock() magistrala.IDProvider { + return &sidProviderMock{} +} diff --git a/pkg/sid/sid.go b/pkg/sid/sid.go new file mode 100644 index 0000000000..1e8cbfd468 --- /dev/null +++ b/pkg/sid/sid.go @@ -0,0 +1,55 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +// Package ulid provides a ULID identity provider. +package sid + +import ( + "encoding/binary" + + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/pkg/errors" + "github.com/gofrs/uuid/v5" + "github.com/sqids/sqids-go" +) + +// ErrGeneratingID indicates error in generating ULID. +var ( + ErrInitializingShortID = errors.New("failed to initialize short id provider") + ErrGeneratingID = errors.New("generating id failed") + ErrEncodeID = errors.New("encoding id failed") +) +var _ magistrala.IDProvider = (*sidProvider)(nil) + +type sidProvider struct { + sidEncoder *sqids.Sqids +} + +// New instantiates a short ID provider. +func New() (magistrala.IDProvider, error) { + sidEncoder, err := sqids.New(sqids.Options{ + Alphabet: "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE", + }) + if err != nil { + return nil, errors.Wrap(ErrInitializingShortID, err) + } + return &sidProvider{sidEncoder}, nil +} + +func (s *sidProvider) ID() (string, error) { + id, err := uuid.NewV4() + if err != nil { + return "", errors.Wrap(ErrGeneratingID, err) + } + idBytes := id.Bytes() + + sid, err := s.sidEncoder.Encode([]uint64{ + binary.BigEndian.Uint64(idBytes[:8]), + binary.BigEndian.Uint64(idBytes[8:]), + }) + if err != nil { + return "", errors.Wrap(ErrEncodeID, err) + } + + return sid, nil +} diff --git a/pkg/svcutil/externaloperationperm.go b/pkg/svcutil/externaloperationperm.go new file mode 100644 index 0000000000..aabb59a604 --- /dev/null +++ b/pkg/svcutil/externaloperationperm.go @@ -0,0 +1,77 @@ +package svcutil + +import "fmt" + +type ExternalOperation int + +func (op ExternalOperation) String(operations []string) string { + if (int(op) < 0) || (int(op) == len(operations)) { + return fmt.Sprintf("UnknownOperation(%d)", op) + } + return operations[op] +} + +type ExternalOperationPerm struct { + opPerm map[ExternalOperation]Permission + expectedOps []ExternalOperation + opNames []string +} + +func NewExternalOperationPerm(expectedOps []ExternalOperation, opNames []string) ExternalOperationPerm { + return ExternalOperationPerm{ + opPerm: make(map[ExternalOperation]Permission), + expectedOps: expectedOps, + opNames: opNames, + } +} + +func (eopp ExternalOperationPerm) isKeyRequired(eop ExternalOperation) bool { + for _, key := range eopp.expectedOps { + if key == eop { + return true + } + } + return false +} + +func (eopp ExternalOperationPerm) AddOperationPermissionMap(eopMap map[ExternalOperation]Permission) error { + // First iteration check all the keys are valid, If any one key is invalid then no key should be added. + for eop := range eopMap { + if !eopp.isKeyRequired(eop) { + return fmt.Errorf("%v is not a valid external operation", eop.String(eopp.opNames)) + } + } + for eop, perm := range eopMap { + eopp.opPerm[eop] = perm + } + return nil +} + +func (eopp ExternalOperationPerm) AddOperationPermission(eop ExternalOperation, perm Permission) error { + if !eopp.isKeyRequired(eop) { + return fmt.Errorf("%v is not a valid external operation", eop.String(eopp.opNames)) + } + eopp.opPerm[eop] = perm + return nil +} + +func (eopp ExternalOperationPerm) Validate() error { + for eop := range eopp.opPerm { + if !eopp.isKeyRequired(eop) { + return fmt.Errorf("ExternalOperationPerm: \"%s\" is not a valid external operation", eop.String(eopp.opNames)) + } + } + for _, eeo := range eopp.expectedOps { + if _, ok := eopp.opPerm[eeo]; !ok { + return fmt.Errorf("ExternalOperationPerm: \"%s\" external operation is missing", eeo.String(eopp.opNames)) + } + } + return nil +} + +func (eopp ExternalOperationPerm) GetPermission(eop ExternalOperation) (Permission, error) { + if perm, ok := eopp.opPerm[eop]; ok { + return perm, nil + } + return "", fmt.Errorf("external operation \"%s\" doesn't have any permissions", eop.String(eopp.opNames)) +} diff --git a/pkg/svcutil/operationperm.go b/pkg/svcutil/operationperm.go new file mode 100644 index 0000000000..916bca6112 --- /dev/null +++ b/pkg/svcutil/operationperm.go @@ -0,0 +1,83 @@ +package svcutil + +import "fmt" + +type Permission string + +func (p Permission) String() string { + return string(p) +} + +type Operation int + +func (op Operation) String(operations []string) string { + if (int(op) < 0) || (int(op) == len(operations)) { + return fmt.Sprintf("UnknownOperation(%d)", op) + } + return operations[op] +} + +type OperationPerm struct { + opPerm map[Operation]Permission + expectedOps []Operation + opNames []string +} + +func NewOperationPerm(expectedOps []Operation, opNames []string) OperationPerm { + return OperationPerm{ + opPerm: make(map[Operation]Permission), + expectedOps: expectedOps, + opNames: opNames, + } +} + +func (opp OperationPerm) isKeyRequired(op Operation) bool { + for _, key := range opp.expectedOps { + if key == op { + return true + } + } + return false +} + +func (opp OperationPerm) AddOperationPermissionMap(opMap map[Operation]Permission) error { + // First iteration check all the keys are valid, If any one key is invalid then no key should be added. + for op := range opMap { + if !opp.isKeyRequired(op) { + return fmt.Errorf("%v is not a valid operation", op.String(opp.opNames)) + } + } + for op, perm := range opMap { + opp.opPerm[op] = perm + } + return nil +} + +func (opp OperationPerm) AddOperationPermission(op Operation, perm Permission) error { + if !opp.isKeyRequired(op) { + return fmt.Errorf("%v is not a valid operation", op.String(opp.opNames)) + } + opp.opPerm[op] = perm + return nil +} + +func (opp OperationPerm) Validate() error { + for op := range opp.opPerm { + if !opp.isKeyRequired(op) { + return fmt.Errorf("OperationPerm: \"%s\" is not a valid operation", op.String(opp.opNames)) + } + } + for _, eeo := range opp.expectedOps { + if _, ok := opp.opPerm[eeo]; !ok { + return fmt.Errorf("OperationPerm: \"%s\" operation is missing", eeo.String(opp.opNames)) + } + } + return nil +} + +func (opp OperationPerm) GetPermission(op Operation) (Permission, error) { + if perm, ok := opp.opPerm[op]; ok { + return perm, nil + } + return "", fmt.Errorf("operation \"%s\" doesn't have any permissions", op.String(opp.opNames)) +} diff --git a/provision/config.go b/provision/config.go index 7540e44015..cf58d65a4a 100644 --- a/provision/config.go +++ b/provision/config.go @@ -7,8 +7,8 @@ import ( "fmt" "os" + "github.com/absmach/magistrala/channels" "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/things" "github.com/pelletier/go-toml" ) @@ -60,15 +60,15 @@ type Cert struct { // Config struct of Provision. type Config struct { - File string `toml:"file" env:"MG_PROVISION_CONFIG_FILE" envDefault:"config.toml"` - Server ServiceConf `toml:"server" mapstructure:"server"` - Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"` - Things []things.Client `toml:"things" mapstructure:"things"` - Channels []groups.Group `toml:"channels" mapstructure:"channels"` - Cert Cert `toml:"cert" mapstructure:"cert"` - BSContent string `env:"MG_PROVISION_BS_CONTENT" envDefault:""` - SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` - InstanceID string `env:"MG_MQTT_ADAPTER_INSTANCE_ID" envDefault:""` + File string `toml:"file" env:"MG_PROVISION_CONFIG_FILE" envDefault:"config.toml"` + Server ServiceConf `toml:"server" mapstructure:"server"` + Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"` + Things []things.Client `toml:"things" mapstructure:"things"` + Channels []channels.Channel `toml:"channels" mapstructure:"channels"` + Cert Cert `toml:"cert" mapstructure:"cert"` + BSContent string `env:"MG_PROVISION_BS_CONTENT" envDefault:""` + SendTelemetry bool `env:"MG_SEND_TELEMETRY" envDefault:"true"` + InstanceID string `env:"MG_MQTT_ADAPTER_INSTANCE_ID" envDefault:""` } // Save - store config in a file. diff --git a/provision/config_test.go b/provision/config_test.go index 6857b82653..f76ca625fe 100644 --- a/provision/config_test.go +++ b/provision/config_test.go @@ -8,8 +8,8 @@ import ( "os" "testing" + "github.com/absmach/magistrala/channels" "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/provision" "github.com/absmach/magistrala/things" "github.com/pelletier/go-toml" @@ -42,7 +42,7 @@ var ( Permissions: []string{"test"}, }, }, - Channels: []groups.Group{ + Channels: []channels.Channel{ { ID: "1234567890", Name: "test", diff --git a/readers/api/endpoint.go b/readers/api/endpoint.go index 699b643ad5..1eef1f1ecf 100644 --- a/readers/api/endpoint.go +++ b/readers/api/endpoint.go @@ -6,23 +6,24 @@ package api import ( "context" - "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/pkg/apiutil" - mgauthz "github.com/absmach/magistrala/pkg/authz" + mgauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/readers" "github.com/go-kit/kit/endpoint" ) -func listMessagesEndpoint(svc readers.MessageRepository, authz mgauthz.Authorization, thingsClient magistrala.ThingsServiceClient) endpoint.Endpoint { +func listMessagesEndpoint(svc readers.MessageRepository, authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(listMessagesReq) if err := req.validate(); err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) } - if err := authorize(ctx, req, authz, thingsClient); err != nil { + if err := authnAuthz(ctx, req, authn, things, channels); err != nil { return nil, errors.Wrap(svcerr.ErrAuthorization, err) } diff --git a/readers/api/endpoint_test.go b/readers/api/endpoint_test.go index 1d982299be..6817ffbd87 100644 --- a/readers/api/endpoint_test.go +++ b/readers/api/endpoint_test.go @@ -11,10 +11,10 @@ import ( "testing" "time" - "github.com/absmach/magistrala" + chmocks "github.com/absmach/magistrala/channels/mocks" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/apiutil" - authzmocks "github.com/absmach/magistrala/pkg/authz/mocks" + authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/transformers/senml" "github.com/absmach/magistrala/readers" @@ -22,7 +22,6 @@ import ( "github.com/absmach/magistrala/readers/mocks" thmocks "github.com/absmach/magistrala/things/mocks" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) const ( @@ -49,8 +48,8 @@ var ( sum float64 = 42 ) -func newServer(repo *mocks.MessageRepository, authz *authzmocks.Authorization, thingsAuthzClient *thmocks.ThingsServiceClient) *httptest.Server { - mux := api.MakeHandler(repo, authz, thingsAuthzClient, svcName, instanceID) +func newServer(repo *mocks.MessageRepository, authn *authnmocks.Authentication, things *thmocks.ThingsServiceClient, channels *chmocks.ChannelsServiceClient) *httptest.Server { + mux := api.MakeHandler(repo, authn, things, channels, svcName, instanceID) return httptest.NewServer(mux) } @@ -128,9 +127,10 @@ func TestReadAll(t *testing.T) { } repo := new(mocks.MessageRepository) - authz := new(authzmocks.Authorization) + authn := new(authnmocks.Authentication) things := new(thmocks.ThingsServiceClient) - ts := newServer(repo, authz, things) + channels := new(chmocks.ChannelsServiceClient) + ts := newServer(repo, authn, things, channels) defer ts.Close() cases := []struct { @@ -968,11 +968,7 @@ func TestReadAll(t *testing.T) { } for _, tc := range cases { - authCall := authz.On("Authorize", mock.Anything, mock.Anything).Return(tc.err) repo.On("ReadAll", chanID, tc.res.PageMetadata).Return(readers.MessagesPage{Total: tc.res.Total, Messages: fromSenml(tc.res.Messages)}, nil) - if tc.key != "" { - authCall = things.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.ThingsAuthzRes{Authorized: tc.authResponse}, tc.err) - } req := testRequest{ client: ts.Client(), method: http.MethodGet, @@ -991,7 +987,6 @@ func TestReadAll(t *testing.T) { assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.status, res.StatusCode)) assert.Equal(t, tc.res.Total, page.Total, fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.res.Total, page.Total)) assert.ElementsMatch(t, tc.res.Messages, page.Messages, fmt.Sprintf("%s: got incorrect body from response", tc.desc)) - authCall.Unset() } } diff --git a/readers/api/transport.go b/readers/api/transport.go index 82a5d17fd5..73ebd379bd 100644 --- a/readers/api/transport.go +++ b/readers/api/transport.go @@ -9,16 +9,17 @@ import ( "net/http" "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/pkg/apiutil" - mgauthz "github.com/absmach/magistrala/pkg/authz" + mgauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" "github.com/absmach/magistrala/readers" "github.com/go-chi/chi/v5" kithttp "github.com/go-kit/kit/transport/http" "github.com/prometheus/client_golang/prometheus/promhttp" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) const ( @@ -55,14 +56,14 @@ const ( var errUserAccess = errors.New("user has no permission") // MakeHandler returns a HTTP handler for API endpoints. -func MakeHandler(svc readers.MessageRepository, authz mgauthz.Authorization, things magistrala.ThingsServiceClient, svcName, instanceID string) http.Handler { +func MakeHandler(svc readers.MessageRepository, authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient, svcName, instanceID string) http.Handler { opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(encodeError), } mux := chi.NewRouter() mux.Get("/channels/{chanID}/messages", kithttp.NewServer( - listMessagesEndpoint(svc, authz, things), + listMessagesEndpoint(svc, authn, things, channels), decodeList, encodeResponse, opts..., @@ -242,38 +243,55 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) { } } -func authorize(ctx context.Context, req listMessagesReq, authz mgauthz.Authorization, things magistrala.ThingsServiceClient) (err error) { +func authnAuthz(ctx context.Context, req listMessagesReq, authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient) error { + clientID, clientType, err := authenticate(ctx, req, authn, things) + if err != nil { + return nil + } + if err := authorize(ctx, clientID, clientType, req.chanID, channels); err != nil { + return err + } + return nil +} + +func authenticate(ctx context.Context, req listMessagesReq, authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient) (clientID string, clientType string, err error) { switch { case req.token != "": - if err = authz.Authorize(ctx, mgauthz.PolicyReq{ - SubjectType: userType, - SubjectKind: tokenKind, - Subject: req.token, - Permission: viewPermission, - ObjectType: groupType, - Object: req.chanID, - }); err != nil { - e, ok := status.FromError(err) - if ok && e.Code() == codes.PermissionDenied { - return errors.Wrap(errUserAccess, err) - } - return err + session, err := authn.Authenticate(ctx, req.token) + if err != nil { + return "", "", err } - return nil + + return session.DomainUserID, policies.UserType, nil case req.key != "": - if _, err = things.Authorize(ctx, &magistrala.ThingsAuthzReq{ - ThingKey: req.key, - ChannelID: req.chanID, - Permission: subscribePermission, - }); err != nil { - e, ok := status.FromError(err) - if ok && e.Code() == codes.PermissionDenied { - return errors.Wrap(errUserAccess, err) - } - return err + res, err := things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ + ThingKey: req.key, + }) + if err != nil { + return "", "", err } - return nil + if !res.GetAuthenticated() { + return "", "", svcerr.ErrAuthentication + } + return res.GetId(), policies.ThingType, nil default: + return "", "", svcerr.ErrAuthentication + } +} + +func authorize(ctx context.Context, clientID, clientType, chanID string, channels grpcChannelsV1.ChannelsServiceClient) (err error) { + res, err := channels.Authorize(ctx, &grpcChannelsV1.AuthzReq{ + ClientId: clientID, + ClientType: clientType, + Permission: viewPermission, + ChannelId: chanID, + }) + + if err != nil { + return errors.Wrap(svcerr.ErrAuthorization, err) + } + if !res.GetAuthorized() { return svcerr.ErrAuthorization } + return nil } diff --git a/things/api/grpc/client.go b/things/api/grpc/client.go index 8b3b5e354d..f82f02d53e 100644 --- a/things/api/grpc/client.go +++ b/things/api/grpc/client.go @@ -8,7 +8,8 @@ import ( "fmt" "time" - "github.com/absmach/magistrala" + grpcCommonV1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/things" @@ -19,64 +20,323 @@ import ( "google.golang.org/grpc/status" ) -const svcName = "magistrala.ThingsService" +const svcName = "things.v1.ThingsService" -var _ magistrala.ThingsServiceClient = (*grpcClient)(nil) +var _ grpcThingsV1.ThingsServiceClient = (*grpcClient)(nil) type grpcClient struct { - timeout time.Duration - authorize endpoint.Endpoint + timeout time.Duration + authenticate endpoint.Endpoint + retrieveEntity endpoint.Endpoint + retrieveEntities endpoint.Endpoint + addConnections endpoint.Endpoint + removeConnections endpoint.Endpoint + removeChannelConnections endpoint.Endpoint + unsetParentGroupFromThings endpoint.Endpoint } // NewClient returns new gRPC client instance. -func NewClient(conn *grpc.ClientConn, timeout time.Duration) magistrala.ThingsServiceClient { +func NewClient(conn *grpc.ClientConn, timeout time.Duration) grpcThingsV1.ThingsServiceClient { return &grpcClient{ - authorize: kitgrpc.NewClient( + authenticate: kitgrpc.NewClient( conn, svcName, - "Authorize", - encodeAuthorizeRequest, - decodeAuthorizeResponse, - magistrala.ThingsAuthzRes{}, + "Authenticate", + encodeAuthenticateRequest, + decodeAuthenticateResponse, + grpcThingsV1.AuthnRes{}, + ).Endpoint(), + + retrieveEntity: kitgrpc.NewClient( + conn, + svcName, + "RetrieveEntity", + encodeRetrieveEntityRequest, + decodeRetrieveEntityResponse, + grpcCommonV1.RetrieveEntityRes{}, + ).Endpoint(), + + retrieveEntities: kitgrpc.NewClient( + conn, + svcName, + "RetrieveEntities", + encodeRetrieveEntitiesRequest, + decodeRetrieveEntitiesResponse, + grpcCommonV1.RetrieveEntitiesRes{}, + ).Endpoint(), + + addConnections: kitgrpc.NewClient( + conn, + svcName, + "AddConnections", + encodeAddConnectionsRequest, + decodeAddConnectionsResponse, + grpcCommonV1.AddConnectionsRes{}, + ).Endpoint(), + + removeConnections: kitgrpc.NewClient( + conn, + svcName, + "RemoveConnections", + encodeRemoveConnectionsRequest, + decodeRemoveConnectionsResponse, + grpcCommonV1.RemoveConnectionsRes{}, + ).Endpoint(), + + removeChannelConnections: kitgrpc.NewClient( + conn, + svcName, + "RemoveChannelConnections", + encodeRemoveChannelConnectionsRequest, + decodeRemoveChannelConnectionsResponse, + grpcThingsV1.RemoveChannelConnectionsRes{}, + ).Endpoint(), + + unsetParentGroupFromThings: kitgrpc.NewClient( + conn, + svcName, + "UnsetParentGroupFromThings", + encodeUnsetParentGroupFromThingsRequest, + decodeUnsetParentGroupFromThingsResponse, + grpcThingsV1.UnsetParentGroupFromThingsRes{}, ).Endpoint(), timeout: timeout, } } -func (client grpcClient) Authorize(ctx context.Context, req *magistrala.ThingsAuthzReq, _ ...grpc.CallOption) (r *magistrala.ThingsAuthzRes, err error) { +func (client grpcClient) Authenticate(ctx context.Context, req *grpcThingsV1.AuthnReq, _ ...grpc.CallOption) (r *grpcThingsV1.AuthnRes, err error) { ctx, cancel := context.WithTimeout(ctx, client.timeout) defer cancel() - res, err := client.authorize(ctx, things.AuthzReq{ - ClientID: req.GetThingID(), - ClientKey: req.GetThingKey(), - ChannelID: req.GetChannelID(), - Permission: req.GetPermission(), + res, err := client.authenticate(ctx, authenticateReq{ + ThingID: req.GetThingId(), + ThingKey: req.GetThingKey(), }) if err != nil { - return &magistrala.ThingsAuthzRes{}, decodeError(err) + return &grpcThingsV1.AuthnRes{}, decodeError(err) + } + + ar := res.(authenticateRes) + return &grpcThingsV1.AuthnRes{Authenticated: ar.authenticated, Id: ar.id}, nil +} + +func encodeAuthenticateRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(authenticateReq) + return &grpcThingsV1.AuthnReq{ + ThingId: req.ThingID, + ThingKey: req.ThingKey, + }, nil +} + +func decodeAuthenticateResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(*grpcThingsV1.AuthnRes) + return authenticateRes{authenticated: res.GetAuthenticated(), id: res.GetId()}, nil +} + +func (client grpcClient) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq, _ ...grpc.CallOption) (r *grpcCommonV1.RetrieveEntityRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + res, err := client.retrieveEntity(ctx, req.GetId()) + if err != nil { + return &grpcCommonV1.RetrieveEntityRes{}, decodeError(err) + } + + ebr := res.(retrieveEntityRes) + + return &grpcCommonV1.RetrieveEntityRes{Entity: &grpcCommonV1.EntityBasic{Id: ebr.id, DomainId: ebr.domain, Status: uint32(ebr.status)}}, nil +} + +func encodeRetrieveEntityRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(string) + return &grpcCommonV1.RetrieveEntityReq{ + Id: req, + }, nil +} + +func decodeRetrieveEntityResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(*grpcCommonV1.RetrieveEntityRes) + + return retrieveEntityRes{ + id: res.Entity.GetId(), + domain: res.Entity.GetDomainId(), + status: uint8(res.Entity.GetStatus()), + }, nil +} + +func (client grpcClient) RetrieveEntities(ctx context.Context, req *grpcCommonV1.RetrieveEntitiesReq, _ ...grpc.CallOption) (r *grpcCommonV1.RetrieveEntitiesRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + res, err := client.retrieveEntities(ctx, req.GetIds()) + if err != nil { + return &grpcCommonV1.RetrieveEntitiesRes{}, decodeError(err) + } + + ep := res.(retrieveEntitiesRes) + + entities := []*grpcCommonV1.EntityBasic{} + for _, thing := range ep.things { + entities = append(entities, &grpcCommonV1.EntityBasic{ + Id: thing.id, + DomainId: thing.domain, + Status: uint32(thing.status), + }) + } + return &grpcCommonV1.RetrieveEntitiesRes{Total: ep.total, Limit: ep.limit, Offset: ep.offset, Entities: entities}, nil +} + +func encodeRetrieveEntitiesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.([]string) + return &grpcCommonV1.RetrieveEntitiesReq{ + Ids: req, + }, nil +} + +func decodeRetrieveEntitiesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(*grpcCommonV1.RetrieveEntitiesRes) + + ths := []thingBasic{} + + for _, entity := range res.Entities { + ths = append(ths, thingBasic{ + id: entity.GetId(), + domain: entity.GetDomainId(), + status: uint8(entity.GetStatus()), + }) + } + return retrieveEntitiesRes{total: res.GetTotal(), limit: res.GetLimit(), offset: res.GetOffset(), things: ths}, nil +} + +func (client grpcClient) AddConnections(ctx context.Context, req *grpcCommonV1.AddConnectionsReq, _ ...grpc.CallOption) (r *grpcCommonV1.AddConnectionsRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + conns := []things.Connection{} + for _, c := range req.Connections { + conns = append(conns, things.Connection{ + ThingID: c.GetThingId(), + ChannelID: c.GetChannelId(), + DomainID: c.GetDomainId(), + }) + } + + res, err := client.addConnections(ctx, conns) + if err != nil { + return &grpcCommonV1.AddConnectionsRes{}, decodeError(err) } - ar := res.(authorizeRes) - return &magistrala.ThingsAuthzRes{Authorized: ar.authorized, Id: ar.id}, nil + cr := res.(connectionsRes) + + return &grpcCommonV1.AddConnectionsRes{Ok: cr.ok}, nil } -func decodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { - res := grpcRes.(*magistrala.ThingsAuthzRes) - return authorizeRes{authorized: res.Authorized, id: res.Id}, nil +func encodeAddConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.([]things.Connection) + + conns := []*grpcCommonV1.Connection{} + + for _, r := range req { + conns = append(conns, &grpcCommonV1.Connection{ + ThingId: r.ThingID, + ChannelId: r.ChannelID, + DomainId: r.DomainID, + }) + } + return &grpcCommonV1.AddConnectionsReq{ + Connections: conns, + }, nil } -func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { - req := grpcReq.(things.AuthzReq) - return &magistrala.ThingsAuthzReq{ - ChannelID: req.ChannelID, - ThingID: req.ClientID, - ThingKey: req.ClientKey, - Permission: req.Permission, +func decodeAddConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(*grpcCommonV1.AddConnectionsRes) + + return connectionsRes{ok: res.GetOk()}, nil +} + +func (client grpcClient) RemoveConnections(ctx context.Context, req *grpcCommonV1.RemoveConnectionsReq, _ ...grpc.CallOption) (r *grpcCommonV1.RemoveConnectionsRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + conns := []things.Connection{} + for _, c := range req.Connections { + conns = append(conns, things.Connection{ + ThingID: c.GetThingId(), + ChannelID: c.GetChannelId(), + DomainID: c.GetDomainId(), + }) + } + + res, err := client.removeConnections(ctx, conns) + if err != nil { + return &grpcCommonV1.RemoveConnectionsRes{}, decodeError(err) + } + + cr := res.(connectionsRes) + + return &grpcCommonV1.RemoveConnectionsRes{Ok: cr.ok}, nil +} + +func encodeRemoveConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.([]things.Connection) + + conns := []*grpcCommonV1.Connection{} + + for _, r := range req { + conns = append(conns, &grpcCommonV1.Connection{ + ThingId: r.ThingID, + ChannelId: r.ChannelID, + DomainId: r.DomainID, + }) + } + return &grpcCommonV1.RemoveConnectionsReq{ + Connections: conns, }, nil } +func decodeRemoveConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(*grpcCommonV1.RemoveConnectionsRes) + + return connectionsRes{ok: res.GetOk()}, nil +} +func (client grpcClient) RemoveChannelConnections(ctx context.Context, req *grpcThingsV1.RemoveChannelConnectionsReq, _ ...grpc.CallOption) (r *grpcThingsV1.RemoveChannelConnectionsRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + if _, err := client.removeChannelConnections(ctx, req); err != nil { + return &grpcThingsV1.RemoveChannelConnectionsRes{}, decodeError(err) + } + + return &grpcThingsV1.RemoveChannelConnectionsRes{}, nil +} + +func encodeRemoveChannelConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + return grpcReq.(*grpcThingsV1.RemoveChannelConnectionsReq), nil +} + +func decodeRemoveChannelConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + return grpcRes.(*grpcThingsV1.RemoveChannelConnectionsRes), nil +} + +func (client grpcClient) UnsetParentGroupFromThings(ctx context.Context, req *grpcThingsV1.UnsetParentGroupFromThingsReq, _ ...grpc.CallOption) (r *grpcThingsV1.UnsetParentGroupFromThingsRes, err error) { + ctx, cancel := context.WithTimeout(ctx, client.timeout) + defer cancel() + + if _, err := client.unsetParentGroupFromThings(ctx, req); err != nil { + return &grpcThingsV1.UnsetParentGroupFromThingsRes{}, decodeError(err) + } + + return &grpcThingsV1.UnsetParentGroupFromThingsRes{}, nil +} + +func encodeUnsetParentGroupFromThingsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + return grpcReq.(*grpcThingsV1.UnsetParentGroupFromThingsReq), nil +} + +func decodeUnsetParentGroupFromThingsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + return grpcRes.(*grpcThingsV1.UnsetParentGroupFromThingsRes), 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 0c00c38a7b..feb1f16344 100644 --- a/things/api/grpc/endpoint.go +++ b/things/api/grpc/endpoint.go @@ -7,25 +7,127 @@ import ( "context" "github.com/absmach/magistrala/things" + pThings "github.com/absmach/magistrala/things/private" "github.com/go-kit/kit/endpoint" ) -func authorizeEndpoint(svc things.Service) endpoint.Endpoint { +func authenticateEndpoint(svc pThings.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(authorizeReq) - - thingID, err := svc.Authorize(ctx, things.AuthzReq{ - ChannelID: req.ChannelID, - ClientID: req.ThingID, - ClientKey: req.ThingKey, - Permission: req.Permission, - }) + req := request.(authenticateReq) + + thingID, err := svc.Authenticate(ctx, req.ThingKey) if err != nil { - return authorizeRes{}, err + return authenticateRes{}, err } - return authorizeRes{ - authorized: true, - id: thingID, + return authenticateRes{ + authenticated: true, + id: thingID, }, err } } + +func retrieveEntityEndpoint(svc pThings.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + + req := request.(retrieveEntityReq) + thing, err := svc.RetrieveById(ctx, req.Id) + + if err != nil { + return retrieveEntityRes{}, err + } + + return retrieveEntityRes{id: thing.ID, domain: thing.Domain, status: uint8(thing.Status)}, nil + + } +} +func retrieveEntitiesEndpoint(svc pThings.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + + req := request.(retrieveEntitiesReq) + tp, err := svc.RetrieveByIds(ctx, req.Ids) + + if err != nil { + return retrieveEntitiesRes{}, err + } + thingsBasic := []thingBasic{} + for _, thing := range tp.Clients { + thingsBasic = append(thingsBasic, thingBasic{id: thing.ID, domain: thing.Domain, status: uint8(thing.Status)}) + } + return retrieveEntitiesRes{ + total: tp.Total, + limit: tp.Limit, + offset: tp.Offset, + things: thingsBasic, + }, nil + + } +} + +func addConnectionsEndpoint(svc pThings.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + + req := request.(connectionsReq) + + var conns []things.Connection + + for _, c := range req.connections { + conns = append(conns, things.Connection{ + ThingID: c.thingID, + ChannelID: c.channelID, + DomainID: c.domainID, + }) + } + if err := svc.AddConnections(ctx, conns); err != nil { + return connectionsRes{ok: false}, err + } + + return connectionsRes{ok: true}, nil + + } +} + +func removeConnectionsEndpoint(svc pThings.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + + req := request.(connectionsReq) + + var conns []things.Connection + + for _, c := range req.connections { + conns = append(conns, things.Connection{ + ThingID: c.thingID, + ChannelID: c.channelID, + DomainID: c.domainID, + }) + } + if err := svc.RemoveConnections(ctx, conns); err != nil { + return connectionsRes{ok: false}, err + } + + return connectionsRes{ok: true}, nil + + } +} +func removeChannelConnectionsEndpoint(svc pThings.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(removeChannelConnectionsReq) + + if err := svc.RemoveChannelConnections(ctx, req.channelID); err != nil { + return removeChannelConnectionsRes{}, err + } + + return removeChannelConnectionsRes{}, nil + } +} + +func unsetParentGroupFromThingsEndpoint(svc pThings.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(unsetParentGroupFromThingsReq) + + if err := svc.UnsetParentGroupFromThings(ctx, req.parentGroupID); err != nil { + return unsetParentGroupFromThingsRes{}, err + } + + return unsetParentGroupFromThingsRes{}, nil + } +} diff --git a/things/api/grpc/endpoint_test.go b/things/api/grpc/endpoint_test.go deleted file mode 100644 index 1c02d5701e..0000000000 --- a/things/api/grpc/endpoint_test.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package grpc_test - -import ( - "context" - "fmt" - "net" - "testing" - "time" - - "github.com/absmach/magistrala" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/policies" - "github.com/absmach/magistrala/things" - grpcapi "github.com/absmach/magistrala/things/api/grpc" - "github.com/absmach/magistrala/things/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" -) - -const port = 7000 - -var ( - thingID = "testID" - clientKey = "testKey" - channelID = "testID" - invalid = "invalid" -) - -func startGRPCServer(svc *mocks.Service, port int) { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - panic(fmt.Sprintf("failed to obtain port: %s", err)) - } - server := grpc.NewServer() - magistrala.RegisterThingsServiceServer(server, grpcapi.NewServer(svc)) - go func() { - if err := server.Serve(listener); err != nil { - panic(fmt.Sprintf("failed to serve: %s", err)) - } - }() -} - -func TestAuthorize(t *testing.T) { - svc := new(mocks.Service) - startGRPCServer(svc, port) - authAddr := fmt.Sprintf("localhost:%d", port) - conn, _ := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) - client := grpcapi.NewClient(conn, time.Second) - - cases := []struct { - desc string - req *magistrala.ThingsAuthzReq - res *magistrala.ThingsAuthzRes - thingID string - identifyKey string - authorizeReq things.AuthzReq - authorizeRes string - authorizeErr error - identifyErr error - err error - code codes.Code - }{ - { - desc: "authorize successfully", - thingID: thingID, - req: &magistrala.ThingsAuthzReq{ - ThingKey: clientKey, - ChannelID: channelID, - Permission: policies.PublishPermission, - }, - authorizeReq: things.AuthzReq{ - ClientKey: clientKey, - ChannelID: channelID, - Permission: policies.PublishPermission, - }, - authorizeRes: thingID, - identifyKey: clientKey, - res: &magistrala.ThingsAuthzRes{Authorized: true, Id: thingID}, - err: nil, - }, - { - desc: "authorize with invalid key", - req: &magistrala.ThingsAuthzReq{ - ThingKey: invalid, - ChannelID: channelID, - Permission: policies.PublishPermission, - }, - authorizeReq: things.AuthzReq{ - ClientKey: invalid, - ChannelID: channelID, - Permission: policies.PublishPermission, - }, - authorizeErr: svcerr.ErrAuthentication, - identifyKey: invalid, - identifyErr: svcerr.ErrAuthentication, - res: &magistrala.ThingsAuthzRes{}, - err: svcerr.ErrAuthentication, - }, - { - desc: "authorize with failed authorization", - thingID: thingID, - req: &magistrala.ThingsAuthzReq{ - ThingKey: clientKey, - ChannelID: channelID, - Permission: policies.PublishPermission, - }, - authorizeReq: things.AuthzReq{ - ClientKey: clientKey, - ChannelID: channelID, - Permission: policies.PublishPermission, - }, - authorizeErr: svcerr.ErrAuthorization, - identifyKey: clientKey, - res: &magistrala.ThingsAuthzRes{Authorized: false}, - err: svcerr.ErrAuthorization, - }, - - { - desc: "authorize with invalid permission", - thingID: thingID, - req: &magistrala.ThingsAuthzReq{ - ThingKey: clientKey, - ChannelID: channelID, - Permission: invalid, - }, - authorizeReq: things.AuthzReq{ - ChannelID: channelID, - ClientKey: clientKey, - Permission: invalid, - }, - identifyKey: clientKey, - authorizeErr: svcerr.ErrAuthorization, - res: &magistrala.ThingsAuthzRes{Authorized: false}, - err: svcerr.ErrAuthorization, - }, - { - desc: "authorize with invalid channel ID", - thingID: thingID, - req: &magistrala.ThingsAuthzReq{ - ThingKey: clientKey, - ChannelID: invalid, - Permission: policies.PublishPermission, - }, - authorizeReq: things.AuthzReq{ - ChannelID: invalid, - ClientKey: clientKey, - Permission: policies.PublishPermission, - }, - identifyKey: clientKey, - authorizeErr: svcerr.ErrAuthorization, - res: &magistrala.ThingsAuthzRes{Authorized: false}, - err: svcerr.ErrAuthorization, - }, - { - desc: "authorize with empty channel ID", - thingID: thingID, - req: &magistrala.ThingsAuthzReq{ - ThingKey: clientKey, - ChannelID: "", - Permission: policies.PublishPermission, - }, - authorizeReq: things.AuthzReq{ - ClientKey: clientKey, - ChannelID: "", - Permission: policies.PublishPermission, - }, - authorizeErr: svcerr.ErrAuthorization, - identifyKey: clientKey, - res: &magistrala.ThingsAuthzRes{Authorized: false}, - err: svcerr.ErrAuthorization, - }, - { - desc: "authorize with empty permission", - thingID: thingID, - req: &magistrala.ThingsAuthzReq{ - ThingKey: clientKey, - ChannelID: channelID, - Permission: "", - }, - authorizeReq: things.AuthzReq{ - ChannelID: channelID, - Permission: "", - ClientKey: clientKey, - }, - identifyKey: clientKey, - authorizeErr: svcerr.ErrAuthorization, - res: &magistrala.ThingsAuthzRes{Authorized: false}, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - svcCall1 := svc.On("Identify", mock.Anything, tc.identifyKey).Return(tc.thingID, tc.identifyErr) - svcCall2 := svc.On("Authorize", mock.Anything, tc.authorizeReq).Return(tc.thingID, tc.authorizeErr) - res, err := client.Authorize(context.Background(), tc.req) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err)) - assert.Equal(t, tc.res, res, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.res, res)) - svcCall1.Unset() - svcCall2.Unset() - } -} diff --git a/things/api/grpc/request.go b/things/api/grpc/request.go index 890335ecff..bae90ef984 100644 --- a/things/api/grpc/request.go +++ b/things/api/grpc/request.go @@ -3,9 +3,23 @@ package grpc -type authorizeReq struct { - ThingID string - ThingKey string - ChannelID string - Permission string +type authenticateReq struct { + ThingID string + ThingKey string +} + +type retrieveEntitiesReq struct { + Ids []string +} + +type retrieveEntityReq struct { + Id string +} + +type removeChannelConnectionsReq struct { + channelID string +} + +type unsetParentGroupFromThingsReq struct { + parentGroupID string } diff --git a/things/api/grpc/responses.go b/things/api/grpc/responses.go index 8e11f1273f..cfc9e7de7d 100644 --- a/things/api/grpc/responses.go +++ b/things/api/grpc/responses.go @@ -3,7 +3,39 @@ package grpc -type authorizeRes struct { - id string - authorized bool +type thingBasic struct { + id string + domain string + status uint8 } + +type authenticateRes struct { + id string + authenticated bool +} + +type retrieveEntitiesRes struct { + total uint64 + limit uint64 + offset uint64 + things []thingBasic +} + +type retrieveEntityRes thingBasic + +type connectionsReq struct { + connections []connection +} + +type connection struct { + thingID string + channelID string + domainID string +} +type connectionsRes struct { + ok bool +} + +type removeChannelConnectionsRes struct{} + +type unsetParentGroupFromThingsRes struct{} diff --git a/things/api/grpc/server.go b/things/api/grpc/server.go index fa337a0bbd..1942b481d4 100644 --- a/things/api/grpc/server.go +++ b/things/api/grpc/server.go @@ -6,56 +6,249 @@ package grpc import ( "context" - "github.com/absmach/magistrala" mgauth "github.com/absmach/magistrala/auth" + grpcCommonV1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/things" + things "github.com/absmach/magistrala/things/private" kitgrpc "github.com/go-kit/kit/transport/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -var _ magistrala.ThingsServiceServer = (*grpcServer)(nil) +var _ grpcThingsV1.ThingsServiceServer = (*grpcServer)(nil) type grpcServer struct { - magistrala.UnimplementedThingsServiceServer - authorize kitgrpc.Handler + grpcThingsV1.UnimplementedThingsServiceServer + authenticate kitgrpc.Handler + retrieveEntity kitgrpc.Handler + retrieveEntities kitgrpc.Handler + addConnections kitgrpc.Handler + removeConnections kitgrpc.Handler + removeChannelConnections kitgrpc.Handler + unsetParentGroupFromThings kitgrpc.Handler } // NewServer returns new AuthServiceServer instance. -func NewServer(svc things.Service) magistrala.ThingsServiceServer { +func NewServer(svc things.Service) grpcThingsV1.ThingsServiceServer { return &grpcServer{ - authorize: kitgrpc.NewServer( - (authorizeEndpoint(svc)), + authenticate: kitgrpc.NewServer( + authenticateEndpoint(svc), decodeAuthorizeRequest, encodeAuthorizeResponse, ), + retrieveEntity: kitgrpc.NewServer( + retrieveEntityEndpoint(svc), + decodeRetrieveEntityRequest, + encodeRetrieveEntityResponse, + ), + retrieveEntities: kitgrpc.NewServer( + retrieveEntitiesEndpoint(svc), + decodeRetrieveEntitiesRequest, + encodeRetrieveEntitiesResponse, + ), + addConnections: kitgrpc.NewServer( + addConnectionsEndpoint(svc), + decodeAddConnectionsRequest, + encodeAddConnectionsResponse, + ), + removeConnections: kitgrpc.NewServer( + removeConnectionsEndpoint(svc), + decodeRemoveConnectionsRequest, + encodeRemoveConnectionsResponse, + ), + removeChannelConnections: kitgrpc.NewServer( + removeChannelConnectionsEndpoint(svc), + decodeRemoveChannelConnectionsRequest, + encodeRemoveChannelConnectionsResponse, + ), + unsetParentGroupFromThings: kitgrpc.NewServer( + unsetParentGroupFromThingsEndpoint(svc), + decodeUnsetParentGroupFromThingsRequest, + encodeUnsetParentGroupFromThingsResponse, + ), } } -func (s *grpcServer) Authorize(ctx context.Context, req *magistrala.ThingsAuthzReq) (*magistrala.ThingsAuthzRes, error) { - _, res, err := s.authorize.ServeGRPC(ctx, req) +func (s *grpcServer) Authenticate(ctx context.Context, req *grpcThingsV1.AuthnReq) (*grpcThingsV1.AuthnRes, error) { + _, res, err := s.authenticate.ServeGRPC(ctx, req) if err != nil { return nil, encodeError(err) } - return res.(*magistrala.ThingsAuthzRes), nil + return res.(*grpcThingsV1.AuthnRes), nil } func decodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { - req := grpcReq.(*magistrala.ThingsAuthzReq) - return authorizeReq{ - ThingID: req.GetThingID(), - ThingKey: req.GetThingKey(), - ChannelID: req.GetChannelID(), - Permission: req.GetPermission(), + req := grpcReq.(*grpcThingsV1.AuthnReq) + return authenticateReq{ + ThingID: req.GetThingId(), + ThingKey: req.GetThingKey(), }, nil } func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { - res := grpcRes.(authorizeRes) - return &magistrala.ThingsAuthzRes{Authorized: res.authorized, Id: res.id}, nil + res := grpcRes.(authenticateRes) + return &grpcThingsV1.AuthnRes{Authenticated: res.authenticated, Id: res.id}, nil +} + +func (s *grpcServer) RetrieveEntity(ctx context.Context, req *grpcCommonV1.RetrieveEntityReq) (*grpcCommonV1.RetrieveEntityRes, error) { + _, res, err := s.retrieveEntity.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcCommonV1.RetrieveEntityRes), nil +} + +func decodeRetrieveEntityRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcCommonV1.RetrieveEntityReq) + return retrieveEntityReq{ + Id: req.GetId(), + }, nil +} + +func encodeRetrieveEntityResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(retrieveEntityRes) + + return &grpcCommonV1.RetrieveEntityRes{ + Entity: &grpcCommonV1.EntityBasic{ + Id: res.id, + DomainId: res.domain, + Status: uint32(res.status), + }, + }, nil +} + +func (s *grpcServer) RetrieveEntities(ctx context.Context, req *grpcCommonV1.RetrieveEntitiesReq) (*grpcCommonV1.RetrieveEntitiesRes, error) { + _, res, err := s.retrieveEntities.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcCommonV1.RetrieveEntitiesRes), nil +} + +func decodeRetrieveEntitiesRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcCommonV1.RetrieveEntitiesReq) + return retrieveEntitiesReq{ + Ids: req.GetIds(), + }, nil +} + +func encodeRetrieveEntitiesResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(retrieveEntitiesRes) + + entities := []*grpcCommonV1.EntityBasic{} + for _, thing := range res.things { + entities = append(entities, &grpcCommonV1.EntityBasic{ + Id: thing.id, + DomainId: thing.domain, + Status: uint32(thing.status), + }) + } + return &grpcCommonV1.RetrieveEntitiesRes{Total: res.total, Limit: res.limit, Offset: res.offset, Entities: entities}, nil +} + +func (s *grpcServer) AddConnections(ctx context.Context, req *grpcCommonV1.AddConnectionsReq) (*grpcCommonV1.AddConnectionsRes, error) { + _, res, err := s.addConnections.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcCommonV1.AddConnectionsRes), nil +} + +func decodeAddConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcCommonV1.AddConnectionsReq) + + conns := []connection{} + for _, c := range req.Connections { + conns = append(conns, connection{ + thingID: c.GetThingId(), + channelID: c.GetChannelId(), + domainID: c.GetDomainId(), + }) + } + return connectionsReq{ + connections: conns, + }, nil +} + +func encodeAddConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(connectionsRes) + + return &grpcCommonV1.AddConnectionsRes{Ok: res.ok}, nil +} + +func (s *grpcServer) RemoveConnections(ctx context.Context, req *grpcCommonV1.RemoveConnectionsReq) (*grpcCommonV1.RemoveConnectionsRes, error) { + _, res, err := s.removeConnections.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcCommonV1.RemoveConnectionsRes), nil +} + +func decodeRemoveConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcCommonV1.RemoveConnectionsReq) + + conns := []connection{} + for _, c := range req.Connections { + conns = append(conns, connection{ + thingID: c.GetThingId(), + channelID: c.GetChannelId(), + domainID: c.GetDomainId(), + }) + } + return connectionsReq{ + connections: conns, + }, nil +} + +func encodeRemoveConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + res := grpcRes.(connectionsRes) + + return &grpcCommonV1.RemoveConnectionsRes{Ok: res.ok}, nil +} + +func (s *grpcServer) RemoveChannelConnections(ctx context.Context, req *grpcThingsV1.RemoveChannelConnectionsReq) (*grpcThingsV1.RemoveChannelConnectionsRes, error) { + _, res, err := s.removeChannelConnections.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcThingsV1.RemoveChannelConnectionsRes), nil +} + +func decodeRemoveChannelConnectionsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcThingsV1.RemoveChannelConnectionsReq) + + return removeChannelConnectionsReq{ + channelID: req.GetChannelId(), + }, nil +} + +func encodeRemoveChannelConnectionsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + _ = grpcRes.(removeChannelConnectionsRes) + return &grpcThingsV1.RemoveChannelConnectionsRes{}, nil +} + +func (s *grpcServer) UnsetParentGroupFromThings(ctx context.Context, req *grpcThingsV1.UnsetParentGroupFromThingsReq) (*grpcThingsV1.UnsetParentGroupFromThingsRes, error) { + _, res, err := s.unsetParentGroupFromThings.ServeGRPC(ctx, req) + if err != nil { + return nil, encodeError(err) + } + return res.(*grpcThingsV1.UnsetParentGroupFromThingsRes), nil +} + +func decodeUnsetParentGroupFromThingsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) { + req := grpcReq.(*grpcThingsV1.UnsetParentGroupFromThingsReq) + + return unsetParentGroupFromThingsReq{ + parentGroupID: req.GetParentGroupId(), + }, nil +} + +func encodeUnsetParentGroupFromThingsResponse(_ context.Context, grpcRes interface{}) (interface{}, error) { + _ = grpcRes.(unsetParentGroupFromThingsRes) + return &grpcThingsV1.UnsetParentGroupFromThingsRes{}, nil } func encodeError(err error) error { diff --git a/things/api/http/channels.go b/things/api/http/channels.go deleted file mode 100644 index 7efd4685f2..0000000000 --- a/things/api/http/channels.go +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package http - -import ( - "context" - "encoding/json" - "log/slog" - "net/http" - "strings" - - "github.com/absmach/magistrala/internal/api" - gapi "github.com/absmach/magistrala/internal/groups/api" - "github.com/absmach/magistrala/pkg/apiutil" - mgauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/policies" - "github.com/go-chi/chi/v5" - kithttp "github.com/go-kit/kit/transport/http" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -func groupsHandler(svc groups.Service, authn mgauthn.Authentication, r *chi.Mux, logger *slog.Logger) http.Handler { - opts := []kithttp.ServerOption{ - kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), - } - - r.Group(func(r chi.Router) { - r.Use(api.AuthenticateMiddleware(authn, true)) - - r.Route("/{domainID}/channels", func(r chi.Router) { - r.Post("/", otelhttp.NewHandler(kithttp.NewServer( - gapi.CreateGroupEndpoint(svc, policies.NewChannelKind), - gapi.DecodeGroupCreate, - api.EncodeResponse, - opts..., - ), "create_channel").ServeHTTP) - - r.Get("/{groupID}", otelhttp.NewHandler(kithttp.NewServer( - gapi.ViewGroupEndpoint(svc), - gapi.DecodeGroupRequest, - api.EncodeResponse, - opts..., - ), "view_channel").ServeHTTP) - - r.Delete("/{groupID}", otelhttp.NewHandler(kithttp.NewServer( - gapi.DeleteGroupEndpoint(svc), - gapi.DecodeGroupRequest, - api.EncodeResponse, - opts..., - ), "delete_channel").ServeHTTP) - - r.Get("/{groupID}/permissions", otelhttp.NewHandler(kithttp.NewServer( - gapi.ViewGroupPermsEndpoint(svc), - gapi.DecodeGroupPermsRequest, - api.EncodeResponse, - opts..., - ), "view_channel_permissions").ServeHTTP) - - r.Put("/{groupID}", otelhttp.NewHandler(kithttp.NewServer( - gapi.UpdateGroupEndpoint(svc), - gapi.DecodeGroupUpdate, - api.EncodeResponse, - opts..., - ), "update_channel").ServeHTTP) - - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "channels", "users"), - gapi.DecodeListGroupsRequest, - api.EncodeResponse, - opts..., - ), "list_channels").ServeHTTP) - - r.Post("/{groupID}/enable", otelhttp.NewHandler(kithttp.NewServer( - gapi.EnableGroupEndpoint(svc), - gapi.DecodeChangeGroupStatus, - api.EncodeResponse, - opts..., - ), "enable_channel").ServeHTTP) - - r.Post("/{groupID}/disable", otelhttp.NewHandler(kithttp.NewServer( - gapi.DisableGroupEndpoint(svc), - gapi.DecodeChangeGroupStatus, - api.EncodeResponse, - opts..., - ), "disable_channel").ServeHTTP) - - // Request to add users to a channel - // This endpoint can be used alternative to /channels/{groupID}/members - r.Post("/{groupID}/users/assign", otelhttp.NewHandler(kithttp.NewServer( - assignUsersEndpoint(svc), - decodeAssignUsersRequest, - api.EncodeResponse, - opts..., - ), "assign_users").ServeHTTP) - - // Request to remove users from a channel - // This endpoint can be used alternative to /channels/{groupID}/members - r.Post("/{groupID}/users/unassign", otelhttp.NewHandler(kithttp.NewServer( - unassignUsersEndpoint(svc), - decodeUnassignUsersRequest, - api.EncodeResponse, - opts..., - ), "unassign_users").ServeHTTP) - - // Request to add user_groups to a channel - // This endpoint can be used alternative to /channels/{groupID}/members - r.Post("/{groupID}/groups/assign", otelhttp.NewHandler(kithttp.NewServer( - assignUserGroupsEndpoint(svc), - decodeAssignUserGroupsRequest, - api.EncodeResponse, - opts..., - ), "assign_groups").ServeHTTP) - - // Request to remove user_groups from a channel - // This endpoint can be used alternative to /channels/{groupID}/members - r.Post("/{groupID}/groups/unassign", otelhttp.NewHandler(kithttp.NewServer( - unassignUserGroupsEndpoint(svc), - decodeUnassignUserGroupsRequest, - api.EncodeResponse, - opts..., - ), "unassign_groups").ServeHTTP) - - r.Post("/{groupID}/things/{thingID}/connect", otelhttp.NewHandler(kithttp.NewServer( - connectChannelThingEndpoint(svc), - decodeConnectChannelThingRequest, - api.EncodeResponse, - opts..., - ), "connect_channel_thing").ServeHTTP) - - r.Post("/{groupID}/things/{thingID}/disconnect", otelhttp.NewHandler(kithttp.NewServer( - disconnectChannelThingEndpoint(svc), - decodeDisconnectChannelThingRequest, - api.EncodeResponse, - opts..., - ), "disconnect_channel_thing").ServeHTTP) - }) - - // Ideal location: things service, things endpoint - // Reason for placing here : - // SpiceDB provides list of channel ids to which thing id attached - // and channel service can access spiceDB and get this channel ids list with given thing id. - // Request to get list of channels to which thingID ({memberID}) belongs - r.Get("/{domainID}/things/{memberID}/channels", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "channels", "things"), - gapi.DecodeListGroupsRequest, - api.EncodeResponse, - opts..., - ), "list_channel_by_thing_id").ServeHTTP) - - // Ideal location: users service, users endpoint - // Reason for placing here : - // SpiceDB provides list of channel ids attached to given user id - // and channel service can access spiceDB and get this user ids list with given thing id. - // Request to get list of channels to which userID ({memberID}) have permission. - r.Get("/{domainID}/users/{memberID}/channels", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "channels", "users"), - gapi.DecodeListGroupsRequest, - api.EncodeResponse, - opts..., - ), "list_channel_by_user_id").ServeHTTP) - - // Ideal location: users service, groups endpoint - // SpiceDB provides list of channel ids attached to given user_group id - // and channel service can access spiceDB and get this user ids list with given user_group id. - // Request to get list of channels to which user_group_id ({memberID}) attached. - r.Get("/{domainID}/groups/{memberID}/channels", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "channels", "groups"), - gapi.DecodeListGroupsRequest, - api.EncodeResponse, - opts..., - ), "list_channel_by_user_group_id").ServeHTTP) - - // Connect channel and thing - r.Post("/{domainID}/connect", otelhttp.NewHandler(kithttp.NewServer( - connectEndpoint(svc), - decodeConnectRequest, - api.EncodeResponse, - opts..., - ), "connect").ServeHTTP) - - // Disconnect channel and thing - r.Post("/{domainID}/disconnect", otelhttp.NewHandler(kithttp.NewServer( - disconnectEndpoint(svc), - decodeDisconnectRequest, - api.EncodeResponse, - opts..., - ), "disconnect").ServeHTTP) - }) - - return r -} - -func decodeAssignUsersRequest(_ 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 := assignUsersRequest{ - groupID: chi.URLParam(r, "groupID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - - return req, nil -} - -func decodeUnassignUsersRequest(_ 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 := assignUsersRequest{ - groupID: chi.URLParam(r, "groupID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - - return req, nil -} - -func decodeAssignUserGroupsRequest(_ 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 := assignUserGroupsRequest{ - groupID: chi.URLParam(r, "groupID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - - return req, nil -} - -func decodeUnassignUserGroupsRequest(_ 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 := assignUserGroupsRequest{ - groupID: chi.URLParam(r, "groupID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - - return req, nil -} - -func decodeConnectChannelThingRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := connectChannelThingRequest{ - ThingID: chi.URLParam(r, "thingID"), - ChannelID: chi.URLParam(r, "groupID"), - } - - return req, nil -} - -func decodeDisconnectChannelThingRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := connectChannelThingRequest{ - ThingID: chi.URLParam(r, "thingID"), - ChannelID: chi.URLParam(r, "groupID"), - } - - return req, nil -} - -func decodeConnectRequest(_ 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 := connectChannelThingRequest{} - 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 decodeDisconnectRequest(_ 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 := connectChannelThingRequest{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) - } - - return req, nil -} diff --git a/things/api/http/clients.go b/things/api/http/clients.go index 285f5c4397..93a1e16da0 100644 --- a/things/api/http/clients.go +++ b/things/api/http/clients.go @@ -4,26 +4,24 @@ package http import ( - "context" - "encoding/json" "log/slog" - "net/http" - "strings" "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/pkg/apiutil" mgauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" + roleManagerHttp "github.com/absmach/magistrala/pkg/roles/rolemanager/api" "github.com/absmach/magistrala/things" "github.com/go-chi/chi/v5" kithttp "github.com/go-kit/kit/transport/http" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -func clientsHandler(svc things.Service, r *chi.Mux, authn mgauthn.Authentication, logger *slog.Logger) http.Handler { +func clientsHandler(svc things.Service, authn mgauthn.Authentication, r *chi.Mux, logger *slog.Logger) *chi.Mux { opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), } + d := roleManagerHttp.NewDecoder("thingID") + r.Group(func(r chi.Router) { r.Use(api.AuthenticateMiddleware(authn, true)) @@ -48,90 +46,76 @@ func clientsHandler(svc things.Service, r *chi.Mux, authn mgauthn.Authentication api.EncodeResponse, opts..., ), "create_things").ServeHTTP) + r = roleManagerHttp.EntityAvailableActionsRouter(svc, d, r, opts) + + r.Route("/{thingID}", func(r chi.Router) { + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + viewClientEndpoint(svc), + decodeViewClient, + api.EncodeResponse, + opts..., + ), "view_thing").ServeHTTP) + + r.Patch("/", otelhttp.NewHandler(kithttp.NewServer( + updateClientEndpoint(svc), + decodeUpdateClient, + api.EncodeResponse, + opts..., + ), "update_thing").ServeHTTP) + + r.Patch("/tags", otelhttp.NewHandler(kithttp.NewServer( + updateClientTagsEndpoint(svc), + decodeUpdateClientTags, + api.EncodeResponse, + opts..., + ), "update_thing_tags").ServeHTTP) + + r.Patch("/secret", otelhttp.NewHandler(kithttp.NewServer( + updateClientSecretEndpoint(svc), + decodeUpdateClientCredentials, + api.EncodeResponse, + opts..., + ), "update_thing_credentials").ServeHTTP) + + r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer( + enableClientEndpoint(svc), + decodeChangeClientStatus, + api.EncodeResponse, + opts..., + ), "enable_thing").ServeHTTP) + + r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer( + disableClientEndpoint(svc), + decodeChangeClientStatus, + api.EncodeResponse, + opts..., + ), "disable_thing").ServeHTTP) + + r.Post("/parent", otelhttp.NewHandler(kithttp.NewServer( + setThingParentGroupEndpoint(svc), + decodeSetThingParentGroupStatus, + api.EncodeResponse, + opts..., + ), "set_thing_parent_group").ServeHTTP) + + r.Delete("/parent", otelhttp.NewHandler(kithttp.NewServer( + removeThingParentGroupEndpoint(svc), + decodeRemoveThingParentGroupStatus, + api.EncodeResponse, + opts..., + ), "remove_thing_parent_group").ServeHTTP) + + r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( + deleteClientEndpoint(svc), + decodeDeleteClientReq, + api.EncodeResponse, + opts..., + ), "delete_thing").ServeHTTP) + roleManagerHttp.EntityRoleMangerRouter(svc, d, r, opts) + }) - r.Get("/{thingID}", otelhttp.NewHandler(kithttp.NewServer( - viewClientEndpoint(svc), - decodeViewClient, - api.EncodeResponse, - opts..., - ), "view_thing").ServeHTTP) - - r.Get("/{thingID}/permissions", otelhttp.NewHandler(kithttp.NewServer( - viewClientPermsEndpoint(svc), - decodeViewClientPerms, - api.EncodeResponse, - opts..., - ), "view_thing_permissions").ServeHTTP) - - r.Patch("/{thingID}", otelhttp.NewHandler(kithttp.NewServer( - updateClientEndpoint(svc), - decodeUpdateClient, - api.EncodeResponse, - opts..., - ), "update_thing").ServeHTTP) - - r.Patch("/{thingID}/tags", otelhttp.NewHandler(kithttp.NewServer( - updateClientTagsEndpoint(svc), - decodeUpdateClientTags, - api.EncodeResponse, - opts..., - ), "update_thing_tags").ServeHTTP) - - r.Patch("/{thingID}/secret", otelhttp.NewHandler(kithttp.NewServer( - updateClientSecretEndpoint(svc), - decodeUpdateClientCredentials, - api.EncodeResponse, - opts..., - ), "update_thing_credentials").ServeHTTP) - - r.Post("/{thingID}/enable", otelhttp.NewHandler(kithttp.NewServer( - enableClientEndpoint(svc), - decodeChangeClientStatus, - api.EncodeResponse, - opts..., - ), "enable_thing").ServeHTTP) - - r.Post("/{thingID}/disable", otelhttp.NewHandler(kithttp.NewServer( - disableClientEndpoint(svc), - decodeChangeClientStatus, - api.EncodeResponse, - opts..., - ), "disable_thing").ServeHTTP) - - r.Post("/{thingID}/share", otelhttp.NewHandler(kithttp.NewServer( - thingShareEndpoint(svc), - decodeThingShareRequest, - api.EncodeResponse, - opts..., - ), "share_thing").ServeHTTP) - - r.Post("/{thingID}/unshare", otelhttp.NewHandler(kithttp.NewServer( - thingUnshareEndpoint(svc), - decodeThingUnshareRequest, - api.EncodeResponse, - opts..., - ), "unshare_thing").ServeHTTP) - - r.Delete("/{thingID}", otelhttp.NewHandler(kithttp.NewServer( - deleteClientEndpoint(svc), - decodeDeleteClientReq, - api.EncodeResponse, - opts..., - ), "delete_thing").ServeHTTP) }) - // Ideal location: things service, channels endpoint - // Reason for placing here : - // SpiceDB provides list of thing ids present in given channel id - // and things service can access spiceDB and get the list of thing ids present in given channel id. - // Request to get list of things present in channelID ({groupID}) . - r.Get("/{domainID}/channels/{groupID}/things", otelhttp.NewHandler(kithttp.NewServer( - listMembersEndpoint(svc), - decodeListMembersRequest, - api.EncodeResponse, - opts..., - ), "list_things_by_channel_id").ServeHTTP) - r.Get("/{domainID}/users/{userID}/things", otelhttp.NewHandler(kithttp.NewServer( listClientsEndpoint(svc), decodeListClients, @@ -141,240 +125,3 @@ func clientsHandler(svc things.Service, r *chi.Mux, authn mgauthn.Authentication }) return r } - -func decodeViewClient(_ context.Context, r *http.Request) (interface{}, error) { - req := viewClientReq{ - id: chi.URLParam(r, "thingID"), - } - - return req, nil -} - -func decodeViewClientPerms(_ context.Context, r *http.Request) (interface{}, error) { - req := viewClientPermsReq{ - id: chi.URLParam(r, "thingID"), - } - - return req, nil -} - -func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) { - s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefClientStatus) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - m, err := apiutil.ReadMetadataQuery(r, api.MetadataKey, nil) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - n, err := apiutil.ReadStringQuery(r, api.NameKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - t, err := apiutil.ReadStringQuery(r, api.TagKey, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - id, err := apiutil.ReadStringQuery(r, api.IDOrder, "") - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - p, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - lp, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - st, err := things.ToStatus(s) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - req := listClientsReq{ - status: st, - offset: o, - limit: l, - metadata: m, - name: n, - tag: t, - permission: p, - listPerms: lp, - userID: chi.URLParam(r, "userID"), - id: id, - } - return req, nil -} - -func decodeUpdateClient(_ 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 := updateClientReq{ - id: chi.URLParam(r, "thingID"), - } - 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 decodeUpdateClientTags(_ 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 := updateClientTagsReq{ - id: chi.URLParam(r, "thingID"), - } - 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 decodeUpdateClientCredentials(_ 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 := updateClientCredentialsReq{ - id: chi.URLParam(r, "thingID"), - } - 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 decodeCreateClientReq(_ 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) - } - - var c things.Client - if err := json.NewDecoder(r.Body).Decode(&c); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) - } - req := createClientReq{ - thing: c, - } - - return req, nil -} - -func decodeCreateClientsReq(_ 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) - } - - c := createClientsReq{} - if err := json.NewDecoder(r.Body).Decode(&c.Things); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) - } - - return c, nil -} - -func decodeChangeClientStatus(_ context.Context, r *http.Request) (interface{}, error) { - req := changeClientStatusReq{ - id: chi.URLParam(r, "thingID"), - } - - return req, nil -} - -func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) { - s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefClientStatus) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - m, err := apiutil.ReadMetadataQuery(r, api.MetadataKey, nil) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - st, err := things.ToStatus(s) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - p, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - lp, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) - if err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - req := listMembersReq{ - Page: things.Page{ - Status: st, - Offset: o, - Limit: l, - Permission: p, - Metadata: m, - ListPerms: lp, - }, - groupID: chi.URLParam(r, "groupID"), - } - 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) - } - - req := thingShareRequest{ - thingID: chi.URLParam(r, "thingID"), - } - 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 decodeThingUnshareRequest(_ 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 := thingShareRequest{ - thingID: chi.URLParam(r, "thingID"), - } - 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 decodeDeleteClientReq(_ context.Context, r *http.Request) (interface{}, error) { - req := deleteClientReq{ - id: chi.URLParam(r, "thingID"), - } - - return req, nil -} diff --git a/things/api/http/decode.go b/things/api/http/decode.go new file mode 100644 index 0000000000..2d1529b798 --- /dev/null +++ b/things/api/http/decode.go @@ -0,0 +1,243 @@ +package http + +import ( + "context" + "encoding/json" + "net/http" + "strings" + + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/magistrala/pkg/errors" + "github.com/absmach/magistrala/things" + "github.com/go-chi/chi/v5" +) + +func decodeViewClient(_ context.Context, r *http.Request) (interface{}, error) { + req := viewClientReq{ + id: chi.URLParam(r, "thingID"), + } + + return req, nil +} + +func decodeViewClientPerms(_ context.Context, r *http.Request) (interface{}, error) { + req := viewClientPermsReq{ + id: chi.URLParam(r, "thingID"), + } + + return req, nil +} + +func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) { + s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefClientStatus) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + m, err := apiutil.ReadMetadataQuery(r, api.MetadataKey, nil) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + n, err := apiutil.ReadStringQuery(r, api.NameKey, "") + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + t, err := apiutil.ReadStringQuery(r, api.TagKey, "") + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + id, err := apiutil.ReadStringQuery(r, api.IDOrder, "") + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + p, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + lp, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + st, err := things.ToStatus(s) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + req := listClientsReq{ + status: st, + offset: o, + limit: l, + metadata: m, + name: n, + tag: t, + permission: p, + listPerms: lp, + userID: chi.URLParam(r, "userID"), + id: id, + } + return req, nil +} + +func decodeUpdateClient(_ 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 := updateClientReq{ + id: chi.URLParam(r, "thingID"), + } + 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 decodeUpdateClientTags(_ 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 := updateClientTagsReq{ + id: chi.URLParam(r, "thingID"), + } + 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 decodeUpdateClientCredentials(_ 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 := updateClientCredentialsReq{ + id: chi.URLParam(r, "thingID"), + } + 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 decodeCreateClientReq(_ 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) + } + + var c things.Client + if err := json.NewDecoder(r.Body).Decode(&c); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) + } + req := createClientReq{ + thing: c, + } + + return req, nil +} + +func decodeCreateClientsReq(_ 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) + } + + c := createClientsReq{} + if err := json.NewDecoder(r.Body).Decode(&c.Things); err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err)) + } + + return c, nil +} + +func decodeChangeClientStatus(_ context.Context, r *http.Request) (interface{}, error) { + req := changeClientStatusReq{ + id: chi.URLParam(r, "thingID"), + } + + return req, nil +} + +func decodeSetThingParentGroupStatus(_ 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 := setThingParentGroupReq{ + id: chi.URLParam(r, "thingID"), + } + 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 decodeRemoveThingParentGroupStatus(_ context.Context, r *http.Request) (interface{}, error) { + req := removeThingParentGroupReq{ + id: chi.URLParam(r, "thingID"), + } + + return req, nil +} + +func decodeListMembersRequest(_ context.Context, r *http.Request) (interface{}, error) { + s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefClientStatus) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + m, err := apiutil.ReadMetadataQuery(r, api.MetadataKey, nil) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + st, err := things.ToStatus(s) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + p, err := apiutil.ReadStringQuery(r, api.PermissionKey, api.DefPermission) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + + lp, err := apiutil.ReadBoolQuery(r, api.ListPerms, api.DefListPerms) + if err != nil { + return nil, errors.Wrap(apiutil.ErrValidation, err) + } + req := listMembersReq{ + Page: things.Page{ + Status: st, + Offset: o, + Limit: l, + Permission: p, + Metadata: m, + ListPerms: lp, + }, + groupID: chi.URLParam(r, "groupID"), + } + return req, nil +} + +func decodeDeleteClientReq(_ context.Context, r *http.Request) (interface{}, error) { + req := deleteClientReq{ + id: chi.URLParam(r, "thingID"), + } + + return req, nil +} diff --git a/things/api/http/endpoints.go b/things/api/http/endpoints.go index 10b9abc697..ef17180578 100644 --- a/things/api/http/endpoints.go +++ b/things/api/http/endpoints.go @@ -11,8 +11,6 @@ import ( "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/policies" "github.com/absmach/magistrala/things" "github.com/go-kit/kit/endpoint" ) @@ -26,7 +24,7 @@ func createClientEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } thing, err := svc.CreateClients(ctx, session, req.thing) @@ -50,7 +48,7 @@ func createClientsEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } page, err := svc.CreateClients(ctx, session, req.Things...) @@ -59,7 +57,7 @@ func createClientsEndpoint(svc things.Service) endpoint.Endpoint { } res := clientsPageRes{ - pageRes: pageRes{ + clientsPageMetaRes: clientsPageMetaRes{ Total: uint64(len(page)), }, Clients: []viewClientRes{}, @@ -81,7 +79,7 @@ func viewClientEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } c, err := svc.View(ctx, session, req.id) @@ -93,27 +91,6 @@ func viewClientEndpoint(svc things.Service) endpoint.Endpoint { } } -func viewClientPermsEndpoint(svc things.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(viewClientPermsReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - p, err := svc.ViewPerms(ctx, session, req.id) - if err != nil { - return nil, err - } - - return viewClientPermsRes{Permissions: p}, nil - } -} - func listClientsEndpoint(svc things.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(listClientsReq) @@ -123,7 +100,7 @@ func listClientsEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } pm := things.Page{ @@ -143,7 +120,7 @@ func listClientsEndpoint(svc things.Service) endpoint.Endpoint { } res := clientsPageRes{ - pageRes: pageRes{ + clientsPageMetaRes: clientsPageMetaRes{ Total: page.Total, Offset: page.Offset, Limit: page.Limit, @@ -158,27 +135,6 @@ func listClientsEndpoint(svc things.Service) endpoint.Endpoint { } } -func listMembersEndpoint(svc things.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(listMembersReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - page, err := svc.ListClientsByGroup(ctx, session, req.groupID, req.Page) - if err != nil { - return nil, err - } - - return buildClientsResponse(page), nil - } -} - func updateClientEndpoint(svc things.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(updateClientReq) @@ -188,7 +144,7 @@ func updateClientEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } cli := things.Client{ @@ -214,7 +170,7 @@ func updateClientTagsEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } cli := things.Client{ @@ -239,7 +195,7 @@ func updateClientSecretEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } client, err := svc.UpdateSecret(ctx, session, req.id, req.Secret) @@ -260,7 +216,7 @@ func enableClientEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } client, err := svc.Enable(ctx, session, req.id) @@ -281,7 +237,7 @@ func disableClientEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } client, err := svc.Disable(ctx, session, req.id) @@ -295,7 +251,7 @@ func disableClientEndpoint(svc things.Service) endpoint.Endpoint { func buildClientsResponse(cp things.MembersPage) clientsPageRes { res := clientsPageRes{ - pageRes: pageRes{ + clientsPageMetaRes: clientsPageMetaRes{ Total: cp.Total, Offset: cp.Offset, Limit: cp.Limit, @@ -309,203 +265,41 @@ func buildClientsResponse(cp things.MembersPage) clientsPageRes { return res } -func assignUsersEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignUsersRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Assign(ctx, session, req.groupID, req.Relation, policies.UsersKind, req.UserIDs...); err != nil { - return nil, err - } - - return assignUsersRes{}, nil - } -} - -func unassignUsersEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignUsersRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Unassign(ctx, session, req.groupID, req.Relation, policies.UsersKind, req.UserIDs...); err != nil { - return nil, err - } - - return unassignUsersRes{}, nil - } -} - -func assignUserGroupsEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignUserGroupsRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Assign(ctx, session, req.groupID, policies.ParentGroupRelation, policies.ChannelsKind, req.UserGroupIDs...); err != nil { - return nil, err - } - - return assignUserGroupsRes{}, nil - } -} - -func unassignUserGroupsEndpoint(svc groups.Service) endpoint.Endpoint { +func setThingParentGroupEndpoint(svc things.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignUserGroupsRequest) + req := request.(setThingParentGroupReq) if err := req.validate(); err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) } session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } - - if err := svc.Unassign(ctx, session, req.groupID, policies.ParentGroupRelation, policies.ChannelsKind, req.UserGroupIDs...); err != nil { - return nil, err - } - - return unassignUserGroupsRes{}, nil - } -} - -func connectChannelThingEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(connectChannelThingRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Assign(ctx, session, req.ChannelID, policies.GroupRelation, policies.ThingsKind, req.ThingID); err != nil { + if err := svc.SetParentGroup(ctx, session, req.ParentGroupID, req.id); err != nil { return nil, err } - return connectChannelThingRes{}, nil + return setParentGroupRes{}, nil } } -func disconnectChannelThingEndpoint(svc groups.Service) endpoint.Endpoint { +func removeThingParentGroupEndpoint(svc things.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(connectChannelThingRequest) + req := request.(removeThingParentGroupReq) if err := req.validate(); err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) } session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } - - if err := svc.Unassign(ctx, session, req.ChannelID, policies.GroupRelation, policies.ThingsKind, req.ThingID); err != nil { - return nil, err - } - - return disconnectChannelThingRes{}, nil - } -} - -func connectEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(connectChannelThingRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Assign(ctx, session, req.ChannelID, policies.GroupRelation, policies.ThingsKind, req.ThingID); err != nil { - return nil, err - } - - return connectChannelThingRes{}, nil - } -} - -func disconnectEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(connectChannelThingRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Unassign(ctx, session, req.ChannelID, policies.GroupRelation, policies.ThingsKind, req.ThingID); err != nil { - return nil, err - } - - return disconnectChannelThingRes{}, nil - } -} - -func thingShareEndpoint(svc things.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(thingShareRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Share(ctx, session, req.thingID, req.Relation, req.UserIDs...); err != nil { - return nil, err - } - - return thingShareRes{}, nil - } -} - -func thingUnshareEndpoint(svc things.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(thingShareRequest) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Unshare(ctx, session, req.thingID, req.Relation, req.UserIDs...); err != nil { + if err := svc.RemoveParentGroup(ctx, session, req.id); err != nil { return nil, err } - return thingUnshareRes{}, nil + return removeParentGroupRes{}, nil } } @@ -518,7 +312,7 @@ func deleteClientEndpoint(svc things.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } if err := svc.Delete(ctx, session, req.id); err != nil { diff --git a/things/api/http/endpoints_test.go b/things/api/http/endpoints_test.go index 3c16c92e4e..054c628396 100644 --- a/things/api/http/endpoints_test.go +++ b/things/api/http/endpoints_test.go @@ -21,7 +21,6 @@ import ( authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" - gmocks "github.com/absmach/magistrala/pkg/groups/mocks" "github.com/absmach/magistrala/things" httpapi "github.com/absmach/magistrala/things/api/http" "github.com/absmach/magistrala/things/mocks" @@ -88,20 +87,19 @@ func toJSON(data interface{}) string { return string(jsonData) } -func newThingsServer() (*httptest.Server, *mocks.Service, *gmocks.Service, *authnmocks.Authentication) { +func newThingsServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) { svc := new(mocks.Service) - gsvc := new(gmocks.Service) authn := new(authnmocks.Authentication) logger := mglog.NewMock() mux := chi.NewRouter() - httpapi.MakeHandler(svc, gsvc, authn, mux, logger, "") + httpapi.MakeHandler(svc, authn, mux, logger, "") - return httptest.NewServer(mux), svc, gsvc, authn + return httptest.NewServer(mux), svc, authn } func TestCreateThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -245,7 +243,7 @@ func TestCreateThing(t *testing.T) { } func TestCreateThings(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() num := 3 @@ -417,7 +415,7 @@ func TestCreateThings(t *testing.T) { } func TestListThings(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -763,7 +761,7 @@ func TestListThings(t *testing.T) { } func TestViewThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -843,7 +841,7 @@ func TestViewThing(t *testing.T) { } func TestViewThingPerms(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -940,7 +938,7 @@ func TestViewThingPerms(t *testing.T) { } func TestUpdateThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() newName := "newname" @@ -1084,7 +1082,7 @@ func TestUpdateThing(t *testing.T) { } func TestUpdateThingsTags(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() newTag := "newtag" @@ -1218,7 +1216,7 @@ func TestUpdateThingsTags(t *testing.T) { } func TestUpdateClientSecret(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -1386,7 +1384,7 @@ func TestUpdateClientSecret(t *testing.T) { } func TestEnableThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -1471,7 +1469,7 @@ func TestEnableThing(t *testing.T) { } func TestDisableThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -1556,7 +1554,7 @@ func TestDisableThing(t *testing.T) { } func TestShareThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -1713,7 +1711,7 @@ func TestShareThing(t *testing.T) { } func TestUnShareThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -1870,7 +1868,7 @@ func TestUnShareThing(t *testing.T) { } func TestDeleteThing(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -1944,7 +1942,7 @@ func TestDeleteThing(t *testing.T) { } func TestListMembers(t *testing.T) { - ts, svc, _, authn := newThingsServer() + ts, svc, authn := newThingsServer() defer ts.Close() cases := []struct { @@ -2344,999 +2342,6 @@ func TestListMembers(t *testing.T) { } } -func TestAssignUsers(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - groupID string - reqBody interface{} - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "assign users to a group successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusCreated, - - err: nil, - }, - { - desc: "assign users to a group with invalid token", - domainID: domainID, - token: inValidToken, - authnRes: mgauthn.Session{}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "assign users to a group with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "assign users to a group with empty group id", - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: "", - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "assign users to a group with empty relation", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "assign users to a group with empty user ids", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "assign users to a group with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: map[string]interface{}{ - "relation": make(chan int), - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: nil, - }, - { - desc: "assign users to a group with invalid content type", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/channels/%s/users/assign", ts.URL, tc.domainID, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Assign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - -func TestUnassignUsers(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - groupID string - reqBody interface{} - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "unassign users from a group successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusNoContent, - - err: nil, - }, - { - desc: "unassign users from a group with invalid token", - domainID: domainID, - token: inValidToken, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "unassign users from a group with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "unassign users from a group with empty group id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: "", - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "unassign users from a group with empty relation", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "unassign users from a group with empty user ids", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "unassign users from a group with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: map[string]interface{}{ - "relation": make(chan int), - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: nil, - }, - { - desc: "unassign users from a group with invalid content type", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/channels/%s/users/unassign", ts.URL, tc.domainID, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Unassign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - -func TestAssignGroupsToChannel(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - groupID string - reqBody interface{} - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "assign groups to a channel successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusCreated, - - err: nil, - }, - { - desc: "assign groups to a channel with invalid token", - domainID: domainID, - token: inValidToken, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "assign groups to a channel with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "assign groups to a channel with empty group id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: "", - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "assign groups to a channel with empty group ids", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "assign groups to a channel with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: map[string]interface{}{ - "group_ids": make(chan int), - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "assign groups to a channel with invalid content type", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/channels/%s/groups/assign", ts.URL, tc.domainID, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Assign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - -func TestUnassignGroupsFromChannel(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - groupID string - reqBody interface{} - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "unassign groups from a channel successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusNoContent, - - err: nil, - }, - { - desc: "unassign groups from a channel with invalid token", - domainID: domainID, - token: inValidToken, - authnRes: mgauthn.Session{}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "unassign groups from a channel with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "unassign groups from a channel with empty group id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: "", - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "unassign groups from a channel with empty group ids", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{}, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "unassign groups from a channel with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: map[string]interface{}{ - "group_ids": make(chan int), - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "unassign groups from a channel with invalid content type", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - - err: apiutil.ErrValidation, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/channels/%s/groups/unassign", ts.URL, tc.domainID, tc.groupID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Unassign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - -func TestConnectThingToChannel(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - channelID string - thingID string - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "connect thing to a channel successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - channelID: validID, - thingID: validID, - contentType: contentType, - status: http.StatusCreated, - err: nil, - }, - { - desc: "connect thing to a channel with invalid token", - domainID: domainID, - token: inValidToken, - channelID: validID, - thingID: validID, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "connect thing to a channel with empty channel id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: validID, UserID: validID, DomainID: domainID}, - channelID: "", - thingID: validID, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "connect thing to a channel with empty thing id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - channelID: validID, - thingID: "", - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/channels/%s/things/%s/connect", ts.URL, tc.domainID, tc.channelID, tc.thingID), - token: tc.token, - contentType: tc.contentType, - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Assign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - -func TestDisconnectThingFromChannel(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - channelID string - thingID string - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "disconnect thing from a channel successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - channelID: validID, - thingID: validID, - contentType: contentType, - status: http.StatusNoContent, - - err: nil, - }, - { - desc: "disconnect thing from a channel with invalid token", - domainID: domainID, - token: inValidToken, - channelID: validID, - thingID: validID, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "disconnect thing from a channel with empty channel id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - channelID: "", - thingID: validID, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "disconnect thing from a channel with empty thing id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - channelID: validID, - thingID: "", - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/channels/%s/things/%s/disconnect", ts.URL, tc.domainID, tc.channelID, tc.thingID), - token: tc.token, - contentType: tc.contentType, - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Unassign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - -func TestConnect(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - reqBody interface{} - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "connect thing to a channel successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: validID, - }, - contentType: contentType, - status: http.StatusCreated, - - err: nil, - }, - { - desc: "connect thing to a channel with invalid token", - domainID: domainID, - token: inValidToken, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: validID, - }, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "connect thing to a channel with empty channel id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: "", - ThingID: validID, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "connect thing to a channel with empty thing id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: "", - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "connect thing to a channel with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: map[string]interface{}{ - "channel_id": make(chan int), - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "connect thing to a channel with invalid content type", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: validID, - }, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrValidation, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/connect", ts.URL, tc.domainID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Assign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - -func TestDisconnect(t *testing.T) { - ts, _, gsvc, authn := newThingsServer() - defer ts.Close() - - cases := []struct { - desc string - domainID string - token string - reqBody interface{} - contentType string - status int - authnRes mgauthn.Session - authnErr error - err error - }{ - { - desc: "Disconnect thing from a channel successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: validID, - }, - contentType: contentType, - status: http.StatusNoContent, - - err: nil, - }, - { - desc: "Disconnect thing from a channel with invalid token", - domainID: domainID, - token: inValidToken, - authnRes: mgauthn.Session{}, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: validID, - }, - contentType: contentType, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "Disconnect thing from a channel with empty channel id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: "", - ThingID: validID, - }, - contentType: contentType, - status: http.StatusBadRequest, - - err: apiutil.ErrValidation, - }, - { - desc: "Disconnect thing from a channel with empty thing id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: "", - }, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "Disconnect thing from a channel with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: map[string]interface{}{ - "channel_id": make(chan int), - }, - contentType: contentType, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "Disconnect thing from a channel with invalid content type", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}, - reqBody: groupReqBody{ - ChannelID: validID, - ThingID: validID, - }, - contentType: "application/xml", - status: http.StatusUnsupportedMediaType, - err: apiutil.ErrValidation, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - client: ts.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/disconnect", ts.URL, tc.domainID), - token: tc.token, - contentType: tc.contentType, - body: strings.NewReader(data), - } - - authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Unassign", mock.Anything, tc.authnRes, 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() - authCall.Unset() - }) - } -} - type respBody struct { Err string `json:"error"` Message string `json:"message"` diff --git a/things/api/http/requests.go b/things/api/http/requests.go index 8c644cd953..62d88e1e5d 100644 --- a/things/api/http/requests.go +++ b/things/api/http/requests.go @@ -175,70 +175,29 @@ func (req changeClientStatusReq) validate() error { return nil } -type assignUsersRequest struct { - groupID string - Relation string `json:"relation"` - UserIDs []string `json:"user_ids"` +type setThingParentGroupReq struct { + id string + ParentGroupID string `json:"parent_group_id"` } -func (req assignUsersRequest) validate() error { - if req.Relation == "" { - return apiutil.ErrMissingRelation - } - - if req.groupID == "" { - return apiutil.ErrMissingID - } - - if len(req.UserIDs) == 0 { - return apiutil.ErrEmptyList - } - - return nil -} - -type assignUserGroupsRequest struct { - groupID string - UserGroupIDs []string `json:"group_ids"` -} - -func (req assignUserGroupsRequest) validate() error { - if req.groupID == "" { +func (req setThingParentGroupReq) validate() error { + if req.id == "" { return apiutil.ErrMissingID } - - if len(req.UserGroupIDs) == 0 { - return apiutil.ErrEmptyList - } - - return nil -} - -type connectChannelThingRequest struct { - ThingID string `json:"thing_id,omitempty"` - ChannelID string `json:"channel_id,omitempty"` -} - -func (req *connectChannelThingRequest) validate() error { - if req.ThingID == "" || req.ChannelID == "" { - return apiutil.ErrMissingID + if req.ParentGroupID == "" { + return apiutil.ErrMissingParentGroupID } return nil } -type thingShareRequest struct { - thingID string - Relation string `json:"relation,omitempty"` - UserIDs []string `json:"user_ids,omitempty"` +type removeThingParentGroupReq struct { + id string } -func (req *thingShareRequest) validate() error { - if req.thingID == "" { +func (req removeThingParentGroupReq) validate() error { + if req.id == "" { return apiutil.ErrMissingID } - if req.Relation == "" || len(req.UserIDs) == 0 { - return apiutil.ErrMalformedPolicy - } return nil } diff --git a/things/api/http/requests_test.go b/things/api/http/requests_test.go index a4529a9b5c..3860c75dc0 100644 --- a/things/api/http/requests_test.go +++ b/things/api/http/requests_test.go @@ -402,186 +402,6 @@ func TestChangeClientStatusReqValidate(t *testing.T) { } } -func TestAssignUsersRequestValidate(t *testing.T) { - cases := []struct { - desc string - req assignUsersRequest - err error - }{ - { - desc: "valid request", - req: assignUsersRequest{ - groupID: validID, - UserIDs: []string{validID}, - Relation: valid, - }, - err: nil, - }, - { - desc: "empty id", - req: assignUsersRequest{ - groupID: "", - UserIDs: []string{validID}, - Relation: valid, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "empty users", - req: assignUsersRequest{ - groupID: validID, - UserIDs: []string{}, - Relation: valid, - }, - err: apiutil.ErrEmptyList, - }, - { - desc: "empty relation", - req: assignUsersRequest{ - groupID: validID, - UserIDs: []string{validID}, - Relation: "", - }, - err: apiutil.ErrMissingRelation, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := tc.req.validate() - assert.Equal(t, tc.err, err, "%s: expected %s got %s\n", tc.desc, tc.err, err) - }) - } -} - -func TestAssignUserGroupsRequestValidate(t *testing.T) { - cases := []struct { - desc string - req assignUserGroupsRequest - err error - }{ - { - desc: "valid request", - req: assignUserGroupsRequest{ - groupID: validID, - UserGroupIDs: []string{validID}, - }, - err: nil, - }, - { - desc: "empty group id", - req: assignUserGroupsRequest{ - groupID: "", - UserGroupIDs: []string{validID}, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "empty user group ids", - req: assignUserGroupsRequest{ - groupID: validID, - UserGroupIDs: []string{}, - }, - err: apiutil.ErrEmptyList, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := tc.req.validate() - assert.Equal(t, tc.err, err, "%s: expected %s got %s\n", tc.desc, tc.err, err) - }) - } -} - -func TestConnectChannelThingRequestValidate(t *testing.T) { - cases := []struct { - desc string - req connectChannelThingRequest - err error - }{ - { - desc: "valid request", - req: connectChannelThingRequest{ - ChannelID: validID, - ThingID: validID, - }, - err: nil, - }, - { - desc: "empty channel id", - req: connectChannelThingRequest{ - ChannelID: "", - ThingID: validID, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "empty thing id", - req: connectChannelThingRequest{ - ChannelID: validID, - ThingID: "", - }, - err: apiutil.ErrMissingID, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := tc.req.validate() - assert.Equal(t, tc.err, err, "%s: expected %s got %s\n", tc.desc, tc.err, err) - }) - } -} - -func TestThingShareRequestValidate(t *testing.T) { - cases := []struct { - desc string - req thingShareRequest - err error - }{ - { - desc: "valid request", - req: thingShareRequest{ - thingID: validID, - UserIDs: []string{validID}, - Relation: valid, - }, - err: nil, - }, - { - desc: "empty thing id", - req: thingShareRequest{ - thingID: "", - UserIDs: []string{validID}, - Relation: valid, - }, - err: apiutil.ErrMissingID, - }, - { - desc: "empty user ids", - req: thingShareRequest{ - thingID: validID, - UserIDs: []string{}, - Relation: valid, - }, - err: apiutil.ErrMalformedPolicy, - }, - { - desc: "empty relation", - req: thingShareRequest{ - thingID: validID, - UserIDs: []string{validID}, - Relation: "", - }, - err: apiutil.ErrMalformedPolicy, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - err := tc.req.validate() - assert.Equal(t, tc.err, err, "%s: expected %s got %s\n", tc.desc, tc.err, err) - }) - } -} - func TestDeleteClientReqValidate(t *testing.T) { cases := []struct { desc string diff --git a/things/api/http/responses.go b/things/api/http/responses.go index c998bb0585..2e9bb3dc6e 100644 --- a/things/api/http/responses.go +++ b/things/api/http/responses.go @@ -12,20 +12,15 @@ import ( ) var ( + _ magistrala.Response = (*createClientRes)(nil) _ magistrala.Response = (*viewClientRes)(nil) _ magistrala.Response = (*viewClientPermsRes)(nil) - _ magistrala.Response = (*createClientRes)(nil) - _ magistrala.Response = (*deleteClientRes)(nil) _ magistrala.Response = (*clientsPageRes)(nil) - _ magistrala.Response = (*viewMembersRes)(nil) - _ magistrala.Response = (*assignUsersGroupsRes)(nil) - _ magistrala.Response = (*unassignUsersGroupsRes)(nil) - _ magistrala.Response = (*connectChannelThingRes)(nil) - _ magistrala.Response = (*disconnectChannelThingRes)(nil) _ magistrala.Response = (*changeClientStatusRes)(nil) + _ magistrala.Response = (*deleteClientRes)(nil) ) -type pageRes struct { +type clientsPageMetaRes struct { Limit uint64 `json:"limit,omitempty"` Offset uint64 `json:"offset"` Total uint64 `json:"total"` @@ -107,7 +102,7 @@ func (res viewClientPermsRes) Empty() bool { } type clientsPageRes struct { - pageRes + clientsPageMetaRes Clients []viewClientRes `json:"things"` } @@ -123,22 +118,6 @@ func (res clientsPageRes) Empty() bool { return false } -type viewMembersRes struct { - things.Client -} - -func (res viewMembersRes) Code() int { - return http.StatusOK -} - -func (res viewMembersRes) Headers() map[string]string { - return map[string]string{} -} - -func (res viewMembersRes) Empty() bool { - return false -} - type changeClientStatusRes struct { things.Client } @@ -155,156 +134,46 @@ func (res changeClientStatusRes) Empty() bool { return false } -type deleteClientRes struct{} - -func (res deleteClientRes) Code() int { - return http.StatusNoContent -} - -func (res deleteClientRes) Headers() map[string]string { - return map[string]string{} -} - -func (res deleteClientRes) Empty() bool { - return true -} - -type assignUsersGroupsRes struct{} - -func (res assignUsersGroupsRes) Code() int { - return http.StatusCreated -} - -func (res assignUsersGroupsRes) Headers() map[string]string { - return map[string]string{} -} - -func (res assignUsersGroupsRes) Empty() bool { - return true -} - -type unassignUsersGroupsRes struct{} - -func (res unassignUsersGroupsRes) Code() int { - return http.StatusNoContent -} - -func (res unassignUsersGroupsRes) Headers() map[string]string { - return map[string]string{} -} - -func (res unassignUsersGroupsRes) Empty() bool { - return true -} - -type assignUsersRes struct{} - -func (res assignUsersRes) Code() int { - return http.StatusCreated +type setParentGroupRes struct { } -func (res assignUsersRes) Headers() map[string]string { - return map[string]string{} +func (res setParentGroupRes) Code() int { + return http.StatusAccepted } -func (res assignUsersRes) Empty() bool { - return true -} - -type unassignUsersRes struct{} - -func (res unassignUsersRes) Code() int { - return http.StatusNoContent -} - -func (res unassignUsersRes) Headers() map[string]string { +func (res setParentGroupRes) Headers() map[string]string { return map[string]string{} } -func (res unassignUsersRes) Empty() bool { +func (res setParentGroupRes) Empty() bool { return true } -type assignUserGroupsRes struct{} - -func (res assignUserGroupsRes) Code() int { - return http.StatusCreated -} - -func (res assignUserGroupsRes) Headers() map[string]string { - return map[string]string{} +type removeParentGroupRes struct { } -func (res assignUserGroupsRes) Empty() bool { - return true -} - -type unassignUserGroupsRes struct{} - -func (res unassignUserGroupsRes) Code() int { +func (res removeParentGroupRes) Code() int { return http.StatusNoContent } -func (res unassignUserGroupsRes) Headers() map[string]string { +func (res removeParentGroupRes) Headers() map[string]string { return map[string]string{} } -func (res unassignUserGroupsRes) Empty() bool { +func (res removeParentGroupRes) Empty() bool { return true } -type connectChannelThingRes struct{} - -func (res connectChannelThingRes) Code() int { - return http.StatusCreated -} - -func (res connectChannelThingRes) Headers() map[string]string { - return map[string]string{} -} - -func (res connectChannelThingRes) Empty() bool { - return true -} - -type disconnectChannelThingRes struct{} - -func (res disconnectChannelThingRes) Code() int { - return http.StatusNoContent -} - -func (res disconnectChannelThingRes) Headers() map[string]string { - return map[string]string{} -} - -func (res disconnectChannelThingRes) Empty() bool { - return true -} - -type thingShareRes struct{} - -func (res thingShareRes) Code() int { - return http.StatusCreated -} - -func (res thingShareRes) Headers() map[string]string { - return map[string]string{} -} - -func (res thingShareRes) Empty() bool { - return true -} - -type thingUnshareRes struct{} +type deleteClientRes struct{} -func (res thingUnshareRes) Code() int { +func (res deleteClientRes) Code() int { return http.StatusNoContent } -func (res thingUnshareRes) Headers() map[string]string { +func (res deleteClientRes) Headers() map[string]string { return map[string]string{} } -func (res thingUnshareRes) Empty() bool { +func (res deleteClientRes) Empty() bool { return true } diff --git a/things/api/http/transport.go b/things/api/http/transport.go index 415e463d02..155439b5cf 100644 --- a/things/api/http/transport.go +++ b/things/api/http/transport.go @@ -9,16 +9,14 @@ import ( "github.com/absmach/magistrala" mgauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/things" "github.com/go-chi/chi/v5" "github.com/prometheus/client_golang/prometheus/promhttp" ) // MakeHandler returns a HTTP handler for Things and Groups API endpoints. -func MakeHandler(tsvc things.Service, grps groups.Service, authn mgauthn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string) http.Handler { - clientsHandler(tsvc, mux, authn, logger) - groupsHandler(grps, authn, mux, logger) +func MakeHandler(tsvc things.Service, authn mgauthn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string) http.Handler { + mux = clientsHandler(tsvc, authn, mux, logger) mux.Get("/health", magistrala.Health("things", instanceID)) mux.Handle("/metrics", promhttp.Handler()) diff --git a/things/clients.go b/things/clients.go index 8894c171e6..83d60337b0 100644 --- a/things/clients.go +++ b/things/clients.go @@ -9,13 +9,13 @@ import ( "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/postgres" + "github.com/absmach/magistrala/pkg/roles" ) -type AuthzReq struct { - ChannelID string - ClientID string - ClientKey string - Permission string +type Connection struct { + ThingID string + ChannelID string + DomainID string } type ClientRepository struct { @@ -55,7 +55,7 @@ type Repository interface { ChangeStatus(ctx context.Context, client Client) (Client, error) // Delete deletes client with given id - Delete(ctx context.Context, id string) error + Delete(ctx context.Context, clientIDs ...string) error // Save persists the client account. A non-nil error is returned to indicate // operation failure. @@ -63,12 +63,38 @@ type Repository interface { // RetrieveBySecret retrieves a client based on the secret (key). RetrieveBySecret(ctx context.Context, key string) (Client, error) + + RetrieveByIds(ctx context.Context, ids []string) (ClientsPage, error) + + AddConnections(ctx context.Context, conns []Connection) error + + RemoveConnections(ctx context.Context, conns []Connection) error + + ThingConnectionsCount(ctx context.Context, id string) (uint64, error) + + DoesThingHaveConnections(ctx context.Context, id string) (bool, error) + + RemoveChannelConnections(ctx context.Context, channelID string) error + + RemoveThingConnections(ctx context.Context, thingID string) error + + // SetParentGroup set parent group id to a given channel id + SetParentGroup(ctx context.Context, th Client) error + + // RemoveParentGroup remove parent group id fr given chanel id + RemoveParentGroup(ctx context.Context, th Client) error + + RetrieveParentGroupThings(ctx context.Context, parentGroupID string) ([]Client, error) + + UnsetParentGroupFromThings(ctx context.Context, parentGroupID string) error + + roles.Repository } // Service specifies an API that must be fullfiled by the domain service // implementation, and all of its decorators (e.g. logging & metrics). // -//go:generate mockery --name Service --filename service.go --quiet --note "Copyright (c) Abstract Machines" +//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines" type Service interface { // CreateClients creates new client. In case of the failed registration, a // non-nil error value is returned. @@ -77,17 +103,9 @@ type Service interface { // View retrieves client info for a given client ID and an authorized token. View(ctx context.Context, session authn.Session, id string) (Client, error) - // ViewPerms retrieves permissions on the client id for the given authorized token. - ViewPerms(ctx context.Context, session authn.Session, id string) ([]string, error) - // ListClients retrieves clients list for a valid auth token. ListClients(ctx context.Context, session authn.Session, reqUserID string, pm Page) (ClientsPage, error) - // ListClientsByGroup retrieves data about subset of clients that are - // connected or not connected to specified channel and belong to the user identified by - // the provided key. - ListClientsByGroup(ctx context.Context, session authn.Session, groupID string, pm Page) (MembersPage, error) - // Update updates the client's name and metadata. Update(ctx context.Context, session authn.Session, client Client) (Client, error) @@ -103,25 +121,19 @@ type Service interface { // Disable logically disables the client identified with the provided ID Disable(ctx context.Context, session authn.Session, id string) (Client, error) - // Share add share policy to client id with given relation for given user ids - Share(ctx context.Context, session authn.Session, id string, relation string, userids ...string) error - - // Unshare remove share policy to client id with given relation for given user ids - Unshare(ctx context.Context, session authn.Session, id string, relation string, userids ...string) error + // Delete deletes client with given ID. + Delete(ctx context.Context, session authn.Session, id string) error - // Identify returns client ID for given client key. - Identify(ctx context.Context, key string) (string, error) + SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error - // Authorize used for Clients authorization. - Authorize(ctx context.Context, req AuthzReq) (string, error) + RemoveParentGroup(ctx context.Context, session authn.Session, id string) error - // Delete deletes client with given ID. - Delete(ctx context.Context, session authn.Session, id string) error + roles.RoleManager } // Cache contains client caching interface. // -//go:generate mockery --name Cache --filename cache.go --quiet --note "Copyright (c) Abstract Machines" +//go:generate mockery --name Cache --output=./mocks --filename cache.go --quiet --note "Copyright (c) Abstract Machines" type Cache interface { // Save stores pair client secret, client id. Save(ctx context.Context, clientSecret, clientID string) error @@ -140,6 +152,7 @@ type Client struct { Name string `json:"name,omitempty"` Tags []string `json:"tags,omitempty"` Domain string `json:"domain_id,omitempty"` + ParentGroup string `josn:"parent_group_id,omitempty"` Credentials Credentials `json:"credentials,omitempty"` Metadata Metadata `json:"metadata,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` diff --git a/things/events/events.go b/things/events/events.go index 5ec7e8e938..4cd33e9207 100644 --- a/things/events/events.go +++ b/things/events/events.go @@ -22,6 +22,8 @@ const ( clientListByGroup = clientPrefix + "list_by_channel" clientIdentify = clientPrefix + "identify" clientAuthorize = clientPrefix + "authorize" + clientSetParent = clientPrefix + "set_parent" + clientRemoveParent = clientPrefix + "remove_parent" ) var ( @@ -334,3 +336,27 @@ func (dce removeClientEvent) Encode() (map[string]interface{}, error) { "id": dce.id, }, nil } + +type setParentGroupEvent struct { + id string + parentGroupID string +} + +func (spge setParentGroupEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": clientSetParent, + "id": spge.id, + "parent_group_id": spge.parentGroupID, + }, nil +} + +type removeParentGroupEvent struct { + id string +} + +func (rpge removeParentGroupEvent) Encode() (map[string]interface{}, error) { + return map[string]interface{}{ + "operation": clientRemoveParent, + "id": rpge.id, + }, nil +} diff --git a/things/events/streams.go b/things/events/streams.go index 295fb37bc5..9fd16f81e0 100644 --- a/things/events/streams.go +++ b/things/events/streams.go @@ -9,6 +9,7 @@ import ( "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/events" "github.com/absmach/magistrala/pkg/events/store" + rmEvents "github.com/absmach/magistrala/pkg/roles/rolemanager/events" "github.com/absmach/magistrala/things" ) @@ -19,6 +20,7 @@ var _ things.Service = (*eventStore)(nil) type eventStore struct { events.Publisher svc things.Service + rmEvents.RoleManagerEventStore } // NewEventStoreMiddleware returns wrapper around things service that sends @@ -28,10 +30,12 @@ func NewEventStoreMiddleware(ctx context.Context, svc things.Service, url string if err != nil { return nil, err } + res := rmEvents.NewRoleManagerEventStore("things", svc, publisher) return &eventStore{ - svc: svc, - Publisher: publisher, + svc: svc, + Publisher: publisher, + RoleManagerEventStore: res, }, nil } @@ -108,22 +112,6 @@ func (es *eventStore) View(ctx context.Context, session authn.Session, id string return thi, nil } -func (es *eventStore) ViewPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - permissions, err := es.svc.ViewPerms(ctx, session, id) - if err != nil { - return permissions, err - } - - event := viewClientPermsEvent{ - permissions, - } - if err := es.Publish(ctx, event); err != nil { - return permissions, err - } - - return permissions, nil -} - func (es *eventStore) ListClients(ctx context.Context, session authn.Session, reqUserID string, pm things.Page) (things.ClientsPage, error) { cp, err := es.svc.ListClients(ctx, session, reqUserID, pm) if err != nil { @@ -140,21 +128,6 @@ func (es *eventStore) ListClients(ctx context.Context, session authn.Session, re return cp, nil } -func (es *eventStore) ListClientsByGroup(ctx context.Context, session authn.Session, chID string, pm things.Page) (things.MembersPage, error) { - mp, err := es.svc.ListClientsByGroup(ctx, session, chID, pm) - if err != nil { - return mp, err - } - event := listClientByGroupEvent{ - pm, chID, - } - if err := es.Publish(ctx, event); err != nil { - return mp, err - } - - return mp, nil -} - func (es *eventStore) Enable(ctx context.Context, session authn.Session, id string) (things.Client, error) { thi, err := es.svc.Enable(ctx, session, id) if err != nil { @@ -187,76 +160,40 @@ func (es *eventStore) changeStatus(ctx context.Context, thi things.Client) (thin return thi, nil } -func (es *eventStore) Identify(ctx context.Context, key string) (string, error) { - thingID, err := es.svc.Identify(ctx, key) - if err != nil { - return thingID, err - } - event := identifyClientEvent{ - thingID: thingID, - } - - if err := es.Publish(ctx, event); err != nil { - return thingID, err - } - return thingID, nil -} - -func (es *eventStore) Authorize(ctx context.Context, req things.AuthzReq) (string, error) { - thingID, err := es.svc.Authorize(ctx, req) - if err != nil { - return thingID, err +func (es *eventStore) Delete(ctx context.Context, session authn.Session, id string) error { + if err := es.svc.Delete(ctx, session, id); err != nil { + return err } - event := authorizeClientEvent{ - thingID: thingID, - channelID: req.ChannelID, - permission: req.Permission, - } + event := removeClientEvent{id} if err := es.Publish(ctx, event); err != nil { - return thingID, err + return err } - return thingID, nil + return nil } -func (es *eventStore) Share(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { - if err := es.svc.Share(ctx, session, id, relation, userids...); err != nil { +func (es *eventStore) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (err error) { + if err := es.svc.SetParentGroup(ctx, session, parentGroupID, id); err != nil { return err } - event := shareClientEvent{ - action: "share", - id: id, - relation: relation, - userIDs: userids, - } - - return es.Publish(ctx, event) -} + event := setParentGroupEvent{parentGroupID: parentGroupID, id: id} -func (es *eventStore) Unshare(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { - if err := es.svc.Unshare(ctx, session, id, relation, userids...); err != nil { + if err := es.Publish(ctx, event); err != nil { return err } - event := shareClientEvent{ - action: "unshare", - id: id, - relation: relation, - userIDs: userids, - } - - return es.Publish(ctx, event) + return nil } -func (es *eventStore) Delete(ctx context.Context, session authn.Session, id string) error { - if err := es.svc.Delete(ctx, session, id); err != nil { +func (es *eventStore) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (err error) { + if err := es.svc.RemoveParentGroup(ctx, session, id); err != nil { return err } - event := removeClientEvent{id} + event := removeParentGroupEvent{id: id} if err := es.Publish(ctx, event); err != nil { return err diff --git a/things/middleware/authorization.go b/things/middleware/authorization.go index 85a3af5d2b..1faefcdf2d 100644 --- a/things/middleware/authorization.go +++ b/things/middleware/authorization.go @@ -7,173 +7,277 @@ import ( "context" "github.com/absmach/magistrala/pkg/authn" + "github.com/absmach/magistrala/pkg/authz" mgauthz "github.com/absmach/magistrala/pkg/authz" + "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/policies" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" + "github.com/absmach/magistrala/pkg/svcutil" "github.com/absmach/magistrala/things" ) +var ( + errView = errors.New("not authorized to view client") + errUpdate = errors.New("not authorized to update client") + errUpdateTags = errors.New("not authorized to update client tags") + errUpdateSecret = errors.New("not authorized to update client secret") + errEnable = errors.New("not authorized to enable client") + errDisable = errors.New("not authorized to disable client") + errDelete = errors.New("not authorized to delete client") + errSetParentGroup = errors.New("not authorized to set parent group to client") + errRemoveParentGroup = errors.New("not authorized to remove parent group from client") + errDomainCreateClients = errors.New("not authorized to create client in domain") + errGroupSetChildClients = errors.New("not authorized to set child client for group") + errGroupRemoveChildClients = errors.New("not authorized to remove child client for group") +) + var _ things.Service = (*authorizationMiddleware)(nil) type authorizationMiddleware struct { - svc things.Service - authz mgauthz.Authorization + svc things.Service + repo things.Repository + authz mgauthz.Authorization + opp svcutil.OperationPerm + extOpp svcutil.ExternalOperationPerm + rmMW.RoleManagerAuthorizationMiddleware } // AuthorizationMiddleware adds authorization to the clients service. -func AuthorizationMiddleware(svc things.Service, authz mgauthz.Authorization) things.Service { - return &authorizationMiddleware{ - svc: svc, - authz: authz, +func AuthorizationMiddleware(entityType string, svc things.Service, authz mgauthz.Authorization, repo things.Repository, thingsOpPerm, rolesOpPerm map[svcutil.Operation]svcutil.Permission, extOpPerm map[svcutil.ExternalOperation]svcutil.Permission) (things.Service, error) { + opp := things.NewOperationPerm() + if err := opp.AddOperationPermissionMap(thingsOpPerm); err != nil { + return nil, err + } + if err := opp.Validate(); err != nil { + return nil, err + } + ram, err := rmMW.NewRoleManagerAuthorizationMiddleware(policies.ThingType, svc, authz, rolesOpPerm) + if err != nil { + return nil, err + } + extOpp := things.NewExternalOperationPerm() + if err := extOpp.AddOperationPermissionMap(extOpPerm); err != nil { + return nil, err + } + if err := extOpp.Validate(); err != nil { + return nil, err } + return &authorizationMiddleware{ + svc: svc, + authz: authz, + repo: repo, + opp: opp, + extOpp: extOpp, + RoleManagerAuthorizationMiddleware: ram, + }, nil } func (am *authorizationMiddleware) CreateClients(ctx context.Context, session authn.Session, client ...things.Client) ([]things.Client, error) { - if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.CreatePermission, policies.DomainType, session.DomainID); err != nil { - return nil, err + if err := am.extAuthorize(ctx, things.DomainOpCreateThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.DomainType, + Object: session.DomainID, + }); err != nil { + return []things.Client{}, errors.Wrap(err, errDomainCreateClients) } return am.svc.CreateClients(ctx, session, client...) } func (am *authorizationMiddleware) View(ctx context.Context, session authn.Session, id string) (things.Client, error) { - if session.DomainUserID == "" { - return things.Client{}, svcerr.ErrDomainAuthorization - } - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.ViewPermission, policies.ThingType, id); err != nil { - return things.Client{}, err + if err := am.authorize(ctx, things.OpViewThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: id, + }); err != nil { + return things.Client{}, errors.Wrap(err, errView) } - return am.svc.View(ctx, session, id) } -func (am *authorizationMiddleware) ViewPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - return am.svc.ViewPerms(ctx, session, id) -} - func (am *authorizationMiddleware) ListClients(ctx context.Context, session authn.Session, reqUserID string, pm things.Page) (things.ClientsPage, error) { - if session.DomainUserID == "" { - return things.ClientsPage{}, svcerr.ErrDomainAuthorization - } - switch { - case reqUserID != "" && reqUserID != session.UserID: - if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err != nil { - return things.ClientsPage{}, err - } - default: - err := am.checkSuperAdmin(ctx, session.UserID) - switch { - case err == nil: - session.SuperAdmin = true - default: - if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID); err != nil { - return things.ClientsPage{}, err - } - } + if err := am.checkSuperAdmin(ctx, session.UserID); err != nil { + session.SuperAdmin = true } return am.svc.ListClients(ctx, session, reqUserID, pm) } -func (am *authorizationMiddleware) ListClientsByGroup(ctx context.Context, session authn.Session, groupID string, pm things.Page) (things.MembersPage, error) { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, pm.Permission, policies.GroupType, groupID); err != nil { - return things.MembersPage{}, err - } - - return am.svc.ListClientsByGroup(ctx, session, groupID, pm) -} - func (am *authorizationMiddleware) Update(ctx context.Context, session authn.Session, client things.Client) (things.Client, error) { - if session.DomainUserID == "" { - return things.Client{}, svcerr.ErrDomainAuthorization - } - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ThingType, client.ID); err != nil { - return things.Client{}, err + if err := am.authorize(ctx, things.OpUpdateThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: client.ID, + }); err != nil { + return things.Client{}, errors.Wrap(err, errUpdate) } return am.svc.Update(ctx, session, client) } func (am *authorizationMiddleware) UpdateTags(ctx context.Context, session authn.Session, client things.Client) (things.Client, error) { - if session.DomainUserID == "" { - return things.Client{}, svcerr.ErrDomainAuthorization - } - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ThingType, client.ID); err != nil { - return things.Client{}, err + if err := am.authorize(ctx, things.OpUpdateThingTags, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: client.ID, + }); err != nil { + return things.Client{}, errors.Wrap(err, errUpdateTags) } return am.svc.UpdateTags(ctx, session, client) } func (am *authorizationMiddleware) UpdateSecret(ctx context.Context, session authn.Session, id, key string) (things.Client, error) { - if session.DomainUserID == "" { - return things.Client{}, svcerr.ErrDomainAuthorization - } - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ThingType, id); err != nil { - return things.Client{}, err + if err := am.authorize(ctx, things.OpUpdateThingSecret, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: id, + }); err != nil { + return things.Client{}, errors.Wrap(err, errUpdateSecret) } - return am.svc.UpdateSecret(ctx, session, id, key) } func (am *authorizationMiddleware) Enable(ctx context.Context, session authn.Session, id string) (things.Client, error) { - if session.DomainUserID == "" { - return things.Client{}, svcerr.ErrDomainAuthorization - } - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.ThingType, id); err != nil { - return things.Client{}, err + if err := am.authorize(ctx, things.OpEnableThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: id, + }); err != nil { + return things.Client{}, errors.Wrap(err, errEnable) } return am.svc.Enable(ctx, session, id) } func (am *authorizationMiddleware) Disable(ctx context.Context, session authn.Session, id string) (things.Client, error) { - if session.DomainUserID == "" { - return things.Client{}, svcerr.ErrDomainAuthorization - } - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.ThingType, id); err != nil { - return things.Client{}, err + if err := am.authorize(ctx, things.OpDisableThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: id, + }); err != nil { + return things.Client{}, errors.Wrap(err, errDisable) } - return am.svc.Disable(ctx, session, id) } -func (am *authorizationMiddleware) Share(ctx context.Context, session authn.Session, id string, relation string, userids ...string) error { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.ThingType, id); err != nil { - return err +func (am *authorizationMiddleware) Delete(ctx context.Context, session authn.Session, id string) error { + if err := am.authorize(ctx, things.OpDeleteThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: id, + }); err != nil { + return errors.Wrap(err, errDelete) } - return am.svc.Share(ctx, session, id, relation, userids...) + return am.svc.Delete(ctx, session, id) } -func (am *authorizationMiddleware) Unshare(ctx context.Context, session authn.Session, id string, relation string, userids ...string) error { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.ThingType, id); err != nil { - return err +func (am *authorizationMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error { + if err := am.authorize(ctx, things.OpSetParentGroup, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: id, + }); err != nil { + return errors.Wrap(err, errSetParentGroup) } - return am.svc.Unshare(ctx, session, id, relation, userids...) + if err := am.extAuthorize(ctx, things.GroupOpSetChildThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.GroupType, + Object: parentGroupID, + }); err != nil { + return errors.Wrap(err, errGroupSetChildClients) + } + return am.svc.SetParentGroup(ctx, session, parentGroupID, id) } -func (am *authorizationMiddleware) Identify(ctx context.Context, key string) (string, error) { - return am.svc.Identify(ctx, key) +func (am *authorizationMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + if err := am.authorize(ctx, things.OpRemoveParentGroup, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.ThingType, + Object: id, + }); err != nil { + return errors.Wrap(err, errRemoveParentGroup) + } + + th, err := am.repo.RetrieveByID(ctx, id) + if err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + if th.ParentGroup != "" { + if err := am.extAuthorize(ctx, things.GroupOpSetChildThing, authz.PolicyReq{ + Domain: session.DomainID, + SubjectType: policies.UserType, + Subject: session.DomainUserID, + ObjectType: policies.GroupType, + Object: th.ParentGroup, + }); err != nil { + return errors.Wrap(err, errGroupRemoveChildClients) + } + return am.svc.RemoveParentGroup(ctx, session, id) + } + return nil } -func (am *authorizationMiddleware) Authorize(ctx context.Context, req things.AuthzReq) (string, error) { - return am.svc.Authorize(ctx, req) +func (am *authorizationMiddleware) authorize(ctx context.Context, op svcutil.Operation, req authz.PolicyReq) error { + perm, err := am.opp.GetPermission(op) + if err != nil { + return err + } + + req.Permission = perm.String() + + if err := am.authz.Authorize(ctx, req); err != nil { + return err + } + + return nil } -func (am *authorizationMiddleware) Delete(ctx context.Context, session authn.Session, id string) error { - if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.ThingType, id); err != nil { +func (am *authorizationMiddleware) extAuthorize(ctx context.Context, extOp svcutil.ExternalOperation, req authz.PolicyReq) error { + perm, err := am.extOpp.GetPermission(extOp) + if err != nil { return err } - return am.svc.Delete(ctx, session, id) + req.Permission = perm.String() + + if err := am.authz.Authorize(ctx, req); err != nil { + return err + } + + return nil } -func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error { +func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, userID string) error { if err := am.authz.Authorize(ctx, mgauthz.PolicyReq{ SubjectType: policies.UserType, - Subject: adminID, + Subject: userID, Permission: policies.AdminPermission, ObjectType: policies.PlatformType, Object: policies.MagistralaObject, @@ -182,19 +286,3 @@ func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID } return nil } - -func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error { - req := mgauthz.PolicyReq{ - Domain: domain, - SubjectType: subjType, - SubjectKind: subjKind, - Subject: subj, - Permission: perm, - ObjectType: objType, - Object: obj, - } - if err := am.authz.Authorize(ctx, req); err != nil { - return err - } - return nil -} diff --git a/things/middleware/logging.go b/things/middleware/logging.go index a176159c8e..670d7d7902 100644 --- a/things/middleware/logging.go +++ b/things/middleware/logging.go @@ -10,6 +10,7 @@ import ( "time" "github.com/absmach/magistrala/pkg/authn" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" "github.com/absmach/magistrala/things" ) @@ -18,10 +19,15 @@ var _ things.Service = (*loggingMiddleware)(nil) type loggingMiddleware struct { logger *slog.Logger svc things.Service + rmMW.RoleManagerLoggingMiddleware } func LoggingMiddleware(svc things.Service, logger *slog.Logger) things.Service { - return &loggingMiddleware{logger, svc} + return &loggingMiddleware{ + logger: logger, + svc: svc, + RoleManagerLoggingMiddleware: rmMW.NewRoleManagerLoggingMiddleware("things", svc, logger), + } } func (lm *loggingMiddleware) CreateClients(ctx context.Context, session authn.Session, clients ...things.Client) (cs []things.Client, err error) { @@ -30,7 +36,7 @@ func (lm *loggingMiddleware) CreateClients(ctx context.Context, session authn.Se slog.String("duration", time.Since(begin).String()), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn(fmt.Sprintf("Create %d things failed", len(clients)), args...) return } @@ -49,7 +55,7 @@ func (lm *loggingMiddleware) View(ctx context.Context, session authn.Session, id ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("View thing failed", args...) return } @@ -58,22 +64,6 @@ func (lm *loggingMiddleware) View(ctx context.Context, session authn.Session, id return lm.svc.View(ctx, session, id) } -func (lm *loggingMiddleware) ViewPerms(ctx context.Context, session authn.Session, id string) (p []string, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("thing_id", id), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("View thing permissions failed", args...) - return - } - lm.logger.Info("View thing permissions completed successfully", args...) - }(time.Now()) - return lm.svc.ViewPerms(ctx, session, id) -} - func (lm *loggingMiddleware) ListClients(ctx context.Context, session authn.Session, reqUserID string, pm things.Page) (cp things.ClientsPage, err error) { defer func(begin time.Time) { args := []any{ @@ -86,7 +76,7 @@ func (lm *loggingMiddleware) ListClients(ctx context.Context, session authn.Sess ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("List things failed", args...) return } @@ -106,7 +96,7 @@ func (lm *loggingMiddleware) Update(ctx context.Context, session authn.Session, ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Update thing failed", args...) return } @@ -145,7 +135,7 @@ func (lm *loggingMiddleware) UpdateSecret(ctx context.Context, session authn.Ses ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Update thing secret failed", args...) return } @@ -164,7 +154,7 @@ func (lm *loggingMiddleware) Enable(ctx context.Context, session authn.Session, ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Enable thing failed", args...) return } @@ -183,7 +173,7 @@ func (lm *loggingMiddleware) Disable(ctx context.Context, session authn.Session, ), } if err != nil { - args = append(args, slog.Any("error", err)) + args = append(args, slog.String("error", err.Error())) lm.logger.Warn("Disable thing failed", args...) return } @@ -192,110 +182,51 @@ func (lm *loggingMiddleware) Disable(ctx context.Context, session authn.Session, return lm.svc.Disable(ctx, session, id) } -func (lm *loggingMiddleware) ListClientsByGroup(ctx context.Context, session authn.Session, channelID string, cp things.Page) (mp things.MembersPage, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("channel_id", channelID), - slog.Group("page", - slog.Uint64("offset", cp.Offset), - slog.Uint64("limit", cp.Limit), - slog.Uint64("total", mp.Total), - ), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("List things by group failed", args...) - return - } - lm.logger.Info("List things by group completed successfully", args...) - }(time.Now()) - return lm.svc.ListClientsByGroup(ctx, session, channelID, cp) -} - -func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) { +func (lm *loggingMiddleware) Delete(ctx context.Context, session authn.Session, id string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), slog.String("thing_id", id), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Identify thing failed", args...) - return - } - lm.logger.Info("Identify thing completed successfully", args...) - }(time.Now()) - return lm.svc.Identify(ctx, key) -} - -func (lm *loggingMiddleware) Authorize(ctx context.Context, req things.AuthzReq) (id string, err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("clientID", req.ClientID), - slog.String("clientKey", req.ClientKey), - slog.String("channelID", req.ChannelID), - slog.String("permission", req.Permission), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Authorize failed", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Delete thing failed", args...) return } - lm.logger.Info("Authorize completed successfully", args...) + lm.logger.Info("Delete thing completed successfully", args...) }(time.Now()) - return lm.svc.Authorize(ctx, req) -} - -func (lm *loggingMiddleware) Share(ctx context.Context, session authn.Session, id, relation string, userids ...string) (err error) { - defer func(begin time.Time) { - args := []any{ - slog.String("duration", time.Since(begin).String()), - slog.String("client_id", id), - slog.Any("user_ids", userids), - slog.String("relation", relation), - } - if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Share client failed", args...) - return - } - lm.logger.Info("Share client completed successfully", args...) - }(time.Now()) - return lm.svc.Share(ctx, session, id, relation, userids...) + return lm.svc.Delete(ctx, session, id) } -func (lm *loggingMiddleware) Unshare(ctx context.Context, session authn.Session, id, relation string, userids ...string) (err error) { +func (lm *loggingMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), - slog.String("client_id", id), - slog.Any("user_ids", userids), - slog.String("relation", relation), + slog.String("parent_group_id", parentGroupID), + slog.String("thing_id", id), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Unshare client failed", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Set parent group to thing failed", args...) return } - lm.logger.Info("Unshare client completed successfully", args...) + lm.logger.Info("Set parent group to thing completed successfully", args...) }(time.Now()) - return lm.svc.Unshare(ctx, session, id, relation, userids...) + return lm.svc.SetParentGroup(ctx, session, parentGroupID, id) } -func (lm *loggingMiddleware) Delete(ctx context.Context, session authn.Session, id string) (err error) { +func (lm *loggingMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), slog.String("client_id", id), } if err != nil { - args = append(args, slog.Any("error", err)) - lm.logger.Warn("Delete client failed", args...) + args = append(args, slog.String("error", err.Error())) + lm.logger.Warn("Remove parent group from thing failed", args...) return } - lm.logger.Info("Delete client completed successfully", args...) + lm.logger.Info("Remove parent group from thing completed successfully", args...) }(time.Now()) - return lm.svc.Delete(ctx, session, id) + return lm.svc.RemoveParentGroup(ctx, session, id) } diff --git a/things/middleware/metrics.go b/things/middleware/metrics.go index 6b6ecd2d62..76b5804879 100644 --- a/things/middleware/metrics.go +++ b/things/middleware/metrics.go @@ -8,6 +8,7 @@ import ( "time" "github.com/absmach/magistrala/pkg/authn" + rmMW "github.com/absmach/magistrala/pkg/roles/rolemanager/middleware" "github.com/absmach/magistrala/things" "github.com/go-kit/kit/metrics" ) @@ -18,14 +19,16 @@ type metricsMiddleware struct { counter metrics.Counter latency metrics.Histogram svc things.Service + rmMW.RoleManagerMetricsMiddleware } // MetricsMiddleware returns a new metrics middleware wrapper. func MetricsMiddleware(svc things.Service, counter metrics.Counter, latency metrics.Histogram) things.Service { return &metricsMiddleware{ - counter: counter, - latency: latency, - svc: svc, + counter: counter, + latency: latency, + svc: svc, + RoleManagerMetricsMiddleware: rmMW.NewRoleManagerMetricsMiddleware("things", svc, counter, latency), } } @@ -45,14 +48,6 @@ func (ms *metricsMiddleware) View(ctx context.Context, session authn.Session, id return ms.svc.View(ctx, session, id) } -func (ms *metricsMiddleware) ViewPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - defer func(begin time.Time) { - ms.counter.With("method", "view_client_permissions").Add(1) - ms.latency.With("method", "view_client_permissions").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.ViewPerms(ctx, session, id) -} - func (ms *metricsMiddleware) ListClients(ctx context.Context, session authn.Session, reqUserID string, pm things.Page) (things.ClientsPage, error) { defer func(begin time.Time) { ms.counter.With("method", "list_clients").Add(1) @@ -101,50 +96,26 @@ func (ms *metricsMiddleware) Disable(ctx context.Context, session authn.Session, return ms.svc.Disable(ctx, session, id) } -func (ms *metricsMiddleware) ListClientsByGroup(ctx context.Context, session authn.Session, groupID string, pm things.Page) (mp things.MembersPage, err error) { - defer func(begin time.Time) { - ms.counter.With("method", "list_clients_by_channel").Add(1) - ms.latency.With("method", "list_clients_by_channel").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.ListClientsByGroup(ctx, session, groupID, pm) -} - -func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string, error) { - defer func(begin time.Time) { - ms.counter.With("method", "identify_client").Add(1) - ms.latency.With("method", "identify_client").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.Identify(ctx, key) -} - -func (ms *metricsMiddleware) Authorize(ctx context.Context, req things.AuthzReq) (id string, err error) { - defer func(begin time.Time) { - ms.counter.With("method", "authorize").Add(1) - ms.latency.With("method", "authorize").Observe(time.Since(begin).Seconds()) - }(time.Now()) - return ms.svc.Authorize(ctx, req) -} - -func (ms *metricsMiddleware) Share(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { +func (ms *metricsMiddleware) Delete(ctx context.Context, session authn.Session, id string) error { defer func(begin time.Time) { - ms.counter.With("method", "share").Add(1) - ms.latency.With("method", "share").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "delete_client").Add(1) + ms.latency.With("method", "delete_client").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.Share(ctx, session, id, relation, userids...) + return ms.svc.Delete(ctx, session, id) } -func (ms *metricsMiddleware) Unshare(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { +func (ms *metricsMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (err error) { defer func(begin time.Time) { - ms.counter.With("method", "unshare").Add(1) - ms.latency.With("method", "unshare").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "set_parent_group").Add(1) + ms.latency.With("method", "set_parent_group").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.Unshare(ctx, session, id, relation, userids...) + return ms.svc.SetParentGroup(ctx, session, parentGroupID, id) } -func (ms *metricsMiddleware) Delete(ctx context.Context, session authn.Session, id string) error { +func (ms *metricsMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (err error) { defer func(begin time.Time) { - ms.counter.With("method", "delete_client").Add(1) - ms.latency.With("method", "delete_client").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "remove_parent_group").Add(1) + ms.latency.With("method", "remove_parent_group").Observe(time.Since(begin).Seconds()) }(time.Now()) - return ms.svc.Delete(ctx, session, id) + return ms.svc.RemoveParentGroup(ctx, session, id) } diff --git a/things/mocks/repository.go b/things/mocks/repository.go index 2917461ba0..5fe85c948a 100644 --- a/things/mocks/repository.go +++ b/things/mocks/repository.go @@ -7,8 +7,10 @@ package mocks import ( context "context" - things "github.com/absmach/magistrala/things" + roles "github.com/absmach/magistrala/pkg/roles" mock "github.com/stretchr/testify/mock" + + things "github.com/absmach/magistrala/things" ) // Repository is an autogenerated mock type for the Repository type @@ -16,6 +18,54 @@ type Repository struct { mock.Mock } +// AddConnections provides a mock function with given fields: ctx, conns +func (_m *Repository) AddConnections(ctx context.Context, conns []things.Connection) error { + ret := _m.Called(ctx, conns) + + if len(ret) == 0 { + panic("no return value specified for AddConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []things.Connection) error); ok { + r0 = rf(ctx, conns) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AddRoles provides a mock function with given fields: ctx, rps +func (_m *Repository) AddRoles(ctx context.Context, rps []roles.RoleProvision) ([]roles.Role, error) { + ret := _m.Called(ctx, rps) + + if len(ret) == 0 { + panic("no return value specified for AddRoles") + } + + var r0 []roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) ([]roles.Role, error)); ok { + return rf(ctx, rps) + } + if rf, ok := ret.Get(0).(func(context.Context, []roles.RoleProvision) []roles.Role); ok { + r0 = rf(ctx, rps) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.Role) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []roles.RoleProvision) error); ok { + r1 = rf(ctx, rps) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ChangeStatus provides a mock function with given fields: ctx, client func (_m *Repository) ChangeStatus(ctx context.Context, client things.Client) (things.Client, error) { ret := _m.Called(ctx, client) @@ -44,17 +94,160 @@ func (_m *Repository) ChangeStatus(ctx context.Context, client things.Client) (t return r0, r1 } -// Delete provides a mock function with given fields: ctx, id -func (_m *Repository) Delete(ctx context.Context, id string) error { - ret := _m.Called(ctx, id) +// Delete provides a mock function with given fields: ctx, clientIDs +func (_m *Repository) Delete(ctx context.Context, clientIDs ...string) error { + _va := make([]interface{}, len(clientIDs)) + for _i := range clientIDs { + _va[_i] = clientIDs[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) if len(ret) == 0 { panic("no return value specified for Delete") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, ...string) error); ok { + r0 = rf(ctx, clientIDs...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DoesThingHaveConnections provides a mock function with given fields: ctx, id +func (_m *Repository) DoesThingHaveConnections(ctx context.Context, id string) (bool, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DoesThingHaveConnections") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveChannelConnections provides a mock function with given fields: ctx, channelID +func (_m *Repository) RemoveChannelConnections(ctx context.Context, channelID string) error { + ret := _m.Called(ctx, channelID) + + if len(ret) == 0 { + panic("no return value specified for RemoveChannelConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, channelID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveConnections provides a mock function with given fields: ctx, conns +func (_m *Repository) RemoveConnections(ctx context.Context, conns []things.Connection) error { + ret := _m.Called(ctx, conns) + + if len(ret) == 0 { + panic("no return value specified for RemoveConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []things.Connection) error); ok { + r0 = rf(ctx, conns) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, members +func (_m *Repository) RemoveMemberFromAllRoles(ctx context.Context, members string) error { + ret := _m.Called(ctx, members) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveParentGroup provides a mock function with given fields: ctx, th +func (_m *Repository) RemoveParentGroup(ctx context.Context, th things.Client) error { + ret := _m.Called(ctx, th) + + if len(ret) == 0 { + panic("no return value specified for RemoveParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, things.Client) error); ok { + r0 = rf(ctx, th) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRoles provides a mock function with given fields: ctx, roleIDs +func (_m *Repository) RemoveRoles(ctx context.Context, roleIDs []string) error { + ret := _m.Called(ctx, roleIDs) + + if len(ret) == 0 { + panic("no return value specified for RemoveRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []string) error); ok { + r0 = rf(ctx, roleIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveThingConnections provides a mock function with given fields: ctx, thingID +func (_m *Repository) RemoveThingConnections(ctx context.Context, thingID string) error { + ret := _m.Called(ctx, thingID) + + if len(ret) == 0 { + panic("no return value specified for RemoveThingConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, thingID) } else { r0 = ret.Error(0) } @@ -118,6 +311,34 @@ func (_m *Repository) RetrieveAllByIDs(ctx context.Context, pm things.Page) (thi return r0, r1 } +// RetrieveAllRoles provides a mock function with given fields: ctx, entityID, limit, offset +func (_m *Repository) RetrieveAllRoles(ctx context.Context, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, entityID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RetrieveAllRoles") + } + + var r0 roles.RolePage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, entityID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, entityID, limit, offset) + } else { + r0 = ret.Get(0).(roles.RolePage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, entityID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RetrieveByID provides a mock function with given fields: ctx, id func (_m *Repository) RetrieveByID(ctx context.Context, id string) (things.Client, error) { ret := _m.Called(ctx, id) @@ -146,6 +367,34 @@ func (_m *Repository) RetrieveByID(ctx context.Context, id string) (things.Clien return r0, r1 } +// RetrieveByIds provides a mock function with given fields: ctx, ids +func (_m *Repository) RetrieveByIds(ctx context.Context, ids []string) (things.ClientsPage, error) { + ret := _m.Called(ctx, ids) + + if len(ret) == 0 { + panic("no return value specified for RetrieveByIds") + } + + var r0 things.ClientsPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) (things.ClientsPage, error)); ok { + return rf(ctx, ids) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) things.ClientsPage); ok { + r0 = rf(ctx, ids) + } else { + r0 = ret.Get(0).(things.ClientsPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RetrieveBySecret provides a mock function with given fields: ctx, key func (_m *Repository) RetrieveBySecret(ctx context.Context, key string) (things.Client, error) { ret := _m.Called(ctx, key) @@ -174,6 +423,377 @@ func (_m *Repository) RetrieveBySecret(ctx context.Context, key string) (things. return r0, r1 } +// RetrieveEntitiesRolesActionsMembers provides a mock function with given fields: ctx, entityIDs +func (_m *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) { + ret := _m.Called(ctx, entityIDs) + + if len(ret) == 0 { + panic("no return value specified for RetrieveEntitiesRolesActionsMembers") + } + + var r0 []roles.EntityActionRole + var r1 []roles.EntityMemberRole + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error)); ok { + return rf(ctx, entityIDs) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) []roles.EntityActionRole); ok { + r0 = rf(ctx, entityIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]roles.EntityActionRole) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) []roles.EntityMemberRole); ok { + r1 = rf(ctx, entityIDs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]roles.EntityMemberRole) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, []string) error); ok { + r2 = rf(ctx, entityIDs) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// RetrieveParentGroupThings provides a mock function with given fields: ctx, parentGroupID +func (_m *Repository) RetrieveParentGroupThings(ctx context.Context, parentGroupID string) ([]things.Client, error) { + ret := _m.Called(ctx, parentGroupID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveParentGroupThings") + } + + var r0 []things.Client + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]things.Client, error)); ok { + return rf(ctx, parentGroupID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []things.Client); ok { + r0 = rf(ctx, parentGroupID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]things.Client) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, parentGroupID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRole provides a mock function with given fields: ctx, roleID +func (_m *Repository) RetrieveRole(ctx context.Context, roleID string) (roles.Role, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (roles.Role, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) roles.Role); ok { + r0 = rf(ctx, roleID) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveRoleByEntityIDAndName provides a mock function with given fields: ctx, entityID, roleName +func (_m *Repository) RetrieveRoleByEntityIDAndName(ctx context.Context, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRoleByEntityIDAndName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (roles.Role, error)); ok { + return rf(ctx, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) roles.Role); ok { + r0 = rf(ctx, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleAddActions(ctx context.Context, role roles.Role, actions []string) ([]string, error) { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleAddMembers(ctx context.Context, role roles.Role, members []string) ([]string, error) { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) ([]string, error)); ok { + return rf(ctx, role, members) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) []string); ok { + r0 = rf(ctx, role, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role, []string) error); ok { + r1 = rf(ctx, role, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, roleID, actions +func (_m *Repository) RoleCheckActionsExists(ctx context.Context, roleID string, actions []string) (bool, error) { + ret := _m.Called(ctx, roleID, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, roleID, members +func (_m *Repository) RoleCheckMembersExists(ctx context.Context, roleID string, members []string) (bool, error) { + ret := _m.Called(ctx, roleID, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) (bool, error)); ok { + return rf(ctx, roleID, members) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string) bool); ok { + r0 = rf(ctx, roleID, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, roleID, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, roleID +func (_m *Repository) RoleListActions(ctx context.Context, roleID string) ([]string, error) { + ret := _m.Called(ctx, roleID) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, roleID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, roleID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, roleID, limit, offset +func (_m *Repository) RoleListMembers(ctx context.Context, roleID string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, roleID, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, roleID, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, roleID, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = rf(ctx, roleID, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, role, actions +func (_m *Repository) RoleRemoveActions(ctx context.Context, role roles.Role, actions []string) error { + ret := _m.Called(ctx, role, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, actions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllActions provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllActions(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, role +func (_m *Repository) RoleRemoveAllMembers(ctx context.Context, role roles.Role) error { + ret := _m.Called(ctx, role) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, role, members +func (_m *Repository) RoleRemoveMembers(ctx context.Context, role roles.Role, members []string) error { + ret := _m.Called(ctx, role, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role, []string) error); ok { + r0 = rf(ctx, role, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Save provides a mock function with given fields: ctx, client func (_m *Repository) Save(ctx context.Context, client ...things.Client) ([]things.Client, error) { _va := make([]interface{}, len(client)) @@ -239,6 +859,70 @@ func (_m *Repository) SearchClients(ctx context.Context, pm things.Page) (things return r0, r1 } +// SetParentGroup provides a mock function with given fields: ctx, th +func (_m *Repository) SetParentGroup(ctx context.Context, th things.Client) error { + ret := _m.Called(ctx, th) + + if len(ret) == 0 { + panic("no return value specified for SetParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, things.Client) error); ok { + r0 = rf(ctx, th) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ThingConnectionsCount provides a mock function with given fields: ctx, id +func (_m *Repository) ThingConnectionsCount(ctx context.Context, id string) (uint64, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for ThingConnectionsCount") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (uint64, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) uint64); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnsetParentGroupFromThings provides a mock function with given fields: ctx, parentGroupID +func (_m *Repository) UnsetParentGroupFromThings(ctx context.Context, parentGroupID string) error { + ret := _m.Called(ctx, parentGroupID) + + if len(ret) == 0 { + panic("no return value specified for UnsetParentGroupFromThings") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, parentGroupID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Update provides a mock function with given fields: ctx, client func (_m *Repository) Update(ctx context.Context, client things.Client) (things.Client, error) { ret := _m.Called(ctx, client) @@ -295,6 +979,34 @@ func (_m *Repository) UpdateIdentity(ctx context.Context, client things.Client) return r0, r1 } +// UpdateRole provides a mock function with given fields: ctx, ro +func (_m *Repository) UpdateRole(ctx context.Context, ro roles.Role) (roles.Role, error) { + ret := _m.Called(ctx, ro) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) (roles.Role, error)); ok { + return rf(ctx, ro) + } + if rf, ok := ret.Get(0).(func(context.Context, roles.Role) roles.Role); ok { + r0 = rf(ctx, ro) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, roles.Role) error); ok { + r1 = rf(ctx, ro) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateSecret provides a mock function with given fields: ctx, client func (_m *Repository) UpdateSecret(ctx context.Context, client things.Client) (things.Client, error) { ret := _m.Called(ctx, client) diff --git a/things/mocks/service.go b/things/mocks/service.go index 9719334dca..e7a5e2cf64 100644 --- a/things/mocks/service.go +++ b/things/mocks/service.go @@ -11,6 +11,8 @@ import ( mock "github.com/stretchr/testify/mock" + roles "github.com/absmach/magistrala/pkg/roles" + things "github.com/absmach/magistrala/things" ) @@ -19,27 +21,27 @@ type Service struct { mock.Mock } -// Authorize provides a mock function with given fields: ctx, req -func (_m *Service) Authorize(ctx context.Context, req things.AuthzReq) (string, error) { - ret := _m.Called(ctx, req) +// AddRole provides a mock function with given fields: ctx, session, entityID, roleName, optionalActions, optionalMembers +func (_m *Service) AddRole(ctx context.Context, session authn.Session, entityID string, roleName string, optionalActions []string, optionalMembers []string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName, optionalActions, optionalMembers) if len(ret) == 0 { - panic("no return value specified for Authorize") + panic("no return value specified for AddRole") } - var r0 string + var r0 roles.Role var r1 error - if rf, ok := ret.Get(0).(func(context.Context, things.AuthzReq) (string, error)); ok { - return rf(ctx, req) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) } - if rf, ok := ret.Get(0).(func(context.Context, things.AuthzReq) string); ok { - r0 = rf(ctx, req) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string, []string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) } else { - r0 = ret.Get(0).(string) + r0 = ret.Get(0).(roles.Role) } - if rf, ok := ret.Get(1).(func(context.Context, things.AuthzReq) error); ok { - r1 = rf(ctx, req) + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, optionalActions, optionalMembers) } else { r1 = ret.Error(1) } @@ -158,27 +160,29 @@ func (_m *Service) Enable(ctx context.Context, session authn.Session, id string) return r0, r1 } -// Identify provides a mock function with given fields: ctx, key -func (_m *Service) Identify(ctx context.Context, key string) (string, error) { - ret := _m.Called(ctx, key) +// ListAvailableActions provides a mock function with given fields: ctx, session +func (_m *Service) ListAvailableActions(ctx context.Context, session authn.Session) ([]string, error) { + ret := _m.Called(ctx, session) if len(ret) == 0 { - panic("no return value specified for Identify") + panic("no return value specified for ListAvailableActions") } - var r0 string + var r0 []string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { - return rf(ctx, key) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) ([]string, error)); ok { + return rf(ctx, session) } - if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { - r0 = rf(ctx, key) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session) []string); ok { + r0 = rf(ctx, session) } else { - r0 = ret.Get(0).(string) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } } - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, key) + if rf, ok := ret.Get(1).(func(context.Context, authn.Session) error); ok { + r1 = rf(ctx, session) } else { r1 = ret.Error(1) } @@ -214,27 +218,81 @@ func (_m *Service) ListClients(ctx context.Context, session authn.Session, reqUs return r0, r1 } -// ListClientsByGroup provides a mock function with given fields: ctx, session, groupID, pm -func (_m *Service) ListClientsByGroup(ctx context.Context, session authn.Session, groupID string, pm things.Page) (things.MembersPage, error) { - ret := _m.Called(ctx, session, groupID, pm) +// RemoveMemberFromAllRoles provides a mock function with given fields: ctx, session, memberID +func (_m *Service) RemoveMemberFromAllRoles(ctx context.Context, session authn.Session, memberID string) error { + ret := _m.Called(ctx, session, memberID) + + if len(ret) == 0 { + panic("no return value specified for RemoveMemberFromAllRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, memberID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveParentGroup provides a mock function with given fields: ctx, session, id +func (_m *Service) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + ret := _m.Called(ctx, session, id) + + if len(ret) == 0 { + panic("no return value specified for RemoveParentGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok { + r0 = rf(ctx, session, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RemoveRole(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RemoveRole") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveAllRoles provides a mock function with given fields: ctx, session, entityID, limit, offset +func (_m *Service) RetrieveAllRoles(ctx context.Context, session authn.Session, entityID string, limit uint64, offset uint64) (roles.RolePage, error) { + ret := _m.Called(ctx, session, entityID, limit, offset) if len(ret) == 0 { - panic("no return value specified for ListClientsByGroup") + panic("no return value specified for RetrieveAllRoles") } - var r0 things.MembersPage + var r0 roles.RolePage var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, things.Page) (things.MembersPage, error)); ok { - return rf(ctx, session, groupID, pm) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) (roles.RolePage, error)); ok { + return rf(ctx, session, entityID, limit, offset) } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, things.Page) things.MembersPage); ok { - r0 = rf(ctx, session, groupID, pm) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, uint64, uint64) roles.RolePage); ok { + r0 = rf(ctx, session, entityID, limit, offset) } else { - r0 = ret.Get(0).(things.MembersPage) + r0 = ret.Get(0).(roles.RolePage) } - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, things.Page) error); ok { - r1 = rf(ctx, session, groupID, pm) + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, limit, offset) } else { r1 = ret.Error(1) } @@ -242,24 +300,219 @@ func (_m *Service) ListClientsByGroup(ctx context.Context, session authn.Session return r0, r1 } -// Share provides a mock function with given fields: ctx, session, id, relation, userids -func (_m *Service) Share(ctx context.Context, session authn.Session, id string, relation string, userids ...string) error { - _va := make([]interface{}, len(userids)) - for _i := range userids { - _va[_i] = userids[_i] +// RetrieveRole provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RetrieveRole(ctx context.Context, session authn.Session, entityID string, roleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRole") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, roleName) } - var _ca []interface{} - _ca = append(_ca, ctx, session, id, relation) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleAddActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleAddActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleAddMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleAddMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) if len(ret) == 0 { - panic("no return value specified for Share") + panic("no return value specified for RoleAddMembers") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) []string); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckActionsExists provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleCheckActionsExists(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckActionsExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, actions) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, actions) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, actions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleCheckMembersExists provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleCheckMembersExists(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) (bool, error) { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleCheckMembersExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) (bool, error)); ok { + return rf(ctx, session, entityID, roleName, members) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) bool); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, []string) error); ok { + r1 = rf(ctx, session, entityID, roleName, members) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleListActions(ctx context.Context, session authn.Session, entityID string, roleName string) ([]string, error) { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleListActions") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) ([]string, error)); ok { + return rf(ctx, session, entityID, roleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) []string); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string) error); ok { + r1 = rf(ctx, session, entityID, roleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleListMembers provides a mock function with given fields: ctx, session, entityID, roleName, limit, offset +func (_m *Service) RoleListMembers(ctx context.Context, session authn.Session, entityID string, roleName string, limit uint64, offset uint64) (roles.MembersPage, error) { + ret := _m.Called(ctx, session, entityID, roleName, limit, offset) + + if len(ret) == 0 { + panic("no return value specified for RoleListMembers") + } + + var r0 roles.MembersPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) (roles.MembersPage, error)); ok { + return rf(ctx, session, entityID, roleName, limit, offset) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, uint64, uint64) roles.MembersPage); ok { + r0 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r0 = ret.Get(0).(roles.MembersPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, uint64, uint64) error); ok { + r1 = rf(ctx, session, entityID, roleName, limit, offset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RoleRemoveActions provides a mock function with given fields: ctx, session, entityID, roleName, actions +func (_m *Service) RoleRemoveActions(ctx context.Context, session authn.Session, entityID string, roleName string, actions []string) error { + ret := _m.Called(ctx, session, entityID, roleName, actions) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveActions") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, ...string) error); ok { - r0 = rf(ctx, session, id, relation, userids...) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, actions) } else { r0 = ret.Error(0) } @@ -267,24 +520,71 @@ func (_m *Service) Share(ctx context.Context, session authn.Session, id string, return r0 } -// Unshare provides a mock function with given fields: ctx, session, id, relation, userids -func (_m *Service) Unshare(ctx context.Context, session authn.Session, id string, relation string, userids ...string) error { - _va := make([]interface{}, len(userids)) - for _i := range userids { - _va[_i] = userids[_i] +// RoleRemoveAllActions provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllActions(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllActions") } - var _ca []interface{} - _ca = append(_ca, ctx, session, id, relation) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveAllMembers provides a mock function with given fields: ctx, session, entityID, roleName +func (_m *Service) RoleRemoveAllMembers(ctx context.Context, session authn.Session, entityID string, roleName string) error { + ret := _m.Called(ctx, session, entityID, roleName) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveAllMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, entityID, roleName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RoleRemoveMembers provides a mock function with given fields: ctx, session, entityID, roleName, members +func (_m *Service) RoleRemoveMembers(ctx context.Context, session authn.Session, entityID string, roleName string, members []string) error { + ret := _m.Called(ctx, session, entityID, roleName, members) + + if len(ret) == 0 { + panic("no return value specified for RoleRemoveMembers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok { + r0 = rf(ctx, session, entityID, roleName, members) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetParentGroup provides a mock function with given fields: ctx, session, parentGroupID, id +func (_m *Service) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error { + ret := _m.Called(ctx, session, parentGroupID, id) if len(ret) == 0 { - panic("no return value specified for Unshare") + panic("no return value specified for SetParentGroup") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, ...string) error); ok { - r0 = rf(ctx, session, id, relation, userids...) + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string) error); ok { + r0 = rf(ctx, session, parentGroupID, id) } else { r0 = ret.Error(0) } @@ -320,6 +620,34 @@ func (_m *Service) Update(ctx context.Context, session authn.Session, client thi return r0, r1 } +// UpdateRoleName provides a mock function with given fields: ctx, session, entityID, oldRoleName, newRoleName +func (_m *Service) UpdateRoleName(ctx context.Context, session authn.Session, entityID string, oldRoleName string, newRoleName string) (roles.Role, error) { + ret := _m.Called(ctx, session, entityID, oldRoleName, newRoleName) + + if len(ret) == 0 { + panic("no return value specified for UpdateRoleName") + } + + var r0 roles.Role + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) (roles.Role, error)); ok { + return rf(ctx, session, entityID, oldRoleName, newRoleName) + } + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string) roles.Role); ok { + r0 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r0 = ret.Get(0).(roles.Role) + } + + if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string) error); ok { + r1 = rf(ctx, session, entityID, oldRoleName, newRoleName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateSecret provides a mock function with given fields: ctx, session, id, key func (_m *Service) UpdateSecret(ctx context.Context, session authn.Session, id string, key string) (things.Client, error) { ret := _m.Called(ctx, session, id, key) @@ -404,36 +732,6 @@ func (_m *Service) View(ctx context.Context, session authn.Session, id string) ( return r0, r1 } -// ViewPerms provides a mock function with given fields: ctx, session, id -func (_m *Service) ViewPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - ret := _m.Called(ctx, session, id) - - if len(ret) == 0 { - panic("no return value specified for ViewPerms") - } - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) ([]string, error)); ok { - return rf(ctx, session, id) - } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) []string); ok { - r0 = rf(ctx, session, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok { - r1 = rf(ctx, session, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // NewService creates a new instance of Service. 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 NewService(t interface { diff --git a/things/mocks/things_client.go b/things/mocks/things_client.go index 136280a869..83c368863c 100644 --- a/things/mocks/things_client.go +++ b/things/mocks/things_client.go @@ -11,9 +11,11 @@ import ( grpc "google.golang.org/grpc" - magistrala "github.com/absmach/magistrala" - mock "github.com/stretchr/testify/mock" + + thingsv1 "github.com/absmach/magistrala/internal/grpc/things/v1" + + v1 "github.com/absmach/magistrala/internal/grpc/common/v1" ) // ThingsServiceClient is an autogenerated mock type for the ThingsServiceClient type @@ -29,8 +31,452 @@ func (_m *ThingsServiceClient) EXPECT() *ThingsServiceClient_Expecter { return &ThingsServiceClient_Expecter{mock: &_m.Mock} } -// Authorize provides a mock function with given fields: ctx, in, opts -func (_m *ThingsServiceClient) Authorize(ctx context.Context, in *magistrala.ThingsAuthzReq, opts ...grpc.CallOption) (*magistrala.ThingsAuthzRes, error) { +// AddConnections provides a mock function with given fields: ctx, in, opts +func (_m *ThingsServiceClient) AddConnections(ctx context.Context, in *v1.AddConnectionsReq, opts ...grpc.CallOption) (*v1.AddConnectionsRes, 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 AddConnections") + } + + var r0 *v1.AddConnectionsRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.AddConnectionsReq, ...grpc.CallOption) (*v1.AddConnectionsRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.AddConnectionsReq, ...grpc.CallOption) *v1.AddConnectionsRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.AddConnectionsRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.AddConnectionsReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingsServiceClient_AddConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddConnections' +type ThingsServiceClient_AddConnections_Call struct { + *mock.Call +} + +// AddConnections is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.AddConnectionsReq +// - opts ...grpc.CallOption +func (_e *ThingsServiceClient_Expecter) AddConnections(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_AddConnections_Call { + return &ThingsServiceClient_AddConnections_Call{Call: _e.mock.On("AddConnections", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ThingsServiceClient_AddConnections_Call) Run(run func(ctx context.Context, in *v1.AddConnectionsReq, opts ...grpc.CallOption)) *ThingsServiceClient_AddConnections_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].(*v1.AddConnectionsReq), variadicArgs...) + }) + return _c +} + +func (_c *ThingsServiceClient_AddConnections_Call) Return(_a0 *v1.AddConnectionsRes, _a1 error) *ThingsServiceClient_AddConnections_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ThingsServiceClient_AddConnections_Call) RunAndReturn(run func(context.Context, *v1.AddConnectionsReq, ...grpc.CallOption) (*v1.AddConnectionsRes, error)) *ThingsServiceClient_AddConnections_Call { + _c.Call.Return(run) + return _c +} + +// Authenticate provides a mock function with given fields: ctx, in, opts +func (_m *ThingsServiceClient) Authenticate(ctx context.Context, in *thingsv1.AuthnReq, opts ...grpc.CallOption) (*thingsv1.AuthnRes, 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 Authenticate") + } + + var r0 *thingsv1.AuthnRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *thingsv1.AuthnReq, ...grpc.CallOption) (*thingsv1.AuthnRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *thingsv1.AuthnReq, ...grpc.CallOption) *thingsv1.AuthnRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*thingsv1.AuthnRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *thingsv1.AuthnReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingsServiceClient_Authenticate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authenticate' +type ThingsServiceClient_Authenticate_Call struct { + *mock.Call +} + +// Authenticate is a helper method to define mock.On call +// - ctx context.Context +// - in *thingsv1.AuthnReq +// - opts ...grpc.CallOption +func (_e *ThingsServiceClient_Expecter) Authenticate(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_Authenticate_Call { + return &ThingsServiceClient_Authenticate_Call{Call: _e.mock.On("Authenticate", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ThingsServiceClient_Authenticate_Call) Run(run func(ctx context.Context, in *thingsv1.AuthnReq, opts ...grpc.CallOption)) *ThingsServiceClient_Authenticate_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].(*thingsv1.AuthnReq), variadicArgs...) + }) + return _c +} + +func (_c *ThingsServiceClient_Authenticate_Call) Return(_a0 *thingsv1.AuthnRes, _a1 error) *ThingsServiceClient_Authenticate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ThingsServiceClient_Authenticate_Call) RunAndReturn(run func(context.Context, *thingsv1.AuthnReq, ...grpc.CallOption) (*thingsv1.AuthnRes, error)) *ThingsServiceClient_Authenticate_Call { + _c.Call.Return(run) + return _c +} + +// RemoveChannelConnections provides a mock function with given fields: ctx, in, opts +func (_m *ThingsServiceClient) RemoveChannelConnections(ctx context.Context, in *thingsv1.RemoveChannelConnectionsReq, opts ...grpc.CallOption) (*thingsv1.RemoveChannelConnectionsRes, 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 RemoveChannelConnections") + } + + var r0 *thingsv1.RemoveChannelConnectionsRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *thingsv1.RemoveChannelConnectionsReq, ...grpc.CallOption) (*thingsv1.RemoveChannelConnectionsRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *thingsv1.RemoveChannelConnectionsReq, ...grpc.CallOption) *thingsv1.RemoveChannelConnectionsRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*thingsv1.RemoveChannelConnectionsRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *thingsv1.RemoveChannelConnectionsReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingsServiceClient_RemoveChannelConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveChannelConnections' +type ThingsServiceClient_RemoveChannelConnections_Call struct { + *mock.Call +} + +// RemoveChannelConnections is a helper method to define mock.On call +// - ctx context.Context +// - in *thingsv1.RemoveChannelConnectionsReq +// - opts ...grpc.CallOption +func (_e *ThingsServiceClient_Expecter) RemoveChannelConnections(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_RemoveChannelConnections_Call { + return &ThingsServiceClient_RemoveChannelConnections_Call{Call: _e.mock.On("RemoveChannelConnections", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ThingsServiceClient_RemoveChannelConnections_Call) Run(run func(ctx context.Context, in *thingsv1.RemoveChannelConnectionsReq, opts ...grpc.CallOption)) *ThingsServiceClient_RemoveChannelConnections_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].(*thingsv1.RemoveChannelConnectionsReq), variadicArgs...) + }) + return _c +} + +func (_c *ThingsServiceClient_RemoveChannelConnections_Call) Return(_a0 *thingsv1.RemoveChannelConnectionsRes, _a1 error) *ThingsServiceClient_RemoveChannelConnections_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ThingsServiceClient_RemoveChannelConnections_Call) RunAndReturn(run func(context.Context, *thingsv1.RemoveChannelConnectionsReq, ...grpc.CallOption) (*thingsv1.RemoveChannelConnectionsRes, error)) *ThingsServiceClient_RemoveChannelConnections_Call { + _c.Call.Return(run) + return _c +} + +// RemoveConnections provides a mock function with given fields: ctx, in, opts +func (_m *ThingsServiceClient) RemoveConnections(ctx context.Context, in *v1.RemoveConnectionsReq, opts ...grpc.CallOption) (*v1.RemoveConnectionsRes, 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 RemoveConnections") + } + + var r0 *v1.RemoveConnectionsRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RemoveConnectionsReq, ...grpc.CallOption) (*v1.RemoveConnectionsRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.RemoveConnectionsReq, ...grpc.CallOption) *v1.RemoveConnectionsRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RemoveConnectionsRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.RemoveConnectionsReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingsServiceClient_RemoveConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveConnections' +type ThingsServiceClient_RemoveConnections_Call struct { + *mock.Call +} + +// RemoveConnections is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.RemoveConnectionsReq +// - opts ...grpc.CallOption +func (_e *ThingsServiceClient_Expecter) RemoveConnections(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_RemoveConnections_Call { + return &ThingsServiceClient_RemoveConnections_Call{Call: _e.mock.On("RemoveConnections", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ThingsServiceClient_RemoveConnections_Call) Run(run func(ctx context.Context, in *v1.RemoveConnectionsReq, opts ...grpc.CallOption)) *ThingsServiceClient_RemoveConnections_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].(*v1.RemoveConnectionsReq), variadicArgs...) + }) + return _c +} + +func (_c *ThingsServiceClient_RemoveConnections_Call) Return(_a0 *v1.RemoveConnectionsRes, _a1 error) *ThingsServiceClient_RemoveConnections_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ThingsServiceClient_RemoveConnections_Call) RunAndReturn(run func(context.Context, *v1.RemoveConnectionsReq, ...grpc.CallOption) (*v1.RemoveConnectionsRes, error)) *ThingsServiceClient_RemoveConnections_Call { + _c.Call.Return(run) + return _c +} + +// RetrieveEntities provides a mock function with given fields: ctx, in, opts +func (_m *ThingsServiceClient) RetrieveEntities(ctx context.Context, in *v1.RetrieveEntitiesReq, opts ...grpc.CallOption) (*v1.RetrieveEntitiesRes, 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 RetrieveEntities") + } + + var r0 *v1.RetrieveEntitiesRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RetrieveEntitiesReq, ...grpc.CallOption) (*v1.RetrieveEntitiesRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.RetrieveEntitiesReq, ...grpc.CallOption) *v1.RetrieveEntitiesRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RetrieveEntitiesRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.RetrieveEntitiesReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingsServiceClient_RetrieveEntities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveEntities' +type ThingsServiceClient_RetrieveEntities_Call struct { + *mock.Call +} + +// RetrieveEntities is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.RetrieveEntitiesReq +// - opts ...grpc.CallOption +func (_e *ThingsServiceClient_Expecter) RetrieveEntities(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_RetrieveEntities_Call { + return &ThingsServiceClient_RetrieveEntities_Call{Call: _e.mock.On("RetrieveEntities", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ThingsServiceClient_RetrieveEntities_Call) Run(run func(ctx context.Context, in *v1.RetrieveEntitiesReq, opts ...grpc.CallOption)) *ThingsServiceClient_RetrieveEntities_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].(*v1.RetrieveEntitiesReq), variadicArgs...) + }) + return _c +} + +func (_c *ThingsServiceClient_RetrieveEntities_Call) Return(_a0 *v1.RetrieveEntitiesRes, _a1 error) *ThingsServiceClient_RetrieveEntities_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ThingsServiceClient_RetrieveEntities_Call) RunAndReturn(run func(context.Context, *v1.RetrieveEntitiesReq, ...grpc.CallOption) (*v1.RetrieveEntitiesRes, error)) *ThingsServiceClient_RetrieveEntities_Call { + _c.Call.Return(run) + return _c +} + +// RetrieveEntity provides a mock function with given fields: ctx, in, opts +func (_m *ThingsServiceClient) RetrieveEntity(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption) (*v1.RetrieveEntityRes, 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 RetrieveEntity") + } + + var r0 *v1.RetrieveEntityRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) (*v1.RetrieveEntityRes, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) *v1.RetrieveEntityRes); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.RetrieveEntityRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingsServiceClient_RetrieveEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveEntity' +type ThingsServiceClient_RetrieveEntity_Call struct { + *mock.Call +} + +// RetrieveEntity is a helper method to define mock.On call +// - ctx context.Context +// - in *v1.RetrieveEntityReq +// - opts ...grpc.CallOption +func (_e *ThingsServiceClient_Expecter) RetrieveEntity(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_RetrieveEntity_Call { + return &ThingsServiceClient_RetrieveEntity_Call{Call: _e.mock.On("RetrieveEntity", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *ThingsServiceClient_RetrieveEntity_Call) Run(run func(ctx context.Context, in *v1.RetrieveEntityReq, opts ...grpc.CallOption)) *ThingsServiceClient_RetrieveEntity_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].(*v1.RetrieveEntityReq), variadicArgs...) + }) + return _c +} + +func (_c *ThingsServiceClient_RetrieveEntity_Call) Return(_a0 *v1.RetrieveEntityRes, _a1 error) *ThingsServiceClient_RetrieveEntity_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ThingsServiceClient_RetrieveEntity_Call) RunAndReturn(run func(context.Context, *v1.RetrieveEntityReq, ...grpc.CallOption) (*v1.RetrieveEntityRes, error)) *ThingsServiceClient_RetrieveEntity_Call { + _c.Call.Return(run) + return _c +} + +// UnsetParentGroupFromThings provides a mock function with given fields: ctx, in, opts +func (_m *ThingsServiceClient) UnsetParentGroupFromThings(ctx context.Context, in *thingsv1.UnsetParentGroupFromThingsReq, opts ...grpc.CallOption) (*thingsv1.UnsetParentGroupFromThingsRes, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] @@ -41,23 +487,23 @@ func (_m *ThingsServiceClient) Authorize(ctx context.Context, in *magistrala.Thi ret := _m.Called(_ca...) if len(ret) == 0 { - panic("no return value specified for Authorize") + panic("no return value specified for UnsetParentGroupFromThings") } - var r0 *magistrala.ThingsAuthzRes + var r0 *thingsv1.UnsetParentGroupFromThingsRes var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.ThingsAuthzReq, ...grpc.CallOption) (*magistrala.ThingsAuthzRes, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *thingsv1.UnsetParentGroupFromThingsReq, ...grpc.CallOption) (*thingsv1.UnsetParentGroupFromThingsRes, error)); ok { return rf(ctx, in, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, *magistrala.ThingsAuthzReq, ...grpc.CallOption) *magistrala.ThingsAuthzRes); ok { + if rf, ok := ret.Get(0).(func(context.Context, *thingsv1.UnsetParentGroupFromThingsReq, ...grpc.CallOption) *thingsv1.UnsetParentGroupFromThingsRes); ok { r0 = rf(ctx, in, opts...) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*magistrala.ThingsAuthzRes) + r0 = ret.Get(0).(*thingsv1.UnsetParentGroupFromThingsRes) } } - if rf, ok := ret.Get(1).(func(context.Context, *magistrala.ThingsAuthzReq, ...grpc.CallOption) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *thingsv1.UnsetParentGroupFromThingsReq, ...grpc.CallOption) error); ok { r1 = rf(ctx, in, opts...) } else { r1 = ret.Error(1) @@ -66,21 +512,21 @@ func (_m *ThingsServiceClient) Authorize(ctx context.Context, in *magistrala.Thi return r0, r1 } -// ThingsServiceClient_Authorize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authorize' -type ThingsServiceClient_Authorize_Call struct { +// ThingsServiceClient_UnsetParentGroupFromThings_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UnsetParentGroupFromThings' +type ThingsServiceClient_UnsetParentGroupFromThings_Call struct { *mock.Call } -// Authorize is a helper method to define mock.On call +// UnsetParentGroupFromThings is a helper method to define mock.On call // - ctx context.Context -// - in *magistrala.ThingsAuthzReq +// - in *thingsv1.UnsetParentGroupFromThingsReq // - opts ...grpc.CallOption -func (_e *ThingsServiceClient_Expecter) Authorize(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_Authorize_Call { - return &ThingsServiceClient_Authorize_Call{Call: _e.mock.On("Authorize", +func (_e *ThingsServiceClient_Expecter) UnsetParentGroupFromThings(ctx interface{}, in interface{}, opts ...interface{}) *ThingsServiceClient_UnsetParentGroupFromThings_Call { + return &ThingsServiceClient_UnsetParentGroupFromThings_Call{Call: _e.mock.On("UnsetParentGroupFromThings", append([]interface{}{ctx, in}, opts...)...)} } -func (_c *ThingsServiceClient_Authorize_Call) Run(run func(ctx context.Context, in *magistrala.ThingsAuthzReq, opts ...grpc.CallOption)) *ThingsServiceClient_Authorize_Call { +func (_c *ThingsServiceClient_UnsetParentGroupFromThings_Call) Run(run func(ctx context.Context, in *thingsv1.UnsetParentGroupFromThingsReq, opts ...grpc.CallOption)) *ThingsServiceClient_UnsetParentGroupFromThings_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]grpc.CallOption, len(args)-2) for i, a := range args[2:] { @@ -88,17 +534,17 @@ func (_c *ThingsServiceClient_Authorize_Call) Run(run func(ctx context.Context, variadicArgs[i] = a.(grpc.CallOption) } } - run(args[0].(context.Context), args[1].(*magistrala.ThingsAuthzReq), variadicArgs...) + run(args[0].(context.Context), args[1].(*thingsv1.UnsetParentGroupFromThingsReq), variadicArgs...) }) return _c } -func (_c *ThingsServiceClient_Authorize_Call) Return(_a0 *magistrala.ThingsAuthzRes, _a1 error) *ThingsServiceClient_Authorize_Call { +func (_c *ThingsServiceClient_UnsetParentGroupFromThings_Call) Return(_a0 *thingsv1.UnsetParentGroupFromThingsRes, _a1 error) *ThingsServiceClient_UnsetParentGroupFromThings_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *ThingsServiceClient_Authorize_Call) RunAndReturn(run func(context.Context, *magistrala.ThingsAuthzReq, ...grpc.CallOption) (*magistrala.ThingsAuthzRes, error)) *ThingsServiceClient_Authorize_Call { +func (_c *ThingsServiceClient_UnsetParentGroupFromThings_Call) RunAndReturn(run func(context.Context, *thingsv1.UnsetParentGroupFromThingsReq, ...grpc.CallOption) (*thingsv1.UnsetParentGroupFromThingsRes, error)) *ThingsServiceClient_UnsetParentGroupFromThings_Call { _c.Call.Return(run) return _c } diff --git a/things/postgres/clients.go b/things/postgres/clients.go index ca0b10daa2..51890a87aa 100644 --- a/things/postgres/clients.go +++ b/things/postgres/clients.go @@ -12,44 +12,57 @@ import ( "time" "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/pkg/postgres" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" "github.com/absmach/magistrala/things" "github.com/jackc/pgtype" ) +const ( + entityTableName = "clients" + entityIDColumnName = "id" + rolesTableNamePrefix = "things" +) + +var _ things.Repository = (*clientRepo)(nil) + type clientRepo struct { - Repository things.ClientRepository + DB postgres.Database + rolesPostgres.Repository } // NewRepository instantiates a PostgreSQL // implementation of Clients repository. func NewRepository(db postgres.Database) things.Repository { + repo := rolesPostgres.NewRepository(db, rolesTableNamePrefix, entityTableName, entityIDColumnName) + return &clientRepo{ - Repository: things.ClientRepository{DB: db}, + DB: db, + Repository: repo, } } func (repo *clientRepo) Save(ctx context.Context, th ...things.Client) ([]things.Client, error) { - tx, err := repo.Repository.DB.BeginTxx(ctx, nil) + tx, err := repo.DB.BeginTxx(ctx, nil) if err != nil { return []things.Client{}, errors.Wrap(repoerr.ErrCreateEntity, err) } - var thingsList []things.Client + var clients []things.Client for _, thi := range th { - q := `INSERT INTO clients (id, name, tags, domain_id, identity, secret, metadata, created_at, updated_at, updated_by, status) + q := `INSERT INTO clients (id, name, tags, domain_id, parent_group_id, identity, secret, metadata, created_at, updated_at, updated_by, status) VALUES (:id, :name, :tags, :domain_id, :identity, :secret, :metadata, :created_at, :updated_at, :updated_by, :status) - RETURNING id, name, tags, identity, secret, metadata, COALESCE(domain_id, '') AS domain_id, status, created_at, updated_at, updated_by` + RETURNING id, name, tags, identity, secret, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by` - dbthi, err := ToDBClient(thi) + dbcli, err := ToDBClient(thi) if err != nil { return []things.Client{}, errors.Wrap(repoerr.ErrCreateEntity, err) } - row, err := repo.Repository.DB.NamedQueryContext(ctx, q, dbthi) + row, err := repo.DB.NamedQueryContext(ctx, q, dbcli) if err != nil { if err := tx.Rollback(); err != nil { return []things.Client{}, postgres.HandleError(repoerr.ErrCreateEntity, err) @@ -59,28 +72,24 @@ func (repo *clientRepo) Save(ctx context.Context, th ...things.Client) ([]things defer row.Close() - if row.Next() { - dbthi = DBClient{} + for row.Next() { + dbthi := DBClient{} if err := row.StructScan(&dbthi); err != nil { return []things.Client{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } - thing, err := ToClient(dbthi) + client, err := ToClient(dbcli) if err != nil { return []things.Client{}, errors.Wrap(repoerr.ErrFailedOpDB, err) } - thingsList = append(thingsList, thing) + clients = append(clients, client) } } - if err = tx.Commit(); err != nil { - return []things.Client{}, errors.Wrap(repoerr.ErrCreateEntity, err) - } - - return thingsList, nil + return clients, nil } func (repo *clientRepo) RetrieveBySecret(ctx context.Context, key string) (things.Client, error) { - q := fmt.Sprintf(`SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, identity, secret, metadata, created_at, updated_at, updated_by, status + q := fmt.Sprintf(`SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, identity, secret, metadata, created_at, updated_at, updated_by, status FROM clients WHERE secret = :secret AND status = %d`, things.EnabledStatus) @@ -88,7 +97,7 @@ func (repo *clientRepo) RetrieveBySecret(ctx context.Context, key string) (thing Secret: key, } - rows, err := repo.Repository.DB.NamedQueryContext(ctx, q, dbt) + rows, err := repo.DB.NamedQueryContext(ctx, q, dbt) if err != nil { return things.Client{}, postgres.HandleError(repoerr.ErrViewEntity, err) } @@ -126,7 +135,7 @@ func (repo *clientRepo) Update(ctx context.Context, thing things.Client) (things q := fmt.Sprintf(`UPDATE clients SET %s updated_at = :updated_at, updated_by = :updated_by WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, secret, metadata, COALESCE(domain_id, '') AS domain_id, status, created_at, updated_at, updated_by`, + RETURNING id, name, tags, identity, secret, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`, upq) thing.Status = things.EnabledStatus return repo.update(ctx, thing, q) @@ -135,7 +144,7 @@ func (repo *clientRepo) Update(ctx context.Context, thing things.Client) (things func (repo *clientRepo) UpdateTags(ctx context.Context, thing things.Client) (things.Client, error) { q := `UPDATE clients SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, status, created_at, updated_at, updated_by` + RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by` thing.Status = things.EnabledStatus return repo.update(ctx, thing, q) } @@ -143,7 +152,7 @@ func (repo *clientRepo) UpdateTags(ctx context.Context, thing things.Client) (th func (repo *clientRepo) UpdateIdentity(ctx context.Context, thing things.Client) (things.Client, error) { q := `UPDATE clients SET identity = :identity, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, status, created_at, updated_at, updated_by` + RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, status, COALESCE(parent_group_id, '') AS parent_group_id, created_at, updated_at, updated_by` thing.Status = things.EnabledStatus return repo.update(ctx, thing, q) } @@ -151,7 +160,7 @@ func (repo *clientRepo) UpdateIdentity(ctx context.Context, thing things.Client) func (repo *clientRepo) UpdateSecret(ctx context.Context, thing things.Client) (things.Client, error) { q := `UPDATE clients SET secret = :secret, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, status, created_at, updated_at, updated_by` + RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by` thing.Status = things.EnabledStatus return repo.update(ctx, thing, q) } @@ -159,20 +168,20 @@ func (repo *clientRepo) UpdateSecret(ctx context.Context, thing things.Client) ( func (repo *clientRepo) ChangeStatus(ctx context.Context, thing things.Client) (things.Client, error) { q := `UPDATE clients SET status = :status, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id - RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, status, created_at, updated_at, updated_by` + RETURNING id, name, tags, identity, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by` return repo.update(ctx, thing, q) } func (repo *clientRepo) RetrieveByID(ctx context.Context, id string) (things.Client, error) { - q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, identity, secret, metadata, created_at, updated_at, updated_by, status + q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, identity, secret, metadata, created_at, updated_at, updated_by, status FROM clients WHERE id = :id` dbt := DBClient{ ID: id, } - row, err := repo.Repository.DB.NamedQueryContext(ctx, q, dbt) + row, err := repo.DB.NamedQueryContext(ctx, q, dbt) if err != nil { return things.Client{}, errors.Wrap(repoerr.ErrViewEntity, err) } @@ -197,14 +206,14 @@ func (repo *clientRepo) RetrieveAll(ctx context.Context, pm things.Page) (things } query = applyOrdering(query, pm) - q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.metadata, COALESCE(c.domain_id, '') AS domain_id, c.status, + q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.metadata, COALESCE(c.domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, c.status, c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM clients c %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, query) dbPage, err := ToDBClientsPage(pm) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - rows, err := repo.Repository.DB.NamedQueryContext(ctx, q, dbPage) + rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } @@ -226,7 +235,7 @@ func (repo *clientRepo) RetrieveAll(ctx context.Context, pm things.Page) (things } cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, query) - total, err := postgres.Total(ctx, repo.Repository.DB, cq, dbPage) + total, err := postgres.Total(ctx, repo.DB, cq, dbPage) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) } @@ -259,7 +268,7 @@ func (repo *clientRepo) SearchClients(ctx context.Context, pm things.Page) (thin return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - rows, err := repo.Repository.DB.NamedQueryContext(ctx, q, dbPage) + rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } @@ -281,7 +290,7 @@ func (repo *clientRepo) SearchClients(ctx context.Context, pm things.Page) (thin } cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, tq) - total, err := postgres.Total(ctx, repo.Repository.DB, cq, dbPage) + total, err := postgres.Total(ctx, repo.DB, cq, dbPage) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) } @@ -310,14 +319,14 @@ func (repo *clientRepo) RetrieveAllByIDs(ctx context.Context, pm things.Page) (t } query = applyOrdering(query, pm) - q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.metadata, COALESCE(c.domain_id, '') AS domain_id, c.status, + q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.metadata, COALESCE(c.domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, c.status, c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM clients c %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, query) dbPage, err := ToDBClientsPage(pm) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } - rows, err := repo.Repository.DB.NamedQueryContext(ctx, q, dbPage) + rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } @@ -339,7 +348,7 @@ func (repo *clientRepo) RetrieveAllByIDs(ctx context.Context, pm things.Page) (t } cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, query) - total, err := postgres.Total(ctx, repo.Repository.DB, cq, dbPage) + total, err := postgres.Total(ctx, repo.DB, cq, dbPage) if err != nil { return things.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) } @@ -362,7 +371,7 @@ func (repo *clientRepo) update(ctx context.Context, thing things.Client, query s return things.Client{}, errors.Wrap(repoerr.ErrUpdateEntity, err) } - row, err := repo.Repository.DB.NamedQueryContext(ctx, query, dbc) + row, err := repo.DB.NamedQueryContext(ctx, query, dbc) if err != nil { return things.Client{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) } @@ -380,10 +389,13 @@ func (repo *clientRepo) update(ctx context.Context, thing things.Client, query s return things.Client{}, repoerr.ErrNotFound } -func (repo *clientRepo) Delete(ctx context.Context, id string) error { - q := "DELETE FROM clients AS c WHERE c.id = $1 ;" +func (repo *clientRepo) Delete(ctx context.Context, clientIDs ...string) error { + q := "DELETE FROM clients AS c WHERE c.id = ANY(:client_ids) ;" - result, err := repo.Repository.DB.ExecContext(ctx, q, id) + params := map[string]interface{}{ + "client_ids": clientIDs, + } + result, err := repo.DB.NamedExecContext(ctx, q, params) if err != nil { return postgres.HandleError(repoerr.ErrRemoveEntity, err) } @@ -405,7 +417,6 @@ type DBClient struct { CreatedAt time.Time `db:"created_at,omitempty"` UpdatedAt sql.NullTime `db:"updated_at,omitempty"` UpdatedBy *string `db:"updated_by,omitempty"` - Groups []groups.Group `db:"groups,omitempty"` Status things.Status `db:"status,omitempty"` } @@ -572,3 +583,246 @@ func applyOrdering(emq string, pm things.Page) string { } return emq } + +func (repo *clientRepo) RetrieveByIds(ctx context.Context, ids []string) (things.ClientsPage, error) { + if len(ids) == 0 { + return things.ClientsPage{}, nil + } + + pm := things.Page{IDs: ids} + query, err := PageQuery(pm) + if err != nil { + return things.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.metadata, COALESCE(c.domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, c.status, + c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM clients c %s ORDER BY c.created_at`, query) + + dbPage, err := ToDBClientsPage(pm) + if err != nil { + return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + } + rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) + if err != nil { + return things.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) + } + defer rows.Close() + + var items []things.Client + for rows.Next() { + dbc := DBClient{} + if err := rows.StructScan(&dbc); err != nil { + return things.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + c, err := ToClient(dbc) + if err != nil { + return things.ClientsPage{}, err + } + + items = append(items, c) + } + cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, query) + + total, err := postgres.Total(ctx, repo.DB, cq, dbPage) + if err != nil { + return things.ClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + page := things.ClientsPage{ + Clients: items, + Page: things.Page{ + Total: total, + Offset: pm.Offset, + Limit: total, + }, + } + + return page, nil +} + +func (repo *clientRepo) AddConnections(ctx context.Context, conns []things.Connection) error { + + dbConns := toDBConnections(conns) + + q := `INSERT INTO connections (channel_id, domain_id, thing_id) + VALUES (:channel_id, :domain_id, :thing_id);` + + if _, err := repo.DB.NamedExecContext(ctx, q, dbConns); err != nil { + return postgres.HandleError(repoerr.ErrCreateEntity, err) + } + + return nil + +} + +func (repo *clientRepo) RemoveConnections(ctx context.Context, conns []things.Connection) (retErr error) { + tx, err := repo.DB.BeginTxx(ctx, nil) + if err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + defer func() { + if retErr != nil { + if errRollBack := tx.Rollback(); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollBack)) + } + } + }() + + query := `DELETE FROM connections WHERE channel_id = :channel_id AND domain_id = :domain_id AND thing_id = :thing_id` + + for _, conn := range conns { + dbConn := toDBConnection(conn) + if _, err := tx.NamedExec(query, dbConn); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, errors.Wrap(fmt.Errorf("failed to delete connection for channel_id: %s, domain_id: %s thing_id %s", conn.ChannelID, conn.DomainID, conn.ThingID), err)) + } + } + if err := tx.Commit(); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (repo *clientRepo) SetParentGroup(ctx context.Context, th things.Client) error { + q := "UPDATE clients SET parent_group_id = :parent_group_id, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id" + + params := map[string]interface{}{ + "parent_group_id": th.ParentGroup, + "updated_at": th.UpdatedAt, + "updated_by": th.UpdatedBy, + "id": th.ID, + } + result, err := repo.DB.NamedExecContext(ctx, q, params) + if err != nil { + return postgres.HandleError(repoerr.ErrUpdateEntity, err) + } + if rows, _ := result.RowsAffected(); rows == 0 { + return repoerr.ErrNotFound + } + return nil +} + +func (repo *clientRepo) RemoveParentGroup(ctx context.Context, th things.Client) error { + q := "UPDATE clients SET parent_group_id = NULL, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id" + dbCh, err := ToDBClient(th) + if err != nil { + return errors.Wrap(repoerr.ErrUpdateEntity, err) + } + result, err := repo.DB.NamedExecContext(ctx, q, dbCh) + if err != nil { + return postgres.HandleError(repoerr.ErrRemoveEntity, err) + } + if rows, _ := result.RowsAffected(); rows == 0 { + return repoerr.ErrNotFound + } + return nil +} + +func (repo *clientRepo) ThingConnectionsCount(ctx context.Context, id string) (uint64, error) { + query := `SELECT COUNT(*) FROM connections WHERE thing_id = :thing_id` + dbConn := dbConnection{ThingID: id} + + total, err := postgres.Total(ctx, repo.DB, query, dbConn) + if err != nil { + return 0, postgres.HandleError(repoerr.ErrViewEntity, err) + } + return total, nil +} + +func (repo *clientRepo) DoesThingHaveConnections(ctx context.Context, id string) (bool, error) { + query := `SELECT 1 FROM connections WHERE thing_id = :thing_id` + dbConn := dbConnection{ThingID: id} + + rows, err := repo.DB.NamedQueryContext(ctx, query, dbConn) + if err != nil { + return false, postgres.HandleError(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + return rows.Next(), nil +} + +func (repo *clientRepo) RemoveChannelConnections(ctx context.Context, channelID string) error { + query := `DELETE FROM connections WHERE channel_id = :channel_id` + + dbConn := dbConnection{ChannelID: channelID} + if _, err := repo.DB.NamedExecContext(ctx, query, dbConn); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (repo *clientRepo) RemoveThingConnections(ctx context.Context, thingID string) error { + query := `DELETE FROM connections WHERE thing_id = :thing_id` + + dbConn := dbConnection{ThingID: thingID} + if _, err := repo.DB.NamedExecContext(ctx, query, dbConn); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +func (repo *clientRepo) RetrieveParentGroupThings(ctx context.Context, parentGroupID string) ([]things.Client, error) { + query := `SELECT c.id, c.name, c.tags, c.metadata, COALESCE(c.domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, c.status, + c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM clients c WHERE c.parent_group_id = :parent_group_id ;` + + params := map[string]interface{}{ + "parent_group_id": parentGroupID, + } + + rows, err := repo.DB.NamedQueryContext(ctx, query, params) + if err != nil { + return []things.Client{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + defer rows.Close() + + var ths []things.Client + for rows.Next() { + dbTh := DBClient{} + if err := rows.StructScan(&dbTh); err != nil { + return []things.Client{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + + th, err := ToClient(dbTh) + if err != nil { + return []things.Client{}, err + } + + ths = append(ths, th) + } + return ths, nil +} + +func (repo *clientRepo) UnsetParentGroupFromThings(ctx context.Context, parentGroupID string) error { + query := "UPDATE clients SET parent_group_id = NULL WHERE parent_group_id = :parent_group_id" + + params := map[string]interface{}{ + "parent_group_id": parentGroupID, + } + if _, err := repo.DB.NamedExecContext(ctx, query, params); err != nil { + return errors.Wrap(repoerr.ErrRemoveEntity, err) + } + return nil +} + +type dbConnection struct { + ThingID string `db:"thing_id"` + ChannelID string `db:"channel_id"` + DomainID string `db:"domain_id"` +} + +func toDBConnections(conns []things.Connection) []dbConnection { + var dbconns []dbConnection + for _, conn := range conns { + dbconns = append(dbconns, toDBConnection(conn)) + } + return dbconns +} + +func toDBConnection(conn things.Connection) dbConnection { + return dbConnection{ + ThingID: conn.ThingID, + ChannelID: conn.ChannelID, + DomainID: conn.DomainID, + } +} diff --git a/things/postgres/init.go b/things/postgres/init.go index 28e07a2cc6..dba0c1096b 100644 --- a/things/postgres/init.go +++ b/things/postgres/init.go @@ -4,12 +4,20 @@ package postgres import ( + "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + rolesPostgres "github.com/absmach/magistrala/pkg/roles/repo/postgres" _ "github.com/jackc/pgx/v5/stdlib" // required for SQL access migrate "github.com/rubenv/sql-migrate" ) -func Migration() *migrate.MemoryMigrationSource { - return &migrate.MemoryMigrationSource{ +func Migration() (*migrate.MemoryMigrationSource, error) { + thingsRolesMigration, err := rolesPostgres.Migration(rolesTableNamePrefix, entityTableName, entityIDColumnName) + if err != nil { + return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err) + } + + thingsMigration := &migrate.MemoryMigrationSource{ Migrations: []*migrate.Migration{ { Id: "clients_01", @@ -17,25 +25,39 @@ func Migration() *migrate.MemoryMigrationSource { // STATUS 0 to imply enabled and 1 to imply disabled Up: []string{ `CREATE TABLE IF NOT EXISTS clients ( - id VARCHAR(36) PRIMARY KEY, - name VARCHAR(1024), - domain_id VARCHAR(36) NOT NULL, - identity VARCHAR(254), - secret VARCHAR(4096) NOT NULL, - tags TEXT[], - metadata JSONB, - created_at TIMESTAMP, - updated_at TIMESTAMP, - updated_by VARCHAR(254), - status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), - UNIQUE (domain_id, secret), - UNIQUE (domain_id, name) + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(1024), + domain_id VARCHAR(36) NOT NULL, + parent_group_id VARCHAR(36) DEFAULT NULL, + identity VARCHAR(254), + secret VARCHAR(4096) NOT NULL, + tags TEXT[], + metadata JSONB, + created_at TIMESTAMP, + updated_at TIMESTAMP, + updated_by VARCHAR(254), + status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0), + UNIQUE (domain_id, secret), + UNIQUE (domain_id, name), + UNIQUE (domain_id, id) + )`, + `CREATE TABLE IF NOT EXISTS connections ( + channel_id VARCHAR(36), + domain_id VARCHAR(36), + thing_id VARCHAR(36), + FOREIGN KEY (thing_id, domain_id) REFERENCES clients (id, domain_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (channel_id, domain_id, thing_id) )`, }, Down: []string{ `DROP TABLE IF EXISTS clients`, + `DROP TABLE IF EXISTS connections`, }, }, }, } + + thingsMigration.Migrations = append(thingsMigration.Migrations, thingsRolesMigration.Migrations...) + + return thingsMigration, nil } diff --git a/things/postgres/setup_test.go b/things/postgres/setup_test.go index a167f6434c..66a7be95bf 100644 --- a/things/postgres/setup_test.go +++ b/things/postgres/setup_test.go @@ -75,7 +75,12 @@ func TestMain(m *testing.M) { SSLRootCert: "", } - if db, err = pgclient.Setup(dbConfig, *cpostgres.Migration()); err != nil { + mig, err := cpostgres.Migration() + if err != nil { + log.Fatalf("Could not get DB migrations: %s", err) + + } + if db, err = pgclient.Setup(dbConfig, *mig); err != nil { log.Fatalf("Could not setup test DB connection: %s", err) } diff --git a/things/private/doc.go b/things/private/doc.go new file mode 100644 index 0000000000..85534043e8 --- /dev/null +++ b/things/private/doc.go @@ -0,0 +1,3 @@ +// Private package is a service wrapper around the underlying Repository. +// This is used for internal service communication purpose only. +package private diff --git a/things/private/mocks/service.go b/things/private/mocks/service.go new file mode 100644 index 0000000000..1396133c30 --- /dev/null +++ b/things/private/mocks/service.go @@ -0,0 +1,188 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + things "github.com/absmach/magistrala/things" +) + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +// AddConnections provides a mock function with given fields: ctx, conns +func (_m *Service) AddConnections(ctx context.Context, conns []things.Connection) error { + ret := _m.Called(ctx, conns) + + if len(ret) == 0 { + panic("no return value specified for AddConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []things.Connection) error); ok { + r0 = rf(ctx, conns) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Authenticate provides a mock function with given fields: ctx, key +func (_m *Service) Authenticate(ctx context.Context, key string) (string, error) { + ret := _m.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Authenticate") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return rf(ctx, key) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, key) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, key) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveChannelConnections provides a mock function with given fields: ctx, channelID +func (_m *Service) RemoveChannelConnections(ctx context.Context, channelID string) error { + ret := _m.Called(ctx, channelID) + + if len(ret) == 0 { + panic("no return value specified for RemoveChannelConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, channelID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveConnections provides a mock function with given fields: ctx, conns +func (_m *Service) RemoveConnections(ctx context.Context, conns []things.Connection) error { + ret := _m.Called(ctx, conns) + + if len(ret) == 0 { + panic("no return value specified for RemoveConnections") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []things.Connection) error); ok { + r0 = rf(ctx, conns) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RetrieveById provides a mock function with given fields: ctx, id +func (_m *Service) RetrieveById(ctx context.Context, id string) (things.Client, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for RetrieveById") + } + + var r0 things.Client + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (things.Client, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) things.Client); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(things.Client) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RetrieveByIds provides a mock function with given fields: ctx, ids +func (_m *Service) RetrieveByIds(ctx context.Context, ids []string) (things.ClientsPage, error) { + ret := _m.Called(ctx, ids) + + if len(ret) == 0 { + panic("no return value specified for RetrieveByIds") + } + + var r0 things.ClientsPage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) (things.ClientsPage, error)); ok { + return rf(ctx, ids) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) things.ClientsPage); ok { + r0 = rf(ctx, ids) + } else { + r0 = ret.Get(0).(things.ClientsPage) + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UnsetParentGroupFromThings provides a mock function with given fields: ctx, parentGroupID +func (_m *Service) UnsetParentGroupFromThings(ctx context.Context, parentGroupID string) error { + ret := _m.Called(ctx, parentGroupID) + + if len(ret) == 0 { + panic("no return value specified for UnsetParentGroupFromThings") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, parentGroupID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewService creates a new instance of Service. 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 NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/things/private/service.go b/things/private/service.go new file mode 100644 index 0000000000..d2aea41d6e --- /dev/null +++ b/things/private/service.go @@ -0,0 +1,119 @@ +package private + +import ( + "context" + + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/things" +) + +//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines" +type Service interface { + // Authenticate returns thing ID for given thing key. + Authenticate(ctx context.Context, key string) (string, error) + + RetrieveById(ctx context.Context, id string) (things.Client, error) + + RetrieveByIds(ctx context.Context, ids []string) (things.ClientsPage, error) + + AddConnections(ctx context.Context, conns []things.Connection) error + + RemoveConnections(ctx context.Context, conns []things.Connection) error + + RemoveChannelConnections(ctx context.Context, channelID string) error + + UnsetParentGroupFromThings(ctx context.Context, parentGroupID string) error +} + +var _ Service = (*service)(nil) + +func New(repo things.Repository, cache things.Cache, evaluator policies.Evaluator, policy policies.Service) Service { + return service{ + repo: repo, + cache: cache, + evaluator: evaluator, + policy: policy, + } +} + +type service struct { + repo things.Repository + cache things.Cache + evaluator policies.Evaluator + policy policies.Service +} + +func (svc service) Authenticate(ctx context.Context, key string) (string, error) { + id, err := svc.cache.ID(ctx, key) + if err == nil { + return id, nil + } + + client, err := svc.repo.RetrieveBySecret(ctx, key) + if err != nil { + return "", errors.Wrap(svcerr.ErrAuthorization, err) + } + if err := svc.cache.Save(ctx, key, client.ID); err != nil { + return "", errors.Wrap(svcerr.ErrAuthorization, err) + } + + return client.ID, nil +} + +func (svc service) RetrieveById(ctx context.Context, ids string) (things.Client, error) { + return svc.repo.RetrieveByID(ctx, ids) +} + +func (svc service) RetrieveByIds(ctx context.Context, ids []string) (things.ClientsPage, error) { + return svc.repo.RetrieveByIds(ctx, ids) +} + +func (svc service) AddConnections(ctx context.Context, conns []things.Connection) (err error) { + return svc.repo.AddConnections(ctx, conns) +} + +func (svc service) RemoveConnections(ctx context.Context, conns []things.Connection) (err error) { + return svc.repo.RemoveConnections(ctx, conns) +} + +func (svc service) RemoveChannelConnections(ctx context.Context, channelID string) error { + return svc.repo.RemoveChannelConnections(ctx, channelID) +} + +func (svc service) UnsetParentGroupFromThings(ctx context.Context, parentGroupID string) (retErr error) { + ths, err := svc.repo.RetrieveParentGroupThings(ctx, parentGroupID) + if err != nil { + return errors.Wrap(svcerr.ErrViewEntity, err) + } + + if len(ths) > 0 { + prs := []policies.Policy{} + for _, th := range ths { + prs = append(prs, policies.Policy{ + SubjectType: policies.GroupType, + Subject: th.ParentGroup, + Relation: policies.ParentGroupRelation, + ObjectType: policies.ThingType, + Object: th.ID, + }) + } + + if err := svc.policy.DeletePolicies(ctx, prs); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + defer func() { + if retErr != nil { + if errRollback := svc.policy.AddPolicies(ctx, prs); err != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errors.ErrRollbackTx, errRollback)) + } + } + }() + + if err := svc.repo.UnsetParentGroupFromThings(ctx, parentGroupID); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + } + return nil +} diff --git a/things/roleactions.go b/things/roleactions.go new file mode 100644 index 0000000000..1cb46e603a --- /dev/null +++ b/things/roleactions.go @@ -0,0 +1,44 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package things + +import "github.com/absmach/magistrala/pkg/roles" + +// Below codes should moved out of service, may be can be kept in `cmd//main.go` + +const ( + ThingUpdate roles.Action = "update" + ThingRead roles.Action = "read" + ThingDelete roles.Action = "delete" + ThingSetParentGroup roles.Action = "set_parent_group" + ThingConnectToChannel roles.Action = "connect_to_channel" + ThingManageRole roles.Action = "manage_role" + ThingAddRoleUsers roles.Action = "add_role_users" + ThingRemoveRoleUsers roles.Action = "remove_role_users" + ThingViewRoleUsers roles.Action = "view_role_users" +) + +const ( + ThingBuiltInRoleAdmin = "admin" +) + +func AvailableActions() []roles.Action { + return []roles.Action{ + ThingUpdate, + ThingRead, + ThingDelete, + ThingSetParentGroup, + ThingConnectToChannel, + ThingManageRole, + ThingAddRoleUsers, + ThingRemoveRoleUsers, + ThingViewRoleUsers, + } +} + +func BuiltInRoles() map[roles.BuiltInRoleName][]roles.Action { + return map[roles.BuiltInRoleName][]roles.Action{ + ThingBuiltInRoleAdmin: AvailableActions(), + } +} diff --git a/things/roleoperations.go b/things/roleoperations.go new file mode 100644 index 0000000000..fca102b1b7 --- /dev/null +++ b/things/roleoperations.go @@ -0,0 +1,166 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package things + +import ( + "github.com/absmach/magistrala/pkg/roles" + "github.com/absmach/magistrala/pkg/svcutil" +) + +// Internal Operations + +const ( + OpViewThing svcutil.Operation = iota + OpUpdateThing + OpUpdateThingTags + OpUpdateThingSecret + OpEnableThing + OpDisableThing + OpDeleteThing + OpSetParentGroup + OpRemoveParentGroup + OpConnectToChannel + OpDisconnectFromChannel +) + +var expectedOperations = []svcutil.Operation{ + OpViewThing, + OpUpdateThing, + OpUpdateThingTags, + OpUpdateThingSecret, + OpEnableThing, + OpDisableThing, + OpDeleteThing, + OpSetParentGroup, + OpRemoveParentGroup, + OpConnectToChannel, + OpDisconnectFromChannel, +} + +var operationNames = []string{ + "OpViewThing", + "OpUpdateThing", + "OpUpdateThingTags", + "OpUpdateThingSecret", + "OpEnableThing", + "OpDisableThing", + "OpDeleteThing", + "OpSetParentGroup", + "OpRemoveParentGroup", + "OpConnectToChannel", + "OpDisconnectFromChannel", +} + +func NewOperationPerm() svcutil.OperationPerm { + return svcutil.NewOperationPerm(expectedOperations, operationNames) +} + +// External Operations +const ( + DomainOpCreateThing svcutil.ExternalOperation = iota + DomainOpListThing + GroupOpSetChildThing + GroupsOpRemoveChildThing + ChannelsOpConnectChannel + ChannelsOpDisconnectChannel +) + +var expectedExternalOperations = []svcutil.ExternalOperation{ + DomainOpCreateThing, + DomainOpListThing, + GroupOpSetChildThing, + GroupsOpRemoveChildThing, + ChannelsOpConnectChannel, + ChannelsOpDisconnectChannel, +} +var externalOperationNames = []string{ + "DomainOpCreateThing", + "DomainOpListThing", + "GroupOpSetChildThing", + "GroupsOpRemoveChildThing", + "ChannelsOpConnectChannel", + "ChannelsOpDisconnectChannel", +} + +func NewExternalOperationPerm() svcutil.ExternalOperationPerm { + return svcutil.NewExternalOperationPerm(expectedExternalOperations, externalOperationNames) +} + +// Below codes should moved out of service, may be can be kept in `cmd//main.go` + +const ( + updatePermission = "update_permission" + readPermission = "read_permission" + deletePermission = "delete_permission" + setParentGroupPermission = "set_parent_group_permission" + connectToChannelPermission = "connect_to_channel_permission" + + manageRolePermission = "manage_role_permission" + addRoleUsersPermission = "add_role_users_permission" + removeRoleUsersPermission = "remove_role_users_permission" + viewRoleUsersPermission = "view_role_users_permission" +) + +func NewOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + OpViewThing: readPermission, + OpUpdateThing: updatePermission, + OpUpdateThingTags: updatePermission, + OpUpdateThingSecret: updatePermission, + OpEnableThing: updatePermission, + OpDisableThing: updatePermission, + OpDeleteThing: deletePermission, + OpSetParentGroup: setParentGroupPermission, + OpRemoveParentGroup: setParentGroupPermission, + OpConnectToChannel: connectToChannelPermission, + OpDisconnectFromChannel: connectToChannelPermission, + } + return opPerm +} + +func NewRolesOperationPermissionMap() map[svcutil.Operation]svcutil.Permission { + opPerm := map[svcutil.Operation]svcutil.Permission{ + roles.OpAddRole: manageRolePermission, + roles.OpRemoveRole: manageRolePermission, + roles.OpUpdateRoleName: manageRolePermission, + roles.OpRetrieveRole: manageRolePermission, + roles.OpRetrieveAllRoles: manageRolePermission, + roles.OpRoleAddActions: manageRolePermission, + roles.OpRoleListActions: manageRolePermission, + roles.OpRoleCheckActionsExists: manageRolePermission, + roles.OpRoleRemoveActions: manageRolePermission, + roles.OpRoleRemoveAllActions: manageRolePermission, + roles.OpRoleAddMembers: addRoleUsersPermission, + roles.OpRoleListMembers: viewRoleUsersPermission, + roles.OpRoleCheckMembersExists: viewRoleUsersPermission, + roles.OpRoleRemoveMembers: removeRoleUsersPermission, + roles.OpRoleRemoveAllMembers: manageRolePermission, + } + return opPerm +} + +const ( + // External Permission + // Domains + domainCreateThingPermission = "thing_create_permission" + domainListThingPermission = "list_things_permission" + // Groups + groupSetChildThingPermission = "thing_create_permission" + groupRemoveChildThingPermission = "thing_create_permission" + // Channels + channelsConnectThingPermission = "connect_to_thing_permission" + channelsDisconnectThingPermission = "connect_to_thing_permission" +) + +func NewExternalOperationPermissionMap() map[svcutil.ExternalOperation]svcutil.Permission { + extOpPerm := map[svcutil.ExternalOperation]svcutil.Permission{ + DomainOpCreateThing: domainCreateThingPermission, + DomainOpListThing: domainListThingPermission, + GroupOpSetChildThing: groupSetChildThingPermission, + GroupsOpRemoveChildThing: groupRemoveChildThingPermission, + ChannelsOpConnectChannel: channelsConnectThingPermission, + ChannelsOpDisconnectChannel: channelsDisconnectThingPermission, + } + return extOpPerm +} diff --git a/things/service.go b/things/service.go index 475902084d..19b619a0e5 100644 --- a/things/service.go +++ b/things/service.go @@ -4,60 +4,59 @@ package things import ( "context" + "fmt" "time" - "github.com/absmach/magistrala" + mg "github.com/absmach/magistrala" mgauth "github.com/absmach/magistrala/auth" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcCommonV1 "github.com/absmach/magistrala/internal/grpc/common/v1" + grpcGroupsV1 "github.com/absmach/magistrala/internal/grpc/groups/v1" + "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/policies" + "github.com/absmach/magistrala/pkg/roles" "golang.org/x/sync/errgroup" ) -type service struct { - evaluator policies.Evaluator - policysvc policies.Service - clients Repository - clientCache Cache - idProvider magistrala.IDProvider -} +var ( + errRollbackRepo = errors.New("failed to rollback repo") + errSetParentGroup = errors.New("thing already have parent") +) +var _ Service = (*service)(nil) -// NewService returns a new Things service implementation. -func NewService(policyEvaluator policies.Evaluator, policyService policies.Service, c Repository, tcache Cache, idp magistrala.IDProvider) Service { - return service{ - evaluator: policyEvaluator, - policysvc: policyService, - clients: c, - clientCache: tcache, - idProvider: idp, - } +type service struct { + repo Repository + policy policies.Service + channels grpcChannelsV1.ChannelsServiceClient + groups grpcGroupsV1.GroupsServiceClient + cache Cache + idProvider mg.IDProvider + roles.ProvisionManageService } -func (svc service) Authorize(ctx context.Context, req AuthzReq) (string, error) { - clientID, err := svc.Identify(ctx, req.ClientKey) +// NewService returns a new Clients service implementation. +func NewService(repo Repository, policy policies.Service, cache Cache, channels grpcChannelsV1.ChannelsServiceClient, groups grpcGroupsV1.GroupsServiceClient, idProvider mg.IDProvider, sIDProvider mg.IDProvider) (Service, error) { + rpms, err := roles.NewProvisionManageService(policies.ThingType, repo, policy, sIDProvider, AvailableActions(), BuiltInRoles()) if err != nil { - return "", err + return service{}, err } - - r := policies.Policy{ - SubjectType: policies.GroupType, - Subject: req.ChannelID, - ObjectType: policies.ThingType, - Object: clientID, - Permission: req.Permission, - } - err = svc.evaluator.CheckPolicy(ctx, r) - if err != nil { - return "", errors.Wrap(svcerr.ErrAuthorization, err) - } - - return clientID, nil + return service{ + repo: repo, + policy: policy, + channels: channels, + groups: groups, + cache: cache, + idProvider: idProvider, + ProvisionManageService: rpms, + }, nil } -func (svc service) CreateClients(ctx context.Context, session authn.Session, cli ...Client) ([]Client, error) { +func (svc service) CreateClients(ctx context.Context, session authn.Session, cls ...Client) (retThings []Client, retErr error) { var clients []Client - for _, c := range cli { + for _, c := range cls { if c.ID == "" { clientID, err := svc.idProvider.ID() if err != nil { @@ -80,45 +79,57 @@ func (svc service) CreateClients(ctx context.Context, session authn.Session, cli clients = append(clients, c) } - err := svc.addClientPolicies(ctx, session.DomainUserID, session.DomainID, clients) + saved, err := svc.repo.Save(ctx, clients...) if err != nil { - return []Client{}, err + return nil, errors.Wrap(svcerr.ErrCreateEntity, err) } + clientIDs := []string{} + for _, c := range saved { + clientIDs = append(clientIDs, c.ID) + } + defer func() { - if err != nil { - if errRollback := svc.addClientPoliciesRollback(ctx, session.DomainUserID, session.DomainID, clients); errRollback != nil { - err = errors.Wrap(errors.Wrap(errors.ErrRollbackTx, errRollback), err) + if retErr != nil { + if errRollBack := svc.repo.Delete(ctx, clientIDs...); errRollBack != nil { + retErr = errors.Wrap(retErr, errors.Wrap(errRollbackRepo, errRollBack)) } } }() - saved, err := svc.clients.Save(ctx, clients...) - if err != nil { - return nil, errors.Wrap(svcerr.ErrCreateEntity, err) + newBuiltInRoleMembers := map[roles.BuiltInRoleName][]roles.Member{ + ThingBuiltInRoleAdmin: {roles.Member(session.UserID)}, + } + + optionalPolicies := []policies.Policy{} + + for _, clientID := range clientIDs { + optionalPolicies = append(optionalPolicies, + policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.DomainType, + Subject: session.DomainID, + Relation: policies.DomainRelation, + ObjectType: policies.ThingType, + Object: clientID, + }, + ) + } + + if _, err := svc.AddNewEntitiesRoles(ctx, session.DomainID, session.UserID, clientIDs, optionalPolicies, newBuiltInRoleMembers); err != nil { + return []Client{}, errors.Wrap(svcerr.ErrAddPolicies, err) } return saved, nil } func (svc service) View(ctx context.Context, session authn.Session, id string) (Client, error) { - client, err := svc.clients.RetrieveByID(ctx, id) + client, err := svc.repo.RetrieveByID(ctx, id) if err != nil { return Client{}, errors.Wrap(svcerr.ErrViewEntity, err) } return client, nil } -func (svc service) ViewPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - permissions, err := svc.listUserClientPermission(ctx, session.DomainUserID, id) - if err != nil { - return nil, err - } - if len(permissions) == 0 { - return nil, svcerr.ErrAuthorization - } - return permissions, nil -} - func (svc service) ListClients(ctx context.Context, session authn.Session, reqUserID string, pm Page) (ClientsPage, error) { var ids []string var err error @@ -148,7 +159,7 @@ func (svc service) ListClients(ctx context.Context, session authn.Session, reqUs return ClientsPage{}, nil } pm.IDs = ids - tp, err := svc.clients.SearchClients(ctx, pm) + tp, err := svc.repo.SearchClients(ctx, pm) if err != nil { return ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err) } @@ -182,7 +193,7 @@ func (svc service) retrievePermissions(ctx context.Context, userID string, clien } func (svc service) listUserClientPermission(ctx context.Context, userID, clientID string) ([]string, error) { - permissions, err := svc.policysvc.ListPermissions(ctx, policies.Policy{ + permissions, err := svc.policy.ListPermissions(ctx, policies.Policy{ SubjectType: policies.UserType, Subject: userID, Object: clientID, @@ -195,7 +206,7 @@ func (svc service) listUserClientPermission(ctx context.Context, userID, clientI } func (svc service) listClientIDs(ctx context.Context, userID, permission string) ([]string, error) { - tids, err := svc.policysvc.ListAllObjects(ctx, policies.Policy{ + tids, err := svc.policy.ListAllObjects(ctx, policies.Policy{ SubjectType: policies.UserType, Subject: userID, Permission: permission, @@ -209,7 +220,7 @@ func (svc service) listClientIDs(ctx context.Context, userID, permission string) func (svc service) filterAllowedClientIDs(ctx context.Context, userID, permission string, clientIDs []string) ([]string, error) { var ids []string - tids, err := svc.policysvc.ListAllObjects(ctx, policies.Policy{ + tids, err := svc.policy.ListAllObjects(ctx, policies.Policy{ SubjectType: policies.UserType, Subject: userID, Permission: permission, @@ -236,7 +247,7 @@ func (svc service) Update(ctx context.Context, session authn.Session, thi Client UpdatedAt: time.Now(), UpdatedBy: session.UserID, } - client, err := svc.clients.Update(ctx, client) + client, err := svc.repo.Update(ctx, client) if err != nil { return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err) } @@ -250,7 +261,7 @@ func (svc service) UpdateTags(ctx context.Context, session authn.Session, thi Cl UpdatedAt: time.Now(), UpdatedBy: session.UserID, } - client, err := svc.clients.UpdateTags(ctx, client) + client, err := svc.repo.UpdateTags(ctx, client) if err != nil { return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err) } @@ -267,7 +278,7 @@ func (svc service) UpdateSecret(ctx context.Context, session authn.Session, id, UpdatedBy: session.UserID, Status: EnabledStatus, } - client, err := svc.clients.UpdateSecret(ctx, client) + client, err := svc.repo.UpdateSecret(ctx, client) if err != nil { return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err) } @@ -299,197 +310,169 @@ func (svc service) Disable(ctx context.Context, session authn.Session, id string return Client{}, errors.Wrap(ErrDisableClient, err) } - if err := svc.clientCache.Remove(ctx, client.ID); err != nil { + if err := svc.cache.Remove(ctx, client.ID); err != nil { return client, errors.Wrap(svcerr.ErrRemoveEntity, err) } return client, nil } -func (svc service) Share(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { - policyList := []policies.Policy{} - for _, userid := range userids { - policyList = append(policyList, policies.Policy{ - SubjectType: policies.UserType, - Subject: mgauth.EncodeDomainUserID(session.DomainID, userid), - Relation: relation, - ObjectType: policies.ThingType, - Object: id, - }) - } - if err := svc.policysvc.AddPolicies(ctx, policyList); err != nil { - return errors.Wrap(svcerr.ErrUpdateEntity, err) - } - - return nil -} - -func (svc service) Unshare(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { - policyList := []policies.Policy{} - for _, userid := range userids { - policyList = append(policyList, policies.Policy{ - SubjectType: policies.UserType, - Subject: mgauth.EncodeDomainUserID(session.DomainID, userid), - Relation: relation, - ObjectType: policies.ThingType, - Object: id, - }) - } - if err := svc.policysvc.DeletePolicies(ctx, policyList); err != nil { +func (svc service) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (retErr error) { + th, err := svc.repo.RetrieveByID(ctx, id) + if err != nil { return errors.Wrap(svcerr.ErrUpdateEntity, err) } - - return nil -} - -func (svc service) Delete(ctx context.Context, session authn.Session, id string) error { - if err := svc.clientCache.Remove(ctx, id); err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) - } - - req := policies.Policy{ - Object: id, - ObjectType: policies.ThingType, - } - - if err := svc.policysvc.DeletePolicyFilter(ctx, req); err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) - } - - if err := svc.clients.Delete(ctx, id); err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) + switch th.ParentGroup { + case parentGroupID: + return nil + case "": + // No action needed, proceed to next code after switch + default: + return errors.Wrap(svcerr.ErrConflict, errSetParentGroup) } - return nil -} - -func (svc service) changeClientStatus(ctx context.Context, session authn.Session, client Client) (Client, error) { - dbClient, err := svc.clients.RetrieveByID(ctx, client.ID) + resp, err := svc.groups.RetrieveEntity(ctx, &grpcCommonV1.RetrieveEntityReq{Id: parentGroupID}) if err != nil { - return Client{}, errors.Wrap(svcerr.ErrViewEntity, err) + return errors.Wrap(svcerr.ErrUpdateEntity, err) } - if dbClient.Status == client.Status { - return Client{}, errors.ErrStatusAlreadyAssigned + if resp.GetEntity().GetDomainId() != session.DomainID { + return errors.Wrap(svcerr.ErrUpdateEntity, fmt.Errorf("parent group id %s has invalid domain id", parentGroupID)) } - - client.UpdatedBy = session.UserID - - client, err = svc.clients.ChangeStatus(ctx, client) - if err != nil { - return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + if resp.GetEntity().GetStatus() != uint32(EnabledStatus) { + return errors.Wrap(svcerr.ErrUpdateEntity, fmt.Errorf("parent group id %s is not in enabled state", parentGroupID)) } - return client, nil -} -func (svc service) ListClientsByGroup(ctx context.Context, session authn.Session, groupID string, pm Page) (MembersPage, error) { - tids, err := svc.policysvc.ListAllObjects(ctx, policies.Policy{ + var pols []policies.Policy + + pols = append(pols, policies.Policy{ + Domain: session.DomainID, SubjectType: policies.GroupType, - Subject: groupID, - Permission: policies.GroupRelation, + Subject: parentGroupID, + Relation: policies.ParentGroupRelation, ObjectType: policies.ThingType, + Object: id, }) - if err != nil { - return MembersPage{}, errors.Wrap(svcerr.ErrNotFound, err) + + if err := svc.policy.AddPolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrAddPolicies, err) } + defer func() { + if retErr != nil { + if errRollback := svc.policy.DeletePolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() + th = Client{ID: id, ParentGroup: parentGroupID, UpdatedBy: session.UserID, UpdatedAt: time.Now()} - pm.IDs = tids.Policies + if err := svc.repo.SetParentGroup(ctx, th); err != nil { + return errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return nil +} - cp, err := svc.clients.RetrieveAllByIDs(ctx, pm) +func (svc service) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (retErr error) { + th, err := svc.repo.RetrieveByID(ctx, id) if err != nil { - return MembersPage{}, errors.Wrap(svcerr.ErrViewEntity, err) + return errors.Wrap(svcerr.ErrViewEntity, err) } - if pm.ListPerms && len(cp.Clients) > 0 { - g, ctx := errgroup.WithContext(ctx) + if th.ParentGroup != "" { + var pols []policies.Policy + pols = append(pols, policies.Policy{ + Domain: session.DomainID, + SubjectType: policies.GroupType, + Subject: th.ParentGroup, + Relation: policies.ParentGroupRelation, + ObjectType: policies.ThingType, + Object: id, + }) - for i := range cp.Clients { - // Copying loop variable "i" to avoid "loop variable captured by func literal" - iter := i - g.Go(func() error { - return svc.retrievePermissions(ctx, session.DomainUserID, &cp.Clients[iter]) - }) + if err := svc.policy.DeletePolicies(ctx, pols); err != nil { + return errors.Wrap(svcerr.ErrAddPolicies, err) } + defer func() { + if retErr != nil { + if errRollback := svc.policy.AddPolicies(ctx, pols); errRollback != nil { + retErr = errors.Wrap(retErr, errors.Wrap(apiutil.ErrRollbackTx, errRollback)) + } + } + }() - if err := g.Wait(); err != nil { - return MembersPage{}, err + th := Client{ID: id, UpdatedBy: session.UserID, UpdatedAt: time.Now()} + + if err := svc.repo.RemoveParentGroup(ctx, th); err != nil { + return err } } - - return MembersPage{ - Page: cp.Page, - Members: cp.Clients, - }, nil + return nil } -func (svc service) Identify(ctx context.Context, key string) (string, error) { - id, err := svc.clientCache.ID(ctx, key) - if err == nil { - return id, nil - } +func (svc service) Delete(ctx context.Context, session authn.Session, id string) error { - client, err := svc.clients.RetrieveBySecret(ctx, key) + ok, err := svc.repo.DoesThingHaveConnections(ctx, id) if err != nil { - return "", errors.Wrap(svcerr.ErrAuthorization, err) + return errors.Wrap(svcerr.ErrRemoveEntity, err) } - if err := svc.clientCache.Save(ctx, key, client.ID); err != nil { - return "", errors.Wrap(svcerr.ErrAuthorization, err) + if ok { + if _, err := svc.channels.RemoveThingConnections(ctx, &grpcChannelsV1.RemoveThingConnectionsReq{ThingId: id}); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } } - return client.ID, nil -} + if _, err := svc.repo.ChangeStatus(ctx, Client{ID: id, Status: DeletedStatus}); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } -func (svc service) addClientPolicies(ctx context.Context, userID, domainID string, clients []Client) error { - policyList := []policies.Policy{} - for _, client := range clients { - policyList = append(policyList, policies.Policy{ - Domain: domainID, - SubjectType: policies.UserType, - Subject: userID, - Relation: policies.AdministratorRelation, - ObjectKind: policies.NewThingKind, - ObjectType: policies.ThingType, - Object: client.ID, - }) - policyList = append(policyList, policies.Policy{ - Domain: domainID, + if err := svc.cache.Remove(ctx, id); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) + } + + filterDeletePolicies := []policies.Policy{ + { + SubjectType: policies.ThingType, + Subject: id, + }, + { + ObjectType: policies.ThingType, + Object: id, + }, + } + deletePolicies := []policies.Policy{ + { SubjectType: policies.DomainType, - Subject: domainID, + Subject: session.DomainID, Relation: policies.DomainRelation, ObjectType: policies.ThingType, - Object: client.ID, - }) + Object: id, + }, } - if err := svc.policysvc.AddPolicies(ctx, policyList); err != nil { - return errors.Wrap(svcerr.ErrCreateEntity, err) + + if err := svc.RemoveEntitiesRoles(ctx, session.DomainID, session.DomainUserID, []string{id}, filterDeletePolicies, deletePolicies); err != nil { + return errors.Wrap(svcerr.ErrDeletePolicies, err) + } + + if err := svc.repo.Delete(ctx, id); err != nil { + return errors.Wrap(svcerr.ErrRemoveEntity, err) } return nil } -func (svc service) addClientPoliciesRollback(ctx context.Context, userID, domainID string, clients []Client) error { - policyList := []policies.Policy{} - for _, client := range clients { - policyList = append(policyList, policies.Policy{ - Domain: domainID, - SubjectType: policies.UserType, - Subject: userID, - Relation: policies.AdministratorRelation, - ObjectKind: policies.NewThingKind, - ObjectType: policies.ThingType, - Object: client.ID, - }) - policyList = append(policyList, policies.Policy{ - Domain: domainID, - SubjectType: policies.DomainType, - Subject: domainID, - Relation: policies.DomainRelation, - ObjectType: policies.ThingType, - Object: client.ID, - }) +func (svc service) changeClientStatus(ctx context.Context, session authn.Session, client Client) (Client, error) { + dbClient, err := svc.repo.RetrieveByID(ctx, client.ID) + if err != nil { + return Client{}, errors.Wrap(svcerr.ErrViewEntity, err) } - if err := svc.policysvc.DeletePolicies(ctx, policyList); err != nil { - return errors.Wrap(svcerr.ErrRemoveEntity, err) + if dbClient.Status == client.Status { + return Client{}, errors.ErrStatusAlreadyAssigned } - return nil + client.UpdatedBy = session.UserID + + client, err = svc.repo.ChangeStatus(ctx, client) + if err != nil { + return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err) + } + return client, nil } diff --git a/things/service_test.go b/things/service_test.go index 79aa727ed3..f9d2db054a 100644 --- a/things/service_test.go +++ b/things/service_test.go @@ -8,17 +8,18 @@ import ( "fmt" "testing" + chmocks "github.com/absmach/magistrala/channels/mocks" + gpmocks "github.com/absmach/magistrala/groups/mocks" "github.com/absmach/magistrala/internal/testsutil" mgauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/policies" policysvc "github.com/absmach/magistrala/pkg/policies" policymocks "github.com/absmach/magistrala/pkg/policies/mocks" "github.com/absmach/magistrala/pkg/uuid" "github.com/absmach/magistrala/things" - "github.com/absmach/magistrala/things/mocks" + thmocks "github.com/absmach/magistrala/things/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -46,18 +47,20 @@ var ( var ( pService *policymocks.Service pEvaluator *policymocks.Evaluator - cache *mocks.Cache - cRepo *mocks.Repository + cache *thmocks.Cache + repo *thmocks.Repository ) func newService() things.Service { pService = new(policymocks.Service) - pEvaluator = new(policymocks.Evaluator) - cache = new(mocks.Cache) + cache = new(thmocks.Cache) idProvider := uuid.NewMock() - cRepo = new(mocks.Repository) - - return things.NewService(pEvaluator, pService, cRepo, cache, idProvider) + sidProvider := uuid.NewMock() + repo = new(thmocks.Repository) + chgRPCClient := new(chmocks.ChannelsServiceClient) + gpgRPCClient := new(gpmocks.GroupsServiceClient) + tsv, _ := things.NewService(repo, pService, cache, chgRPCClient, gpgRPCClient, idProvider, sidProvider) + return tsv } func TestCreateClients(t *testing.T) { @@ -268,7 +271,7 @@ func TestCreateClients(t *testing.T) { } for _, tc := range cases { - repoCall := cRepo.On("Save", context.Background(), mock.Anything).Return([]things.Client{tc.thing}, tc.saveErr) + repoCall := repo.On("Save", context.Background(), mock.Anything).Return([]things.Client{tc.thing}, tc.saveErr) policyCall := pService.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPolicyErr) policyCall1 := pService.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePolicyErr) expected, err := svc.CreateClients(context.Background(), mgauthn.Session{}, tc.thing) @@ -326,7 +329,7 @@ func TestViewClient(t *testing.T) { } for _, tc := range cases { - repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall1 := repo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.response, tc.err) rThing, err := svc.View(context.Background(), mgauthn.Session{}, 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, rThing, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, rThing)) @@ -460,7 +463,7 @@ func TestListClients(t *testing.T) { for _, tc := range cases { listAllObjectsCall := pService.On("ListAllObjects", mock.Anything, mock.Anything).Return(tc.listObjectsResponse, tc.listObjectsErr) - retrieveAllCall := cRepo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) + retrieveAllCall := repo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) listPermissionsCall := pService.On("ListPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) page, err := svc.ListClients(context.Background(), tc.session, 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)) @@ -587,7 +590,7 @@ func TestListClients(t *testing.T) { Permission: "", ObjectType: policysvc.ThingType, }).Return(tc.listObjectsResponse, tc.listObjectsErr) - retrieveAllCall := cRepo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) + retrieveAllCall := repo.On("SearchClients", mock.Anything, mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) listPermissionsCall := pService.On("ListPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) page, err := svc.ListClients(context.Background(), tc.session, 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)) @@ -640,7 +643,7 @@ func TestUpdateClient(t *testing.T) { } for _, tc := range cases { - repoCall1 := cRepo.On("Update", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) + repoCall1 := repo.On("Update", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) updatedThing, err := svc.Update(context.Background(), tc.session, tc.thing) 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, updatedThing, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedThing)) @@ -679,7 +682,7 @@ func TestUpdateTags(t *testing.T) { } for _, tc := range cases { - repoCall1 := cRepo.On("UpdateTags", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) + repoCall1 := repo.On("UpdateTags", context.Background(), mock.Anything).Return(tc.updateResponse, tc.updateErr) updatedThing, err := svc.UpdateTags(context.Background(), tc.session, tc.thing) 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, updatedThing, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedThing)) @@ -725,7 +728,7 @@ func TestUpdateSecret(t *testing.T) { } for _, tc := range cases { - repoCall := cRepo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.updateSecretResponse, tc.updateErr) + repoCall := repo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.updateSecretResponse, tc.updateErr) updatedThing, err := svc.UpdateSecret(context.Background(), tc.session, tc.thing.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, updatedThing, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateSecretResponse, updatedThing)) @@ -794,8 +797,8 @@ func TestEnable(t *testing.T) { } for _, tc := range cases { - repoCall := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) - repoCall1 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) + repoCall := repo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) + repoCall1 := repo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) _, err := svc.Enable(context.Background(), tc.session, 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() @@ -875,8 +878,8 @@ func TestDisable(t *testing.T) { } for _, tc := range cases { - repoCall := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) - repoCall1 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) + repoCall := repo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveIDErr) + repoCall1 := repo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) repoCall2 := cache.On("Remove", mock.Anything, mock.Anything).Return(tc.removeErr) _, err := svc.Disable(context.Background(), tc.session, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) @@ -886,179 +889,6 @@ func TestDisable(t *testing.T) { } } -func TestListMembers(t *testing.T) { - svc := newService() - - nThings := uint64(10) - aThings := []things.Client{} - domainID := testsutil.GenerateUUID(t) - for i := uint64(0); i < nThings; i++ { - identity := fmt.Sprintf("member_%d@example.com", i) - thing := things.Client{ - ID: testsutil.GenerateUUID(t), - Domain: domainID, - Name: identity, - Credentials: things.Credentials{ - Identity: identity, - Secret: "password", - }, - Tags: []string{"tag1", "tag2"}, - Metadata: things.Metadata{"role": "thing"}, - } - aThings = append(aThings, thing) - } - aThings[0].Permissions = []string{"admin"} - - cases := []struct { - desc string - groupID string - page things.Page - session mgauthn.Session - listObjectsResponse policysvc.PolicyPage - listPermissionsResponse policysvc.Permissions - retreiveAllByIDsResponse things.ClientsPage - response things.MembersPage - identifyErr error - authorizeErr error - listObjectsErr error - listPermissionsErr error - retreiveAllByIDsErr error - err error - }{ - { - desc: "list members with authorized token", - session: mgauthn.Session{UserID: validID, DomainID: domainID}, - groupID: testsutil.GenerateUUID(t), - listObjectsResponse: policysvc.PolicyPage{}, - listPermissionsResponse: []string{}, - retreiveAllByIDsResponse: things.ClientsPage{ - Page: things.Page{ - Total: 0, - Offset: 0, - Limit: 0, - }, - Clients: []things.Client{}, - }, - response: things.MembersPage{ - Page: things.Page{ - Total: 0, - Offset: 0, - Limit: 0, - }, - Members: []things.Client{}, - }, - err: nil, - }, - { - desc: "list members with offset and limit", - session: mgauthn.Session{UserID: validID, DomainID: domainID}, - groupID: testsutil.GenerateUUID(t), - page: things.Page{ - Offset: 6, - Limit: nThings, - Status: things.AllStatus, - }, - listObjectsResponse: policysvc.PolicyPage{}, - listPermissionsResponse: []string{}, - retreiveAllByIDsResponse: things.ClientsPage{ - Page: things.Page{ - Total: nThings - 6 - 1, - }, - Clients: aThings[6 : nThings-1], - }, - response: things.MembersPage{ - Page: things.Page{ - Total: nThings - 6 - 1, - }, - Members: aThings[6 : nThings-1], - }, - err: nil, - }, - { - desc: "list members with an invalid id", - session: mgauthn.Session{UserID: validID, DomainID: domainID}, - groupID: wrongID, - listObjectsResponse: policysvc.PolicyPage{}, - listPermissionsResponse: []string{}, - retreiveAllByIDsResponse: things.ClientsPage{}, - response: things.MembersPage{ - Page: things.Page{ - Total: 0, - Offset: 0, - Limit: 0, - }, - }, - retreiveAllByIDsErr: svcerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - { - desc: "list members with permissions", - session: mgauthn.Session{UserID: validID, DomainID: domainID}, - groupID: testsutil.GenerateUUID(t), - page: things.Page{ - ListPerms: true, - }, - listObjectsResponse: policysvc.PolicyPage{}, - listPermissionsResponse: []string{"admin"}, - retreiveAllByIDsResponse: things.ClientsPage{ - Page: things.Page{ - Total: 1, - }, - Clients: []things.Client{aThings[0]}, - }, - response: things.MembersPage{ - Page: things.Page{ - Total: 1, - }, - Members: []things.Client{aThings[0]}, - }, - err: nil, - }, - { - desc: "list members with failed to list objects", - session: mgauthn.Session{UserID: validID, DomainID: domainID}, - groupID: testsutil.GenerateUUID(t), - page: things.Page{ - ListPerms: true, - }, - listObjectsResponse: policysvc.PolicyPage{}, - listObjectsErr: svcerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - { - desc: "list members with failed to list permissions", - session: mgauthn.Session{UserID: validID, DomainID: domainID}, - groupID: testsutil.GenerateUUID(t), - page: things.Page{ - ListPerms: true, - }, - retreiveAllByIDsResponse: things.ClientsPage{ - Page: things.Page{ - Total: 1, - }, - Clients: []things.Client{aThings[0]}, - }, - response: things.MembersPage{}, - listObjectsResponse: policysvc.PolicyPage{}, - listPermissionsResponse: []string{}, - listPermissionsErr: svcerr.ErrNotFound, - err: svcerr.ErrNotFound, - }, - } - - for _, tc := range cases { - policyCall := pService.On("ListAllObjects", mock.Anything, mock.Anything).Return(tc.listObjectsResponse, tc.listObjectsErr) - repoCall := cRepo.On("RetrieveAllByIDs", context.Background(), mock.Anything).Return(tc.retreiveAllByIDsResponse, tc.retreiveAllByIDsErr) - repoCall1 := pService.On("ListPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionsErr) - page, err := svc.ListClientsByGroup(context.Background(), tc.session, 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)) - policyCall.Unset() - repoCall.Unset() - repoCall1.Unset() - } -} - func TestDelete(t *testing.T) { svc := newService() @@ -1108,7 +938,7 @@ func TestDelete(t *testing.T) { for _, tc := range cases { repoCall := cache.On("Remove", mock.Anything, tc.clientID).Return(tc.removeErr) policyCall := pService.On("DeletePolicyFilter", context.Background(), mock.Anything).Return(tc.deletePolicyErr) - repoCall1 := cRepo.On("Delete", context.Background(), tc.clientID).Return(tc.deleteErr) + repoCall1 := repo.On("Delete", context.Background(), tc.clientID).Return(tc.deleteErr) err := svc.Delete(context.Background(), mgauthn.Session{}, 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() @@ -1116,278 +946,3 @@ func TestDelete(t *testing.T) { repoCall1.Unset() } } - -func TestShare(t *testing.T) { - svc := newService() - - clientID := "clientID" - - cases := []struct { - desc string - session mgauthn.Session - clientID string - relation string - userID string - addPoliciesErr error - err error - }{ - { - desc: "share client successfully", - session: mgauthn.Session{UserID: validID, DomainID: validID}, - clientID: clientID, - err: nil, - }, - { - desc: "share client with failed to add policies", - session: mgauthn.Session{UserID: validID, DomainID: validID}, - clientID: clientID, - addPoliciesErr: svcerr.ErrInvalidPolicy, - err: svcerr.ErrInvalidPolicy, - }, - } - - for _, tc := range cases { - policyCall := pService.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPoliciesErr) - err := svc.Share(context.Background(), tc.session, 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)) - policyCall.Unset() - } -} - -func TestUnShare(t *testing.T) { - svc := newService() - - clientID := "clientID" - - cases := []struct { - desc string - session mgauthn.Session - clientID string - relation string - userID string - deletePoliciesErr error - err error - }{ - { - desc: "unshare client successfully", - session: mgauthn.Session{UserID: validID, DomainID: validID}, - clientID: clientID, - err: nil, - }, - { - desc: "share client with failed to delete policies", - session: mgauthn.Session{UserID: validID, DomainID: validID}, - clientID: clientID, - deletePoliciesErr: svcerr.ErrInvalidPolicy, - err: svcerr.ErrInvalidPolicy, - }, - } - - for _, tc := range cases { - policyCall := pService.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesErr) - err := svc.Unshare(context.Background(), tc.session, 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)) - policyCall.Unset() - } -} - -func TestViewClientPerms(t *testing.T) { - svc := newService() - - validID := valid - - cases := []struct { - desc string - session mgauthn.Session - clientID string - listPermResponse policysvc.Permissions - listPermErr error - err error - }{ - { - desc: "view client permissions successfully", - session: mgauthn.Session{UserID: validID, DomainID: validID}, - clientID: validID, - listPermResponse: policysvc.Permissions{"admin"}, - err: nil, - }, - { - desc: "view permissions with failed retrieve list permissions response", - session: mgauthn.Session{UserID: validID, DomainID: validID}, - clientID: validID, - listPermResponse: []string{}, - listPermErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - policyCall := pService.On("ListPermissions", mock.Anything, mock.Anything, []string{}).Return(tc.listPermResponse, tc.listPermErr) - res, err := svc.ViewPerms(context.Background(), tc.session, tc.clientID) - 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.ElementsMatch(t, tc.listPermResponse, res, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.listPermResponse, res)) - } - policyCall.Unset() - } -} - -func TestIdentify(t *testing.T) { - svc := newService() - - valid := valid - - cases := []struct { - desc string - key string - cacheIDResponse string - cacheIDErr error - repoIDResponse things.Client - retrieveBySecretErr error - saveErr error - err error - }{ - { - desc: "identify client with valid key from cache", - key: valid, - cacheIDResponse: thing.ID, - err: nil, - }, - { - desc: "identify client with valid key from repo", - key: valid, - cacheIDResponse: "", - cacheIDErr: repoerr.ErrNotFound, - repoIDResponse: thing, - err: nil, - }, - { - desc: "identify client with invalid key", - key: invalid, - cacheIDResponse: "", - cacheIDErr: repoerr.ErrNotFound, - repoIDResponse: things.Client{}, - retrieveBySecretErr: repoerr.ErrNotFound, - err: repoerr.ErrNotFound, - }, - { - desc: "identify client with failed to save to cache", - key: valid, - cacheIDResponse: "", - cacheIDErr: repoerr.ErrNotFound, - repoIDResponse: thing, - saveErr: errors.ErrMalformedEntity, - err: svcerr.ErrAuthorization, - }, - } - - 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() - } -} - -func TestAuthorize(t *testing.T) { - svc := newService() - - cases := []struct { - desc string - request things.AuthzReq - cacheIDRes string - cacheIDErr error - retrieveBySecretRes things.Client - retrieveBySecretErr error - cacheSaveErr error - checkPolicyErr error - id string - err error - }{ - { - desc: "authorize client with valid key not in cache", - request: things.AuthzReq{ClientKey: valid, ChannelID: valid, Permission: policies.PublishPermission}, - cacheIDRes: "", - cacheIDErr: repoerr.ErrNotFound, - retrieveBySecretRes: things.Client{ID: valid}, - retrieveBySecretErr: nil, - cacheSaveErr: nil, - checkPolicyErr: nil, - id: valid, - err: nil, - }, - { - desc: "authorize thing with valid key in cache", - request: things.AuthzReq{ClientKey: valid, ChannelID: valid, Permission: policies.PublishPermission}, - cacheIDRes: valid, - checkPolicyErr: nil, - id: valid, - }, - { - desc: "authorize thing with invalid key not in cache for non existing thing", - request: things.AuthzReq{ClientKey: valid, ChannelID: valid, Permission: policies.PublishPermission}, - cacheIDRes: "", - cacheIDErr: repoerr.ErrNotFound, - retrieveBySecretRes: things.Client{}, - retrieveBySecretErr: repoerr.ErrNotFound, - err: repoerr.ErrNotFound, - }, - { - desc: "authorize thing with valid key not in cache with failed to save to cache", - request: things.AuthzReq{ClientKey: valid, ChannelID: valid, Permission: policies.PublishPermission}, - cacheIDRes: "", - cacheIDErr: repoerr.ErrNotFound, - retrieveBySecretRes: things.Client{ID: valid}, - cacheSaveErr: errors.ErrMalformedEntity, - err: svcerr.ErrAuthorization, - }, - { - desc: "authorize thing with valid key not in cache and failed to authorize", - request: things.AuthzReq{ClientKey: valid, ChannelID: valid, Permission: policies.PublishPermission}, - cacheIDRes: "", - cacheIDErr: repoerr.ErrNotFound, - retrieveBySecretRes: things.Client{ID: valid}, - retrieveBySecretErr: nil, - cacheSaveErr: nil, - checkPolicyErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - { - desc: "authorize thing with valid key not in cache and not authorize", - request: things.AuthzReq{ClientKey: valid, ChannelID: valid, Permission: policies.PublishPermission}, - cacheIDRes: "", - cacheIDErr: repoerr.ErrNotFound, - retrieveBySecretRes: things.Client{ID: valid}, - retrieveBySecretErr: nil, - cacheSaveErr: nil, - checkPolicyErr: svcerr.ErrAuthorization, - err: svcerr.ErrAuthorization, - }, - } - - for _, tc := range cases { - cacheCall := cache.On("ID", context.Background(), tc.request.ClientKey).Return(tc.cacheIDRes, tc.cacheIDErr) - repoCall := cRepo.On("RetrieveBySecret", context.Background(), tc.request.ClientKey).Return(tc.retrieveBySecretRes, tc.retrieveBySecretErr) - cacheCall1 := cache.On("Save", context.Background(), tc.request.ClientKey, tc.retrieveBySecretRes.ID).Return(tc.cacheSaveErr) - policyCall := pEvaluator.On("CheckPolicy", context.Background(), policies.Policy{ - SubjectType: policies.GroupType, - Subject: tc.request.ChannelID, - ObjectType: policies.ThingType, - Object: valid, - Permission: tc.request.Permission, - }).Return(tc.checkPolicyErr) - 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() - policyCall.Unset() - } -} diff --git a/things/tracing/tracing.go b/things/tracing/tracing.go index 20fe07b5fc..7d4f23171e 100644 --- a/things/tracing/tracing.go +++ b/things/tracing/tracing.go @@ -7,6 +7,7 @@ import ( "context" "github.com/absmach/magistrala/pkg/authn" + rmTrace "github.com/absmach/magistrala/pkg/roles/rolemanager/tracing" "github.com/absmach/magistrala/things" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -17,11 +18,16 @@ var _ things.Service = (*tracingMiddleware)(nil) type tracingMiddleware struct { tracer trace.Tracer svc things.Service + rmTrace.RoleManagerTracing } // New returns a new group service with tracing capabilities. func New(svc things.Service, tracer trace.Tracer) things.Service { - return &tracingMiddleware{tracer, svc} + return &tracingMiddleware{ + tracer: tracer, + svc: svc, + RoleManagerTracing: rmTrace.NewRoleManagerTracing("group", svc, tracer), + } } // CreateClients traces the "CreateClients" operation of the wrapped policies.Service. @@ -39,13 +45,6 @@ func (tm *tracingMiddleware) View(ctx context.Context, session authn.Session, id return tm.svc.View(ctx, session, id) } -// ViewPerms traces the "ViewPerms" operation of the wrapped policies.Service. -func (tm *tracingMiddleware) ViewPerms(ctx context.Context, session authn.Session, id string) ([]string, error) { - ctx, span := tm.tracer.Start(ctx, "svc_view_client_permissions", trace.WithAttributes(attribute.String("id", id))) - defer span.End() - return tm.svc.ViewPerms(ctx, session, id) -} - // ListClients traces the "ListClients" operation of the wrapped policies.Service. func (tm *tracingMiddleware) ListClients(ctx context.Context, session authn.Session, reqUserID string, pm things.Page) (things.ClientsPage, error) { ctx, span := tm.tracer.Start(ctx, "svc_list_clients") @@ -96,47 +95,24 @@ func (tm *tracingMiddleware) Disable(ctx context.Context, session authn.Session, return tm.svc.Disable(ctx, session, id) } -// ListClientsByGroup traces the "ListClientsByGroup" operation of the wrapped policies.Service. -func (tm *tracingMiddleware) ListClientsByGroup(ctx context.Context, session authn.Session, groupID string, pm things.Page) (things.MembersPage, error) { - ctx, span := tm.tracer.Start(ctx, "svc_list_clients_by_channel", trace.WithAttributes(attribute.String("groupID", groupID))) - defer span.End() - - return tm.svc.ListClientsByGroup(ctx, session, groupID, pm) -} - -// 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))) - defer span.End() - - return tm.svc.Identify(ctx, key) -} - -// Authorize traces the "Authorize" operation of the wrapped things.Service. -func (tm *tracingMiddleware) Authorize(ctx context.Context, req things.AuthzReq) (string, error) { - ctx, span := tm.tracer.Start(ctx, "connect", trace.WithAttributes(attribute.String("thingKey", req.ClientKey), attribute.String("channelID", req.ChannelID))) - defer span.End() - - return tm.svc.Authorize(ctx, req) -} - -// Share traces the "Share" operation of the wrapped things.Service. -func (tm *tracingMiddleware) Share(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { - ctx, span := tm.tracer.Start(ctx, "share", trace.WithAttributes(attribute.String("id", id), attribute.String("relation", relation), attribute.StringSlice("user_ids", userids))) +// Delete traces the "Delete" operation of the wrapped things.Service. +func (tm *tracingMiddleware) Delete(ctx context.Context, session authn.Session, id string) error { + ctx, span := tm.tracer.Start(ctx, "delete_client", trace.WithAttributes(attribute.String("id", id))) defer span.End() - return tm.svc.Share(ctx, session, id, relation, userids...) + return tm.svc.Delete(ctx, session, id) } -// Unshare traces the "Unshare" operation of the wrapped things.Service. -func (tm *tracingMiddleware) Unshare(ctx context.Context, session authn.Session, id, relation string, userids ...string) error { - ctx, span := tm.tracer.Start(ctx, "unshare", trace.WithAttributes(attribute.String("id", id), attribute.String("relation", relation), attribute.StringSlice("user_ids", userids))) +func (tm *tracingMiddleware) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) error { + ctx, span := tm.tracer.Start(ctx, "set_parent_group", trace.WithAttributes( + attribute.String("id", id), + attribute.String("parent_group_id", parentGroupID), + )) defer span.End() - return tm.svc.Unshare(ctx, session, id, relation, userids...) + return tm.svc.SetParentGroup(ctx, session, parentGroupID, id) } -// Delete traces the "Delete" operation of the wrapped things.Service. -func (tm *tracingMiddleware) Delete(ctx context.Context, session authn.Session, id string) error { - ctx, span := tm.tracer.Start(ctx, "delete_client", trace.WithAttributes(attribute.String("id", id))) +func (tm *tracingMiddleware) RemoveParentGroup(ctx context.Context, session authn.Session, id string) error { + ctx, span := tm.tracer.Start(ctx, "remove_parent_group", trace.WithAttributes(attribute.String("id", id))) defer span.End() - return tm.svc.Delete(ctx, session, id) + return tm.svc.RemoveParentGroup(ctx, session, id) } diff --git a/tools/config/mockery.yaml b/tools/config/mockery.yaml index 69e231658a..bbe4fe1f7a 100644 --- a/tools/config/mockery.yaml +++ b/tools/config/mockery.yaml @@ -6,23 +6,41 @@ filename: "{{.InterfaceName}}.go" outpkg: "mocks" boilerplate-file: "./tools/config/boilerplate.txt" packages: - github.com/absmach/magistrala: + github.com/absmach/magistrala/internal/grpc/things/v1: interfaces: ThingsServiceClient: config: dir: "./things/mocks" mockname: "ThingsServiceClient" filename: "things_client.go" + github.com/absmach/magistrala/internal/grpc/domains/v1: + interfaces: DomainsServiceClient: config: - dir: "./auth/mocks" + dir: "./domains/mocks" mockname: "DomainsServiceClient" filename: "domains_client.go" + github.com/absmach/magistrala/internal/grpc/token/v1: + interfaces: TokenServiceClient: config: dir: "./auth/mocks" mockname: "TokenServiceClient" filename: "token_client.go" + github.com/absmach/magistrala/internal/grpc/channels/v1: + interfaces: + ChannelsServiceClient: + config: + dir: "./channels/mocks" + mockname: "ChannelsServiceClient" + filename: "channels_client.go" + github.com/absmach/magistrala/internal/grpc/groups/v1: + interfaces: + GroupsServiceClient: + config: + dir: "./groups/mocks" + mockname: "GroupsServiceClient" + filename: "groups_client.go" github.com/absmach/magistrala/certs/pki/amcerts: interfaces: diff --git a/users/api/endpoint_test.go b/users/api/endpoint_test.go index 32d219cb1a..29529abb80 100644 --- a/users/api/endpoint_test.go +++ b/users/api/endpoint_test.go @@ -13,9 +13,9 @@ import ( "strings" "testing" - "github.com/absmach/magistrala" authmocks "github.com/absmach/magistrala/auth/mocks" "github.com/absmach/magistrala/internal/api" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" @@ -23,7 +23,6 @@ import ( authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" - gmocks "github.com/absmach/magistrala/pkg/groups/mocks" oauth2mocks "github.com/absmach/magistrala/pkg/oauth2/mocks" "github.com/absmach/magistrala/users" httpapi "github.com/absmach/magistrala/users/api" @@ -86,19 +85,17 @@ func (tr testRequest) make() (*http.Response, error) { return tr.user.Do(req) } -func newUsersServer() (*httptest.Server, *mocks.Service, *gmocks.Service, *authnmocks.Authentication) { +func newUsersServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) { svc := new(mocks.Service) - gsvc := new(gmocks.Service) - logger := mglog.NewMock() mux := chi.NewRouter() provider := new(oauth2mocks.Provider) provider.On("Name").Return("test") authn := new(authnmocks.Authentication) token := new(authmocks.TokenServiceClient) - httpapi.MakeHandler(svc, authn, token, true, gsvc, mux, logger, "", passRegex, provider) + httpapi.MakeHandler(svc, authn, token, true, mux, logger, "", passRegex, provider) - return httptest.NewServer(mux), svc, gsvc, authn + return httptest.NewServer(mux), svc, authn } func toJSON(data interface{}) string { @@ -110,7 +107,7 @@ func toJSON(data interface{}) string { } func TestRegister(t *testing.T) { - us, svc, _, _ := newUsersServer() + us, svc, _ := newUsersServer() defer us.Close() cases := []struct { @@ -254,7 +251,7 @@ func TestRegister(t *testing.T) { } func TestView(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -340,7 +337,7 @@ func TestView(t *testing.T) { } func TestViewProfile(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -418,7 +415,7 @@ func TestViewProfile(t *testing.T) { } func TestListUsers(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -787,7 +784,7 @@ func TestListUsers(t *testing.T) { } func TestSearchUsers(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -922,7 +919,7 @@ func TestSearchUsers(t *testing.T) { } func TestUpdate(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() newName := "newname" @@ -1061,7 +1058,7 @@ func TestUpdate(t *testing.T) { } func TestUpdateTags(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() defer us.Close() @@ -1202,7 +1199,7 @@ func TestUpdateTags(t *testing.T) { } func TestUpdateEmail(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() newuseremail := "newuseremail@example.com" @@ -1647,7 +1644,7 @@ func TestUpdateProfilePicture(t *testing.T) { } func TestPasswordResetRequest(t *testing.T) { - us, svc, _, _ := newUsersServer() + us, svc, _ := newUsersServer() defer us.Close() testemail := "test@example.com" @@ -1745,7 +1742,7 @@ func TestPasswordResetRequest(t *testing.T) { } func TestPasswordReset(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() strongPass := "StrongPassword" @@ -1851,7 +1848,7 @@ func TestPasswordReset(t *testing.T) { } func TestUpdateRole(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -1979,7 +1976,7 @@ func TestUpdateRole(t *testing.T) { } func TestUpdateSecret(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -2118,7 +2115,7 @@ func TestUpdateSecret(t *testing.T) { } func TestIssueToken(t *testing.T) { - us, svc, _, _ := newUsersServer() + us, svc, _ := newUsersServer() defer us.Close() validUsername := "valid" @@ -2184,7 +2181,7 @@ func TestIssueToken(t *testing.T) { body: strings.NewReader(tc.data), } - svcCall := svc.On("IssueToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&magistrala.Token{AccessToken: validToken}, tc.err) + svcCall := svc.On("IssueToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&grpcTokenV1.Token{AccessToken: validToken}, tc.err) res, err := req.make() assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) if tc.err != nil { @@ -2203,7 +2200,7 @@ func TestIssueToken(t *testing.T) { } func TestRefreshToken(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -2273,37 +2270,35 @@ func TestRefreshToken(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - req := testRequest{ - user: us.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/users/tokens/refresh", us.URL), - contentType: tc.contentType, - body: strings.NewReader(tc.data), - token: tc.token, - } - authnCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := svc.On("RefreshToken", mock.Anything, tc.authnRes, tc.token, mock.Anything).Return(&magistrala.Token{AccessToken: validToken}, tc.err) - res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) - if tc.err != nil { - 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)) + req := testRequest{ + user: us.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/users/tokens/refresh", us.URL), + contentType: tc.contentType, + body: strings.NewReader(tc.data), + token: tc.token, + } + authnCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) + svcCall := svc.On("RefreshToken", mock.Anything, tc.authnRes, tc.token, mock.Anything).Return(&grpcTokenV1.Token{AccessToken: validToken}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + if tc.err != nil { + 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, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) - svcCall.Unset() - authnCall.Unset() - }) + 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() + authnCall.Unset() } } func TestEnable(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { desc string @@ -2402,7 +2397,7 @@ func TestEnable(t *testing.T) { } func TestDisable(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -2493,7 +2488,7 @@ func TestDisable(t *testing.T) { } func TestDelete(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -2570,7 +2565,7 @@ func TestDelete(t *testing.T) { } func TestListUsersByUserGroupId(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -2897,7 +2892,7 @@ func TestListUsersByUserGroupId(t *testing.T) { } func TestListUsersByChannelID(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -3235,7 +3230,7 @@ func TestListUsersByChannelID(t *testing.T) { } func TestListUsersByDomainID(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -3579,7 +3574,7 @@ func TestListUsersByDomainID(t *testing.T) { } func TestListUsersByThingID(t *testing.T) { - us, svc, _, authn := newUsersServer() + us, svc, authn := newUsersServer() defer us.Close() cases := []struct { @@ -3892,449 +3887,6 @@ func TestListUsersByThingID(t *testing.T) { } } -func TestAssignUsers(t *testing.T) { - us, _, gsvc, authn := newUsersServer() - defer us.Close() - - cases := []struct { - desc string - domainID string - token string - groupID string - reqBody interface{} - authnRes mgauthn.Session - authnErr error - status int - err error - }{ - { - desc: "assign users to a group successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusCreated, - err: nil, - }, - { - desc: "assign users to a group with invalid token", - domainID: domainID, - token: inValidToken, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "assign users to a group with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "assign users to a group with empty relation", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "assign users to a group with empty user ids", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "assign users to a group with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: map[string]interface{}{ - "relation": make(chan int), - }, - status: http.StatusBadRequest, - err: nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - user: us.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/groups/%s/users/assign", us.URL, tc.domainID, tc.groupID), - token: tc.token, - body: strings.NewReader(data), - } - authnCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Assign", mock.Anything, tc.authnRes, 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() - authnCall.Unset() - }) - } -} - -func TestUnassignUsers(t *testing.T) { - us, _, gsvc, authn := newUsersServer() - defer us.Close() - - cases := []struct { - desc string - domainID string - token string - groupID string - reqBody interface{} - authnRes mgauthn.Session - authnErr error - status int - err error - }{ - { - desc: "unassign users from a group successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusNoContent, - err: nil, - }, - { - desc: "unassign users from a group with invalid token", - domainID: domainID, - token: inValidToken, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "unassign users from a group with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "unassign users from a group with empty relation", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "", - UserIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "unassign users from a group with empty user ids", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - Relation: "member", - UserIDs: []string{}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "unassign users from a group with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: map[string]interface{}{ - "relation": make(chan int), - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - user: us.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/groups/%s/users/unassign", us.URL, tc.domainID, tc.groupID), - token: tc.token, - body: strings.NewReader(data), - } - - authnCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Unassign", mock.Anything, tc.authnRes, 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() - authnCall.Unset() - }) - } -} - -func TestAssignGroups(t *testing.T) { - us, _, gsvc, authn := newUsersServer() - defer us.Close() - - cases := []struct { - desc string - domainID string - token string - groupID string - reqBody interface{} - authnRes mgauthn.Session - authnErr error - status int - err error - }{ - { - desc: "assign groups to a parent group successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusCreated, - err: nil, - }, - { - desc: "assign groups to a parent group with invalid token", - domainID: domainID, - token: inValidToken, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "assign groups to a parent group with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "assign groups to a parent group with empty parent group id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: "", - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "assign groups to a parent group with empty group ids", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "assign groups to a parent group with invalid request body", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: map[string]interface{}{ - "group_ids": make(chan int), - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - user: us.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/groups/%s/groups/assign", us.URL, tc.domainID, tc.groupID), - token: tc.token, - body: strings.NewReader(data), - } - - authnCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Assign", mock.Anything, tc.authnRes, tc.groupID, mock.Anything, "groups", 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() - authnCall.Unset() - }) - } -} - -func TestUnassignGroups(t *testing.T) { - us, _, gsvc, authn := newUsersServer() - defer us.Close() - - cases := []struct { - desc string - token string - domainID string - groupID string - reqBody interface{} - authnRes mgauthn.Session - authnErr error - status int - err error - }{ - { - desc: "unassign groups from a parent group successfully", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusNoContent, - err: nil, - }, - { - desc: "unassign groups from a parent group with invalid token", - domainID: domainID, - token: inValidToken, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - authnErr: svcerr.ErrAuthentication, - err: svcerr.ErrAuthentication, - }, - { - desc: "unassign groups from a parent group with empty token", - domainID: domainID, - token: "", - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusUnauthorized, - err: apiutil.ErrBearerToken, - }, - { - desc: "unassign groups from a parent group with empty group id", - domainID: domainID, - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID}, - groupID: "", - reqBody: groupReqBody{ - GroupIDs: []string{testsutil.GenerateUUID(t), testsutil.GenerateUUID(t)}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "unassign groups from a parent group with empty group ids", - token: validToken, - authnRes: mgauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID}, - groupID: validID, - reqBody: groupReqBody{ - GroupIDs: []string{}, - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - { - desc: "unassign groups from a parent group with invalid request body", - token: validToken, - groupID: validID, - reqBody: map[string]interface{}{ - "group_ids": make(chan int), - }, - status: http.StatusBadRequest, - err: apiutil.ErrValidation, - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - data := toJSON(tc.reqBody) - req := testRequest{ - user: us.Client(), - method: http.MethodPost, - url: fmt.Sprintf("%s/%s/groups/%s/groups/unassign", us.URL, tc.domainID, tc.groupID), - token: tc.token, - body: strings.NewReader(data), - } - - authnCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr) - svcCall := gsvc.On("Unassign", mock.Anything, mock.Anything, tc.groupID, mock.Anything, "groups", 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() - authnCall.Unset() - }) - } -} - type respBody struct { Err string `json:"error"` Message string `json:"message"` diff --git a/users/api/endpoints.go b/users/api/endpoints.go index dcb8986f4f..3fb20c3081 100644 --- a/users/api/endpoints.go +++ b/users/api/endpoints.go @@ -27,7 +27,7 @@ func registrationEndpoint(svc users.Service, selfRegister bool) endpoint.Endpoin if !selfRegister { session, ok = ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } } @@ -52,7 +52,7 @@ func viewEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user, err := svc.View(ctx, session, req.id) if err != nil { @@ -67,7 +67,7 @@ func viewProfileEndpoint(svc users.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } client, err := svc.ViewProfile(ctx, session) if err != nil { @@ -87,7 +87,7 @@ func listUsersEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } pm := users.Page{ @@ -174,7 +174,7 @@ func listMembersByGroupEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } page, err := svc.ListMembers(ctx, session, req.objectKind, req.objectID, req.Page) @@ -197,7 +197,7 @@ func listMembersByChannelEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } page, err := svc.ListMembers(ctx, session, req.objectKind, req.objectID, req.Page) @@ -219,7 +219,7 @@ func listMembersByThingEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } page, err := svc.ListMembers(ctx, session, req.objectKind, req.objectID, req.Page) @@ -241,7 +241,7 @@ func listMembersByDomainEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } page, err := svc.ListMembers(ctx, session, req.objectKind, req.objectID, req.Page) @@ -262,7 +262,7 @@ func updateEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user := users.User{ @@ -290,7 +290,7 @@ func updateTagsEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user := users.User{ @@ -316,7 +316,7 @@ func updateEmailEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user, err := svc.UpdateEmail(ctx, session, req.id, req.Email) @@ -365,7 +365,7 @@ func passwordResetEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } if err := svc.ResetSecret(ctx, session, req.Password); err != nil { return nil, err @@ -384,7 +384,7 @@ func updateSecretEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user, err := svc.UpdateSecret(ctx, session, req.OldSecret, req.NewSecret) if err != nil { @@ -456,7 +456,7 @@ func updateRoleEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user, err := svc.UpdateRole(ctx, session, user) @@ -497,7 +497,7 @@ func refreshTokenEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } token, err := svc.RefreshToken(ctx, session, req.RefreshToken) @@ -522,7 +522,7 @@ func enableEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user, err := svc.Enable(ctx, session, req.id) @@ -543,7 +543,7 @@ func disableEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } user, err := svc.Disable(ctx, session, req.id) @@ -564,7 +564,7 @@ func deleteEndpoint(svc users.Service) endpoint.Endpoint { session, ok := ctx.Value(api.SessionKey).(authn.Session) if !ok { - return nil, svcerr.ErrAuthorization + return nil, svcerr.ErrAuthentication } if err := svc.Delete(ctx, session, req.id); err != nil { diff --git a/users/api/groups.go b/users/api/groups.go deleted file mode 100644 index 72cb478c6f..0000000000 --- a/users/api/groups.go +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) Abstract Machines -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - "encoding/json" - "log/slog" - "net/http" - - "github.com/absmach/magistrala/internal/api" - gapi "github.com/absmach/magistrala/internal/groups/api" - "github.com/absmach/magistrala/pkg/apiutil" - "github.com/absmach/magistrala/pkg/authn" - mgauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" - "github.com/absmach/magistrala/pkg/groups" - "github.com/absmach/magistrala/pkg/policies" - "github.com/go-chi/chi/v5" - "github.com/go-kit/kit/endpoint" - kithttp "github.com/go-kit/kit/transport/http" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -// MakeHandler returns a HTTP handler for Groups API endpoints. -func groupsHandler(svc groups.Service, authn mgauthn.Authentication, r *chi.Mux, logger *slog.Logger) http.Handler { - opts := []kithttp.ServerOption{ - kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), - } - - r.Group(func(r chi.Router) { - r.Use(api.AuthenticateMiddleware(authn, true)) - - r.Route("/{domainID}/groups", func(r chi.Router) { - r.Post("/", otelhttp.NewHandler(kithttp.NewServer( - gapi.CreateGroupEndpoint(svc, policies.NewGroupKind), - gapi.DecodeGroupCreate, - api.EncodeResponse, - opts..., - ), "create_group").ServeHTTP) - - r.Get("/{groupID}", otelhttp.NewHandler(kithttp.NewServer( - gapi.ViewGroupEndpoint(svc), - gapi.DecodeGroupRequest, - api.EncodeResponse, - opts..., - ), "view_group").ServeHTTP) - - r.Delete("/{groupID}", otelhttp.NewHandler(kithttp.NewServer( - gapi.DeleteGroupEndpoint(svc), - gapi.DecodeGroupRequest, - api.EncodeResponse, - opts..., - ), "delete_group").ServeHTTP) - - r.Get("/{groupID}/permissions", otelhttp.NewHandler(kithttp.NewServer( - gapi.ViewGroupPermsEndpoint(svc), - gapi.DecodeGroupPermsRequest, - api.EncodeResponse, - opts..., - ), "view_group_permissions").ServeHTTP) - - r.Put("/{groupID}", otelhttp.NewHandler(kithttp.NewServer( - gapi.UpdateGroupEndpoint(svc), - gapi.DecodeGroupUpdate, - api.EncodeResponse, - opts..., - ), "update_group").ServeHTTP) - - r.Get("/", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "groups", "users"), - gapi.DecodeListGroupsRequest, - api.EncodeResponse, - opts..., - ), "list_groups").ServeHTTP) - - r.Get("/{groupID}/children", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "groups", "users"), - gapi.DecodeListChildrenRequest, - api.EncodeResponse, - opts..., - ), "list_children").ServeHTTP) - - r.Get("/{groupID}/parents", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "groups", "users"), - gapi.DecodeListParentsRequest, - api.EncodeResponse, - opts..., - ), "list_parents").ServeHTTP) - - r.Post("/{groupID}/enable", otelhttp.NewHandler(kithttp.NewServer( - gapi.EnableGroupEndpoint(svc), - gapi.DecodeChangeGroupStatus, - api.EncodeResponse, - opts..., - ), "enable_group").ServeHTTP) - - r.Post("/{groupID}/disable", otelhttp.NewHandler(kithttp.NewServer( - gapi.DisableGroupEndpoint(svc), - gapi.DecodeChangeGroupStatus, - api.EncodeResponse, - opts..., - ), "disable_group").ServeHTTP) - - r.Post("/{groupID}/users/assign", otelhttp.NewHandler(kithttp.NewServer( - assignUsersEndpoint(svc), - decodeAssignUsersRequest, - api.EncodeResponse, - opts..., - ), "assign_users").ServeHTTP) - - r.Post("/{groupID}/users/unassign", otelhttp.NewHandler(kithttp.NewServer( - unassignUsersEndpoint(svc), - decodeUnassignUsersRequest, - api.EncodeResponse, - opts..., - ), "unassign_users").ServeHTTP) - - r.Post("/{groupID}/groups/assign", otelhttp.NewHandler(kithttp.NewServer( - assignGroupsEndpoint(svc), - decodeAssignGroupsRequest, - api.EncodeResponse, - opts..., - ), "assign_groups").ServeHTTP) - - r.Post("/{groupID}/groups/unassign", otelhttp.NewHandler(kithttp.NewServer( - unassignGroupsEndpoint(svc), - decodeUnassignGroupsRequest, - api.EncodeResponse, - opts..., - ), "unassign_groups").ServeHTTP) - }) - - // The ideal placeholder name should be {channelID}, but gapi.DecodeListGroupsRequest uses {memberID} as a placeholder for the ID. - // So here, we are using {memberID} as the placeholder. - r.Get("/{domainID}/channels/{memberID}/groups", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "groups", "channels"), - gapi.DecodeListGroupsRequest, - api.EncodeResponse, - opts..., - ), "list_groups_by_channel_id").ServeHTTP) - - r.Get("/{domainID}/users/{memberID}/groups", otelhttp.NewHandler(kithttp.NewServer( - gapi.ListGroupsEndpoint(svc, "groups", "users"), - gapi.DecodeListGroupsRequest, - api.EncodeResponse, - opts..., - ), "list_groups_by_user_id").ServeHTTP) - }) - - return r -} - -func decodeAssignUsersRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := assignUsersReq{ - groupID: chi.URLParam(r, "groupID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - return req, nil -} - -func decodeUnassignUsersRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := unassignUsersReq{ - groupID: chi.URLParam(r, "groupID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - return req, nil -} - -func assignUsersEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignUsersReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - if err := svc.Assign(ctx, session, req.groupID, req.Relation, "users", req.UserIDs...); err != nil { - return nil, err - } - return assignUsersRes{}, nil - } -} - -func unassignUsersEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(unassignUsersReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Unassign(ctx, session, req.groupID, req.Relation, "users", req.UserIDs...); err != nil { - return nil, err - } - return unassignUsersRes{}, nil - } -} - -func decodeAssignGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := assignGroupsReq{ - groupID: chi.URLParam(r, "groupID"), - domainID: chi.URLParam(r, "domainID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - return req, nil -} - -func decodeUnassignGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) { - req := unassignGroupsReq{ - groupID: chi.URLParam(r, "groupID"), - domainID: chi.URLParam(r, "domainID"), - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity)) - } - return req, nil -} - -func assignGroupsEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(assignGroupsReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - if err := svc.Assign(ctx, session, req.groupID, policies.ParentGroupRelation, policies.GroupsKind, req.GroupIDs...); err != nil { - return nil, err - } - return assignUsersRes{}, nil - } -} - -func unassignGroupsEndpoint(svc groups.Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - req := request.(unassignGroupsReq) - if err := req.validate(); err != nil { - return nil, errors.Wrap(apiutil.ErrValidation, err) - } - - session, ok := ctx.Value(api.SessionKey).(authn.Session) - if !ok { - return nil, svcerr.ErrAuthorization - } - - if err := svc.Unassign(ctx, session, req.groupID, policies.ParentGroupRelation, policies.GroupsKind, req.GroupIDs...); err != nil { - return nil, err - } - return unassignUsersRes{}, nil - } -} diff --git a/users/api/transport.go b/users/api/transport.go index e3334b2ab8..5918f3791f 100644 --- a/users/api/transport.go +++ b/users/api/transport.go @@ -9,8 +9,8 @@ import ( "regexp" "github.com/absmach/magistrala" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" mgauthn "github.com/absmach/magistrala/pkg/authn" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/pkg/oauth2" "github.com/absmach/magistrala/users" "github.com/go-chi/chi/v5" @@ -18,9 +18,8 @@ import ( ) // MakeHandler returns a HTTP handler for Users and Groups API endpoints. -func MakeHandler(cls users.Service, authn mgauthn.Authentication, tokenClient magistrala.TokenServiceClient, selfRegister bool, grps groups.Service, mux *chi.Mux, logger *slog.Logger, instanceID string, pr *regexp.Regexp, providers ...oauth2.Provider) http.Handler { - usersHandler(cls, authn, tokenClient, selfRegister, mux, logger, pr, providers...) - groupsHandler(grps, authn, mux, logger) +func MakeHandler(cls users.Service, authn mgauthn.Authentication, tokensvc grpcTokenV1.TokenServiceClient, selfRegister bool, mux *chi.Mux, logger *slog.Logger, instanceID string, pr *regexp.Regexp, providers ...oauth2.Provider) http.Handler { + mux = usersHandler(cls, authn, tokensvc, selfRegister, mux, logger, pr, providers...) mux.Get("/health", magistrala.Health("users", instanceID)) mux.Handle("/metrics", promhttp.Handler()) diff --git a/users/api/users.go b/users/api/users.go index c712034d70..4597c9e50e 100644 --- a/users/api/users.go +++ b/users/api/users.go @@ -11,9 +11,9 @@ import ( "regexp" "strings" - "github.com/absmach/magistrala" mgauth "github.com/absmach/magistrala/auth" "github.com/absmach/magistrala/internal/api" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/apiutil" mgauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" @@ -28,7 +28,7 @@ import ( var passRegex = regexp.MustCompile("^.{8,}$") // usersHandler returns a HTTP handler for API endpoints. -func usersHandler(svc users.Service, authn mgauthn.Authentication, tokenClient magistrala.TokenServiceClient, selfRegister bool, r *chi.Mux, logger *slog.Logger, pr *regexp.Regexp, providers ...oauth2.Provider) http.Handler { +func usersHandler(svc users.Service, authn mgauthn.Authentication, tokenClient grpcTokenV1.TokenServiceClient, selfRegister bool, r *chi.Mux, logger *slog.Logger, pr *regexp.Regexp, providers ...oauth2.Provider) *chi.Mux { passRegex = pr opts := []kithttp.ServerOption{ @@ -668,7 +668,7 @@ func queryPageParams(r *http.Request, defPermission string) (users.Page, error) } // oauth2CallbackHandler is a http.HandlerFunc that handles OAuth2 callbacks. -func oauth2CallbackHandler(oauth oauth2.Provider, svc users.Service, tokenClient magistrala.TokenServiceClient) http.HandlerFunc { +func oauth2CallbackHandler(oauth oauth2.Provider, svc users.Service, tokenClient grpcTokenV1.TokenServiceClient) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !oauth.IsEnabled() { http.Redirect(w, r, oauth.ErrorURL()+"?error=oauth%20provider%20is%20disabled", http.StatusSeeOther) @@ -703,7 +703,7 @@ func oauth2CallbackHandler(oauth oauth2.Provider, svc users.Service, tokenClient return } - jwt, err := tokenClient.Issue(r.Context(), &magistrala.IssueReq{ + jwt, err := tokenClient.Issue(r.Context(), &grpcTokenV1.IssueReq{ UserId: user.ID, Type: uint32(mgauth.AccessKey), }) diff --git a/users/delete_handler.go b/users/delete_handler.go index cbe623b684..b98890627f 100644 --- a/users/delete_handler.go +++ b/users/delete_handler.go @@ -14,7 +14,7 @@ import ( "log/slog" "time" - "github.com/absmach/magistrala" + grpcDomainsV1 "github.com/absmach/magistrala/internal/grpc/domains/v1" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/policies" ) @@ -23,14 +23,14 @@ const defLimit = uint64(100) type handler struct { users Repository - domains magistrala.DomainsServiceClient + domains grpcDomainsV1.DomainsServiceClient policies policies.Service checkInterval time.Duration deleteAfter time.Duration logger *slog.Logger } -func NewDeleteHandler(ctx context.Context, users Repository, policyService policies.Service, domainsClient magistrala.DomainsServiceClient, defCheckInterval, deleteAfter time.Duration, logger *slog.Logger) { +func NewDeleteHandler(ctx context.Context, users Repository, policyService policies.Service, domainsClient grpcDomainsV1.DomainsServiceClient, defCheckInterval, deleteAfter time.Duration, logger *slog.Logger) { handler := &handler{ users: users, domains: domainsClient, @@ -73,7 +73,7 @@ func (h *handler) handle(ctx context.Context) { continue } - deletedRes, err := h.domains.DeleteUserFromDomains(ctx, &magistrala.DeleteUserReq{ + deletedRes, err := h.domains.DeleteUserFromDomains(ctx, &grpcDomainsV1.DeleteUserReq{ Id: u.ID, }) if err != nil { diff --git a/users/events/streams.go b/users/events/streams.go index 0820a0e279..8ade66706b 100644 --- a/users/events/streams.go +++ b/users/events/streams.go @@ -6,7 +6,7 @@ package events import ( "context" - "github.com/absmach/magistrala" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/events" "github.com/absmach/magistrala/pkg/events/store" @@ -290,7 +290,7 @@ func (es *eventStore) GenerateResetToken(ctx context.Context, email, host string return es.Publish(ctx, event) } -func (es *eventStore) IssueToken(ctx context.Context, username, secret string) (*magistrala.Token, error) { +func (es *eventStore) IssueToken(ctx context.Context, username, secret string) (*grpcTokenV1.Token, error) { token, err := es.svc.IssueToken(ctx, username, secret) if err != nil { return token, err @@ -307,7 +307,7 @@ func (es *eventStore) IssueToken(ctx context.Context, username, secret string) ( return token, nil } -func (es *eventStore) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*magistrala.Token, error) { +func (es *eventStore) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*grpcTokenV1.Token, error) { token, err := es.svc.RefreshToken(ctx, session, refreshToken) if err != nil { return token, err diff --git a/users/middleware/authorization.go b/users/middleware/authorization.go index 5001be3a12..6b434472b6 100644 --- a/users/middleware/authorization.go +++ b/users/middleware/authorization.go @@ -6,8 +6,8 @@ package middleware import ( "context" - "github.com/absmach/magistrala" mgauth "github.com/absmach/magistrala/auth" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/authz" mgauthz "github.com/absmach/magistrala/pkg/authz" @@ -147,9 +147,10 @@ func (am *authorizationMiddleware) SendPasswordReset(ctx context.Context, host, } func (am *authorizationMiddleware) UpdateRole(ctx context.Context, session authn.Session, user users.User) (users.User, error) { - if err := am.checkSuperAdmin(ctx, session.UserID); err == nil { - session.SuperAdmin = true + if err := am.checkSuperAdmin(ctx, session.UserID); err != nil { + return users.User{}, err } + session.SuperAdmin = true if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, user.ID, policies.MembershipPermission, policies.PlatformType, policies.MagistralaObject); err != nil { return users.User{}, err } @@ -185,11 +186,11 @@ func (am *authorizationMiddleware) Identify(ctx context.Context, session authn.S return am.svc.Identify(ctx, session) } -func (am *authorizationMiddleware) IssueToken(ctx context.Context, username, secret string) (*magistrala.Token, error) { +func (am *authorizationMiddleware) IssueToken(ctx context.Context, username, secret string) (*grpcTokenV1.Token, error) { return am.svc.IssueToken(ctx, username, secret) } -func (am *authorizationMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*magistrala.Token, error) { +func (am *authorizationMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*grpcTokenV1.Token, error) { return am.svc.RefreshToken(ctx, session, refreshToken) } diff --git a/users/middleware/logging.go b/users/middleware/logging.go index d261b722a1..d89b592884 100644 --- a/users/middleware/logging.go +++ b/users/middleware/logging.go @@ -8,7 +8,7 @@ import ( "log/slog" "time" - "github.com/absmach/magistrala" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/users" ) @@ -50,7 +50,7 @@ func (lm *loggingMiddleware) Register(ctx context.Context, session authn.Session // IssueToken logs the issue_token request. It logs the username type and the time it took to complete the request. // If the request fails, it logs the error. -func (lm *loggingMiddleware) IssueToken(ctx context.Context, username, secret string) (t *magistrala.Token, err error) { +func (lm *loggingMiddleware) IssueToken(ctx context.Context, username, secret string) (t *grpcTokenV1.Token, err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), @@ -70,7 +70,7 @@ func (lm *loggingMiddleware) IssueToken(ctx context.Context, username, secret st // RefreshToken logs the refresh_token request. It logs the refreshtoken, token type and the time it took to complete the request. // If the request fails, it logs the error. -func (lm *loggingMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (t *magistrala.Token, err error) { +func (lm *loggingMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (t *grpcTokenV1.Token, err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), diff --git a/users/middleware/metrics.go b/users/middleware/metrics.go index ab6321ac97..666c7393cd 100644 --- a/users/middleware/metrics.go +++ b/users/middleware/metrics.go @@ -7,7 +7,7 @@ import ( "context" "time" - "github.com/absmach/magistrala" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/users" "github.com/go-kit/kit/metrics" @@ -40,7 +40,7 @@ func (ms *metricsMiddleware) Register(ctx context.Context, session authn.Session } // IssueToken instruments IssueToken method with metrics. -func (ms *metricsMiddleware) IssueToken(ctx context.Context, username, secret string) (*magistrala.Token, error) { +func (ms *metricsMiddleware) IssueToken(ctx context.Context, username, secret string) (*grpcTokenV1.Token, error) { defer func(begin time.Time) { ms.counter.With("method", "issue_token").Add(1) ms.latency.With("method", "issue_token").Observe(time.Since(begin).Seconds()) @@ -49,7 +49,7 @@ func (ms *metricsMiddleware) IssueToken(ctx context.Context, username, secret st } // RefreshToken instruments RefreshToken method with metrics. -func (ms *metricsMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (token *magistrala.Token, err error) { +func (ms *metricsMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (token *grpcTokenV1.Token, err error) { defer func(begin time.Time) { ms.counter.With("method", "refresh_token").Add(1) ms.latency.With("method", "refresh_token").Observe(time.Since(begin).Seconds()) diff --git a/users/mocks/service.go b/users/mocks/service.go index 83dfe9e688..36541f4887 100644 --- a/users/mocks/service.go +++ b/users/mocks/service.go @@ -9,11 +9,11 @@ import ( authn "github.com/absmach/magistrala/pkg/authn" - magistrala "github.com/absmach/magistrala" - mock "github.com/stretchr/testify/mock" users "github.com/absmach/magistrala/users" + + v1 "github.com/absmach/magistrala/internal/grpc/token/v1" ) // Service is an autogenerated mock type for the Service type @@ -142,23 +142,23 @@ func (_m *Service) Identify(ctx context.Context, session authn.Session) (string, } // IssueToken provides a mock function with given fields: ctx, identity, secret -func (_m *Service) IssueToken(ctx context.Context, identity string, secret string) (*magistrala.Token, error) { +func (_m *Service) IssueToken(ctx context.Context, identity string, secret string) (*v1.Token, error) { ret := _m.Called(ctx, identity, secret) if len(ret) == 0 { panic("no return value specified for IssueToken") } - var r0 *magistrala.Token + var r0 *v1.Token var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*magistrala.Token, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1.Token, error)); ok { return rf(ctx, identity, secret) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *magistrala.Token); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1.Token); ok { r0 = rf(ctx, identity, secret) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*magistrala.Token) + r0 = ret.Get(0).(*v1.Token) } } @@ -274,23 +274,23 @@ func (_m *Service) OAuthCallback(ctx context.Context, user users.User) (users.Us } // RefreshToken provides a mock function with given fields: ctx, session, refreshToken -func (_m *Service) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*magistrala.Token, error) { +func (_m *Service) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*v1.Token, error) { ret := _m.Called(ctx, session, refreshToken) if len(ret) == 0 { panic("no return value specified for RefreshToken") } - var r0 *magistrala.Token + var r0 *v1.Token var r1 error - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (*magistrala.Token, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (*v1.Token, error)); ok { return rf(ctx, session, refreshToken) } - if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) *magistrala.Token); ok { + if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) *v1.Token); ok { r0 = rf(ctx, session, refreshToken) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*magistrala.Token) + r0 = ret.Get(0).(*v1.Token) } } diff --git a/users/postgres/users.go b/users/postgres/users.go index 374186e3fa..ad6d9b59d5 100644 --- a/users/postgres/users.go +++ b/users/postgres/users.go @@ -11,10 +11,10 @@ import ( "strings" "time" + "github.com/absmach/magistrala/groups" "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" - "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/pkg/postgres" "github.com/absmach/magistrala/users" "github.com/jackc/pgtype" diff --git a/users/service.go b/users/service.go index 80b6b3c601..e978a51350 100644 --- a/users/service.go +++ b/users/service.go @@ -10,6 +10,8 @@ import ( "github.com/absmach/magistrala" mgauth "github.com/absmach/magistrala/auth" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" + "github.com/absmach/magistrala/pkg/apiutil" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" @@ -26,7 +28,7 @@ var ( ) type service struct { - token magistrala.TokenServiceClient + token grpcTokenV1.TokenServiceClient users Repository idProvider magistrala.IDProvider policies policies.Service @@ -35,7 +37,7 @@ type service struct { } // NewService returns a new Users service implementation. -func NewService(token magistrala.TokenServiceClient, urepo Repository, policyService policies.Service, emailer Emailer, hasher Hasher, idp magistrala.IDProvider) Service { +func NewService(token grpcTokenV1.TokenServiceClient, urepo Repository, policyService policies.Service, emailer Emailer, hasher Hasher, idp magistrala.IDProvider) Service { return service{ token: token, users: urepo, @@ -81,7 +83,7 @@ func (svc service) Register(ctx context.Context, session authn.Session, u User, defer func() { if err != nil { if errRollback := svc.addUserPolicyRollback(ctx, u.ID, u.Role); errRollback != nil { - err = errors.Wrap(errors.Wrap(errors.ErrRollbackTx, errRollback), err) + err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err) } } }() @@ -92,7 +94,7 @@ func (svc service) Register(ctx context.Context, session authn.Session, u User, return user, nil } -func (svc service) IssueToken(ctx context.Context, identity, secret string) (*magistrala.Token, error) { +func (svc service) IssueToken(ctx context.Context, identity, secret string) (*grpcTokenV1.Token, error) { var dbUser User var err error @@ -103,31 +105,31 @@ func (svc service) IssueToken(ctx context.Context, identity, secret string) (*ma } if err != nil { - return &magistrala.Token{}, errors.Wrap(svcerr.ErrAuthentication, err) + return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrAuthentication, err) } if err := svc.hasher.Compare(secret, dbUser.Credentials.Secret); err != nil { - return &magistrala.Token{}, errors.Wrap(svcerr.ErrLogin, err) + return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrLogin, err) } - token, err := svc.token.Issue(ctx, &magistrala.IssueReq{UserId: dbUser.ID, Type: uint32(mgauth.AccessKey)}) + token, err := svc.token.Issue(ctx, &grpcTokenV1.IssueReq{UserId: dbUser.ID, Type: uint32(mgauth.AccessKey)}) if err != nil { - return &magistrala.Token{}, errors.Wrap(errIssueToken, err) + return &grpcTokenV1.Token{}, errors.Wrap(errIssueToken, err) } return token, nil } -func (svc service) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*magistrala.Token, error) { +func (svc service) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*grpcTokenV1.Token, error) { dbUser, err := svc.users.RetrieveByID(ctx, session.UserID) if err != nil { - return &magistrala.Token{}, errors.Wrap(svcerr.ErrAuthentication, err) + return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrAuthentication, err) } if dbUser.Status == DisabledStatus { - return &magistrala.Token{}, errors.Wrap(svcerr.ErrAuthentication, errLoginDisableUser) + return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrAuthentication, errLoginDisableUser) } - return svc.token.Refresh(ctx, &magistrala.RefreshReq{RefreshToken: refreshToken}) + return svc.token.Refresh(ctx, &grpcTokenV1.RefreshReq{RefreshToken: refreshToken}) } func (svc service) View(ctx context.Context, session authn.Session, id string) (User, error) { @@ -285,7 +287,7 @@ func (svc service) GenerateResetToken(ctx context.Context, email, host string) e if err != nil { return errors.Wrap(svcerr.ErrViewEntity, err) } - issueReq := &magistrala.IssueReq{ + issueReq := &grpcTokenV1.IssueReq{ UserId: user.ID, Type: uint32(mgauth.RecoveryKey), } diff --git a/users/service_test.go b/users/service_test.go index 8c891afc0f..8efe70b089 100644 --- a/users/service_test.go +++ b/users/service_test.go @@ -9,9 +9,9 @@ import ( "strings" "testing" - "github.com/absmach/magistrala" mgauth "github.com/absmach/magistrala/auth" authmocks "github.com/absmach/magistrala/auth/mocks" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" @@ -779,7 +779,7 @@ func TestUpdateSecret(t *testing.T) { retrieveByIDResponse users.User retrieveByEmailResponse users.User updateSecretResponse users.User - issueResponse *magistrala.Token + issueResponse *grpcTokenV1.Token response users.User retrieveByIDErr error retrieveByEmailErr error @@ -795,7 +795,7 @@ func TestUpdateSecret(t *testing.T) { retrieveByEmailResponse: rUser, retrieveByIDResponse: user, updateSecretResponse: responseUser, - issueResponse: &magistrala.Token{AccessToken: validToken}, + issueResponse: &grpcTokenV1.Token{AccessToken: validToken}, response: responseUser, err: nil, }, @@ -1635,7 +1635,7 @@ func TestIssueToken(t *testing.T) { desc string user users.User retrieveByUsernameResponse users.User - issueResponse *magistrala.Token + issueResponse *grpcTokenV1.Token retrieveByUsernameErr error issueErr error err error @@ -1644,14 +1644,14 @@ func TestIssueToken(t *testing.T) { desc: "issue token for an existing user", user: user, retrieveByUsernameResponse: rUser, - issueResponse: &magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + issueResponse: &grpcTokenV1.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, err: nil, }, { desc: "issue token for non-empty domain id", user: user, retrieveByUsernameResponse: rUser, - issueResponse: &magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + issueResponse: &grpcTokenV1.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, err: nil, }, { @@ -1671,7 +1671,7 @@ func TestIssueToken(t *testing.T) { desc: "issue token with empty domain id", user: user, retrieveByUsernameResponse: rUser, - issueResponse: &magistrala.Token{}, + issueResponse: &grpcTokenV1.Token{}, issueErr: svcerr.ErrAuthentication, err: svcerr.ErrAuthentication, }, @@ -1679,29 +1679,27 @@ func TestIssueToken(t *testing.T) { desc: "issue token with grpc error", user: user, retrieveByUsernameResponse: rUser, - issueResponse: &magistrala.Token{}, + issueResponse: &grpcTokenV1.Token{}, issueErr: svcerr.ErrAuthentication, err: svcerr.ErrAuthentication, }, } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - repoCall := cRepo.On("RetrieveByUsername", context.Background(), tc.user.Credentials.Username).Return(tc.retrieveByUsernameResponse, tc.retrieveByUsernameErr) - authCall := auth.On("Issue", context.Background(), &magistrala.IssueReq{UserId: tc.user.ID, Type: uint32(mgauth.AccessKey)}).Return(tc.issueResponse, tc.issueErr) - token, err := svc.IssueToken(context.Background(), tc.user.Credentials.Username, tc.user.Credentials.Secret) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.NotEmpty(t, token.GetAccessToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetAccessToken())) - assert.NotEmpty(t, token.GetRefreshToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetRefreshToken())) - ok := repoCall.Parent.AssertCalled(t, "RetrieveByUsername", context.Background(), tc.user.Credentials.Username) - assert.True(t, ok, fmt.Sprintf("RetrieveByUsername was not called on %s", tc.desc)) - ok = authCall.Parent.AssertCalled(t, "Issue", context.Background(), &magistrala.IssueReq{UserId: tc.user.ID, Type: uint32(mgauth.AccessKey)}) - assert.True(t, ok, fmt.Sprintf("Issue was not called on %s", tc.desc)) - } - authCall.Unset() - repoCall.Unset() - }) + repoCall := cRepo.On("RetrieveByUsername", context.Background(), tc.user.Credentials.Username).Return(tc.retrieveByUsernameResponse, tc.retrieveByUsernameErr) + authCall := auth.On("Issue", context.Background(), &grpcTokenV1.IssueReq{UserId: tc.user.ID, Type: uint32(mgauth.AccessKey)}).Return(tc.issueResponse, tc.issueErr) + token, err := svc.IssueToken(context.Background(), tc.user.Credentials.Username, tc.user.Credentials.Secret) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + assert.NotEmpty(t, token.GetAccessToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetAccessToken())) + assert.NotEmpty(t, token.GetRefreshToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetRefreshToken())) + ok := repoCall.Parent.AssertCalled(t, "RetrieveByUsername", context.Background(), tc.user.Credentials.Username) + assert.True(t, ok, fmt.Sprintf("RetrieveByUsername was not called on %s", tc.desc)) + ok = authCall.Parent.AssertCalled(t, "Issue", context.Background(), &grpcTokenV1.IssueReq{UserId: tc.user.ID, Type: uint32(mgauth.AccessKey)}) + assert.True(t, ok, fmt.Sprintf("Issue was not called on %s", tc.desc)) + } + authCall.Unset() + repoCall.Unset() } } @@ -1714,7 +1712,7 @@ func TestRefreshToken(t *testing.T) { cases := []struct { desc string session authn.Session - refreshResp *magistrala.Token + refreshResp *grpcTokenV1.Token refresErr error repoResp users.User repoErr error @@ -1723,14 +1721,21 @@ func TestRefreshToken(t *testing.T) { { desc: "refresh token with refresh token for an existing user", session: authn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}, - refreshResp: &magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + refreshResp: &grpcTokenV1.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + repoResp: rUser, + err: nil, + }, + { + desc: "refresh token with refresh token for empty domain id", + session: authn.Session{UserID: validID}, + refreshResp: &grpcTokenV1.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, repoResp: rUser, err: nil, }, { desc: "refresh token with access token for an existing user", session: authn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}, - refreshResp: &magistrala.Token{}, + refreshResp: &grpcTokenV1.Token{}, refresErr: svcerr.ErrAuthentication, repoResp: rUser, err: svcerr.ErrAuthentication, @@ -1750,7 +1755,7 @@ func TestRefreshToken(t *testing.T) { { desc: "refresh token with empty domain id", session: authn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}, - refreshResp: &magistrala.Token{}, + refreshResp: &grpcTokenV1.Token{}, refresErr: svcerr.ErrAuthentication, repoResp: rUser, err: svcerr.ErrAuthentication, @@ -1758,22 +1763,20 @@ func TestRefreshToken(t *testing.T) { } for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - authCall := authsvc.On("Refresh", context.Background(), &magistrala.RefreshReq{RefreshToken: validToken}).Return(tc.refreshResp, tc.refresErr) - repoCall := crepo.On("RetrieveByID", context.Background(), tc.session.UserID).Return(tc.repoResp, tc.repoErr) - token, err := svc.RefreshToken(context.Background(), tc.session, validToken) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.NotEmpty(t, token.GetAccessToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetAccessToken())) - assert.NotEmpty(t, token.GetRefreshToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetRefreshToken())) - ok := authCall.Parent.AssertCalled(t, "Refresh", context.Background(), &magistrala.RefreshReq{RefreshToken: validToken}) - assert.True(t, ok, fmt.Sprintf("Refresh was not called on %s", tc.desc)) - ok = repoCall.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.session.UserID) - assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) - } - authCall.Unset() - repoCall.Unset() - }) + authCall := authsvc.On("Refresh", context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: validToken}).Return(tc.refreshResp, tc.refresErr) + repoCall := crepo.On("RetrieveByID", context.Background(), tc.session.UserID).Return(tc.repoResp, tc.repoErr) + token, err := svc.RefreshToken(context.Background(), tc.session, validToken) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + assert.NotEmpty(t, token.GetAccessToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetAccessToken())) + assert.NotEmpty(t, token.GetRefreshToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetRefreshToken())) + ok := authCall.Parent.AssertCalled(t, "Refresh", context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: validToken}) + assert.True(t, ok, fmt.Sprintf("Refresh was not called on %s", tc.desc)) + ok = repoCall.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.session.UserID) + assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) + } + authCall.Unset() + repoCall.Unset() } } @@ -1785,7 +1788,7 @@ func TestGenerateResetToken(t *testing.T) { email string host string retrieveByEmailResponse users.User - issueResponse *magistrala.Token + issueResponse *grpcTokenV1.Token retrieveByEmailErr error issueErr error err error @@ -1795,7 +1798,7 @@ func TestGenerateResetToken(t *testing.T) { email: "existingemail@example.com", host: "examplehost", retrieveByEmailResponse: user, - issueResponse: &magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + issueResponse: &grpcTokenV1.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, err: nil, }, { @@ -1814,7 +1817,7 @@ func TestGenerateResetToken(t *testing.T) { email: "existingemail@example.com", host: "examplehost", retrieveByEmailResponse: user, - issueResponse: &magistrala.Token{}, + issueResponse: &grpcTokenV1.Token{}, issueErr: svcerr.ErrAuthorization, err: svcerr.ErrAuthorization, }, diff --git a/users/tracing/tracing.go b/users/tracing/tracing.go index 81ad0dcb58..5bdc9c37dc 100644 --- a/users/tracing/tracing.go +++ b/users/tracing/tracing.go @@ -6,7 +6,7 @@ package tracing import ( "context" - "github.com/absmach/magistrala" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/authn" users "github.com/absmach/magistrala/users" "go.opentelemetry.io/otel/attribute" @@ -34,7 +34,7 @@ func (tm *tracingMiddleware) Register(ctx context.Context, session authn.Session } // IssueToken traces the "IssueToken" operation of the wrapped users.Service. -func (tm *tracingMiddleware) IssueToken(ctx context.Context, username, secret string) (*magistrala.Token, error) { +func (tm *tracingMiddleware) IssueToken(ctx context.Context, username, secret string) (*grpcTokenV1.Token, error) { ctx, span := tm.tracer.Start(ctx, "svc_issue_token", trace.WithAttributes(attribute.String("username", username))) defer span.End() @@ -42,7 +42,7 @@ func (tm *tracingMiddleware) IssueToken(ctx context.Context, username, secret st } // RefreshToken traces the "RefreshToken" operation of the wrapped users.Service. -func (tm *tracingMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*magistrala.Token, error) { +func (tm *tracingMiddleware) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*grpcTokenV1.Token, error) { ctx, span := tm.tracer.Start(ctx, "svc_refresh_token", trace.WithAttributes(attribute.String("refresh_token", refreshToken))) defer span.End() diff --git a/users/users.go b/users/users.go index 8fe96042cf..0472fc4f95 100644 --- a/users/users.go +++ b/users/users.go @@ -8,7 +8,7 @@ import ( "net/mail" "time" - "github.com/absmach/magistrala" + grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1" "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" "github.com/absmach/magistrala/pkg/postgres" @@ -202,12 +202,12 @@ type Service interface { Identify(ctx context.Context, session authn.Session) (string, error) // IssueToken issues a new access and refresh token when provided with either a username or email. - IssueToken(ctx context.Context, identity, secret string) (*magistrala.Token, error) + IssueToken(ctx context.Context, identity, secret string) (*grpcTokenV1.Token, error) // RefreshToken refreshes expired access tokens. // After an access token expires, the refresh token is used to get // a new pair of access and refresh tokens. - RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*magistrala.Token, error) + RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*grpcTokenV1.Token, error) // OAuthCallback handles the callback from any supported OAuth provider. // It processes the OAuth tokens and either signs in or signs up the user based on the provided state. diff --git a/ws/adapter.go b/ws/adapter.go index 8fdeae4100..a4b7408cd4 100644 --- a/ws/adapter.go +++ b/ws/adapter.go @@ -7,7 +7,8 @@ import ( "context" "fmt" - "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" @@ -41,15 +42,17 @@ type Service interface { var _ Service = (*adapterService)(nil) type adapterService struct { - things magistrala.ThingsServiceClient - pubsub messaging.PubSub + things grpcThingsV1.ThingsServiceClient + channels grpcChannelsV1.ChannelsServiceClient + pubsub messaging.PubSub } // New instantiates the WS adapter implementation. -func New(thingsClient magistrala.ThingsServiceClient, pubsub messaging.PubSub) Service { +func New(things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient, pubsub messaging.PubSub) Service { return &adapterService{ - things: thingsClient, - pubsub: pubsub, + things: things, + channels: channels, + pubsub: pubsub, } } @@ -85,18 +88,30 @@ func (svc *adapterService) Subscribe(ctx context.Context, thingKey, chanID, subt // authorize checks if the thingKey is authorized to access the channel // and returns the thingID if it is. func (svc *adapterService) authorize(ctx context.Context, thingKey, chanID, action string) (string, error) { - ar := &magistrala.ThingsAuthzReq{ + authnReq := &grpcThingsV1.AuthnReq{ + ThingKey: thingKey, + } + authnRes, err := svc.things.Authenticate(ctx, authnReq) + if err != nil { + return "", errors.Wrap(svcerr.ErrAuthentication, err) + } + if !authnRes.GetAuthenticated() { + return "", errors.Wrap(svcerr.ErrAuthentication, err) + } + + authzReq := &grpcChannelsV1.AuthzReq{ + ClientType: policies.ThingType, + ClientId: authnRes.GetId(), Permission: action, - ThingKey: thingKey, - ChannelID: chanID, + ChannelId: chanID, } - res, err := svc.things.Authorize(ctx, ar) + authzRes, err := svc.channels.Authorize(ctx, authzReq) if err != nil { return "", errors.Wrap(svcerr.ErrAuthorization, err) } - if !res.GetAuthorized() { + if !authzRes.GetAuthorized() { return "", errors.Wrap(svcerr.ErrAuthorization, err) } - return res.GetId(), nil + return authnRes.GetId(), nil } diff --git a/ws/adapter_test.go b/ws/adapter_test.go index 40323a2aa9..ed849f91ac 100644 --- a/ws/adapter_test.go +++ b/ws/adapter_test.go @@ -8,7 +8,7 @@ import ( "fmt" "testing" - "github.com/absmach/magistrala" + chmocks "github.com/absmach/magistrala/channels/mocks" "github.com/absmach/magistrala/internal/testsutil" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" @@ -37,15 +37,16 @@ var msg = messaging.Message{ Payload: []byte(`[{"n":"current","t":-5,"v":1.2}]`), } -func newService() (ws.Service, *mocks.PubSub, *thmocks.ThingsServiceClient) { +func newService() (ws.Service, *mocks.PubSub, *thmocks.ThingsServiceClient, *chmocks.ChannelsServiceClient) { pubsub := new(mocks.PubSub) things := new(thmocks.ThingsServiceClient) + channels := new(chmocks.ChannelsServiceClient) - return ws.New(things, pubsub), pubsub, things + return ws.New(things, channels, pubsub), pubsub, things, channels } func TestSubscribe(t *testing.T) { - svc, pubsub, things := newService() + svc, pubsub, _, _ := newService() c := ws.NewClient(nil) @@ -115,11 +116,8 @@ func TestSubscribe(t *testing.T) { Handler: c, } repocall := pubsub.On("Subscribe", mock.Anything, subConfig).Return(tc.err) - repocall1 := things.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.ThingsAuthzRes{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) repocall.Unset() - repocall1.Unset() } } diff --git a/ws/api/endpoint_test.go b/ws/api/endpoint_test.go index ddd99a93a3..5e53519714 100644 --- a/ws/api/endpoint_test.go +++ b/ws/api/endpoint_test.go @@ -12,8 +12,11 @@ import ( "strings" "testing" - "github.com/absmach/magistrala" + chmocks "github.com/absmach/magistrala/channels/mocks" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" mglog "github.com/absmach/magistrala/logger" + authnMocks "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/messaging/mocks" thmocks "github.com/absmach/magistrala/things/mocks" "github.com/absmach/magistrala/ws" @@ -36,9 +39,9 @@ const ( var msg = []byte(`[{"n":"current","t":-1,"v":1.6}]`) -func newService(things magistrala.ThingsServiceClient) (ws.Service, *mocks.PubSub) { +func newService(things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient) (ws.Service, *mocks.PubSub) { pubsub := new(mocks.PubSub) - return ws.New(things, pubsub), pubsub + return ws.New(things, channels, pubsub), pubsub } func newHTTPServer(svc ws.Service) *httptest.Server { @@ -91,16 +94,15 @@ func handshake(tsURL, chanID, subtopic, thingKey string, addHeader bool) (*webso func TestHandshake(t *testing.T) { things := new(thmocks.ThingsServiceClient) - svc, pubsub := newService(things) + channels := new(chmocks.ChannelsServiceClient) + authn := new(authnMocks.Authentication) + svc, pubsub := newService(things, channels) target := newHTTPServer(svc) defer target.Close() - handler := ws.NewHandler(pubsub, mglog.NewMock(), things) + handler := ws.NewHandler(pubsub, mglog.NewMock(), authn, things, channels) ts, err := newProxyHTPPServer(handler, target) require.Nil(t, err) defer ts.Close() - things.On("Authorize", mock.Anything, &magistrala.ThingsAuthzReq{ThingKey: thingKey, ChannelID: id, Permission: "publish"}).Return(&magistrala.ThingsAuthzRes{Authorized: true, Id: "1"}, nil) - things.On("Authorize", mock.Anything, &magistrala.ThingsAuthzReq{ThingKey: thingKey, ChannelID: id, Permission: "subscribe"}).Return(&magistrala.ThingsAuthzRes{Authorized: true, Id: "2"}, nil) - things.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthZRes{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) diff --git a/ws/handler.go b/ws/handler.go index 4935963017..cbdc1a8ea8 100644 --- a/ws/handler.go +++ b/ws/handler.go @@ -12,7 +12,10 @@ import ( "strings" "time" - "github.com/absmach/magistrala" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcThingsV1 "github.com/absmach/magistrala/internal/grpc/things/v1" + "github.com/absmach/magistrala/pkg/apiutil" + mgauthn "github.com/absmach/magistrala/pkg/authn" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" @@ -50,17 +53,21 @@ var channelRegExp = regexp.MustCompile(`^\/?channels\/([\w\-]+)\/messages(\/[^?] // Event implements events.Event interface. type handler struct { - pubsub messaging.PubSub - things magistrala.ThingsServiceClient - logger *slog.Logger + pubsub messaging.PubSub + things grpcThingsV1.ThingsServiceClient + channels grpcChannelsV1.ChannelsServiceClient + authn mgauthn.Authentication + logger *slog.Logger } // NewHandler creates new Handler entity. -func NewHandler(pubsub messaging.PubSub, logger *slog.Logger, thingsClient magistrala.ThingsServiceClient) session.Handler { +func NewHandler(pubsub messaging.PubSub, logger *slog.Logger, authn mgauthn.Authentication, things grpcThingsV1.ThingsServiceClient, channels grpcChannelsV1.ChannelsServiceClient) session.Handler { return &handler{ - logger: logger, - pubsub: pubsub, - things: thingsClient, + logger: logger, + pubsub: pubsub, + authn: authn, + things: things, + channels: channels, } } @@ -111,8 +118,8 @@ func (h *handler) AuthSubscribe(ctx context.Context, topics *[]string) error { token = string(s.Password) } - for _, v := range *topics { - if err := h.authAccess(ctx, token, v, policies.SubscribePermission); err != nil { + for _, topic := range *topics { + if err := h.authAccess(ctx, token, topic, policies.SubscribePermission); err != nil { return err } } @@ -152,20 +159,36 @@ func (h *handler) Publish(ctx context.Context, topic *string, payload *[]byte) e return errors.Wrap(errFailedParseSubtopic, err) } - var token string + var clientID, clientType string switch { case strings.HasPrefix(string(s.Password), "Thing"): - token = strings.ReplaceAll(string(s.Password), "Thing ", "") + thingKey := extractThingKey(string(s.Password)) + authnRes, err := h.things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ThingKey: thingKey}) + if err != nil { + return errors.Wrap(svcerr.ErrAuthentication, err) + } + if !authnRes.Authenticated { + return svcerr.ErrAuthentication + } + clientType = policies.ThingType + clientID = authnRes.GetId() default: - token = string(s.Password) + token := string(s.Password) + authnSession, err := h.authn.Authenticate(ctx, extractBearerToken(token)) + if err != nil { + return err + } + clientType = policies.UserType + clientID = authnSession.DomainUserID } - ar := &magistrala.ThingsAuthzReq{ + ar := &grpcChannelsV1.AuthzReq{ Permission: policies.PublishPermission, - ThingKey: token, - ChannelID: chanID, + ClientId: clientID, + ClientType: clientType, + ChannelId: chanID, } - res, err := h.things.Authorize(ctx, ar) + res, err := h.channels.Authorize(ctx, ar) if err != nil { return err } @@ -174,12 +197,15 @@ func (h *handler) Publish(ctx context.Context, topic *string, payload *[]byte) e } msg := messaging.Message{ - Protocol: protocol, - Channel: chanID, - Subtopic: subtopic, - Publisher: res.GetId(), - Payload: *payload, - Created: time.Now().UnixNano(), + Protocol: protocol, + Channel: chanID, + Subtopic: subtopic, + Payload: *payload, + Created: time.Now().UnixNano(), + } + + if clientType == policies.ThingType { + msg.Publisher = clientID } if err := h.pubsub.Publish(ctx, msg.GetChannel(), &msg); err != nil { @@ -215,7 +241,29 @@ func (h *handler) Disconnect(ctx context.Context) error { return nil } -func (h *handler) authAccess(ctx context.Context, password, topic, action string) error { +func (h *handler) authAccess(ctx context.Context, token, topic, action string) error { + var clientID, clientType string + switch { + case strings.HasPrefix(token, "Thing"): + thingKey := extractThingKey(token) + authnRes, err := h.things.Authenticate(ctx, &grpcThingsV1.AuthnReq{ThingKey: thingKey}) + if err != nil { + return errors.Wrap(svcerr.ErrAuthentication, err) + } + if !authnRes.Authenticated { + return svcerr.ErrAuthentication + } + clientType = policies.ThingType + clientID = authnRes.GetId() + default: + authnSession, err := h.authn.Authenticate(ctx, extractBearerToken(token)) + if err != nil { + return err + } + clientType = policies.UserType + clientID = authnSession.DomainUserID + } + // Topics are in the format: // channels//messages//.../ct/ if !channelRegExp.MatchString(topic) { @@ -229,12 +277,13 @@ func (h *handler) authAccess(ctx context.Context, password, topic, action string chanID := channelParts[1] - ar := &magistrala.ThingsAuthzReq{ + ar := &grpcChannelsV1.AuthzReq{ Permission: action, - ThingKey: password, - ChannelID: chanID, + ClientId: clientID, + ClientType: clientType, + ChannelId: chanID, } - res, err := h.things.Authorize(ctx, ar) + res, err := h.channels.Authorize(ctx, ar) if err != nil { return errors.Wrap(svcerr.ErrAuthorization, err) } @@ -273,3 +322,21 @@ func parseSubtopic(subtopic string) (string, error) { subtopic = strings.Join(filteredElems, ".") return subtopic, nil } + +// extractThingKey returns value of the thing key. If there is no thing key - an empty value is returned. +func extractThingKey(topic string) string { + if !strings.HasPrefix(topic, apiutil.ThingPrefix) { + return "" + } + + return strings.TrimPrefix(topic, apiutil.ThingPrefix) +} + +// extractBearerToken +func extractBearerToken(token string) string { + if !strings.HasPrefix(token, apiutil.BearerPrefix) { + return "" + } + + return strings.TrimPrefix(token, apiutil.BearerPrefix) +}