Files
magistrala/clients/service.go
T
Dušan Borovčanin 61d0427898 NOISSUE - Rename to Magistrala (#3427)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-04-06 15:23:42 +02:00

407 lines
12 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package clients
import (
"context"
"time"
mg "github.com/absmach/magistrala"
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
grpcCommonV1 "github.com/absmach/magistrala/api/grpc/common/v1"
grpcGroupsV1 "github.com/absmach/magistrala/api/grpc/groups/v1"
apiutil "github.com/absmach/magistrala/api/http/util"
"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"
)
var (
errRollbackRepo = errors.New("failed to rollback repo")
errSetParentGroup = errors.NewRequestError("client already have parent")
errSetSameParentGroup = errors.NewRequestError("client already assigned to the parent group")
errParentGroupDomainID = errors.NewRequestError("parent group has invalid domain id")
errParentGroupDisabled = errors.NewRequestError("parent group is not enabled")
)
var _ Service = (*service)(nil)
type service struct {
repo Repository
policy policies.Service
channels grpcChannelsV1.ChannelsServiceClient
groups grpcGroupsV1.GroupsServiceClient
cache Cache
idProvider mg.IDProvider
roles.ProvisionManageService
}
// 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, availableActions []roles.Action, builtInRoles map[roles.BuiltInRoleName][]roles.Action) (Service, error) {
rpms, err := roles.NewProvisionManageService(policies.ClientType, repo, policy, sIDProvider, availableActions, builtInRoles)
if err != nil {
return service{}, err
}
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, cls ...Client) (retClients []Client, retRps []roles.RoleProvision, retErr error) {
var clients []Client
for _, c := range cls {
if c.ID == "" {
clientID, err := svc.idProvider.ID()
if err != nil {
return []Client{}, []roles.RoleProvision{}, errors.Wrap(svcerr.ErrIssueProviderID, err)
}
c.ID = clientID
}
if c.Credentials.Secret == "" {
key, err := svc.idProvider.ID()
if err != nil {
return []Client{}, []roles.RoleProvision{}, errors.Wrap(svcerr.ErrIssueProviderID, err)
}
c.Credentials.Secret = key
}
if c.Status != DisabledStatus && c.Status != EnabledStatus {
return []Client{}, []roles.RoleProvision{}, svcerr.ErrInvalidStatus
}
c.Domain = session.DomainID
c.CreatedAt = time.Now().UTC()
clients = append(clients, c)
}
newClients, err := svc.repo.Save(ctx, clients...)
if err != nil {
return []Client{}, []roles.RoleProvision{}, errors.Wrap(svcerr.ErrCreateEntity, err)
}
newClientIDs := []string{}
for _, newClient := range newClients {
newClientIDs = append(newClientIDs, newClient.ID)
}
defer func() {
if retErr != nil {
if errRollBack := svc.repo.Delete(ctx, newClientIDs...); 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 _, newClientID := range newClientIDs {
optionalPolicies = append(optionalPolicies,
policies.Policy{
Domain: session.DomainID,
SubjectType: policies.DomainType,
Subject: session.DomainID,
Relation: policies.DomainRelation,
ObjectType: policies.ClientType,
Object: newClientID,
},
)
}
rp, err := svc.AddNewEntitiesRoles(ctx, session.DomainID, session.UserID, newClientIDs, optionalPolicies, newBuiltInRoleMembers)
if err != nil {
return []Client{}, []roles.RoleProvision{}, errors.Wrap(svcerr.ErrAddPolicies, err)
}
return newClients, rp, nil
}
func (svc service) View(ctx context.Context, session authn.Session, id string, withRoles bool) (Client, error) {
var client Client
var err error
switch withRoles {
case true:
client, err = svc.repo.RetrieveByIDWithRoles(ctx, id, session.UserID)
default:
client, err = svc.repo.RetrieveByID(ctx, id)
}
if err != nil {
return Client{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return client, nil
}
func (svc service) ListClients(ctx context.Context, session authn.Session, pm Page) (ClientsPage, error) {
switch session.SuperAdmin {
case true:
pm.Domain = session.DomainID
cp, err := svc.repo.RetrieveAll(ctx, pm)
if err != nil {
return ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return cp, nil
default:
cp, err := svc.repo.RetrieveUserClients(ctx, session.DomainID, session.UserID, pm)
if err != nil {
return ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return cp, nil
}
}
func (svc service) ListUserClients(ctx context.Context, session authn.Session, userID string, pm Page) (ClientsPage, error) {
cp, err := svc.repo.RetrieveUserClients(ctx, session.DomainID, userID, pm)
if err != nil {
return ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return cp, nil
}
func (svc service) Update(ctx context.Context, session authn.Session, cli Client) (Client, error) {
client := Client{
ID: cli.ID,
Name: cli.Name,
Metadata: cli.Metadata,
PrivateMetadata: cli.PrivateMetadata,
UpdatedAt: time.Now().UTC(),
UpdatedBy: session.UserID,
}
client, err := svc.repo.Update(ctx, client)
if err != nil {
return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return client, nil
}
func (svc service) UpdateTags(ctx context.Context, session authn.Session, cli Client) (Client, error) {
client := Client{
ID: cli.ID,
Tags: cli.Tags,
UpdatedAt: time.Now().UTC(),
UpdatedBy: session.UserID,
}
client, err := svc.repo.UpdateTags(ctx, client)
if err != nil {
return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return client, nil
}
func (svc service) UpdateSecret(ctx context.Context, session authn.Session, id, key string) (Client, error) {
client := Client{
ID: id,
Credentials: Credentials{
Secret: key,
},
UpdatedAt: time.Now().UTC(),
UpdatedBy: session.UserID,
Status: EnabledStatus,
}
client, err := svc.repo.UpdateSecret(ctx, client)
if err != nil {
return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
if err := svc.cache.Remove(ctx, client.ID); err != nil {
return client, errors.Wrap(svcerr.ErrRemoveEntity, err)
}
return client, nil
}
func (svc service) Enable(ctx context.Context, session authn.Session, id string) (Client, error) {
client := Client{
ID: id,
Status: EnabledStatus,
UpdatedAt: time.Now().UTC(),
}
client, err := svc.changeClientStatus(ctx, session, client)
if err != nil {
return Client{}, errors.Wrap(ErrEnableClient, err)
}
return client, nil
}
func (svc service) Disable(ctx context.Context, session authn.Session, id string) (Client, error) {
client := Client{
ID: id,
Status: DisabledStatus,
UpdatedAt: time.Now().UTC(),
}
client, err := svc.changeClientStatus(ctx, session, client)
if err != nil {
return Client{}, errors.Wrap(ErrDisableClient, err)
}
if err := svc.cache.Remove(ctx, client.ID); err != nil {
return client, errors.Wrap(svcerr.ErrRemoveEntity, err)
}
return client, nil
}
func (svc service) SetParentGroup(ctx context.Context, session authn.Session, parentGroupID string, id string) (retErr error) {
cli, err := svc.repo.RetrieveByID(ctx, id)
if err != nil {
return errors.Wrap(svcerr.ErrUpdateEntity, err)
}
switch cli.ParentGroup {
case parentGroupID:
return errors.Wrap(svcerr.ErrConflict, errSetSameParentGroup)
case "":
// No action needed, proceed to next code after switch
default:
return errors.Wrap(svcerr.ErrConflict, errSetParentGroup)
}
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, errParentGroupDomainID)
}
if resp.GetEntity().GetStatus() != uint32(EnabledStatus) {
return errors.Wrap(svcerr.ErrUpdateEntity, errParentGroupDisabled)
}
var pols []policies.Policy
pols = append(pols, policies.Policy{
Domain: session.DomainID,
SubjectType: policies.GroupType,
Subject: parentGroupID,
Relation: policies.ParentGroupRelation,
ObjectType: policies.ClientType,
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))
}
}
}()
cli = Client{ID: id, ParentGroup: parentGroupID, UpdatedBy: session.UserID, UpdatedAt: time.Now().UTC()}
if err := svc.repo.SetParentGroup(ctx, cli); err != nil {
return errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return nil
}
func (svc service) RemoveParentGroup(ctx context.Context, session authn.Session, id string) (retErr error) {
cli, err := svc.repo.RetrieveByID(ctx, id)
if err != nil {
return errors.Wrap(svcerr.ErrViewEntity, err)
}
if cli.ParentGroup != "" {
var pols []policies.Policy
pols = append(pols, policies.Policy{
Domain: session.DomainID,
SubjectType: policies.GroupType,
Subject: cli.ParentGroup,
Relation: policies.ParentGroupRelation,
ObjectType: policies.ClientType,
Object: 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))
}
}
}()
cli := Client{ID: id, UpdatedBy: session.UserID, UpdatedAt: time.Now().UTC()}
if err := svc.repo.RemoveParentGroup(ctx, cli); err != nil {
return errors.Wrap(svcerr.ErrUpdateEntity, err)
}
}
return nil
}
func (svc service) Delete(ctx context.Context, session authn.Session, id string) error {
ok, err := svc.repo.DoesClientHaveConnections(ctx, id)
if err != nil {
return errors.Wrap(svcerr.ErrRemoveEntity, err)
}
if ok {
if _, err := svc.channels.RemoveClientConnections(ctx, &grpcChannelsV1.RemoveClientConnectionsReq{ClientId: id}); err != nil {
return errors.Wrap(svcerr.ErrRemoveEntity, err)
}
}
if _, err := svc.repo.ChangeStatus(ctx, Client{ID: id, Status: DeletedStatus}); err != nil {
return errors.Wrap(svcerr.ErrRemoveEntity, err)
}
if err := svc.cache.Remove(ctx, id); err != nil {
return errors.Wrap(svcerr.ErrRemoveEntity, err)
}
filterDeletePolicies := []policies.Policy{
{
SubjectType: policies.ClientType,
Subject: id,
},
{
ObjectType: policies.ClientType,
Object: id,
},
}
deletePolicies := []policies.Policy{
{
SubjectType: policies.DomainType,
Subject: session.DomainID,
Relation: policies.DomainRelation,
ObjectType: policies.ClientType,
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 errors.Wrap(svcerr.ErrRemoveEntity, err)
}
return nil
}
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 dbClient.Status == client.Status {
return Client{}, svcerr.ErrStatusAlreadyAssigned
}
client.UpdatedBy = session.UserID
client, err = svc.repo.ChangeStatus(ctx, client)
if err != nil {
return Client{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return client, nil
}