Mainflux.mainflux/users/service.go

494 lines
13 KiB
Go

// 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
}