// Copyright (c) Mainflux // SPDX-License-Identifier: Apache-2.0 package users import ( "context" "regexp" "time" "github.com/mainflux/mainflux" "github.com/mainflux/mainflux/auth" "github.com/mainflux/mainflux/internal/apiutil" mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" "github.com/mainflux/mainflux/users/postgres" ) const ( ownerRelation = "owner" userKind = "users" tokenKind = "token" thingsKind = "things" groupsKind = "groups" userType = "user" groupType = "group" thingType = "thing" ) var ( // ErrRecoveryToken indicates error in generating password recovery token. ErrRecoveryToken = errors.New("failed to generate password recovery token") // ErrPasswordFormat indicates weak password. ErrPasswordFormat = errors.New("password does not meet the requirements") ) type service struct { clients postgres.Repository idProvider mainflux.IDProvider auth mainflux.AuthServiceClient hasher Hasher email Emailer passRegex *regexp.Regexp } // NewService returns a new Users service implementation. func NewService(crepo postgres.Repository, auth mainflux.AuthServiceClient, emailer Emailer, hasher Hasher, idp mainflux.IDProvider, pr *regexp.Regexp) Service { return service{ clients: crepo, auth: auth, hasher: hasher, email: emailer, idProvider: idp, passRegex: pr, } } func (svc service) RegisterClient(ctx context.Context, token string, cli mfclients.Client) (mfclients.Client, error) { // We don't check the error currently since we can register client with empty token ownerID, _ := svc.Identify(ctx, token) clientID, err := svc.idProvider.ID() if err != nil { return mfclients.Client{}, err } if cli.Owner == "" && ownerID != "" { cli.Owner = ownerID } if cli.Credentials.Secret == "" { return mfclients.Client{}, apiutil.ErrMissingSecret } hash, err := svc.hasher.Hash(cli.Credentials.Secret) if err != nil { return mfclients.Client{}, errors.Wrap(errors.ErrMalformedEntity, err) } cli.Credentials.Secret = hash if cli.Status != mfclients.DisabledStatus && cli.Status != mfclients.EnabledStatus { return mfclients.Client{}, apiutil.ErrInvalidStatus } if cli.Role != mfclients.UserRole && cli.Role != mfclients.AdminRole { return mfclients.Client{}, apiutil.ErrInvalidRole } cli.ID = clientID cli.CreatedAt = time.Now() client, err := svc.clients.Save(ctx, cli) if err != nil { return mfclients.Client{}, err } if err := svc.addOwnerPolicy(ctx, ownerID, client.ID); err != nil { return mfclients.Client{}, err } return client, nil } func (svc service) IssueToken(ctx context.Context, identity, secret string) (*mainflux.Token, error) { dbUser, err := svc.clients.RetrieveByIdentity(ctx, identity) if err != nil { return &mainflux.Token{}, err } if err := svc.hasher.Compare(secret, dbUser.Credentials.Secret); err != nil { return &mainflux.Token{}, errors.Wrap(errors.ErrLogin, err) } return svc.auth.Issue(ctx, &mainflux.IssueReq{Id: dbUser.ID, Type: 0}) } func (svc service) RefreshToken(ctx context.Context, refreshToken string) (*mainflux.Token, error) { return svc.auth.Refresh(ctx, &mainflux.RefreshReq{Value: refreshToken}) } func (svc service) ViewClient(ctx context.Context, token string, id string) (mfclients.Client, error) { tokenUserID, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } if tokenUserID != id { if err := svc.isOwner(ctx, id, tokenUserID); err != nil { return mfclients.Client{}, err } } client, err := svc.clients.RetrieveByID(ctx, id) if err != nil { return mfclients.Client{}, err } client.Credentials.Secret = "" return client, nil } func (svc service) ViewProfile(ctx context.Context, token string) (mfclients.Client, error) { id, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } client, err := svc.clients.RetrieveByID(ctx, id) if err != nil { return mfclients.Client{}, err } client.Credentials.Secret = "" return client, nil } func (svc service) ListClients(ctx context.Context, token string, pm mfclients.Page) (mfclients.ClientsPage, error) { id, err := svc.Identify(ctx, token) if err != nil { return mfclients.ClientsPage{}, err } pm.Owner = id clients, err := svc.clients.RetrieveAll(ctx, pm) if err != nil { return mfclients.ClientsPage{}, err } return clients, nil } func (svc service) UpdateClient(ctx context.Context, token string, cli mfclients.Client) (mfclients.Client, error) { tokenUserID, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } if tokenUserID != cli.ID { if err := svc.isOwner(ctx, cli.ID, tokenUserID); err != nil { return mfclients.Client{}, err } } client := mfclients.Client{ ID: cli.ID, Name: cli.Name, Metadata: cli.Metadata, UpdatedAt: time.Now(), UpdatedBy: tokenUserID, } return svc.clients.Update(ctx, client) } func (svc service) UpdateClientTags(ctx context.Context, token string, cli mfclients.Client) (mfclients.Client, error) { tokenUserID, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } if tokenUserID != cli.ID { if err := svc.isOwner(ctx, cli.ID, tokenUserID); err != nil { return mfclients.Client{}, err } } client := mfclients.Client{ ID: cli.ID, Tags: cli.Tags, UpdatedAt: time.Now(), UpdatedBy: tokenUserID, } return svc.clients.UpdateTags(ctx, client) } func (svc service) UpdateClientIdentity(ctx context.Context, token, clientID, identity string) (mfclients.Client, error) { tokenUserID, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } if tokenUserID != clientID { if err := svc.isOwner(ctx, clientID, tokenUserID); err != nil { return mfclients.Client{}, err } } cli := mfclients.Client{ ID: clientID, Credentials: mfclients.Credentials{ Identity: identity, }, UpdatedAt: time.Now(), UpdatedBy: tokenUserID, } return svc.clients.UpdateIdentity(ctx, cli) } func (svc service) GenerateResetToken(ctx context.Context, email, host string) error { client, err := svc.clients.RetrieveByIdentity(ctx, email) if err != nil || client.Credentials.Identity == "" { return errors.ErrNotFound } issueReq := &mainflux.IssueReq{ Id: client.ID, Type: uint32(auth.RecoveryKey), } token, err := svc.auth.Issue(ctx, issueReq) if err != nil { return errors.Wrap(ErrRecoveryToken, err) } return svc.SendPasswordReset(ctx, host, email, client.Name, token.AccessToken) } func (svc service) ResetSecret(ctx context.Context, resetToken, secret string) error { id, err := svc.Identify(ctx, resetToken) if err != nil { return errors.Wrap(errors.ErrAuthentication, err) } c, err := svc.clients.RetrieveByID(ctx, id) if err != nil { return err } if c.Credentials.Identity == "" { return errors.ErrNotFound } if !svc.passRegex.MatchString(secret) { return ErrPasswordFormat } secret, err = svc.hasher.Hash(secret) if err != nil { return err } c = mfclients.Client{ Credentials: mfclients.Credentials{ Identity: c.Credentials.Identity, Secret: secret, }, UpdatedAt: time.Now(), UpdatedBy: id, } if _, err := svc.clients.UpdateSecret(ctx, c); err != nil { return err } return nil } func (svc service) UpdateClientSecret(ctx context.Context, token, oldSecret, newSecret string) (mfclients.Client, error) { id, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } if !svc.passRegex.MatchString(newSecret) { return mfclients.Client{}, ErrPasswordFormat } dbClient, err := svc.clients.RetrieveByID(ctx, id) if err != nil { return mfclients.Client{}, err } if _, err := svc.IssueToken(ctx, dbClient.Credentials.Identity, oldSecret); err != nil { return mfclients.Client{}, err } newSecret, err = svc.hasher.Hash(newSecret) if err != nil { return mfclients.Client{}, err } dbClient.Credentials.Secret = newSecret dbClient.UpdatedAt = time.Now() dbClient.UpdatedBy = id return svc.clients.UpdateSecret(ctx, dbClient) } func (svc service) SendPasswordReset(_ context.Context, host, email, user, token string) error { to := []string{email} return svc.email.SendPasswordReset(to, host, user, token) } func (svc service) UpdateClientOwner(ctx context.Context, token string, cli mfclients.Client) (mfclients.Client, error) { tokenUserID, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } if tokenUserID != cli.ID { if err := svc.isOwner(ctx, cli.ID, tokenUserID); err != nil { return mfclients.Client{}, err } } client := mfclients.Client{ ID: cli.ID, Owner: cli.Owner, UpdatedAt: time.Now(), UpdatedBy: tokenUserID, } if err := svc.updateOwnerPolicy(ctx, tokenUserID, cli.Owner, cli.ID); err != nil { return mfclients.Client{}, err } return svc.clients.UpdateOwner(ctx, client) } func (svc service) EnableClient(ctx context.Context, token, id string) (mfclients.Client, error) { client := mfclients.Client{ ID: id, UpdatedAt: time.Now(), Status: mfclients.EnabledStatus, } client, err := svc.changeClientStatus(ctx, token, client) if err != nil { return mfclients.Client{}, errors.Wrap(mfclients.ErrEnableClient, err) } return client, nil } func (svc service) DisableClient(ctx context.Context, token, id string) (mfclients.Client, error) { client := mfclients.Client{ ID: id, UpdatedAt: time.Now(), Status: mfclients.DisabledStatus, } client, err := svc.changeClientStatus(ctx, token, client) if err != nil { return mfclients.Client{}, errors.Wrap(mfclients.ErrDisableClient, err) } return client, nil } func (svc service) changeClientStatus(ctx context.Context, token string, client mfclients.Client) (mfclients.Client, error) { tokenUserID, err := svc.Identify(ctx, token) if err != nil { return mfclients.Client{}, err } if tokenUserID != client.ID { if err := svc.isOwner(ctx, client.ID, tokenUserID); err != nil { return mfclients.Client{}, err } } dbClient, err := svc.clients.RetrieveByID(ctx, client.ID) if err != nil { return mfclients.Client{}, err } if dbClient.Status == client.Status { return mfclients.Client{}, mfclients.ErrStatusAlreadyAssigned } client.UpdatedBy = tokenUserID return svc.clients.ChangeStatus(ctx, client) } func (svc service) ListMembers(ctx context.Context, token, objectKind string, objectID string, pm mfclients.Page) (mfclients.MembersPage, error) { var objectType string var authzPerm string switch objectKind { case thingsKind: objectType = thingType authzPerm = pm.Permission case groupsKind: fallthrough default: objectType = groupType authzPerm = auth.SwitchToPermission(pm.Permission) } if _, err := svc.authorize(ctx, userType, tokenKind, token, authzPerm, objectType, objectID); err != nil { return mfclients.MembersPage{}, err } uids, err := svc.auth.ListAllSubjects(ctx, &mainflux.ListSubjectsReq{ SubjectType: userType, Permission: pm.Permission, Object: objectID, ObjectType: objectType, }) if err != nil { return mfclients.MembersPage{}, err } if len(uids.Policies) == 0 { return mfclients.MembersPage{ Page: mfclients.Page{Total: 0, Offset: pm.Offset, Limit: pm.Limit}, }, nil } pm.IDs = uids.Policies cp, err := svc.clients.RetrieveAll(ctx, pm) if err != nil { return mfclients.MembersPage{}, err } return mfclients.MembersPage{ Page: cp.Page, Members: cp.Clients, }, nil } func (svc *service) isOwner(ctx context.Context, clientID, ownerID string) error { _, err := svc.authorize(ctx, userType, userKind, ownerID, ownerRelation, userType, clientID) return err } func (svc *service) authorize(ctx context.Context, subjType, subjKind, subj, perm, objType, obj string) (string, error) { req := &mainflux.AuthorizeReq{ SubjectType: subjType, SubjectKind: subjKind, Subject: subj, Permission: perm, ObjectType: objType, Object: obj, } res, err := svc.auth.Authorize(ctx, req) if err != nil { return "", errors.Wrap(errors.ErrAuthorization, err) } if !res.GetAuthorized() { return "", errors.ErrAuthorization } return res.GetId(), nil } func (svc service) Identify(ctx context.Context, token string) (string, error) { user, err := svc.auth.Identify(ctx, &mainflux.IdentityReq{Token: token}) if err != nil { return "", err } return user.GetId(), nil } func (svc service) updateOwnerPolicy(ctx context.Context, previousOwnerID, ownerID, userID string) error { if previousOwnerID != "" { if _, err := svc.auth.DeletePolicy(ctx, &mainflux.DeletePolicyReq{ SubjectType: userType, Subject: previousOwnerID, Relation: ownerRelation, ObjectType: userType, Object: userID, }); err != nil { return err } } return svc.addOwnerPolicy(ctx, ownerID, userID) } func (svc service) addOwnerPolicy(ctx context.Context, ownerID, userID string) error { if ownerID != "" { if _, err := svc.auth.AddPolicy(ctx, &mainflux.AddPolicyReq{ SubjectType: userType, Subject: ownerID, Relation: ownerRelation, ObjectType: userType, Object: userID, }); err != nil { return err } } return nil }