mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
6c7e5d893d
Signed-off-by: Arvindh <arvindh91@gmail.com>
859 lines
23 KiB
Go
859 lines
23 KiB
Go
// Copyright (c) Abstract Machines
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package users
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/mail"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/absmach/magistrala"
|
|
grpcTokenV1 "github.com/absmach/magistrala/api/grpc/token/v1"
|
|
apiutil "github.com/absmach/magistrala/api/http/util"
|
|
smqauth "github.com/absmach/magistrala/auth"
|
|
"github.com/absmach/magistrala/pkg/authn"
|
|
"github.com/absmach/magistrala/pkg/errors"
|
|
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
|
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
|
"github.com/absmach/magistrala/pkg/policies"
|
|
"github.com/gofrs/uuid/v5"
|
|
)
|
|
|
|
const defaultUsernamePrefix = "user"
|
|
|
|
var (
|
|
errRecoveryToken = errors.NewServiceError("failed to generate password recovery token")
|
|
errLoginDisableUser = errors.NewAuthNError("failed to login in disabled user")
|
|
errMatchUserVerification = errors.NewRequestError("user verification does not match with stored verification")
|
|
errSimilarUpdateEmail = errors.NewRequestError("new email is similar to the current email")
|
|
|
|
usernameRegExp = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{34}[a-z0-9]$`)
|
|
)
|
|
|
|
type service struct {
|
|
token grpcTokenV1.TokenServiceClient
|
|
users Repository
|
|
idProvider magistrala.IDProvider
|
|
policies policies.Service
|
|
hasher Hasher
|
|
email Emailer
|
|
}
|
|
|
|
// NewService returns a new Users service implementation.
|
|
func NewService(token grpcTokenV1.TokenServiceClient, urepo Repository, policyService policies.Service, emailer Emailer, hasher Hasher, idp magistrala.IDProvider) Service {
|
|
return service{
|
|
token: token,
|
|
users: urepo,
|
|
policies: policyService,
|
|
hasher: hasher,
|
|
email: emailer,
|
|
idProvider: idp,
|
|
}
|
|
}
|
|
|
|
func (svc service) Register(ctx context.Context, session authn.Session, u User, selfRegister bool) (uc User, err error) {
|
|
if !selfRegister {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
|
|
userID, err := svc.idProvider.ID()
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrIssueProviderID, err)
|
|
}
|
|
|
|
if u.Credentials.Secret != "" {
|
|
hash, err := svc.hasher.Hash(u.Credentials.Secret)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrHashPassword, err)
|
|
}
|
|
u.Credentials.Secret = hash
|
|
}
|
|
|
|
if u.Status != DisabledStatus && u.Status != EnabledStatus {
|
|
return User{}, svcerr.ErrInvalidStatus
|
|
}
|
|
if u.Role != UserRole && u.Role != AdminRole {
|
|
return User{}, svcerr.ErrInvalidRole
|
|
}
|
|
u.ID = userID
|
|
u.CreatedAt = time.Now().UTC()
|
|
|
|
if err := svc.addUserPolicy(ctx, u.ID, u.Role); err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrAddPolicies, err)
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
if errRollback := svc.addUserPolicyRollback(ctx, u.ID, u.Role); errRollback != nil {
|
|
err = errors.Wrap(errors.Wrap(apiutil.ErrRollbackTx, errRollback), err)
|
|
}
|
|
}
|
|
}()
|
|
user, err := svc.users.Save(ctx, u)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) SendVerification(ctx context.Context, session authn.Session) error {
|
|
dbUser, err := svc.users.RetrieveByID(ctx, session.UserID)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
|
|
if !dbUser.VerifiedAt.IsZero() {
|
|
return svcerr.ErrUserAlreadyVerified
|
|
}
|
|
|
|
uv, err := svc.users.RetrieveUserVerification(ctx, dbUser.ID, dbUser.Email)
|
|
if err != nil && err != repoerr.ErrNotFound {
|
|
return errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err = uv.Valid(); err != nil {
|
|
uv, err = NewUserVerification(dbUser.ID, dbUser.Email)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
if err := svc.users.AddUserVerification(ctx, uv); err != nil {
|
|
return errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
}
|
|
|
|
uvs, err := uv.Encode()
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
|
|
if err := svc.email.SendVerification([]string{dbUser.Email}, dbUser.Credentials.Username, uvs); err != nil {
|
|
return errors.Wrap(svcerr.ErrCreateEntity, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (svc service) VerifyEmail(ctx context.Context, token string) (User, error) {
|
|
var received UserVerification
|
|
if err := received.Decode(token); err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrInvalidUserVerification, err)
|
|
}
|
|
|
|
stored, err := svc.users.RetrieveUserVerification(ctx, received.UserID, received.Email)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
|
|
if err := stored.Match(received); err != nil {
|
|
return User{}, errors.Wrap(errMatchUserVerification, err)
|
|
}
|
|
|
|
if err := stored.Valid(); err != nil {
|
|
if err == svcerr.ErrUserVerificationExpired {
|
|
return User{}, err
|
|
}
|
|
return User{}, errors.Wrap(svcerr.ErrMalformedEntity, err)
|
|
}
|
|
|
|
stored.UsedAt = time.Now().UTC()
|
|
if err = svc.users.UpdateUserVerification(ctx, stored); err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
|
|
user := User{
|
|
ID: stored.UserID,
|
|
Email: stored.Email,
|
|
VerifiedAt: time.Now().UTC(),
|
|
}
|
|
user, err = svc.users.UpdateVerifiedAt(ctx, user)
|
|
if err == repoerr.ErrNotFound {
|
|
return User{}, svcerr.ErrInvalidUserVerification
|
|
}
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) IssueToken(ctx context.Context, identity, secret, description string) (*grpcTokenV1.Token, error) {
|
|
var dbUser User
|
|
var err error
|
|
|
|
if _, parseErr := mail.ParseAddress(identity); parseErr != nil {
|
|
dbUser, err = svc.users.RetrieveByUsername(ctx, identity)
|
|
} else {
|
|
dbUser, err = svc.users.RetrieveByEmail(ctx, identity)
|
|
}
|
|
|
|
if err == repoerr.ErrNotFound {
|
|
return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrLogin, err)
|
|
}
|
|
|
|
if err != nil {
|
|
return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrAuthentication, err)
|
|
}
|
|
|
|
if err := svc.hasher.Compare(secret, dbUser.Credentials.Secret); err != nil {
|
|
return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrLogin, err)
|
|
}
|
|
|
|
token, err := svc.token.Issue(ctx, &grpcTokenV1.IssueReq{
|
|
UserId: dbUser.ID,
|
|
UserRole: uint32(dbUser.Role + 1),
|
|
Type: uint32(smqauth.AccessKey),
|
|
Verified: !dbUser.VerifiedAt.IsZero(),
|
|
Description: description,
|
|
})
|
|
if err != nil {
|
|
return &grpcTokenV1.Token{}, err
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func (svc service) RefreshToken(ctx context.Context, session authn.Session, refreshToken string) (*grpcTokenV1.Token, error) {
|
|
dbUser, err := svc.users.RetrieveByID(ctx, session.UserID)
|
|
if err != nil {
|
|
return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrAuthentication, err)
|
|
}
|
|
if dbUser.Status == DisabledStatus {
|
|
return &grpcTokenV1.Token{}, errors.Wrap(svcerr.ErrAuthentication, errLoginDisableUser)
|
|
}
|
|
token, err := svc.token.Refresh(ctx, &grpcTokenV1.RefreshReq{RefreshToken: refreshToken, Verified: !dbUser.VerifiedAt.IsZero()})
|
|
if err != nil {
|
|
return &grpcTokenV1.Token{}, err
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func (svc service) RevokeRefreshToken(ctx context.Context, session authn.Session, tokenID string) error {
|
|
dbUser, err := svc.users.RetrieveByID(ctx, session.UserID)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrAuthentication, err)
|
|
}
|
|
if dbUser.Status == DisabledStatus {
|
|
return errors.Wrap(svcerr.ErrAuthentication, errLoginDisableUser)
|
|
}
|
|
_, err = svc.token.Revoke(ctx, &grpcTokenV1.RevokeReq{UserId: session.UserID, TokenId: tokenID})
|
|
if err != nil {
|
|
if errors.Contains(err, svcerr.ErrNotFound) {
|
|
return errors.Wrap(svcerr.ErrNotFound, err)
|
|
}
|
|
return errors.Wrap(svcerr.ErrRemoveEntity, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc service) ListActiveRefreshTokens(ctx context.Context, session authn.Session) (*grpcTokenV1.ListUserRefreshTokensRes, error) {
|
|
dbUser, err := svc.users.RetrieveByID(ctx, session.UserID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrAuthentication, err)
|
|
}
|
|
if dbUser.Status == DisabledStatus {
|
|
return nil, errors.Wrap(svcerr.ErrAuthentication, errLoginDisableUser)
|
|
}
|
|
|
|
refreshTokens, err := svc.token.ListUserRefreshTokens(ctx, &grpcTokenV1.ListUserRefreshTokensReq{UserId: session.UserID})
|
|
if err != nil {
|
|
return nil, errors.Wrap(svcerr.ErrAuthentication, err)
|
|
}
|
|
|
|
return refreshTokens, nil
|
|
}
|
|
|
|
func (svc service) View(ctx context.Context, session authn.Session, id string) (User, error) {
|
|
user, err := svc.users.RetrieveByID(ctx, id)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
|
|
if session.UserID != id {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
ID: user.ID,
|
|
Metadata: user.Metadata,
|
|
Credentials: Credentials{Username: user.Credentials.Username},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
user.Credentials.Secret = ""
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) ViewProfile(ctx context.Context, session authn.Session) (User, error) {
|
|
user, err := svc.users.RetrieveByID(ctx, session.UserID)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
user.Credentials.Secret = ""
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) ListUsers(ctx context.Context, session authn.Session, pm Page) (UsersPage, error) {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return UsersPage{}, err
|
|
}
|
|
|
|
pm.Role = AllRole
|
|
pg, err := svc.users.RetrieveAll(ctx, pm)
|
|
if err != nil {
|
|
return UsersPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
return pg, err
|
|
}
|
|
|
|
func (svc service) SearchUsers(ctx context.Context, pm Page) (UsersPage, error) {
|
|
page := Page{
|
|
Offset: pm.Offset,
|
|
Limit: pm.Limit,
|
|
FirstName: pm.FirstName,
|
|
LastName: pm.LastName,
|
|
Username: pm.Username,
|
|
Id: pm.Id,
|
|
Role: UserRole,
|
|
}
|
|
|
|
cp, err := svc.users.SearchUsers(ctx, page)
|
|
if err != nil {
|
|
return UsersPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
|
|
return cp, nil
|
|
}
|
|
|
|
func (svc service) Update(ctx context.Context, session authn.Session, id string, usr UserReq) (User, error) {
|
|
if session.UserID != id {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
u, err := svc.users.RetrieveByID(ctx, id)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
if u.AuthProvider != "" {
|
|
if changed(usr.FirstName, u.FirstName) ||
|
|
changed(usr.LastName, u.LastName) ||
|
|
changed(usr.ProfilePicture, u.ProfilePicture) {
|
|
return User{}, svcerr.ErrExternalAuthProviderCouldNotUpdate
|
|
}
|
|
}
|
|
updatedAt := time.Now().UTC()
|
|
usr.UpdatedAt = &updatedAt
|
|
usr.UpdatedBy = &session.UserID
|
|
|
|
user, err := svc.users.Update(ctx, id, usr)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) UpdateTags(ctx context.Context, session authn.Session, id string, usr UserReq) (User, error) {
|
|
if session.UserID != id {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
|
|
updatedAt := time.Now().UTC()
|
|
usr.UpdatedAt = &updatedAt
|
|
usr.UpdatedBy = &session.UserID
|
|
|
|
user, err := svc.users.Update(ctx, id, usr)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) UpdateProfilePicture(ctx context.Context, session authn.Session, id string, usr UserReq) (User, error) {
|
|
if session.UserID != id {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
|
|
u, err := svc.users.RetrieveByID(ctx, id)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
if u.AuthProvider != "" {
|
|
return User{}, svcerr.ErrExternalAuthProviderCouldNotUpdate
|
|
}
|
|
|
|
updatedAt := time.Now().UTC()
|
|
usr.UpdatedAt = &updatedAt
|
|
usr.UpdatedBy = &session.UserID
|
|
|
|
user, err := svc.users.Update(ctx, id, usr)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) UpdateEmail(ctx context.Context, session authn.Session, userID, email string) (User, error) {
|
|
if session.UserID != userID {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
oldUsr, err := svc.users.RetrieveByID(ctx, userID)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
if oldUsr.AuthProvider != "" {
|
|
return User{}, svcerr.ErrExternalAuthProviderCouldNotUpdate
|
|
}
|
|
if oldUsr.Email == email {
|
|
return User{}, errSimilarUpdateEmail
|
|
}
|
|
|
|
usr := User{
|
|
ID: userID,
|
|
Email: email,
|
|
UpdatedAt: time.Now().UTC(),
|
|
UpdatedBy: session.UserID,
|
|
VerifiedAt: time.Time{},
|
|
}
|
|
|
|
user, err := svc.users.UpdateEmail(ctx, usr)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) SendPasswordReset(ctx context.Context, email string) error {
|
|
user, err := svc.users.RetrieveByEmail(ctx, email)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
|
|
issueReq := &grpcTokenV1.IssueReq{
|
|
UserId: user.ID,
|
|
UserRole: uint32(user.Role + 1),
|
|
Type: uint32(smqauth.RecoveryKey),
|
|
}
|
|
token, err := svc.token.Issue(ctx, issueReq)
|
|
if err != nil {
|
|
return errors.Wrap(errRecoveryToken, err)
|
|
}
|
|
|
|
if err := svc.email.SendPasswordReset([]string{email}, user.Credentials.Username, token.AccessToken); err != nil {
|
|
return errors.NewInternalErrorWithErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc service) ResetSecret(ctx context.Context, session authn.Session, secret string) error {
|
|
u, err := svc.users.RetrieveByID(ctx, session.UserID)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
|
|
secret, err = svc.hasher.Hash(secret)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrMalformedEntity, err)
|
|
}
|
|
u = User{
|
|
ID: u.ID,
|
|
Email: u.Email,
|
|
Credentials: Credentials{
|
|
Secret: secret,
|
|
},
|
|
UpdatedAt: time.Now().UTC(),
|
|
UpdatedBy: session.UserID,
|
|
}
|
|
if _, err := svc.users.UpdateSecret(ctx, u); err != nil {
|
|
return errors.Wrap(svcerr.ErrAuthorization, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (svc service) UpdateSecret(ctx context.Context, session authn.Session, oldSecret, newSecret string) (User, error) {
|
|
dbUser, err := svc.users.RetrieveByID(ctx, session.UserID)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
if _, err := svc.IssueToken(ctx, dbUser.Credentials.Username, oldSecret, ""); err != nil {
|
|
return User{}, err
|
|
}
|
|
newSecret, err = svc.hasher.Hash(newSecret)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrMalformedEntity, err)
|
|
}
|
|
dbUser.Credentials.Secret = newSecret
|
|
dbUser.UpdatedAt = time.Now().UTC()
|
|
dbUser.UpdatedBy = session.UserID
|
|
|
|
dbUser, err = svc.users.UpdateSecret(ctx, dbUser)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
|
|
return dbUser, nil
|
|
}
|
|
|
|
func (svc service) UpdateUsername(ctx context.Context, session authn.Session, id, username string) (User, error) {
|
|
if session.UserID != id {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
|
|
usr := User{
|
|
ID: id,
|
|
Credentials: Credentials{
|
|
Username: username,
|
|
},
|
|
UpdatedAt: time.Now().UTC(),
|
|
UpdatedBy: session.UserID,
|
|
}
|
|
updatedUser, err := svc.users.UpdateUsername(ctx, usr)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
return updatedUser, nil
|
|
}
|
|
|
|
func (svc service) UpdateRole(ctx context.Context, session authn.Session, usr User) (User, error) {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
usr = User{
|
|
ID: usr.ID,
|
|
Role: usr.Role,
|
|
UpdatedAt: time.Now().UTC(),
|
|
UpdatedBy: session.UserID,
|
|
}
|
|
|
|
if err := svc.updateUserPolicy(ctx, usr.ID, usr.Role); err != nil {
|
|
return User{}, err
|
|
}
|
|
|
|
u, err := svc.users.UpdateRole(ctx, usr)
|
|
if err != nil {
|
|
// If failed to update role in DB, then revert back to platform admin policies in spicedb
|
|
if errRollback := svc.updateUserPolicy(ctx, usr.ID, UserRole); errRollback != nil {
|
|
return User{}, errors.Wrap(errRollback, err)
|
|
}
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func (svc service) Enable(ctx context.Context, session authn.Session, id string) (User, error) {
|
|
u := User{
|
|
ID: id,
|
|
UpdatedAt: time.Now().UTC(),
|
|
Status: EnabledStatus,
|
|
}
|
|
user, err := svc.changeUserStatus(ctx, session, u)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrEnableUser, err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) Disable(ctx context.Context, session authn.Session, id string) (User, error) {
|
|
user := User{
|
|
ID: id,
|
|
UpdatedAt: time.Now().UTC(),
|
|
Status: DisabledStatus,
|
|
}
|
|
user, err := svc.changeUserStatus(ctx, session, user)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrDisableUser, err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) changeUserStatus(ctx context.Context, session authn.Session, user User) (User, error) {
|
|
if session.UserID != user.ID {
|
|
if err := svc.checkSuperAdmin(ctx, session); err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
dbu, err := svc.users.RetrieveByID(ctx, user.ID)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
|
}
|
|
if dbu.Status == user.Status {
|
|
return User{}, svcerr.ErrStatusAlreadyAssigned
|
|
}
|
|
user.UpdatedBy = session.UserID
|
|
|
|
user, err = svc.users.ChangeStatus(ctx, user)
|
|
if err != nil {
|
|
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (svc service) Delete(ctx context.Context, session authn.Session, id string) error {
|
|
user := User{
|
|
ID: id,
|
|
UpdatedAt: time.Now().UTC(),
|
|
Status: DeletedStatus,
|
|
}
|
|
|
|
if _, err := svc.changeUserStatus(ctx, session, user); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc *service) checkSuperAdmin(ctx context.Context, session authn.Session) error {
|
|
if !session.SuperAdmin {
|
|
if err := svc.users.CheckSuperAdmin(ctx, session.UserID); err != nil {
|
|
return errors.Wrap(svcerr.ErrAuthorization, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc service) OAuthCallback(ctx context.Context, user User) (User, error) {
|
|
u, err := svc.users.RetrieveByEmail(ctx, user.Email)
|
|
|
|
if errors.Contains(err, repoerr.ErrNotFound) {
|
|
user.Credentials.Username = generateUsername(user.Email)
|
|
u, err = svc.Register(ctx, authn.Session{}, user, true)
|
|
if err != nil {
|
|
if errors.Contains(err, errors.ErrUsernameNotAvailable) {
|
|
return User{}, errors.ErrTryAgain
|
|
}
|
|
return User{}, err
|
|
}
|
|
}
|
|
|
|
if err != nil && !errors.Contains(err, repoerr.ErrNotFound) {
|
|
return User{}, err
|
|
}
|
|
|
|
if u.VerifiedAt.IsZero() {
|
|
user.ID = u.ID
|
|
user.VerifiedAt = time.Now()
|
|
u, err = svc.users.UpdateVerifiedAt(ctx, user)
|
|
if err != nil {
|
|
return User{}, err
|
|
}
|
|
}
|
|
|
|
return User{ID: u.ID, Role: u.Role, VerifiedAt: u.VerifiedAt}, nil
|
|
}
|
|
|
|
func (svc service) OAuthAddUserPolicy(ctx context.Context, user User) error {
|
|
return svc.addUserPolicy(ctx, user.ID, user.Role)
|
|
}
|
|
|
|
func (svc service) Identify(ctx context.Context, session authn.Session) (string, error) {
|
|
return session.UserID, nil
|
|
}
|
|
|
|
func (svc service) addUserPolicy(ctx context.Context, userID string, role Role) error {
|
|
policyList := []policies.Policy{}
|
|
|
|
policyList = append(policyList, policies.Policy{
|
|
SubjectType: policies.UserType,
|
|
Subject: userID,
|
|
Relation: policies.MemberRelation,
|
|
ObjectType: policies.PlatformType,
|
|
Object: policies.MagistralaObject,
|
|
})
|
|
|
|
if role == AdminRole {
|
|
policyList = append(policyList, policies.Policy{
|
|
SubjectType: policies.UserType,
|
|
Subject: userID,
|
|
Relation: policies.AdministratorRelation,
|
|
ObjectType: policies.PlatformType,
|
|
Object: policies.MagistralaObject,
|
|
})
|
|
}
|
|
err := svc.policies.AddPolicies(ctx, policyList)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrAddPolicies, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc service) addUserPolicyRollback(ctx context.Context, userID string, role Role) error {
|
|
policyList := []policies.Policy{}
|
|
|
|
policyList = append(policyList, policies.Policy{
|
|
SubjectType: policies.UserType,
|
|
Subject: userID,
|
|
Relation: policies.MemberRelation,
|
|
ObjectType: policies.PlatformType,
|
|
Object: policies.MagistralaObject,
|
|
})
|
|
|
|
if role == AdminRole {
|
|
policyList = append(policyList, policies.Policy{
|
|
SubjectType: policies.UserType,
|
|
Subject: userID,
|
|
Relation: policies.AdministratorRelation,
|
|
ObjectType: policies.PlatformType,
|
|
Object: policies.MagistralaObject,
|
|
})
|
|
}
|
|
err := svc.policies.DeletePolicies(ctx, policyList)
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrDeletePolicies, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc service) updateUserPolicy(ctx context.Context, userID string, role Role) error {
|
|
switch role {
|
|
case AdminRole:
|
|
err := svc.policies.AddPolicy(ctx, policies.Policy{
|
|
SubjectType: policies.UserType,
|
|
Subject: userID,
|
|
Relation: policies.AdministratorRelation,
|
|
ObjectType: policies.PlatformType,
|
|
Object: policies.MagistralaObject,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrAddPolicies, err)
|
|
}
|
|
|
|
return nil
|
|
case UserRole:
|
|
fallthrough
|
|
default:
|
|
err := svc.policies.DeletePolicyFilter(ctx, policies.Policy{
|
|
SubjectType: policies.UserType,
|
|
Subject: userID,
|
|
Relation: policies.AdministratorRelation,
|
|
ObjectType: policies.PlatformType,
|
|
Object: policies.MagistralaObject,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(svcerr.ErrDeletePolicies, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func generateUsername(email string) string {
|
|
uniqueSuffix := generateRandomID()
|
|
emailPrefix := extractEmailPrefix(email)
|
|
return fmt.Sprintf("%s_%s", emailPrefix, uniqueSuffix)
|
|
}
|
|
|
|
func extractEmailPrefix(email string) string {
|
|
parts := strings.Split(email, "@")
|
|
if len(parts) == 0 {
|
|
return defaultUsernamePrefix
|
|
}
|
|
|
|
prefix := parts[0]
|
|
cleaned := usernameRegExp.ReplaceAllString(prefix, "")
|
|
|
|
cleaned = sanitizeForUsername(cleaned, 15)
|
|
if cleaned == "" {
|
|
cleaned = defaultUsernamePrefix
|
|
}
|
|
|
|
return cleaned
|
|
}
|
|
|
|
func generateRandomID() string {
|
|
// Generate 8 random bytes (will result in 16 hex chars, truncated to 10)
|
|
randomBytes := make([]byte, 8)
|
|
if _, err := rand.Read(randomBytes); err != nil {
|
|
// Fallback: use UUID if crypto/rand fails (should never happen)
|
|
id, uuidErr := uuid.NewV4()
|
|
if uuidErr != nil {
|
|
// Last resort fallback
|
|
return fmt.Sprintf("%x", time.Now().UnixNano())[:10]
|
|
}
|
|
return hex.EncodeToString(id.Bytes())[:10]
|
|
}
|
|
return hex.EncodeToString(randomBytes)[:10]
|
|
}
|
|
|
|
// sanitizeForUsername extracts and cleans a string for use in username generation.
|
|
// As per the username requirements:
|
|
// - It keeps only lowercase alphanumeric characters, hyphens, and underscores
|
|
// - ensures valid boundaries (no hyphens/underscores at start/end)
|
|
// - removes consecutive hyphens/underscores (to pass validation)
|
|
// and finally limits the result to maxLen characters.
|
|
func sanitizeForUsername(s string, maxLen int) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
|
|
// Convert to lowercase
|
|
s = strings.ToLower(s)
|
|
|
|
// Filter characters - keep only alphanumeric, hyphen, underscore
|
|
buf := make([]byte, 0, len(s))
|
|
var lastChar byte
|
|
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
|
|
// Keep alphanumeric, hyphen, underscore
|
|
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
|
|
// Skip if current char is hyphen/underscore and same as last char
|
|
if (c == '-' || c == '_') && c == lastChar {
|
|
continue // Skip consecutive hyphens or underscores
|
|
}
|
|
buf = append(buf, c)
|
|
lastChar = c
|
|
} else {
|
|
lastChar = 0 // Reset on special char
|
|
}
|
|
}
|
|
|
|
cleaned := string(buf)
|
|
|
|
// Trim invalid boundary characters
|
|
cleaned = strings.Trim(cleaned, "-_")
|
|
|
|
// Limit length
|
|
if len(cleaned) > maxLen {
|
|
cleaned = cleaned[:maxLen]
|
|
// Re-trim in case truncation exposed hyphen/underscore at end
|
|
cleaned = strings.TrimRight(cleaned, "-_")
|
|
}
|
|
|
|
return cleaned
|
|
}
|
|
|
|
func changed(updated *string, old string) bool {
|
|
if updated == nil {
|
|
return false
|
|
}
|
|
|
|
return *updated != old
|
|
}
|