rls_multi_tenant 0.1.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 (31) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +46 -0
  3. data/README.md +141 -0
  4. data/lib/rls_multi_tenant/concerns/multi_tenant.rb +41 -0
  5. data/lib/rls_multi_tenant/concerns/tenant_context.rb +74 -0
  6. data/lib/rls_multi_tenant/generators/install/install_generator.rb +91 -0
  7. data/lib/rls_multi_tenant/generators/install/templates/create_app_user.rb +33 -0
  8. data/lib/rls_multi_tenant/generators/install/templates/create_tenant.rb +20 -0
  9. data/lib/rls_multi_tenant/generators/install/templates/db_admin.rake +27 -0
  10. data/lib/rls_multi_tenant/generators/install/templates/enable_rls.rb +27 -0
  11. data/lib/rls_multi_tenant/generators/install/templates/enable_uuid_extension.rb +5 -0
  12. data/lib/rls_multi_tenant/generators/install/templates/rls_multi_tenant.rb +15 -0
  13. data/lib/rls_multi_tenant/generators/install/templates/tenant_model.rb +7 -0
  14. data/lib/rls_multi_tenant/generators/migration/migration_generator.rb +57 -0
  15. data/lib/rls_multi_tenant/generators/migration/templates/create_app_user.rb +33 -0
  16. data/lib/rls_multi_tenant/generators/migration/templates/create_tenant.rb +20 -0
  17. data/lib/rls_multi_tenant/generators/migration/templates/enable_rls.rb +27 -0
  18. data/lib/rls_multi_tenant/generators/migration/templates/enable_uuid_extension.rb +5 -0
  19. data/lib/rls_multi_tenant/generators/model/model_generator.rb +81 -0
  20. data/lib/rls_multi_tenant/generators/model/templates/migration.rb +35 -0
  21. data/lib/rls_multi_tenant/generators/model/templates/model.rb +5 -0
  22. data/lib/rls_multi_tenant/generators/task/task_generator.rb +31 -0
  23. data/lib/rls_multi_tenant/generators/task/templates/db_admin.rake +24 -0
  24. data/lib/rls_multi_tenant/railtie.rb +37 -0
  25. data/lib/rls_multi_tenant/rls_helper.rb +66 -0
  26. data/lib/rls_multi_tenant/rls_multi_tenant.rb +45 -0
  27. data/lib/rls_multi_tenant/security_validator.rb +61 -0
  28. data/lib/rls_multi_tenant/version.rb +5 -0
  29. data/lib/rls_multi_tenant.rb +48 -0
  30. data/rls_multi_tenant.gemspec +44 -0
  31. metadata +203 -0
@@ -0,0 +1,27 @@
1
+ class EnableRlsFor<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def change
3
+ reversible do |dir|
4
+ dir.up do
5
+ # Enable Row Level Security
6
+ execute 'ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY'
7
+
8
+ # Create RLS policy
9
+ execute "CREATE POLICY <%= table_name %>_app_user ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']} USING (<%= RlsMultiTenant.tenant_id_column %> = NULLIF(current_setting('rls.tenant_id', TRUE), '')::uuid)"
10
+
11
+ # Grant permissions
12
+ execute "GRANT SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
13
+ end
14
+
15
+ dir.down do
16
+ # Revoke permissions
17
+ execute "REVOKE SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> FROM #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
18
+
19
+ # Drop policy
20
+ execute "DROP POLICY <%= table_name %>_app_user ON <%= table_name %>"
21
+
22
+ # Disable RLS
23
+ execute "ALTER TABLE <%= table_name %> DISABLE ROW LEVEL SECURITY"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ class EnableUuidExtension < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def change
3
+ enable_extension 'uuid-ossp'
4
+ end
5
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'ostruct'
5
+
6
+ module RlsMultiTenant
7
+ module Generators
8
+ class ModelGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ argument :name, type: :string, desc: "Model name"
12
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
13
+
14
+ def initialize(args, *options)
15
+ super
16
+ @attributes = parse_attributes!
17
+ end
18
+
19
+ desc "Generate a multi-tenant model with RLS policies"
20
+
21
+ def create_model_file
22
+ template "model.rb", File.join("app/models", "#{model_name.underscore}.rb")
23
+ end
24
+
25
+ def create_migration
26
+ template "migration.rb", File.join("db/migrate", "#{migration_file_name}.rb")
27
+ end
28
+
29
+ def show_instructions
30
+ say "\n" + "="*60, :green
31
+ say "Multi-tenant model '#{model_name}' created successfully!", :green
32
+ say "="*60, :green
33
+ say "\nWhat was created:", :yellow
34
+ say "1. Model: app/models/#{model_name.underscore}.rb (with MultiTenant concern included)", :yellow
35
+ say "2. Migration: #{migration_file_name}.rb (with tenant_id column and RLS policies)", :yellow
36
+ say "\nNext steps:", :yellow
37
+ say "1. Run migrations: rails db_as:admin[migrate]", :yellow
38
+ say "2. The model is ready to use with multi-tenant functionality", :yellow
39
+ say "3. RLS policies are automatically configured", :yellow
40
+ say "="*60, :green
41
+ end
42
+
43
+ private
44
+
45
+ def model_name
46
+ @model_name ||= name.classify
47
+ end
48
+
49
+ def table_name
50
+ @table_name ||= name.underscore.pluralize
51
+ end
52
+
53
+ def migration_file_name
54
+ "#{migration_timestamp}_create_#{table_name}"
55
+ end
56
+
57
+ def migration_timestamp
58
+ Time.current.strftime("%Y%m%d%H%M%S")
59
+ end
60
+
61
+ def tenant_id_column
62
+ RlsMultiTenant.tenant_id_column
63
+ end
64
+
65
+ def tenant_class_name
66
+ RlsMultiTenant.tenant_class_name
67
+ end
68
+
69
+ def migration_class_name
70
+ "Create#{model_name.pluralize}"
71
+ end
72
+
73
+ def parse_attributes!
74
+ attributes.map do |attr|
75
+ name, type = attr.split(':')
76
+ OpenStruct.new(name: name, type: type || 'string')
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,35 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= Rails.version.to_f %>]
2
+ def change
3
+ # Create the table
4
+ create_table :<%= table_name %>, id: :uuid do |t|
5
+ t.references :<%= tenant_id_column.to_s.gsub('_id', '') %>, null: false, foreign_key: { to_table: :<%= tenant_class_name.underscore.pluralize %> }, type: :uuid
6
+ <% @attributes.each do |attribute| -%>
7
+ t.<%= attribute.type %> :<%= attribute.name %>
8
+ <% end -%>
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ # Enable Row Level Security
14
+ execute "ALTER TABLE <%= table_name %> ENABLE ROW LEVEL SECURITY"
15
+
16
+ reversible do |dir|
17
+ dir.up do
18
+ execute "GRANT SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
19
+ end
20
+ dir.down do
21
+ execute "REVOKE SELECT, INSERT, UPDATE, DELETE ON <%= table_name %> FROM #{ENV['<%= RlsMultiTenant.app_user_env_var %>']}"
22
+ end
23
+ end
24
+
25
+ # Define RLS policy
26
+ reversible do |dir|
27
+ dir.up do
28
+ execute "CREATE POLICY <%= table_name %>_app_user ON <%= table_name %> TO #{ENV['<%= RlsMultiTenant.app_user_env_var %>']} USING (<%= tenant_id_column %> = NULLIF(current_setting('rls.<%= tenant_id_column %>', TRUE), '')::uuid)"
29
+ end
30
+ dir.down do
31
+ execute "DROP POLICY <%= table_name %>_app_user ON <%= table_name %>"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= model_name %> < ApplicationRecord
4
+ include RlsMultiTenant::Concerns::MultiTenant
5
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RlsMultiTenant
6
+ module Generators
7
+ class TaskGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Generate RLS Multi-tenant rake tasks"
11
+
12
+ def create_db_admin_task
13
+ template "db_admin.rake", "lib/tasks/db_admin.rake"
14
+ show_instructions
15
+ end
16
+
17
+ def show_instructions
18
+ say "\n" + "="*60, :green
19
+ say "RLS Multi-tenant rake tasks created successfully!", :green
20
+ say "="*60, :green
21
+ say "\nWhat was created:", :yellow
22
+ say "1. lib/tasks/db_admin.rake - Database admin tasks", :yellow
23
+ say "\nUsage:", :yellow
24
+ say "rails db_as:admin[migrate] # Run migrations with admin privileges", :yellow
25
+ say "rails db_as:admin[seed] # Run seeds with admin privileges", :yellow
26
+ say "\nNote: This is required because the app user doesn't have migration privileges", :yellow
27
+ say "="*60, :green
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ namespace :db_as do
2
+ desc "Run a db: task with admin privileges. Usage: `rails db_as:admin[migrate]`"
3
+ task :admin, [:sub_task] do |t, args|
4
+ sub_task = args[:sub_task]
5
+
6
+ # Check if a sub-task was provided
7
+ if sub_task.nil? || !Rake::Task.task_defined?("db:#{sub_task}")
8
+ puts "Usage: rails db_as:admin[sub_task]"
9
+ puts "Valid sub-tasks: #{Rake.application.tasks.map(&:name).select { |name| name.start_with?("db:") }.map { |name| name.split(':', 2).last }.uniq.sort.join(', ')}"
10
+ exit
11
+ end
12
+
13
+ database_url = "postgresql://#{ENV['POSTGRES_USER']}:#{ENV['POSTGRES_PASSWORD']}@#{ENV['POSTGRES_HOST']}:5432/#{ENV['POSTGRES_DB']}"
14
+
15
+ # Set the DATABASE_URL with admin user details.
16
+ ENV['DATABASE_URL'] = database_url
17
+ ENV['CACHE_DATABASE_URL'] = "#{database_url}_cache"
18
+ ENV['QUEUE_DATABASE_URL'] = "#{database_url}_queue"
19
+ ENV['CABLE_DATABASE_URL'] = "#{database_url}_cable"
20
+
21
+ puts "Executing db:#{sub_task} with admin privileges..."
22
+ Rake::Task["db:#{sub_task}"].invoke
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ class Railtie < Rails::Railtie
5
+ # Load generators after Rails is fully initialized
6
+ initializer "rls_multi_tenant.load_generators", after: :set_routes_reloader do |app|
7
+ require "rls_multi_tenant/generators/install/install_generator"
8
+ require "rls_multi_tenant/generators/migration/migration_generator"
9
+ require "rls_multi_tenant/generators/model/model_generator"
10
+ require "rls_multi_tenant/generators/task/task_generator"
11
+ end
12
+
13
+ initializer "rls_multi_tenant.configure" do |app|
14
+ # Configure the gem
15
+ RlsMultiTenant.configure do |config|
16
+ config.tenant_class_name = "Tenant"
17
+ config.tenant_id_column = :tenant_id
18
+ config.app_user_env_var = "POSTGRES_APP_USER"
19
+ config.enable_security_validation = true
20
+ end
21
+ end
22
+
23
+ initializer "rls_multi_tenant.security_validation", after: :load_config_initializers do |app|
24
+ if RlsMultiTenant.enable_security_validation
25
+ app.config.after_initialize do
26
+ begin
27
+ RlsMultiTenant::SecurityValidator.validate_environment!
28
+ RlsMultiTenant::SecurityValidator.validate_database_user!
29
+ rescue => e
30
+ Rails.logger.error "RLS Multi-tenant initialization failed: #{e.message}"
31
+ raise e
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ module RlsHelper
5
+ class << self
6
+ # Enable RLS on a table with a policy
7
+ def enable_rls_for_table(table_name, tenant_column: RlsMultiTenant.tenant_id_column, app_user: nil)
8
+ app_user ||= ENV[RlsMultiTenant.app_user_env_var]
9
+
10
+ raise ConfigurationError, "App user not configured" if app_user.blank?
11
+
12
+ # Enable RLS
13
+ ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY")
14
+
15
+ # Create policy (drop if exists first)
16
+ policy_name = "#{table_name}_app_user"
17
+ ActiveRecord::Base.connection.execute("DROP POLICY IF EXISTS #{policy_name} ON #{table_name}")
18
+
19
+ policy_sql = "CREATE POLICY #{policy_name} ON #{table_name} TO #{app_user} " \
20
+ "USING (#{tenant_column} = NULLIF(current_setting('rls.tenant_id', TRUE), '')::uuid)"
21
+
22
+ ActiveRecord::Base.connection.execute(policy_sql)
23
+
24
+ # Grant permissions
25
+ ActiveRecord::Base.connection.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON #{table_name} TO #{app_user}")
26
+
27
+ Rails.logger&.info "✅ RLS enabled for table #{table_name} with policy #{policy_name}"
28
+ end
29
+
30
+ # Disable RLS on a table
31
+ def disable_rls_for_table(table_name, app_user: nil)
32
+ app_user ||= ENV[RlsMultiTenant.app_user_env_var]
33
+
34
+ raise ConfigurationError, "App user not configured" if app_user.blank?
35
+
36
+ # Revoke permissions
37
+ ActiveRecord::Base.connection.execute("REVOKE SELECT, INSERT, UPDATE, DELETE ON #{table_name} FROM #{app_user}")
38
+
39
+ # Drop policy
40
+ policy_name = "#{table_name}_app_user"
41
+ ActiveRecord::Base.connection.execute("DROP POLICY IF EXISTS #{policy_name} ON #{table_name}")
42
+
43
+ # Disable RLS
44
+ ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} DISABLE ROW LEVEL SECURITY")
45
+
46
+ Rails.logger&.info "✅ RLS disabled for table #{table_name}"
47
+ end
48
+
49
+ # Check if RLS is enabled on a table
50
+ def rls_enabled?(table_name)
51
+ result = ActiveRecord::Base.connection.execute(
52
+ "SELECT relrowsecurity FROM pg_class WHERE relname = '#{table_name}'"
53
+ ).first
54
+
55
+ result&.dig('relrowsecurity') == true
56
+ end
57
+
58
+ # Get all RLS policies for a table
59
+ def rls_policies(table_name)
60
+ ActiveRecord::Base.connection.execute(
61
+ "SELECT policyname, permissive, roles, cmd, qual FROM pg_policies WHERE tablename = '#{table_name}'"
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rls_multi_tenant/version"
4
+ require "rls_multi_tenant/concerns/multi_tenant"
5
+ require "rls_multi_tenant/concerns/tenant_context"
6
+ require "rls_multi_tenant/security_validator"
7
+ require "rls_multi_tenant/rls_helper"
8
+ require "rls_multi_tenant/railtie" if defined?(Rails)
9
+
10
+ module RlsMultiTenant
11
+ class Error < StandardError; end
12
+ class ConfigurationError < Error; end
13
+ class SecurityError < Error; end
14
+
15
+ # Configuration options
16
+ class << self
17
+ attr_accessor :tenant_class_name, :tenant_id_column, :app_user_env_var, :enable_security_validation
18
+
19
+ def configure
20
+ yield self
21
+ end
22
+
23
+ def tenant_class
24
+ @tenant_class ||= tenant_class_name.constantize
25
+ end
26
+
27
+ def tenant_id_column
28
+ @tenant_id_column ||= :tenant_id
29
+ end
30
+
31
+ def app_user_env_var
32
+ @app_user_env_var ||= "POSTGRES_APP_USER"
33
+ end
34
+
35
+ def enable_security_validation
36
+ @enable_security_validation.nil? ? true : @enable_security_validation
37
+ end
38
+ end
39
+
40
+ # Default configuration
41
+ self.tenant_class_name = "Tenant"
42
+ self.tenant_id_column = :tenant_id
43
+ self.app_user_env_var = "POSTGRES_APP_USER"
44
+ self.enable_security_validation = true
45
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ class SecurityValidator
5
+ class << self
6
+ def validate_database_user!
7
+ return unless RlsMultiTenant.enable_security_validation
8
+ return if skip_validation?
9
+
10
+ begin
11
+ # Get the current database configuration
12
+ db_config = ActiveRecord::Base.connection_db_config
13
+ username = db_config.configuration_hash[:username]
14
+
15
+ # Check if the current user has SUPERUSER privileges
16
+ superuser_check = ActiveRecord::Base.connection.execute(
17
+ "SELECT rolname, rolsuper FROM pg_roles WHERE rolname = current_user"
18
+ ).first
19
+
20
+ if superuser_check && superuser_check['rolsuper']
21
+ raise SecurityError, "Database user '#{username}' has SUPERUSER privileges. " \
22
+ "In order to use RLS Multi-tenant, you must use a non-privileged user without SUPERUSER rights."
23
+ end
24
+
25
+ # Log the security check result
26
+ Rails.logger&.info "✅ RLS Multi-tenant security check passed: Using user '#{username}' without SUPERUSER privileges"
27
+
28
+ rescue => e
29
+ Rails.logger&.error "❌ RLS Multi-tenant security check failed: #{e.message}"
30
+ raise e
31
+ end
32
+ end
33
+
34
+ def validate_environment!
35
+ return if skip_validation?
36
+ return unless RlsMultiTenant.enable_security_validation
37
+
38
+ app_user = ENV[RlsMultiTenant.app_user_env_var]
39
+
40
+ if app_user.blank?
41
+ raise ConfigurationError, "#{RlsMultiTenant.app_user_env_var} environment variable must be set"
42
+ elsif ["postgres", "root"].include?(app_user)
43
+ raise SecurityError, "Cannot use privileged PostgreSQL user '#{app_user}'. " \
44
+ "In order to use RLS Multi-tenant, you must use a non-privileged user without SUPERUSER rights."
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def skip_validation?
51
+ # Skip validation if we're running install generator
52
+ return true if ARGV.any? { |arg| arg.include?('rls_multi_tenant:install') }
53
+
54
+ # Skip validation if we're in admin mode (set by db_as:admin task)
55
+ return true if ENV['RLS_MULTI_TENANT_ADMIN_MODE'] == 'true'
56
+
57
+ false
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RlsMultiTenant
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rls_multi_tenant/version"
4
+ require "rls_multi_tenant/concerns/multi_tenant"
5
+ require "rls_multi_tenant/concerns/tenant_context"
6
+ require "rls_multi_tenant/security_validator"
7
+ require "rls_multi_tenant/rls_helper"
8
+ # require "rls_multi_tenant/generators/install/install_generator" if defined?(Rails)
9
+ # require "rls_multi_tenant/generators/migration/migration_generator" if defined?(Rails)
10
+ # require "rls_multi_tenant/generators/model/model_generator" if defined?(Rails)
11
+ require "rls_multi_tenant/railtie" if defined?(Rails)
12
+
13
+ module RlsMultiTenant
14
+ class Error < StandardError; end
15
+ class ConfigurationError < Error; end
16
+ class SecurityError < Error; end
17
+
18
+ # Configuration options
19
+ class << self
20
+ attr_accessor :tenant_class_name, :tenant_id_column, :app_user_env_var, :enable_security_validation
21
+
22
+ def configure
23
+ yield self
24
+ end
25
+
26
+ def tenant_class
27
+ @tenant_class ||= tenant_class_name.constantize
28
+ end
29
+
30
+ def tenant_id_column
31
+ @tenant_id_column ||= :tenant_id
32
+ end
33
+
34
+ def app_user_env_var
35
+ @app_user_env_var ||= "POSTGRES_APP_USER"
36
+ end
37
+
38
+ def enable_security_validation
39
+ @enable_security_validation.nil? ? true : @enable_security_validation
40
+ end
41
+ end
42
+
43
+ # Default configuration
44
+ self.tenant_class_name = "Tenant"
45
+ self.tenant_id_column = :tenant_id
46
+ self.app_user_env_var = "POSTGRES_APP_USER"
47
+ self.enable_security_validation = true
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rls_multi_tenant/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rls_multi_tenant"
7
+ spec.version = RlsMultiTenant::VERSION
8
+ spec.authors = ["Coding Ways"]
9
+ spec.email = ["info@codingways.com"]
10
+
11
+ spec.summary = "Rails gem for PostgreSQL Row Level Security (RLS) multi-tenant applications"
12
+ spec.description = "A comprehensive gem that provides RLS-based multi-tenancy for Rails applications using PostgreSQL, including automatic tenant context switching, security validations, and migration helpers."
13
+ spec.homepage = "https://github.com/codingways/rls_multi_tenant"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/codingways/rls_multi_tenant"
19
+ spec.metadata["changelog_uri"] = "https://github.com/codingways/rls_multi_tenant/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Dependencies
33
+ spec.add_dependency "rails", ">= 7.0"
34
+ spec.add_dependency "pg", ">= 1.0"
35
+
36
+ # Development dependencies
37
+ spec.add_development_dependency "rspec-rails", "~> 6.0"
38
+ spec.add_development_dependency "rubocop", "~> 1.50"
39
+ spec.add_development_dependency "rubocop-rails", "~> 2.20"
40
+ spec.add_development_dependency "rubocop-rspec", "~> 2.20"
41
+ spec.add_development_dependency "simplecov", "~> 0.22"
42
+ spec.add_development_dependency "generator_spec", "~> 0.9"
43
+ spec.add_development_dependency "bundler-audit", "~> 0.9"
44
+ end