494 lines
13 KiB
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
|
|
}
|