package usecase import ( "context" "errors" "fmt" libmongo "gateway/internal/library/mongo" redislib "gateway/internal/library/redis" permcfg "gateway/internal/model/permission/config" permission "gateway/internal/model/permission/domain" domrepo "gateway/internal/model/permission/domain/repository" dom "gateway/internal/model/permission/domain/usecase" permrepo "gateway/internal/model/permission/repository" ) // FactoryParam configures the permission module. Repositories may be // pre-built (used by tests / cmd seed) or auto-constructed from MongoConf. type FactoryParam struct { MongoConf *libmongo.Conf Redis *redislib.Client Config permcfg.Config // Optional pre-built repositories. When set, MongoConf is ignored // for that repository. Permissions domrepo.PermissionRepository Roles domrepo.RoleRepository RolePermissions domrepo.RolePermissionRepository UserRoles domrepo.UserRoleRepository RoleMappings domrepo.RoleMappingRepository // Optional Casbin model text (overrides Config.Casbin.ModelPath). CasbinModelText string } // Module bundles all permission usecase ports. type Module struct { Permission dom.PermissionUseCase Role dom.RoleUseCase RolePermission dom.RolePermissionUseCase UserRole dom.UserRoleUseCase RoleMapping dom.RoleMappingUseCase AuthorizationQuery dom.AuthorizationQueryUseCase RBAC dom.RBACUseCase Permissions domrepo.PermissionRepository Roles domrepo.RoleRepository RolePermissions domrepo.RolePermissionRepository UserRoles domrepo.UserRoleRepository RoleMappings domrepo.RoleMappingRepository } // NewModuleFromParam wires the seven usecases against the configured // repositories. Mongo is required for catalog/role/user-role/mapping; // Redis is required for the Casbin enforcer + pub/sub broadcast. // // When Redis is missing, RBAC stays nil and Permission/Role mutations // continue to work but Check() always denies. Mongo missing returns an // error because the catalog cannot live anywhere else. func NewModuleFromParam(param FactoryParam) (*Module, error) { cfg := param.Config.Defaults() mod := &Module{ Permissions: param.Permissions, Roles: param.Roles, RolePermissions: param.RolePermissions, UserRoles: param.UserRoles, RoleMappings: param.RoleMappings, } if mod.Permissions == nil { if param.MongoConf == nil || param.MongoConf.Host == "" { return nil, fmt.Errorf("permission: mongo config required") } mod.Permissions = permrepo.NewPermissionRepository(permrepo.PermissionRepositoryParam{Conf: param.MongoConf}) } if mod.Roles == nil { mod.Roles = permrepo.NewRoleRepository(permrepo.RoleRepositoryParam{Conf: param.MongoConf}) } if mod.RolePermissions == nil { mod.RolePermissions = permrepo.NewRolePermissionRepository(permrepo.RolePermissionRepositoryParam{Conf: param.MongoConf}) } if mod.UserRoles == nil { mod.UserRoles = permrepo.NewUserRoleRepository(permrepo.UserRoleRepositoryParam{Conf: param.MongoConf}) } if mod.RoleMappings == nil { mod.RoleMappings = permrepo.NewRoleMappingRepository(permrepo.RoleMappingRepositoryParam{Conf: param.MongoConf}) } mod.Permission = NewPermissionUseCase(PermissionUseCaseParam{Permissions: mod.Permissions}) var reloader PolicyReloader if cfg.Casbin.Enabled && param.Redis != nil && param.Redis.Zero() != nil { // Plug the bridge so rbac_usecase can build a Redis adapter // without importing repository (avoids cycle). RedisAdapterFactory = func(client *redislib.Client) (domrepo.CasbinPolicyAdapter, error) { if client == nil || client.Zero() == nil { return nil, nil } return permrepo.NewCasbinRedisAdapter(client) } rbacUC, err := NewRBACUseCase(RBACUseCaseParam{ Roles: mod.Roles, Permissions: mod.Permissions, RolePermissions: mod.RolePermissions, UserRoles: mod.UserRoles, Redis: param.Redis, ModelPath: cfg.Casbin.ModelPath, CasbinModelText: param.CasbinModelText, ReloadChannel: cfg.Reload.Channel, }) if err != nil && !errors.Is(err, permission.ErrCasbinNotConfigured) { return nil, err } mod.RBAC = rbacUC if rbacUC != nil { reloader = rbacUC.BroadcastReload } } mod.Role = NewRoleUseCase(RoleUseCaseParam{ Roles: mod.Roles, RolePermissions: mod.RolePermissions, UserRoles: mod.UserRoles, }) mod.RolePermission = NewRolePermissionUseCase(RolePermissionUseCaseParam{ Roles: mod.Roles, Permissions: mod.Permissions, RolePermissions: mod.RolePermissions, Reloader: reloader, }) mod.UserRole = NewUserRoleUseCase(UserRoleUseCaseParam{ Roles: mod.Roles, UserRoles: mod.UserRoles, Reloader: reloader, }) mod.RoleMapping = NewRoleMappingUseCase(RoleMappingUseCaseParam{ Roles: mod.Roles, Mappings: mod.RoleMappings, }) mod.AuthorizationQuery = NewAuthorizationQueryUseCase(AuthorizationQueryUseCaseParam{ Roles: mod.Roles, Permissions: mod.Permissions, RolePermissions: mod.RolePermissions, UserRoles: mod.UserRoles, }) return mod, nil } // StartBackground starts the policy reload subscriber when configured. // Safe to call when RBAC is nil (no-op). func (m *Module) StartBackground(ctx context.Context) error { if m == nil || m.RBAC == nil { return nil } return m.RBAC.StartReloadSubscriber(ctx) } // StopBackground tears down the subscriber. Safe to call when never // started. func (m *Module) StopBackground() { if m == nil || m.RBAC == nil { return } m.RBAC.StopReloadSubscriber() }