diff --git a/modules/core/domain/entities/permission/rbac.go b/modules/core/domain/entities/permission/rbac.go deleted file mode 100644 index b15b6358..00000000 --- a/modules/core/domain/entities/permission/rbac.go +++ /dev/null @@ -1,43 +0,0 @@ -package permission - -import ( - "errors" - "github.com/google/uuid" -) - -var ( - ErrPermissionNotFound = errors.New("permission not found") -) - -type RBAC interface { - Register(permissions ...*Permission) - Get(id uuid.UUID) (*Permission, error) - Permissions() []*Permission -} - -type rbac struct { - permissions []*Permission -} - -func NewRbac() RBAC { - return &rbac{ - permissions: []*Permission{}, - } -} - -func (r *rbac) Register(permissions ...*Permission) { - r.permissions = append(r.permissions, permissions...) -} - -func (r *rbac) Get(id uuid.UUID) (*Permission, error) { - for _, p := range r.permissions { - if p.ID == id { - return p, nil - } - } - return nil, ErrPermissionNotFound -} - -func (r *rbac) Permissions() []*Permission { - return r.permissions -} diff --git a/modules/core/module.go b/modules/core/module.go index 01218172..ab94ed56 100644 --- a/modules/core/module.go +++ b/modules/core/module.go @@ -47,16 +47,24 @@ func (m *Module) Register(app application.Application) error { userRepo := persistence.NewUserRepository(uploadRepo) roleRepo := persistence.NewRoleRepository() + // Create services + userService := services.NewUserService(userRepo, app.EventPublisher()) + tabService := services.NewTabService(persistence.NewTabRepository()) + + // Set up dependencies + userService.SetTabService(tabService) + userService.SetApplication(app) + app.RegisterServices( services.NewUploadService(uploadRepo, fsStorage, app.EventPublisher()), - services.NewUserService(userRepo, app.EventPublisher()), + userService, services.NewSessionService(persistence.NewSessionRepository(), app.EventPublisher()), ) app.RegisterServices( services.NewAuthService(app), services.NewCurrencyService(persistence.NewCurrencyRepository(), app.EventPublisher()), services.NewRoleService(roleRepo, app.EventPublisher()), - services.NewTabService(persistence.NewTabRepository()), + tabService, services.NewGroupService(persistence.NewGroupRepository(userRepo, roleRepo), app.EventPublisher()), ) app.RegisterControllers( diff --git a/modules/core/presentation/controllers/dtos/role_dto.go b/modules/core/presentation/controllers/dtos/role_dto.go index c01861e2..46807543 100644 --- a/modules/core/presentation/controllers/dtos/role_dto.go +++ b/modules/core/presentation/controllers/dtos/role_dto.go @@ -3,12 +3,15 @@ package dtos import ( "context" "fmt" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" + "github.com/iota-uz/iota-sdk/modules/core/domain/aggregates/role" "github.com/iota-uz/iota-sdk/modules/core/domain/entities/permission" "github.com/iota-uz/iota-sdk/pkg/composables" "github.com/iota-uz/iota-sdk/pkg/constants" + "github.com/iota-uz/iota-sdk/pkg/rbac" + + "github.com/go-playground/validator/v10" + "github.com/google/uuid" "github.com/nicksnyder/go-i18n/v2/i18n" ) @@ -42,7 +45,7 @@ func (r *CreateRoleDTO) Ok(ctx context.Context) (map[string]string, bool) { return errorMessages, len(errorMessages) == 0 } -func (r *CreateRoleDTO) ToEntity(rbac permission.RBAC) (role.Role, error) { +func (r *CreateRoleDTO) ToEntity(rbac rbac.RBAC) (role.Role, error) { perms := make([]*permission.Permission, 0, len(r.Permissions)) for permID := range r.Permissions { permUUID, err := uuid.Parse(permID) @@ -90,7 +93,7 @@ func (r *UpdateRoleDTO) Ok(ctx context.Context) (map[string]string, bool) { return errorMessages, len(errorMessages) == 0 } -func (r *UpdateRoleDTO) ToEntity(roleEntity role.Role, rbac permission.RBAC) (role.Role, error) { +func (r *UpdateRoleDTO) ToEntity(roleEntity role.Role, rbac rbac.RBAC) (role.Role, error) { perms := make([]*permission.Permission, 0, len(r.Permissions)) for permID := range r.Permissions { permUUID, err := uuid.Parse(permID) diff --git a/modules/core/services/user_service.go b/modules/core/services/user_service.go index 03655a33..96378393 100644 --- a/modules/core/services/user_service.go +++ b/modules/core/services/user_service.go @@ -4,13 +4,21 @@ import ( "context" "github.com/iota-uz/iota-sdk/modules/core/domain/aggregates/user" + "github.com/iota-uz/iota-sdk/modules/core/domain/entities/tab" + "github.com/iota-uz/iota-sdk/modules/core/permissions" + "github.com/iota-uz/iota-sdk/pkg/application" "github.com/iota-uz/iota-sdk/pkg/composables" + "github.com/iota-uz/iota-sdk/pkg/constants" "github.com/iota-uz/iota-sdk/pkg/eventbus" + "github.com/iota-uz/iota-sdk/pkg/types" + "github.com/nicksnyder/go-i18n/v2/i18n" ) type UserService struct { - repo user.Repository - publisher eventbus.EventBus + repo user.Repository + publisher eventbus.EventBus + app application.Application + tabService *TabService } func NewUserService(repo user.Repository, publisher eventbus.EventBus) *UserService { @@ -20,6 +28,58 @@ func NewUserService(repo user.Repository, publisher eventbus.EventBus) *UserServ } } +func (s *UserService) SetTabService(tabService *TabService) { + s.tabService = tabService +} + +func (s *UserService) SetApplication(app application.Application) { + s.app = app +} + +func (s *UserService) getAccessibleNavItems(items []types.NavigationItem, user user.User) []string { + var result []string + + for _, item := range items { + if item.HasPermission(user) { + if item.Href != "" { + result = append(result, item.Href) + } + + if len(item.Children) > 0 { + childItems := s.getAccessibleNavItems(item.Children, user) + result = append(result, childItems...) + } + } + } + + return result +} + +func (s *UserService) createUserTabs(ctx context.Context, user user.User) error { + if s.app == nil || s.tabService == nil { + return nil + } + + items := s.app.NavItems(i18n.NewLocalizer(s.app.Bundle(), string(user.UILanguage()))) + hrefs := s.getAccessibleNavItems(items, user) + + tabs := make([]*tab.CreateDTO, 0, len(hrefs)) + for i, href := range hrefs { + tabs = append(tabs, &tab.CreateDTO{ + Href: href, + UserID: user.ID(), + Position: uint(i), + }) + } + + if len(tabs) > 0 { + ctxWithUser := context.WithValue(ctx, constants.UserKey, user) + _, err := s.tabService.CreateManyUserTabs(ctxWithUser, user.ID(), tabs) + return err + } + return nil +} + func (s *UserService) GetByEmail(ctx context.Context, email string) (user.User, error) { return s.repo.GetByEmail(ctx, email) } @@ -33,14 +93,23 @@ func (s *UserService) GetAll(ctx context.Context) ([]user.User, error) { } func (s *UserService) GetByID(ctx context.Context, id uint) (user.User, error) { + if err := composables.CanUser(ctx, permissions.UserRead); err != nil { + return nil, err + } return s.repo.GetByID(ctx, id) } func (s *UserService) GetPaginated(ctx context.Context, params *user.FindParams) ([]user.User, error) { + if err := composables.CanUser(ctx, permissions.UserRead); err != nil { + return nil, err + } return s.repo.GetPaginated(ctx, params) } func (s *UserService) GetPaginatedWithTotal(ctx context.Context, params *user.FindParams) ([]user.User, int64, error) { + if err := composables.CanUser(ctx, permissions.UserRead); err != nil { + return nil, 0, err + } us, err := s.repo.GetPaginated(ctx, params) if err != nil { return nil, 0, err @@ -53,6 +122,9 @@ func (s *UserService) GetPaginatedWithTotal(ctx context.Context, params *user.Fi } func (s *UserService) Create(ctx context.Context, data user.User) error { + if err := composables.CanUser(ctx, permissions.UserCreate); err != nil { + return err + } tx, err := composables.BeginTx(ctx) if err != nil { return err @@ -70,6 +142,11 @@ func (s *UserService) Create(ctx context.Context, data user.User) error { if err != nil { return err } + + if err := s.createUserTabs(ctx, created); err != nil { + return err + } + if err := tx.Commit(ctx); err != nil { return err } @@ -87,6 +164,9 @@ func (s *UserService) UpdateLastLogin(ctx context.Context, id uint) error { } func (s *UserService) Update(ctx context.Context, data user.User) error { + if err := composables.CanUser(ctx, permissions.UserUpdate); err != nil { + return err + } tx, err := composables.BeginTx(ctx) if err != nil { return err @@ -114,6 +194,9 @@ func (s *UserService) Update(ctx context.Context, data user.User) error { } func (s *UserService) Delete(ctx context.Context, id uint) (user.User, error) { + if err := composables.CanUser(ctx, permissions.UserDelete); err != nil { + return nil, err + } tx, err := composables.BeginTx(ctx) if err != nil { return nil, err diff --git a/pkg/application/application.go b/pkg/application/application.go index 7aae8e7b..6aa9564a 100644 --- a/pkg/application/application.go +++ b/pkg/application/application.go @@ -16,9 +16,9 @@ import ( "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" - "github.com/iota-uz/iota-sdk/modules/core/domain/entities/permission" "github.com/iota-uz/iota-sdk/pkg/configuration" "github.com/iota-uz/iota-sdk/pkg/eventbus" + "github.com/iota-uz/iota-sdk/pkg/rbac" "github.com/iota-uz/iota-sdk/pkg/spotlight" "github.com/iota-uz/iota-sdk/pkg/types" ) @@ -93,7 +93,7 @@ func New(pool *pgxpool.Pool, eventPublisher eventbus.EventBus) Application { return &application{ pool: pool, eventPublisher: eventPublisher, - rbac: permission.NewRbac(), + rbac: rbac.NewRbac(), controllers: make(map[string]Controller), services: make(map[reflect.Type]interface{}), spotlight: spotlight.New(), @@ -106,7 +106,7 @@ func New(pool *pgxpool.Pool, eventPublisher eventbus.EventBus) Application { type application struct { pool *pgxpool.Pool eventPublisher eventbus.EventBus - rbac permission.RBAC + rbac rbac.RBAC services map[reflect.Type]interface{} controllers map[string]Controller middleware []mux.MiddlewareFunc @@ -131,7 +131,7 @@ func (app *application) RegisterNavItems(items ...types.NavigationItem) { app.navItems = append(app.navItems, items...) } -func (app *application) RBAC() permission.RBAC { +func (app *application) RBAC() rbac.RBAC { return app.rbac } diff --git a/pkg/application/interface.go b/pkg/application/interface.go index ae36a8cf..f9c4babc 100644 --- a/pkg/application/interface.go +++ b/pkg/application/interface.go @@ -5,16 +5,16 @@ import ( "embed" "reflect" - "github.com/iota-uz/iota-sdk/modules/core/domain/entities/permission" + "github.com/iota-uz/iota-sdk/pkg/eventbus" + "github.com/iota-uz/iota-sdk/pkg/rbac" "github.com/iota-uz/iota-sdk/pkg/spotlight" - "github.com/jackc/pgx/v5/pgxpool" + "github.com/iota-uz/iota-sdk/pkg/types" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/executor" "github.com/benbjohnson/hashfs" "github.com/gorilla/mux" - "github.com/iota-uz/iota-sdk/pkg/eventbus" - "github.com/iota-uz/iota-sdk/pkg/types" + "github.com/jackc/pgx/v5/pgxpool" "github.com/nicksnyder/go-i18n/v2/i18n" ) @@ -32,7 +32,7 @@ type Application interface { Middleware() []mux.MiddlewareFunc Assets() []*embed.FS HashFsAssets() []*hashfs.FS - RBAC() permission.RBAC + RBAC() rbac.RBAC Spotlight() spotlight.Spotlight Migrations() MigrationManager NavItems(localizer *i18n.Localizer) []types.NavigationItem diff --git a/pkg/composables/auth.go b/pkg/composables/auth.go index a0cab202..55a5ca2c 100644 --- a/pkg/composables/auth.go +++ b/pkg/composables/auth.go @@ -8,6 +8,7 @@ import ( "github.com/iota-uz/iota-sdk/modules/core/domain/entities/permission" "github.com/iota-uz/iota-sdk/modules/core/domain/entities/session" "github.com/iota-uz/iota-sdk/pkg/constants" + "github.com/iota-uz/iota-sdk/pkg/rbac" ) var ( @@ -41,11 +42,21 @@ func MustUseUser(ctx context.Context) user.User { func CanUser(ctx context.Context, permission *permission.Permission) error { u, err := UseUser(ctx) if err != nil { - return err + return nil } if !u.Can(permission) { - return nil - // return service.ErrForbidden + return ErrForbidden + } + return nil +} + +func CanUserAll(ctx context.Context, perms ...rbac.Permission) error { + u, err := UseUser(ctx) + if err != nil || len(perms) == 0 { + return nil // don't check if the user isn't in the context + } + if !rbac.And(perms...).Can(u) { + return ErrForbidden } return nil } diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go new file mode 100644 index 00000000..54c2f52d --- /dev/null +++ b/pkg/rbac/rbac.go @@ -0,0 +1,105 @@ +package rbac + +import ( + "errors" + + "github.com/iota-uz/iota-sdk/modules/core/domain/aggregates/user" + "github.com/iota-uz/iota-sdk/modules/core/domain/entities/permission" + + "github.com/google/uuid" +) + +var ( + ErrPermissionNotFound = errors.New("permission not found") +) + +type Permission interface { + Can(u user.User) bool +} + +type rbacPermission struct { + *permission.Permission +} + +var _ Permission = (*rbacPermission)(nil) + +func (p rbacPermission) Can(u user.User) bool { + return u.Can(p.Permission) +} + +type or struct { + permissions []Permission +} + +var _ Permission = (*or)(nil) + +func (o or) Can(u user.User) bool { + for _, p := range o.permissions { + if p.Can(u) { + return true + } + } + return false +} + +type and struct { + permissions []Permission +} + +var _ Permission = (*and)(nil) + +func (a and) Can(u user.User) bool { + for _, p := range a.permissions { + if !p.Can(u) { + return false + } + } + return true +} + +func Or(perms ...Permission) Permission { + return or{permissions: perms} +} + +func And(perms ...Permission) Permission { + return and{permissions: perms} +} + +func Perm(p *permission.Permission) Permission { + return rbacPermission{Permission: p} +} + +type RBAC interface { + Register(permissions ...*permission.Permission) + Get(id uuid.UUID) (*permission.Permission, error) + Permissions() []*permission.Permission +} + +type rbac struct { + permissions []*permission.Permission +} + +var _ RBAC = (*rbac)(nil) + +func NewRbac() RBAC { + return &rbac{ + permissions: []*permission.Permission{}, + } +} + +func (r *rbac) Register(permissions ...*permission.Permission) { + r.permissions = append(r.permissions, permissions...) +} + +func (r *rbac) Get(id uuid.UUID) (*permission.Permission, error) { + for _, p := range r.permissions { + if p.ID == id { + return p, nil + } + } + return nil, ErrPermissionNotFound +} + +func (r *rbac) Permissions() []*permission.Permission { + return r.permissions +}