vert-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +126 -0
  4. data/lib/vert/authorization/controller_methods.rb +84 -0
  5. data/lib/vert/authorization/dynamic_policy.rb +156 -0
  6. data/lib/vert/authorization/permission_resolver.rb +253 -0
  7. data/lib/vert/authorization/policy_finder.rb +72 -0
  8. data/lib/vert/clients/document_service_client.rb +104 -0
  9. data/lib/vert/concerns/auditable.rb +24 -0
  10. data/lib/vert/concerns/company_scoped.rb +48 -0
  11. data/lib/vert/concerns/current_attributes.rb +53 -0
  12. data/lib/vert/concerns/document_storeable.rb +180 -0
  13. data/lib/vert/concerns/multi_tenant.rb +45 -0
  14. data/lib/vert/concerns/soft_deletable.rb +46 -0
  15. data/lib/vert/concerns/uuid_primary_key.rb +42 -0
  16. data/lib/vert/configuration.rb +65 -0
  17. data/lib/vert/generators/install_generator.rb +66 -0
  18. data/lib/vert/generators/rls_migration_generator.rb +57 -0
  19. data/lib/vert/generators/templates/application_record.rb.tt +8 -0
  20. data/lib/vert/generators/templates/create_outbox_events.rb.tt +24 -0
  21. data/lib/vert/generators/templates/create_rls_functions.rb.tt +27 -0
  22. data/lib/vert/generators/templates/current.rb.tt +10 -0
  23. data/lib/vert/generators/templates/enable_rls_on_tables.rb.tt +39 -0
  24. data/lib/vert/generators/templates/health_controller.rb.tt +5 -0
  25. data/lib/vert/generators/templates/initializer.rb.tt +39 -0
  26. data/lib/vert/generators/templates/outbox_event.rb.tt +11 -0
  27. data/lib/vert/health/checker.rb +119 -0
  28. data/lib/vert/health/routes.rb +44 -0
  29. data/lib/vert/outbox/event.rb +68 -0
  30. data/lib/vert/outbox/publisher.rb +105 -0
  31. data/lib/vert/outbox/publisher_job.rb +30 -0
  32. data/lib/vert/railtie.rb +54 -0
  33. data/lib/vert/rls/connection_handler.rb +56 -0
  34. data/lib/vert/rls/consumer_context.rb +31 -0
  35. data/lib/vert/rls/context_middleware.rb +37 -0
  36. data/lib/vert/rls/job_context.rb +56 -0
  37. data/lib/vert/version.rb +5 -0
  38. data/lib/vert.rb +58 -0
  39. data/vert.gemspec +43 -0
  40. metadata +223 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 802f4618d6763aeb190d05eb6bd29218d07ae0b3c229e968366a7c200bc08fb3
4
+ data.tar.gz: cf5140dd8bfb233db4163479ca714ac30049c69524a1139c36140b0046bc3091
5
+ SHA512:
6
+ metadata.gz: 9e1afa0a09e98a12da919ab46ee34535eacc15d4e74e47edfe7da199b3b3f36895375abd360e7efb0ff87421bece811e0488ca7ddb10ed0dfffceb0f63387e57
7
+ data.tar.gz: ef379164049f6887b3748fdaa06b481e18c1836fa81172c5a63e28d8999d77c58bf08aaeda418f1ccf7be9cbcf94bf2f9b0c37cf3ad6a9d6df0b7bc43f9e3b88
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2025-03-14
4
+
5
+ ### Added
6
+
7
+ - Initial release.
8
+ - Configuration via `Vert.configure` (optional RLS, Outbox, Health, Authorization, concerns).
9
+ - Concerns: Current, UuidPrimaryKey, MultiTenant, Auditable, SoftDeletable, CompanyScoped, DocumentStoreable.
10
+ - Outbox: Event, Publisher, PublisherJob.
11
+ - RLS: ConnectionHandler, ContextMiddleware, JobContext, BaseConsumer.
12
+ - Health: Checker, Routes, ControllerMixin.
13
+ - Authorization: PermissionResolver, DynamicPolicy, PolicyFinder, ControllerMethods.
14
+ - Generators: `vert:install`, `vert:rls_migration`.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # Vert
2
+
3
+ Gem genérica de padrões para aplicações Rails: contexto de request, RLS, Outbox, Health, autorização RBAC/ABAC e concerns opcionais (multi-tenant, auditável, soft delete, UUID, document store). Tudo é **opcional** e configurável via initializer.
4
+
5
+ ## Instalação
6
+
7
+ Adicione ao `Gemfile`:
8
+
9
+ ```ruby
10
+ gem "vert"
11
+ ```
12
+
13
+ Execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ rails generate vert:install
18
+ ```
19
+
20
+ Edite `config/initializers/vert.rb` e ative apenas os recursos que precisar.
21
+
22
+ ## Configuração
23
+
24
+ Em `config/initializers/vert.rb`:
25
+
26
+ ```ruby
27
+ Vert.configure do |config|
28
+ config.enable_rls = true
29
+ config.enable_outbox = true
30
+ config.enable_health = true
31
+ config.auto_mount_health_routes = false
32
+ config.rabbitmq_url = ENV.fetch("RABBITMQ_URL", "amqp://guest:guest@localhost:5672/")
33
+ config.exchange_name = "vert.events"
34
+ config.document_service_url = ENV["DOCUMENT_SERVICE_URL"]
35
+ config.enable_authorization = true
36
+ end
37
+ ```
38
+
39
+ | Opção | Descrição | Padrão |
40
+ |-------|-----------|--------|
41
+ | `enable_rls` | Row Level Security (PostgreSQL) | `false` |
42
+ | `enable_outbox` | Publicação de eventos via Outbox | `false` |
43
+ | `enable_health` | Endpoints e checks de health | `true` |
44
+ | `auto_mount_health_routes` | Montar rotas de health automaticamente | `false` |
45
+ | `enable_authorization` | RBAC/ABAC com Pundit | `false` |
46
+ | `health_check_database` | Incluir check de DB no health | `true` |
47
+ | `health_check_redis` | Incluir check de Redis | `false` |
48
+ | `health_check_rabbitmq` | Incluir check de RabbitMQ | `false` |
49
+ | `health_check_sidekiq` | Incluir check de Sidekiq | `false` |
50
+
51
+ ## Uso
52
+
53
+ ### Contexto de request (Current)
54
+
55
+ Sempre disponível. Defina o contexto após autenticação:
56
+
57
+ ```ruby
58
+ Vert::Current.set_context(tenant_id: "...", user_id: "...", company_id: "...")
59
+ ```
60
+
61
+ Leitura: `Vert::Current.tenant_id`, `Vert::Current.user_id`, `Vert::Current.company_set?`, `Vert::Current.require_tenant!`.
62
+
63
+ Para herdar no app: `class Current < Vert::Current; end` (gerado pelo `vert:install`).
64
+
65
+ ### RLS (Row Level Security)
66
+
67
+ 1. `config.enable_rls = true`
68
+ 2. Gere migrações: `rails generate vert:rls_migration --tables orders items`
69
+ 3. No `ApplicationController`: `include Vert::Rls::ControllerContext`
70
+ 4. Configure `Vert::Current` antes de cada request (ex.: no auth).
71
+
72
+ ### Outbox
73
+
74
+ 1. `config.enable_outbox = true`
75
+ 2. Model `OutboxEvent` e migration criados pelo `vert:install`
76
+ 3. Dentro de uma transação: `OutboxEvent.publish_for(record, event_type: "order.created", payload: { ... })`
77
+ 4. Agende `Vert::Outbox::PublisherJob` (ex.: Sidekiq-Cron a cada 10s).
78
+
79
+ ### Health
80
+
81
+ - `GET /health` → `Vert::Health.check_all`
82
+ - `GET /health/live` → liveness
83
+ - `GET /health/ready` → readiness (DB)
84
+
85
+ Rotas podem ser montadas pelo generator ou com `Vert.config.auto_mount_health_routes = true`. Checks customizados:
86
+
87
+ ```ruby
88
+ Vert::Health.add_check(:external_api) do
89
+ # return { status: "ok" } or { status: "error", message: "..." }
90
+ end
91
+ ```
92
+
93
+ ### Autorização (Pundit)
94
+
95
+ 1. `config.enable_authorization = true`
96
+ 2. No `ApplicationController`: `include Pundit::Authorization` e `include Vert::Authorization::ControllerMethods`
97
+ 3. Políticas: herde de `Vert::Authorization::DynamicPolicy` e use `has_permission?("resource.action")`
98
+ 4. No controller: `authorize_with_context(record)` ou `authorize_with_context(record, :approve?)`
99
+
100
+ ### Concerns em models
101
+
102
+ Inclua conforme necessário (não dependem de flags, exceto document_storeable que usa `config.document_service_url`):
103
+
104
+ - `Vert::Concerns::UuidPrimaryKey`
105
+ - `Vert::Concerns::MultiTenant`
106
+ - `Vert::Concerns::Auditable`
107
+ - `Vert::Concerns::SoftDeletable`
108
+ - `Vert::Concerns::CompanyScoped`
109
+ - `Vert::Concerns::DocumentStoreable` (requer document service)
110
+
111
+ ### Jobs e contexto
112
+
113
+ Para propagar tenant/user em jobs Sidekiq:
114
+
115
+ ```ruby
116
+ class MyJob < ApplicationJob
117
+ include Vert::Rls::JobContext
118
+ def perform(id)
119
+ # Vert::Current.tenant_id disponível
120
+ end
121
+ end
122
+ ```
123
+
124
+ ## Licença
125
+
126
+ MIT.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vert
4
+ module Authorization
5
+ module ControllerMethods
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ rescue_from Pundit::NotAuthorizedError, with: :render_forbidden if defined?(Pundit)
10
+ end
11
+
12
+ def authorize_with_context(record, query = nil, context = {})
13
+ return record unless Vert.config.enable_authorization && defined?(Pundit)
14
+ query ||= "#{action_name}?"
15
+ policy_context = authorization_context.merge(context)
16
+ policy = policy_with_context(record, policy_context)
17
+ unless policy.public_send(query)
18
+ raise Pundit::NotAuthorizedError, query: query, record: record, policy: policy
19
+ end
20
+ record
21
+ end
22
+
23
+ def policy_with_context(record, context = {})
24
+ policy_class = PolicyFinder.new(record).policy
25
+ policy_class.new(current_user, record, context)
26
+ end
27
+
28
+ def has_permission?(permission_code, context = {})
29
+ return false unless Vert.config.enable_authorization
30
+ PermissionResolver.has_permission?(current_user, permission_code, authorization_context.merge(context))
31
+ end
32
+
33
+ def can_see_field?(resource, field)
34
+ allowed = PermissionResolver.get_allowed_fields(current_user, "#{resource}.read", authorization_context)
35
+ denied = PermissionResolver.get_denied_fields(current_user, "#{resource}.read", authorization_context)
36
+ return false if denied.include?(field.to_s)
37
+ return true if allowed.nil?
38
+ allowed.include?(field.to_s)
39
+ end
40
+
41
+ def allowed_fields_for(resource)
42
+ PermissionResolver.get_allowed_fields(current_user, "#{resource}.read", authorization_context)
43
+ end
44
+
45
+ def denied_fields_for(resource)
46
+ PermissionResolver.get_denied_fields(current_user, "#{resource}.read", authorization_context)
47
+ end
48
+
49
+ def current_user_permissions
50
+ PermissionResolver.user_permissions(current_user, authorization_context)
51
+ end
52
+
53
+ protected
54
+
55
+ def authorization_context
56
+ {
57
+ tenant_id: current_tenant_id,
58
+ company_id: current_company_id,
59
+ user_id: current_user&.id,
60
+ action: action_name
61
+ }
62
+ end
63
+
64
+ def current_tenant_id
65
+ Vert::Current.tenant_id
66
+ end
67
+
68
+ def current_company_id
69
+ Vert::Current.company_id
70
+ end
71
+
72
+ private
73
+
74
+ def render_forbidden(exception)
75
+ render json: {
76
+ error: "Access denied",
77
+ message: "You do not have permission to perform this action",
78
+ permission: exception.query&.to_s&.delete_suffix("?"),
79
+ resource: exception.record.is_a?(Class) ? exception.record.name : exception.record.class.name
80
+ }, status: :forbidden
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vert
4
+ module Authorization
5
+ class DynamicPolicy
6
+ attr_reader :user, :record, :context
7
+
8
+ def initialize(user, record, context = {})
9
+ @user = user
10
+ @record = record
11
+ @context = default_context.merge(context)
12
+ end
13
+
14
+ def index?
15
+ has_permission?("#{resource_name}.list")
16
+ end
17
+
18
+ def show?
19
+ has_permission?("#{resource_name}.read")
20
+ end
21
+
22
+ def create?
23
+ has_permission?("#{resource_name}.create")
24
+ end
25
+
26
+ def new?
27
+ create?
28
+ end
29
+
30
+ def update?
31
+ has_permission?("#{resource_name}.update")
32
+ end
33
+
34
+ def edit?
35
+ update?
36
+ end
37
+
38
+ def destroy?
39
+ has_permission?("#{resource_name}.delete")
40
+ end
41
+
42
+ def export?
43
+ has_permission?("#{resource_name}.export")
44
+ end
45
+
46
+ def import?
47
+ has_permission?("#{resource_name}.import")
48
+ end
49
+
50
+ def approve?
51
+ has_permission?("#{resource_name}.approve")
52
+ end
53
+
54
+ def reject?
55
+ has_permission?("#{resource_name}.reject")
56
+ end
57
+
58
+ def cancel?
59
+ has_permission?("#{resource_name}.cancel")
60
+ end
61
+
62
+ def print?
63
+ has_permission?("#{resource_name}.print")
64
+ end
65
+
66
+ class Scope
67
+ attr_reader :user, :scope, :context
68
+
69
+ def initialize(user, scope, context = {})
70
+ @user = user
71
+ @scope = scope
72
+ @context = context
73
+ end
74
+
75
+ def resolve
76
+ if PermissionResolver.has_permission?(user, "#{resource_name}.list", context)
77
+ own_records_only? ? scope.where(created_by: user.id) : scope.all
78
+ else
79
+ scope.none
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def resource_name
86
+ scope.model_name.plural
87
+ end
88
+
89
+ def own_records_only?
90
+ PermissionResolver.get_condition(user, "#{resource_name}.list", "own_records_only", context) == true
91
+ end
92
+ end
93
+
94
+ protected
95
+
96
+ def has_permission?(permission_code)
97
+ return true if user_super_admin?
98
+ PermissionResolver.has_permission?(user, permission_code, context)
99
+ end
100
+
101
+ def permission_condition(condition_key)
102
+ PermissionResolver.get_condition(user, "#{resource_name}.#{current_action}", condition_key, context)
103
+ end
104
+
105
+ def allowed_fields
106
+ PermissionResolver.get_allowed_fields(user, "#{resource_name}.read", context)
107
+ end
108
+
109
+ def denied_fields
110
+ PermissionResolver.get_denied_fields(user, "#{resource_name}.read", context)
111
+ end
112
+
113
+ def can_see_field?(field_name)
114
+ field = field_name.to_s
115
+ denied = denied_fields
116
+ return false if denied.include?(field)
117
+ allowed = allowed_fields
118
+ return true if allowed.nil?
119
+ allowed.include?(field)
120
+ end
121
+
122
+ def resource_name
123
+ @resource_name ||= begin
124
+ klass = record.is_a?(Class) ? record : record.class
125
+ klass.model_name.plural
126
+ end
127
+ end
128
+
129
+ def service_name
130
+ @service_name ||= resource_name.split("/").first
131
+ end
132
+
133
+ private
134
+
135
+ def default_context
136
+ {
137
+ tenant_id: Vert::Current.tenant_id,
138
+ company_id: Vert::Current.company_id,
139
+ user_id: user&.id
140
+ }
141
+ end
142
+
143
+ def current_action
144
+ context[:action] || caller_action
145
+ end
146
+
147
+ def caller_action
148
+ caller_locations(2, 1).first&.label&.delete_suffix("?")
149
+ end
150
+
151
+ def user_super_admin?
152
+ user.respond_to?(:super_admin?) && user.super_admin?
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vert
4
+ module Authorization
5
+ class PermissionResolver
6
+ CACHE_TTL = 5.minutes
7
+ CACHE_PREFIX = "vert:permissions"
8
+
9
+ class << self
10
+ def has_permission?(user, permission_code, context = {})
11
+ return false unless user
12
+ return true if super_admin?(user)
13
+
14
+ cached = get_cached_permission(user, permission_code, context)
15
+ return cached unless cached.nil?
16
+
17
+ result = resolve_permission(user, permission_code, context)
18
+ cache_permission(user, permission_code, context, result)
19
+ result
20
+ end
21
+
22
+ def get_condition(user, permission_code, condition_key, context = {})
23
+ return nil unless user
24
+ conditions = get_permission_conditions(user, permission_code, context)
25
+ conditions&.dig(condition_key.to_s)
26
+ end
27
+
28
+ def get_allowed_fields(user, permission_code, context = {})
29
+ return nil if super_admin?(user)
30
+ fields = get_field_restrictions(user, permission_code, context)
31
+ fields&.dig("granted_fields")
32
+ end
33
+
34
+ def get_denied_fields(user, permission_code, context = {})
35
+ return [] if super_admin?(user)
36
+ fields = get_field_restrictions(user, permission_code, context)
37
+ fields&.dig("denied_fields") || []
38
+ end
39
+
40
+ def user_permissions(user, context = {})
41
+ return [] unless user
42
+ return ["*"] if super_admin?(user)
43
+
44
+ cache_key = user_permissions_cache_key(user, context)
45
+ cached = redis_get(cache_key)
46
+ return cached if cached
47
+
48
+ permissions = collect_user_permissions(user, context)
49
+ redis_set(cache_key, permissions, CACHE_TTL)
50
+ permissions
51
+ end
52
+
53
+ def invalidate_user_cache(user_id)
54
+ pattern = "#{CACHE_PREFIX}:#{user_id}:*"
55
+ redis_delete_pattern(pattern)
56
+ end
57
+
58
+ def invalidate_role_cache(role_id)
59
+ if defined?(UserRole)
60
+ UserRole.where(role_id: role_id).pluck(:user_id).each { |user_id| invalidate_user_cache(user_id) }
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def resolve_permission(user, permission_code, context)
67
+ company_id = context[:company_id]
68
+ return false if has_direct_deny?(user, permission_code, company_id)
69
+ return true if has_direct_grant?(user, permission_code, company_id)
70
+
71
+ effective_roles(user, company_id).each do |role|
72
+ return true if role_has_permission?(role, permission_code)
73
+ end
74
+ false
75
+ end
76
+
77
+ def has_direct_deny?(user, permission_code, company_id)
78
+ return false unless defined?(UserPermission)
79
+ UserPermission
80
+ .joins(:permission)
81
+ .where(user_id: user.id, grant_type: "deny")
82
+ .where("permissions.code = ?", permission_code)
83
+ .where("company_id IS NULL OR company_id = ?", company_id)
84
+ .where("valid_from <= ? AND (valid_until IS NULL OR valid_until >= ?)", Time.current, Time.current)
85
+ .exists?
86
+ end
87
+
88
+ def has_direct_grant?(user, permission_code, company_id)
89
+ return false unless defined?(UserPermission)
90
+ UserPermission
91
+ .joins(:permission)
92
+ .where(user_id: user.id, grant_type: "grant")
93
+ .where("permissions.code = ?", permission_code)
94
+ .where("company_id IS NULL OR company_id = ?", company_id)
95
+ .where("valid_from <= ? AND (valid_until IS NULL OR valid_until >= ?)", Time.current, Time.current)
96
+ .exists?
97
+ end
98
+
99
+ def effective_roles(user, company_id)
100
+ return [] unless defined?(UserRole)
101
+ role_ids = UserRole
102
+ .where(user_id: user.id)
103
+ .where("company_id IS NULL OR company_id = ?", company_id)
104
+ .where("valid_from <= ? AND (valid_until IS NULL OR valid_until >= ?)", Time.current, Time.current)
105
+ .pluck(:role_id)
106
+ return [] if role_ids.empty?
107
+ Role.where(id: role_ids).or(Role.where(id: inherited_role_ids(role_ids))).where(is_active: true).order(priority: :desc)
108
+ end
109
+
110
+ def inherited_role_ids(role_ids, collected = [])
111
+ return collected if role_ids.empty?
112
+ parent_ids = Role.where(id: role_ids).where.not(parent_role_id: nil).pluck(:parent_role_id)
113
+ new_parents = parent_ids - collected
114
+ return collected if new_parents.empty?
115
+ inherited_role_ids(new_parents, collected + new_parents)
116
+ end
117
+
118
+ def role_has_permission?(role, permission_code)
119
+ return false unless defined?(RolePermission)
120
+ RolePermission.joins(:permission).where(role_id: role.id).where("permissions.code = ?", permission_code).exists?
121
+ end
122
+
123
+ def get_permission_conditions(user, permission_code, context)
124
+ company_id = context[:company_id]
125
+ conditions = {}
126
+ if defined?(UserPermission)
127
+ user_conditions = UserPermission
128
+ .joins(:permission)
129
+ .where(user_id: user.id, grant_type: "grant")
130
+ .where("permissions.code = ?", permission_code)
131
+ .where("company_id IS NULL OR company_id = ?", company_id)
132
+ .pluck(:conditions).compact
133
+ user_conditions.each { |c| conditions.merge!(c) if c.is_a?(Hash) }
134
+ end
135
+ effective_roles(user, company_id).each do |role|
136
+ if defined?(RolePermission)
137
+ role_conditions = RolePermission
138
+ .joins(:permission)
139
+ .where(role_id: role.id)
140
+ .where("permissions.code = ?", permission_code)
141
+ .pluck(:conditions).compact
142
+ role_conditions.each { |c| conditions.merge!(c) if c.is_a?(Hash) }
143
+ end
144
+ end
145
+ if defined?(Permission)
146
+ perm = Permission.find_by(code: permission_code)
147
+ conditions.merge!(perm.conditions) if perm&.conditions.is_a?(Hash)
148
+ end
149
+ conditions
150
+ end
151
+
152
+ def get_field_restrictions(user, permission_code, context)
153
+ company_id = context[:company_id]
154
+ if defined?(UserPermission)
155
+ user_perm = UserPermission
156
+ .joins(:permission)
157
+ .where(user_id: user.id)
158
+ .where("permissions.code = ?", permission_code)
159
+ .where("company_id IS NULL OR company_id = ?", company_id)
160
+ .first
161
+ return user_perm.attributes.slice("granted_fields", "denied_fields") if user_perm
162
+ end
163
+ effective_roles(user, company_id).each do |role|
164
+ if defined?(RolePermission)
165
+ role_perm = RolePermission
166
+ .joins(:permission)
167
+ .where(role_id: role.id)
168
+ .where("permissions.code = ?", permission_code)
169
+ .first
170
+ return role_perm.attributes.slice("granted_fields", "denied_fields") if role_perm
171
+ end
172
+ end
173
+ nil
174
+ end
175
+
176
+ def collect_user_permissions(user, context)
177
+ permissions = Set.new
178
+ company_id = context[:company_id]
179
+ if defined?(UserPermission)
180
+ UserPermission
181
+ .joins(:permission)
182
+ .where(user_id: user.id, grant_type: "grant")
183
+ .where("company_id IS NULL OR company_id = ?", company_id)
184
+ .where("valid_from <= ? AND (valid_until IS NULL OR valid_until >= ?)", Time.current, Time.current)
185
+ .pluck("permissions.code")
186
+ .each { |code| permissions.add(code) }
187
+ end
188
+ effective_roles(user, company_id).each do |role|
189
+ if defined?(RolePermission)
190
+ RolePermission.joins(:permission).where(role_id: role.id).pluck("permissions.code").each { |code| permissions.add(code) }
191
+ end
192
+ end
193
+ if defined?(UserPermission)
194
+ UserPermission
195
+ .joins(:permission)
196
+ .where(user_id: user.id, grant_type: "deny")
197
+ .where("company_id IS NULL OR company_id = ?", company_id)
198
+ .pluck("permissions.code")
199
+ .each { |code| permissions.delete(code) }
200
+ end
201
+ permissions.to_a
202
+ end
203
+
204
+ def super_admin?(user)
205
+ user.respond_to?(:super_admin?) && user.super_admin?
206
+ end
207
+
208
+ def cache_key(user, permission_code, context)
209
+ company_id = context[:company_id] || "all"
210
+ "#{CACHE_PREFIX}:#{user.id}:#{company_id}:#{permission_code}"
211
+ end
212
+
213
+ def user_permissions_cache_key(user, context)
214
+ company_id = context[:company_id] || "all"
215
+ "#{CACHE_PREFIX}:#{user.id}:#{company_id}:all"
216
+ end
217
+
218
+ def get_cached_permission(user, permission_code, context)
219
+ result = redis_get(cache_key(user, permission_code, context))
220
+ result.nil? ? nil : (result == "true")
221
+ end
222
+
223
+ def cache_permission(user, permission_code, context, result)
224
+ redis_set(cache_key(user, permission_code, context), result.to_s, CACHE_TTL)
225
+ end
226
+
227
+ def redis_get(key)
228
+ return nil unless redis_available?
229
+ value = redis.get(key)
230
+ value.nil? ? nil : (JSON.parse(value) rescue value)
231
+ end
232
+
233
+ def redis_set(key, value, ttl)
234
+ redis.setex(key, ttl.to_i, value.to_json) if redis_available?
235
+ end
236
+
237
+ def redis_delete_pattern(pattern)
238
+ return unless redis_available?
239
+ keys = redis.keys(pattern)
240
+ redis.del(*keys) if keys.any?
241
+ end
242
+
243
+ def redis_available?
244
+ defined?(Redis) && redis.present?
245
+ end
246
+
247
+ def redis
248
+ @redis ||= (Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0")) if defined?(Redis))
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end