mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-26 13:48:53 +08:00

* 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>
477 lines
13 KiB
Go
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
|
|
}
|