action_policy 0.4.1 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +230 -172
  3. data/LICENSE.txt +1 -1
  4. data/README.md +7 -11
  5. data/lib/.rbnext/2.7/action_policy/behaviours/policy_for.rb +62 -0
  6. data/lib/.rbnext/2.7/action_policy/i18n.rb +56 -0
  7. data/lib/.rbnext/2.7/action_policy/policy/cache.rb +101 -0
  8. data/lib/.rbnext/2.7/action_policy/policy/pre_check.rb +162 -0
  9. data/lib/.rbnext/2.7/action_policy/rspec/be_authorized_to.rb +89 -0
  10. data/lib/.rbnext/2.7/action_policy/rspec/have_authorized_scope.rb +124 -0
  11. data/lib/.rbnext/2.7/action_policy/utils/pretty_print.rb +159 -0
  12. data/lib/.rbnext/3.0/action_policy/behaviour.rb +115 -0
  13. data/lib/.rbnext/3.0/action_policy/behaviours/policy_for.rb +62 -0
  14. data/lib/.rbnext/3.0/action_policy/behaviours/scoping.rb +35 -0
  15. data/lib/.rbnext/3.0/action_policy/behaviours/thread_memoized.rb +59 -0
  16. data/lib/.rbnext/3.0/action_policy/ext/policy_cache_key.rb +72 -0
  17. data/lib/.rbnext/3.0/action_policy/policy/aliases.rb +69 -0
  18. data/lib/.rbnext/3.0/action_policy/policy/authorization.rb +87 -0
  19. data/lib/.rbnext/3.0/action_policy/policy/cache.rb +101 -0
  20. data/lib/.rbnext/3.0/action_policy/policy/core.rb +161 -0
  21. data/lib/.rbnext/3.0/action_policy/policy/defaults.rb +31 -0
  22. data/lib/.rbnext/3.0/action_policy/policy/execution_result.rb +37 -0
  23. data/lib/.rbnext/3.0/action_policy/policy/pre_check.rb +162 -0
  24. data/lib/.rbnext/3.0/action_policy/policy/reasons.rb +210 -0
  25. data/lib/.rbnext/3.0/action_policy/policy/scoping.rb +160 -0
  26. data/lib/.rbnext/3.0/action_policy/rspec/be_authorized_to.rb +89 -0
  27. data/lib/.rbnext/3.0/action_policy/rspec/have_authorized_scope.rb +124 -0
  28. data/lib/.rbnext/3.0/action_policy/utils/pretty_print.rb +159 -0
  29. data/lib/.rbnext/3.0/action_policy/utils/suggest_message.rb +19 -0
  30. data/lib/action_policy.rb +7 -1
  31. data/lib/action_policy/behaviour.rb +22 -16
  32. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  33. data/lib/action_policy/behaviours/scoping.rb +2 -1
  34. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  35. data/lib/action_policy/ext/module_namespace.rb +1 -6
  36. data/lib/action_policy/ext/policy_cache_key.rb +15 -33
  37. data/lib/action_policy/ext/{symbol_classify.rb → symbol_camelize.rb} +6 -6
  38. data/lib/action_policy/i18n.rb +1 -1
  39. data/lib/action_policy/lookup_chain.rb +41 -21
  40. data/lib/action_policy/policy/aliases.rb +7 -12
  41. data/lib/action_policy/policy/authorization.rb +14 -17
  42. data/lib/action_policy/policy/cache.rb +34 -18
  43. data/lib/action_policy/policy/core.rb +25 -12
  44. data/lib/action_policy/policy/defaults.rb +3 -9
  45. data/lib/action_policy/policy/execution_result.rb +3 -9
  46. data/lib/action_policy/policy/pre_check.rb +19 -58
  47. data/lib/action_policy/policy/reasons.rb +30 -20
  48. data/lib/action_policy/policy/scoping.rb +5 -6
  49. data/lib/action_policy/rails/controller.rb +6 -1
  50. data/lib/action_policy/rails/ext/active_record.rb +7 -0
  51. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  52. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  53. data/lib/action_policy/rspec/dsl.rb +3 -3
  54. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  55. data/lib/action_policy/testing.rb +1 -12
  56. data/lib/action_policy/utils/pretty_print.rb +21 -24
  57. data/lib/action_policy/utils/suggest_message.rb +1 -3
  58. data/lib/action_policy/version.rb +1 -1
  59. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +1 -1
  60. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  61. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  62. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  63. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  64. metadata +55 -119
  65. data/.gitattributes +0 -2
  66. data/.github/FUNDING.yml +0 -1
  67. data/.github/ISSUE_TEMPLATE.md +0 -18
  68. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  69. data/.gitignore +0 -15
  70. data/.rubocop.yml +0 -54
  71. data/.tidelift.yml +0 -6
  72. data/.travis.yml +0 -31
  73. data/Gemfile +0 -22
  74. data/Rakefile +0 -27
  75. data/action_policy.gemspec +0 -44
  76. data/benchmarks/namespaced_lookup_cache.rb +0 -71
  77. data/bin/console +0 -14
  78. data/bin/setup +0 -8
  79. data/docs/.nojekyll +0 -0
  80. data/docs/CNAME +0 -1
  81. data/docs/README.md +0 -77
  82. data/docs/_sidebar.md +0 -27
  83. data/docs/aliases.md +0 -122
  84. data/docs/assets/docsify-search.js +0 -364
  85. data/docs/assets/docsify.min.js +0 -3
  86. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  87. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  88. data/docs/assets/images/banner.png +0 -0
  89. data/docs/assets/images/cache.png +0 -0
  90. data/docs/assets/images/cache.svg +0 -70
  91. data/docs/assets/images/layer.png +0 -0
  92. data/docs/assets/images/layer.svg +0 -35
  93. data/docs/assets/prism-ruby.min.js +0 -1
  94. data/docs/assets/styles.css +0 -347
  95. data/docs/assets/vue.min.css +0 -1
  96. data/docs/authorization_context.md +0 -92
  97. data/docs/behaviour.md +0 -113
  98. data/docs/caching.md +0 -273
  99. data/docs/controller_action_aliases.md +0 -109
  100. data/docs/custom_lookup_chain.md +0 -48
  101. data/docs/custom_policy.md +0 -53
  102. data/docs/debugging.md +0 -55
  103. data/docs/decorators.md +0 -27
  104. data/docs/favicon.ico +0 -0
  105. data/docs/graphql.md +0 -302
  106. data/docs/i18n.md +0 -44
  107. data/docs/index.html +0 -43
  108. data/docs/instrumentation.md +0 -84
  109. data/docs/lookup_chain.md +0 -17
  110. data/docs/namespaces.md +0 -77
  111. data/docs/non_rails.md +0 -28
  112. data/docs/pre_checks.md +0 -57
  113. data/docs/pundit_migration.md +0 -80
  114. data/docs/quick_start.md +0 -118
  115. data/docs/rails.md +0 -120
  116. data/docs/reasons.md +0 -120
  117. data/docs/scoping.md +0 -255
  118. data/docs/testing.md +0 -333
  119. data/docs/writing_policies.md +0 -107
  120. data/gemfiles/jruby.gemfile +0 -8
  121. data/gemfiles/rails42.gemfile +0 -8
  122. data/gemfiles/rails6.gemfile +0 -8
  123. data/gemfiles/railsmaster.gemfile +0 -6
  124. data/lib/action_policy/ext/string_match.rb +0 -14
  125. data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -1,2 +0,0 @@
1
- docs/**/* linguist-vendored
2
- .github/**/* linguist-vendored
@@ -1 +0,0 @@
1
- tidelift: "rubygems/action_policy"
@@ -1,18 +0,0 @@
1
- <!--
2
- This template is for bug reports. If you are reporting a bug, please continue on. If you are here for another reason,
3
- feel free to skip the rest of this template.
4
- -->
5
-
6
- ### Tell us about your environment
7
-
8
- **Ruby Version:**
9
-
10
- **Framework Version (Rails, whatever):**
11
-
12
- **Action Policy Version:**
13
-
14
- ### What did you do?
15
-
16
- ### What did you expect to happen?
17
-
18
- ### What actually happened?
@@ -1,29 +0,0 @@
1
- <!--
2
- First of all, thanks for contributing!
3
-
4
- If it's a typo fix or minor documentation update feel free to skip the rest of this template!
5
- -->
6
-
7
- <!--
8
- If it's a bug fix, then link it to the issue, for example:
9
-
10
- Fixes #xxx
11
- -->
12
-
13
-
14
- <!--
15
- Otherwise, describe the changes:
16
-
17
- ### What is the purpose of this pull request?
18
-
19
- ### What changes did you make? (overview)
20
-
21
- ### Is there anything you'd like reviewers to focus on?
22
-
23
- -->
24
-
25
- PR checklist:
26
-
27
- - [ ] Tests included
28
- - [ ] Documentation updated
29
- - [ ] Changelog entry added
data/.gitignore DELETED
@@ -1,15 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- Gemfile.local
11
-
12
- spec/dummy/log/*
13
- spec/dummy/db/*.sqlite3
14
- spec/dummy/db/*.sqlite3-journal
15
- spec/dummy/tmp/
@@ -1,54 +0,0 @@
1
- require:
2
- - standard/cop/semantic_blocks
3
- - rubocop-md
4
-
5
- inherit_gem:
6
- standard: config/base.yml
7
-
8
- AllCops:
9
- Exclude:
10
- - 'bin/*'
11
- - 'tmp/**/*'
12
- - 'Gemfile'
13
- - 'vendor/**/*'
14
- - 'gemfiles/**/*'
15
- - 'lib/generators/**/templates/**/*'
16
- DisplayCopNames: true
17
- TargetRubyVersion: 2.4
18
-
19
- Standard/SemanticBlocks:
20
- Enabled: false
21
-
22
- Style/FrozenStringLiteralComment:
23
- Enabled: true
24
-
25
- Style/TrailingCommaInArrayLiteral:
26
- EnforcedStyleForMultiline: no_comma
27
-
28
- Style/TrailingCommaInHashLiteral:
29
- EnforcedStyleForMultiline: no_comma
30
-
31
- Layout/AlignParameters:
32
- EnforcedStyle: with_first_parameter
33
-
34
- Lint/Void:
35
- Exclude:
36
- - '**/*.md'
37
-
38
- # See https://github.com/rubocop-hq/rubocop/issues/4222
39
- Lint/AmbiguousBlockAssociation:
40
- Exclude:
41
- - 'spec/**/*'
42
- - '**/*.md'
43
-
44
- Lint/DuplicateMethods:
45
- Exclude:
46
- - '**/*.md'
47
-
48
- Naming/FileName:
49
- Exclude:
50
- - '**/*.md'
51
-
52
- Layout/InitialIndentation:
53
- Exclude:
54
- - 'CHANGELOG.md'
@@ -1,6 +0,0 @@
1
- ci:
2
- type:
3
- development:
4
- tests:
5
- unlicensed: skip
6
- outdated: skip
@@ -1,31 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- cache: bundler
4
- notifications:
5
- email: false
6
-
7
- before_install:
8
- - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
9
- - gem install bundler -v '< 2'
10
-
11
- script:
12
- - bundle exec rake test:ci
13
-
14
- matrix:
15
- fast_finish: true
16
- include:
17
- - rvm: ruby-head
18
- gemfile: gemfiles/railsmaster.gemfile
19
- - rvm: jruby-9.2.8.0
20
- gemfile: gemfiles/jruby.gemfile
21
- - rvm: 2.6.0
22
- gemfile: gemfiles/rails6.gemfile
23
- - rvm: 2.5.3
24
- gemfile: Gemfile
25
- - rvm: 2.4.3
26
- gemfile: gemfiles/rails42.gemfile
27
- allow_failures:
28
- - rvm: ruby-head
29
- gemfile: gemfiles/railsmaster.gemfile
30
- - rvm: jruby-9.2.8.0
31
- gemfile: gemfiles/jruby.gemfile
data/Gemfile DELETED
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- gemspec
6
-
7
- gem "pry-byebug", platform: :mri
8
-
9
- gem "method_source"
10
- gem "unparser"
11
-
12
- gem 'sqlite3', "~> 1.3.0", platform: :mri
13
- gem 'activerecord-jdbcsqlite3-adapter', '~> 50.0', platform: :jruby
14
- gem 'jdbc-sqlite3', platform: :jruby
15
-
16
- local_gemfile = File.join(__dir__, "Gemfile.local")
17
-
18
- if File.exist?(local_gemfile)
19
- eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
20
- else
21
- gem "rails", "~> 5.0"
22
- end
data/Rakefile DELETED
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rake/testtask"
4
- require "rubocop/rake_task"
5
- require "rspec/core/rake_task"
6
-
7
- RuboCop::RakeTask.new
8
-
9
- RSpec::Core::RakeTask.new(:spec)
10
-
11
- Rake::TestTask.new(:test) do |t|
12
- t.libs << "test"
13
- t.libs << "lib"
14
- t.test_files = FileList["test/**/*_test.rb"]
15
- end
16
-
17
- namespace :test do
18
- task :isolated do
19
- Dir.glob("test/**/*_test.rb").all? do |file|
20
- sh(Gem.ruby, "-w", "-Ilib:test", file)
21
- end || raise("Failures")
22
- end
23
-
24
- task ci: %w[rubocop test:isolated spec]
25
- end
26
-
27
- task default: [:rubocop, :test, :spec]
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path("lib", __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require "action_policy/version"
6
-
7
- Gem::Specification.new do |spec|
8
- spec.name = "action_policy"
9
- spec.version = ActionPolicy::VERSION
10
- spec.authors = ["Vladimir Dementyev"]
11
- spec.email = ["dementiev.vm@gmail.com"]
12
-
13
- spec.summary = "Authorization framework for Ruby/Rails application"
14
- spec.description = "Authorization framework for Ruby/Rails application"
15
- spec.homepage = "https://github.com/palkan/action_policy"
16
- spec.license = "MIT"
17
-
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features)/})
20
- end
21
-
22
- spec.metadata = {
23
- "bug_tracker_uri" => "http://github.com/palkan/action_policy/issues",
24
- "changelog_uri" => "https://github.com/palkan/action_policy/blob/master/CHANGELOG.md",
25
- "documentation_uri" => "https://actionpolicy.evilmartians.io/",
26
- "homepage_uri" => "https://actionpolicy.evilmartians.io/",
27
- "source_code_uri" => "http://github.com/palkan/action_policy"
28
- }
29
-
30
- spec.require_paths = ["lib"]
31
-
32
- spec.required_ruby_version = ">= 2.4.0"
33
-
34
- spec.add_development_dependency "ammeter", "~> 1.1.3"
35
- spec.add_development_dependency "bundler", ">= 1.15"
36
- spec.add_development_dependency "minitest", "~> 5.0"
37
- spec.add_development_dependency "rake", "~> 10.0"
38
- spec.add_development_dependency "rspec", "~> 3.3"
39
- spec.add_development_dependency "rubocop", "~> 0.67.0"
40
- spec.add_development_dependency "rubocop-md", "~> 0.2"
41
- spec.add_development_dependency "standard", "~> 0.0.39"
42
- spec.add_development_dependency "benchmark-ips", "~> 2.7.0"
43
- spec.add_development_dependency "i18n"
44
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # This benchmark measures the efficiency of NamespaceCache.
5
- #
6
- # Run it multiple times with cache on/off to see the results:
7
- #
8
- # $ bundle exec ruby namespaced_lookup_cache.rb
9
- # $ bundle exec ruby namespaced_lookup_cache.rb
10
- # $ NO_CACHE=1 bundle exec ruby namespaced_lookup_cache.rb
11
- # $ NO_CACHE=1 bundle exec ruby namespaced_lookup_cache.rb
12
- #
13
-
14
- $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
15
-
16
- require "action_policy"
17
- require "benchmark/ips"
18
-
19
- GC.disable
20
-
21
- class A; end
22
-
23
- class B; end
24
-
25
- module X
26
- class BPolicy < ActionPolicy::Base; end
27
-
28
- module Y
29
- module Z
30
- class APolicy < ActionPolicy::Base; end
31
- end
32
- end
33
- end
34
-
35
- a = A.new
36
- b = B.new
37
-
38
- if ENV["NO_CACHE"]
39
- ActionPolicy::LookupChain.namespace_cache_enabled = false
40
- end
41
-
42
- Benchmark.ips do |x|
43
- x.warmup = 0
44
-
45
- x.report("cache A") do
46
- ActionPolicy.lookup(a, namespace: X::Y::Z)
47
- end
48
-
49
- x.report("cache B") do
50
- ActionPolicy.lookup(b, namespace: X::Y::Z)
51
- end
52
-
53
- x.report("no cache A") do
54
- ActionPolicy.lookup(a, namespace: X::Y::Z)
55
- end
56
-
57
- x.report("no cache B") do
58
- ActionPolicy.lookup(b, namespace: X::Y::Z)
59
- end
60
-
61
- x.hold! "temp_results"
62
-
63
- x.compare!
64
- end
65
-
66
- #
67
- # Comparison:
68
- # cache B: 178577.4 i/s
69
- # cache A: 173061.4 i/s - same-ish: difference falls within error
70
- # no cache A: 97991.7 i/s - same-ish: difference falls within error
71
- # no cache B: 42505.4 i/s - 4.20x slower
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "action_policy"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
File without changes
data/docs/CNAME DELETED
@@ -1 +0,0 @@
1
- actionpolicy.evilmartians.io
@@ -1,77 +0,0 @@
1
- [![Gem Version](https://badge.fury.io/rb/action_policy.svg)](https://badge.fury.io/rb/action_policy)
2
- [![Build Status](https://travis-ci.org/palkan/action_policy.svg?branch=master)](https://travis-ci.org/palkan/action_policy)
3
-
4
- ## Action Policy
5
-
6
- > Authorization framework for Ruby and Rails.
7
- <br>Composable. Extensible. Performant.
8
-
9
- **NOTE:** this documentation is for the version "0.3.0+".
10
-
11
- ## What is it?
12
-
13
- _Authorization_ is an act of giving **someone** official
14
- permission to **do something** (to not be confused with [_authentication_](https://en.wikipedia.org/wiki/Authentication)).
15
-
16
- Action Policy provides flexible tools to build an _authorization layer_ for your application.
17
-
18
- <div class="chart-container">
19
- <img src="assets/images/layer.svg" alt="Authorization layer" width="80%">
20
- </div>
21
-
22
- **NOTE:** Action Policy does not force you to use a specific authorization model (i.e., roles, permissions, etc.) and does not provide one. It only answers a single question: **How to verify access?**
23
-
24
- ## Where to go from here?
25
- - [Quick start](./quick_start.md)
26
- - [Using with Rails](./rails.md)
27
- - [Using with other Ruby frameworks](./non_rails.md)
28
- - [Using with GraphQL Ruby](./graphql.md)
29
-
30
- ## Project State
31
-
32
- The project is being used in production since mid 2018. Major features have been implemented, API has been stabilized. Check out our [development board](https://github.com/palkan/action_policy/projects/1) to see what's coming next.
33
-
34
- ## History
35
-
36
- Action Policy gem is an _extraction_-kind of a library. Most of the code has been used in production for several years in different [Evil Martians][] projects.
37
-
38
- We have decided to collect all our authorization techniques and pack them into a standalone gem–and that is how Action Policy was born!
39
-
40
- ## What about the existing solutions?
41
-
42
- Why did we decide to build our own authorization gem instead of using the existing solutions, such as [Pundit][] and [CanCanCan][]?
43
-
44
- **TL;DR they didn't solve all of our problems.**
45
-
46
- [Pundit][] has been our framework of choice for a long time. Being too _dead-simple_, it required a lot of hacking to fulfill business logic requirements.
47
-
48
- These _hacks_ later became Action Policy (initially, we even called it "Pundit, re-visited").
49
-
50
- We also took a few ideas from [CanCanCan][]—such as [default rules and rule aliases](./aliases.md).
51
-
52
- It is also worth noting that Action Policy (despite having a _Railsy_ name) is designed to be **Rails-free**. On the other hand, it contains some Rails-specific extensions and seamlessly integrates into the framework.
53
-
54
- So, what are the main reasons to consider Action Policy as your authorization tool?
55
-
56
- - **Performance**: multiple [caching strategies](./caching.md) out-of-the-box make authorization overhead as small as possible–especially useful when your rules involve DB queries; you can also monitor the performance and detect the bottlenecks using the built-in [instrumentation](./instrumentation) features.
57
-
58
- - **Composition & Customization**: use [only the features you need](./custom_policy.md) or easily extend the functionality–it's just Ruby classes and modules, (almost) zero magic! And you can add authorization [anywhere in your code](./non_rails.md), not only in controllers.
59
-
60
- - **Code Organization**: use [namespaces](./namespaces.md) to organize your policies (for example, when you have multiple authorization strategies); add [pre-checks](./pre_checks.md) to make rules more readable and better express your business-logic.
61
-
62
- - **...and more**: [testability](./testing.md), [i18n](./i18n.md) integrations, [actionable errors](./reasons.md).
63
-
64
- Learn more about the motivation behind the Action Policy and its features by watching this [RailsConf talk](https://www.youtube.com/watch?v=NVwx0DARDis).
65
-
66
- ## Resources
67
-
68
- - Seattle.rb, 2019 "A Denial!" talk [[slides](https://speakerdeck.com/palkan/seattle-dot-rb-2019-a-denial)]
69
-
70
- - RailsConf, 2018 "Access Denied" talk [[video](https://www.youtube.com/watch?v=NVwx0DARDis), [slides](https://speakerdeck.com/palkan/railsconf-2018-access-denied-the-missing-guide-to-authorization-in-rails)]
71
-
72
- <a href="https://evilmartians.com/?utm_source=action_policy">
73
- <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
74
-
75
- [CanCanCan]: https://github.com/CanCanCommunity/cancancan
76
- [Pundit]: https://github.com/varvet/pundit
77
- [Evil Martians]: https://evilmartians.com