1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-26 13:48:53 +08:00
Washington Kigani Kamadi 822a607e31
NOISSUE - Change error description (#1892)
* Change error description

Signed-off-by: WashingtonKK <washingtonkigan@gmail.com>

* Fix typo

Signed-off-by: WashingtonKK <washingtonkigan@gmail.com>

* Create specific error for login

Signed-off-by: WashingtonKK <washingtonkigan@gmail.com>

* Add period to comment

Signed-off-by: WashingtonKK <washingtonkigan@gmail.com>

* fix error description

Signed-off-by: WashingtonKK <washingtonkigan@gmail.com>

---------

Signed-off-by: WashingtonKK <washingtonkigan@gmail.com>
2023-08-23 14:48:03 +02:00

477 lines
13 KiB
Go

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package clients
import (
"context"
"regexp"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/internal/apiutil"
mfclients "github.com/mainflux/mainflux/pkg/clients"
"github.com/mainflux/mainflux/pkg/errors"
"github.com/mainflux/mainflux/users/clients/postgres"
"github.com/mainflux/mainflux/users/jwt"
"github.com/mainflux/mainflux/users/policies"
)
const (
myKey = "mine"
clientsObjectKey = "clients"
updateRelationKey = "c_update"
listRelationKey = "c_list"
deleteRelationKey = "c_delete"
entityType = "client"
)
var (
// ErrMissingResetToken indicates malformed or missing reset token
// for reseting password.
ErrMissingResetToken = errors.New("missing reset token")
// ErrRecoveryToken indicates error in generating password recovery token.
ErrRecoveryToken = errors.New("failed to generate password recovery token")
// ErrGetToken indicates error in getting signed token.
ErrGetToken = errors.New("failed to fetch signed token")
// ErrPasswordFormat indicates weak password.
ErrPasswordFormat = errors.New("password does not meet the requirements")
)
// Service unites Clients and JWT services.
type Service interface {
ClientService
jwt.Service
}
type service struct {
clients postgres.Repository
policies policies.Repository
idProvider mainflux.IDProvider
hasher Hasher
tokens jwt.Repository
email Emailer
passRegex *regexp.Regexp
}
// NewService returns a new Clients service implementation.
func NewService(c postgres.Repository, p policies.Repository, t jwt.Repository, e Emailer, h Hasher, idp mainflux.IDProvider, pr *regexp.Regexp) Service {
return service{
clients: c,
policies: p,
hasher: h,
tokens: t,
email: e,
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
}
return client, nil
}
func (svc service) IssueToken(ctx context.Context, identity, secret string) (jwt.Token, error) {
dbUser, err := svc.clients.RetrieveByIdentity(ctx, identity)
if err != nil {
return jwt.Token{}, errors.Wrap(errors.ErrAuthentication, err)
}
if err := svc.hasher.Compare(secret, dbUser.Credentials.Secret); err != nil {
return jwt.Token{}, errors.Wrap(errors.ErrLogin, err)
}
claims := jwt.Claims{
ClientID: dbUser.ID,
Email: dbUser.Credentials.Identity,
}
return svc.tokens.Issue(ctx, claims)
}
func (svc service) RefreshToken(ctx context.Context, refreshToken string) (jwt.Token, error) {
claims, err := svc.tokens.Parse(ctx, refreshToken)
if err != nil {
return jwt.Token{}, errors.Wrap(errors.ErrAuthentication, err)
}
if claims.Type != jwt.RefreshToken {
return jwt.Token{}, errors.Wrap(errors.ErrAuthentication, err)
}
if _, err := svc.clients.RetrieveByID(ctx, claims.ClientID); err != nil {
return jwt.Token{}, errors.Wrap(errors.ErrAuthentication, err)
}
return svc.tokens.Issue(ctx, claims)
}
func (svc service) ViewClient(ctx context.Context, token string, id string) (mfclients.Client, error) {
ir, err := svc.Identify(ctx, token)
if err != nil {
return mfclients.Client{}, err
}
if err := svc.authorize(ctx, ir, id, listRelationKey); 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
}
switch err := svc.authorize(ctx, id, clientsObjectKey, listRelationKey); err {
// If the user is admin, fetch all users from database.
case nil:
switch {
// visibility = all
case pm.SharedBy == myKey && pm.Owner == myKey:
pm.SharedBy = ""
pm.Owner = ""
// visibility = shared
case pm.SharedBy == myKey && pm.Owner != myKey:
pm.SharedBy = id
pm.Owner = ""
// visibility = mine
case pm.Owner == myKey && pm.SharedBy != myKey:
pm.Owner = id
pm.SharedBy = ""
}
// If the user is not admin, fetch users that they own or are shared with them.
default:
switch {
// visibility = all
case pm.SharedBy == myKey && pm.Owner == myKey:
pm.SharedBy = id
pm.Owner = id
// visibility = shared
case pm.SharedBy == myKey && pm.Owner != myKey:
pm.SharedBy = id
pm.Owner = ""
// visibility = mine
case pm.Owner == myKey && pm.SharedBy != myKey:
pm.Owner = id
pm.SharedBy = ""
default:
pm.Owner = id
}
pm.Action = listRelationKey
}
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) {
id, err := svc.Identify(ctx, token)
if err != nil {
return mfclients.Client{}, err
}
if err := svc.authorize(ctx, id, cli.ID, updateRelationKey); err != nil {
return mfclients.Client{}, err
}
client := mfclients.Client{
ID: cli.ID,
Name: cli.Name,
Metadata: cli.Metadata,
UpdatedAt: time.Now(),
UpdatedBy: id,
}
return svc.clients.Update(ctx, client)
}
func (svc service) UpdateClientTags(ctx context.Context, token string, cli mfclients.Client) (mfclients.Client, error) {
id, err := svc.Identify(ctx, token)
if err != nil {
return mfclients.Client{}, err
}
if err := svc.authorize(ctx, id, cli.ID, updateRelationKey); err != nil {
return mfclients.Client{}, err
}
client := mfclients.Client{
ID: cli.ID,
Tags: cli.Tags,
UpdatedAt: time.Now(),
UpdatedBy: id,
}
return svc.clients.UpdateTags(ctx, client)
}
func (svc service) UpdateClientIdentity(ctx context.Context, token, clientID, identity string) (mfclients.Client, error) {
id, err := svc.Identify(ctx, token)
if err != nil {
return mfclients.Client{}, err
}
if err := svc.authorize(ctx, id, clientID, updateRelationKey); err != nil {
return mfclients.Client{}, err
}
cli := mfclients.Client{
ID: clientID,
Credentials: mfclients.Credentials{
Identity: identity,
},
UpdatedAt: time.Now(),
UpdatedBy: id,
}
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
}
claims := jwt.Claims{
ClientID: client.ID,
Email: client.Credentials.Identity,
}
t, err := svc.tokens.Issue(ctx, claims)
if err != nil {
return errors.Wrap(ErrRecoveryToken, err)
}
return svc.SendPasswordReset(ctx, host, email, client.Name, t.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) {
id, err := svc.Identify(ctx, token)
if err != nil {
return mfclients.Client{}, err
}
if err := svc.authorize(ctx, id, cli.ID, updateRelationKey); err != nil {
return mfclients.Client{}, err
}
client := mfclients.Client{
ID: cli.ID,
Owner: cli.Owner,
UpdatedAt: time.Now(),
UpdatedBy: id,
}
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) {
id, err := svc.Identify(ctx, token)
if err != nil {
return mfclients.Client{}, err
}
if err := svc.authorize(ctx, id, client.ID, deleteRelationKey); 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 = id
return svc.clients.ChangeStatus(ctx, client)
}
func (svc service) ListMembers(ctx context.Context, token, groupID string, pm mfclients.Page) (mfclients.MembersPage, error) {
id, err := svc.Identify(ctx, token)
if err != nil {
return mfclients.MembersPage{}, err
}
// If the user is admin, fetch all members from the database.
if err := svc.authorize(ctx, id, groupID, listRelationKey); err == nil {
return svc.clients.Members(ctx, groupID, pm)
}
pm.Subject = id
pm.Action = "g_list"
return svc.clients.Members(ctx, groupID, pm)
}
func (svc service) authorize(ctx context.Context, subject, object, action string) error {
if subject == object {
return nil
}
policy := policies.Policy{Subject: subject, Object: object, Actions: []string{action}}
if err := policy.Validate(); err != nil {
return err
}
if err := svc.policies.CheckAdmin(ctx, policy.Subject); err == nil {
return nil
}
aReq := policies.AccessRequest{Subject: subject, Object: object, Action: action, Entity: entityType}
if _, err := svc.policies.EvaluateUserAccess(ctx, aReq); err != nil {
return err
}
return nil
}
func (svc service) Identify(ctx context.Context, token string) (string, error) {
claims, err := svc.tokens.Parse(ctx, token)
if err != nil {
return "", err
}
if claims.Type != jwt.AccessToken {
return "", errors.ErrAuthentication
}
return claims.ClientID, nil
}