ruby_ability_graph 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +661 -0
- data/README.md +139 -0
- data/exe/ruby-ability-graph +6 -0
- data/lib/ruby_ability_graph/cli.rb +160 -0
- data/lib/ruby_ability_graph/enumerator.rb +250 -0
- data/lib/ruby_ability_graph/harness.rb +130 -0
- data/lib/ruby_ability_graph/html_report.rb +452 -0
- data/lib/ruby_ability_graph/inspector.rb +146 -0
- data/lib/ruby_ability_graph/policy_checker.rb +59 -0
- data/lib/ruby_ability_graph/role_stand_in.rb +34 -0
- data/lib/ruby_ability_graph/rule_classifier.rb +71 -0
- data/lib/ruby_ability_graph/scan_options.rb +132 -0
- data/lib/ruby_ability_graph/scan_presenter.rb +74 -0
- data/lib/ruby_ability_graph/table_formatter.rb +64 -0
- data/lib/ruby_ability_graph/terminal_safe.rb +12 -0
- data/lib/ruby_ability_graph/version.rb +5 -0
- data/lib/ruby_ability_graph.rb +19 -0
- metadata +78 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyAbilityGraph
|
|
4
|
+
# Classifies a single CanCan::Rule's condition:
|
|
5
|
+
# "resolved" (unconditional or flat scalar hash, fully structured) or
|
|
6
|
+
# "unsupported" (block, association-reaching, or otherwise opaque).
|
|
7
|
+
# Shape only -- source/dynamic-generation handling lives in Enumerator.
|
|
8
|
+
class RuleClassifier
|
|
9
|
+
Classification = Struct.new(:confidence, :condition, :reason, :source, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
def self.call(rule:, model:)
|
|
12
|
+
new(rule: rule, model: model).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(rule:, model:)
|
|
16
|
+
@rule = rule
|
|
17
|
+
@model = model
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
# only_block?, not @rule.block -- cancancan only made `block` a public
|
|
22
|
+
# reader from ~3.x on (it's a private ivar in, e.g., 1.17.0, still
|
|
23
|
+
# bundled by real apps -- found via dogfooding dradis-ce). only_block?
|
|
24
|
+
# (conditions_empty? && block-present) is public across both and, since
|
|
25
|
+
# both versions treat a Hash-conditions-plus-block combo as a raise-on-
|
|
26
|
+
# declaration error, it's equivalent to "has a block" for every rule
|
|
27
|
+
# that could actually exist.
|
|
28
|
+
return unsupported("block_condition") if @rule.only_block?
|
|
29
|
+
|
|
30
|
+
conditions = @rule.conditions
|
|
31
|
+
return resolved(nil) if blank?(conditions)
|
|
32
|
+
return unsupported("unrecognized_condition_value") unless conditions.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
classify_hash(conditions)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def blank?(conditions)
|
|
40
|
+
conditions.nil? || (conditions.respond_to?(:empty?) && conditions.empty?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def classify_hash(conditions)
|
|
44
|
+
conditions.each do |key, value|
|
|
45
|
+
return unsupported("association_chained") if value.is_a?(Hash)
|
|
46
|
+
return unsupported("requires_association_traversal") if association_key?(key)
|
|
47
|
+
return unsupported("unrecognized_condition_value") unless scalar?(value)
|
|
48
|
+
end
|
|
49
|
+
resolved(conditions)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Deliberately no special-casing by key name (e.g. tenant_id/team_id) --
|
|
53
|
+
# a flat scalar comparison is resolved regardless of what it's called.
|
|
54
|
+
def association_key?(key)
|
|
55
|
+
@model.respond_to?(:reflect_on_association) && !@model.reflect_on_association(key).nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def scalar?(value)
|
|
59
|
+
value.nil? || value == true || value == false ||
|
|
60
|
+
value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(Symbol)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolved(condition)
|
|
64
|
+
Classification.new(confidence: "resolved", condition: condition, reason: nil, source: nil)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def unsupported(reason)
|
|
68
|
+
Classification.new(confidence: "unsupported", condition: nil, reason: reason, source: nil)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module RubyAbilityGraph
|
|
6
|
+
# Parses and validates `scan` subcommand argv into an options hash + app
|
|
7
|
+
# path. Split out from CLI so that class stays a thin command dispatcher
|
|
8
|
+
# as flags accumulate (see .rubocop.yml's CLI Metrics/ClassLength note).
|
|
9
|
+
class ScanOptions
|
|
10
|
+
def self.parse(argv)
|
|
11
|
+
new.parse(argv)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse(argv)
|
|
15
|
+
options = defaults
|
|
16
|
+
build_parser(options).parse!(argv)
|
|
17
|
+
|
|
18
|
+
app_path = argv.shift
|
|
19
|
+
abort(CLI::USAGE) unless app_path
|
|
20
|
+
|
|
21
|
+
validate!(options)
|
|
22
|
+
[options, app_path]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def defaults
|
|
28
|
+
{
|
|
29
|
+
ability_file: Harness::DEFAULT_ABILITY_FILE, ability_class: "Ability",
|
|
30
|
+
requires: [], rails_boot: false, format: "table"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_parser(options)
|
|
35
|
+
OptionParser.new do |opts|
|
|
36
|
+
add_roles_file_option!(opts, options)
|
|
37
|
+
add_ability_file_option!(opts, options)
|
|
38
|
+
add_ability_class_option!(opts, options)
|
|
39
|
+
add_ruby_bin_option!(opts, options)
|
|
40
|
+
add_format_option!(opts, options)
|
|
41
|
+
add_policy_file_option!(opts, options)
|
|
42
|
+
add_html_report_option!(opts, options)
|
|
43
|
+
add_loader_strategy_options!(opts, options)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_roles_file_option!(opts, options)
|
|
48
|
+
opts.on("--roles-file FILE", "YAML file mapping role name => user stand-in attributes") do |v|
|
|
49
|
+
options[:roles_file] = v
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_ability_file_option!(opts, options)
|
|
54
|
+
opts.on("--ability-file FILE", "Path to the Ability class file, relative to APP_PATH") do |v|
|
|
55
|
+
options[:ability_file] = v
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_ability_class_option!(opts, options)
|
|
60
|
+
opts.on("--ability-class NAME", "Constant name of the Ability class, e.g. Spree::Ability for a " \
|
|
61
|
+
"namespaced/engine-provided one (default: Ability)") do |v|
|
|
62
|
+
options[:ability_class] = v
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_ruby_bin_option!(opts, options)
|
|
67
|
+
opts.on("--ruby-bin PATH", "Ruby executable for the target subprocess (default: " \
|
|
68
|
+
"\"ruby\" via PATH); target can use a different Ruby version. " \
|
|
69
|
+
"See README.") do |v|
|
|
70
|
+
options[:ruby_bin] = v
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_format_option!(opts, options)
|
|
75
|
+
opts.on("--format FORMAT", %w[table json], "Output format: table (default) or json") do |v|
|
|
76
|
+
options[:format] = v
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_policy_file_option!(opts, options)
|
|
81
|
+
opts.on("--policy-file FILE", "YAML file declaring role/action/model access expectations " \
|
|
82
|
+
"(see README); violations are flagged and exit non-zero") do |v|
|
|
83
|
+
options[:policy_file] = v
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_html_report_option!(opts, options)
|
|
88
|
+
opts.on("--html-report FILE", "Write a self-contained, interactive HTML graph of the results " \
|
|
89
|
+
"(see README) to this path, relative to APP_PATH") do |v|
|
|
90
|
+
options[:html_report] = v
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def add_loader_strategy_options!(opts, options)
|
|
95
|
+
add_require_option!(opts, options)
|
|
96
|
+
add_rails_boot_option!(opts, options)
|
|
97
|
+
add_rails_env_option!(opts, options)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_require_option!(opts, options)
|
|
101
|
+
opts.on("--require FILE", "Path, relative to APP_PATH, to preload before the " \
|
|
102
|
+
"Ability file (repeatable), see #2. Not compatible " \
|
|
103
|
+
"with --rails-boot.") do |v|
|
|
104
|
+
options[:requires] << v
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def add_rails_boot_option!(opts, options)
|
|
109
|
+
opts.on("--rails-boot", "Run inside the target's own `bin/rails runner` for real " \
|
|
110
|
+
"Zeitwerk autoloading, see #3. Not compatible with --require.") do
|
|
111
|
+
options[:rails_boot] = true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def add_rails_env_option!(opts, options)
|
|
116
|
+
opts.on("--rails-env ENV", "RAILS_ENV to boot under with --rails-boot (default: " \
|
|
117
|
+
"#{Harness::DEFAULT_RAILS_ENV.inspect}).") do |v|
|
|
118
|
+
options[:rails_env] = v
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate!(options)
|
|
123
|
+
abort("--rails-env only applies with --rails-boot.") if options[:rails_env] && !options[:rails_boot]
|
|
124
|
+
|
|
125
|
+
return unless options[:rails_boot] && !options[:requires].empty?
|
|
126
|
+
|
|
127
|
+
abort("--require is not compatible with --rails-boot -- once Zeitwerk is live via bin/rails runner, " \
|
|
128
|
+
"referenced classes resolve on their own; a manual --require list is superfluous. " \
|
|
129
|
+
"Drop one or the other.")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "table_formatter"
|
|
5
|
+
require_relative "terminal_safe"
|
|
6
|
+
|
|
7
|
+
module RubyAbilityGraph
|
|
8
|
+
# Turns a scan's results (plus an optional PolicyChecker::Report) into the
|
|
9
|
+
# CLI's printed output, in either format, and decides the process exit
|
|
10
|
+
# status -- kept separate from CLI so that class stays a thin dispatcher.
|
|
11
|
+
class ScanPresenter
|
|
12
|
+
SCHEMA_VERSION = 1
|
|
13
|
+
|
|
14
|
+
def initialize(format:, results:, policy_report:)
|
|
15
|
+
@format = format
|
|
16
|
+
@results = results
|
|
17
|
+
@policy_report = policy_report
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render
|
|
21
|
+
@format == "json" ? render_json : render_table
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# nil policy_report means no --policy-file was given. An unmatched policy
|
|
25
|
+
# (typo'd model/action) counts as a problem too, same as a real violation.
|
|
26
|
+
def problems?
|
|
27
|
+
!@policy_report.nil? && (@policy_report.violations.any? || @policy_report.unmatched.any?)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def render_json
|
|
33
|
+
payload = { "schema_version" => SCHEMA_VERSION, "results" => @results }
|
|
34
|
+
if @policy_report
|
|
35
|
+
payload["policy_violations"] = @policy_report.violations
|
|
36
|
+
payload["policy_unmatched"] = @policy_report.unmatched
|
|
37
|
+
end
|
|
38
|
+
JSON.pretty_generate(payload)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_table
|
|
42
|
+
return TableFormatter.call(@results) if @policy_report.nil?
|
|
43
|
+
|
|
44
|
+
sections = [render_violations, render_unmatched].compact
|
|
45
|
+
"#{TableFormatter.call(@results)}\n\n#{sections.join("\n\n")}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_violations
|
|
49
|
+
return "Policy check: no violations." if @policy_report.violations.empty?
|
|
50
|
+
|
|
51
|
+
lines = ["Policy violations (#{@policy_report.violations.size}):"] +
|
|
52
|
+
@policy_report.violations.map { |v| violation_line(v) }
|
|
53
|
+
lines.join("\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_unmatched
|
|
57
|
+
return nil if @policy_report.unmatched.empty?
|
|
58
|
+
|
|
59
|
+
lines = ["Policy entries with no matching scan result -- check for typos " \
|
|
60
|
+
"(#{@policy_report.unmatched.size}):"] + @policy_report.unmatched.map { |p| unmatched_line(p) }
|
|
61
|
+
lines.join("\n")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def violation_line(violation)
|
|
65
|
+
role, action, model = %w[role action model].map { |k| TerminalSafe.sanitize(violation[k]) }
|
|
66
|
+
allowed_roles = violation["allowed_roles"].map { |r| TerminalSafe.sanitize(r) }.join(", ")
|
|
67
|
+
" #{role} can #{action} #{model} but is not in allowed_roles (#{allowed_roles}) [#{violation['confidence']}]"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def unmatched_line(policy)
|
|
71
|
+
" #{TerminalSafe.sanitize(policy['model'])} / #{TerminalSafe.sanitize(policy['action'])}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "terminal_safe"
|
|
4
|
+
|
|
5
|
+
module RubyAbilityGraph
|
|
6
|
+
# Renders scan results (role x action x model rows, string-keyed as they
|
|
7
|
+
# come back from Harness) as a plain-text table -- the human-readable
|
|
8
|
+
# counterpart to `--format json`, plus a resolved/unsupported coverage
|
|
9
|
+
# summary line.
|
|
10
|
+
class TableFormatter
|
|
11
|
+
HEADERS = %w[ROLE ACTION MODEL ALLOWED CONFIDENCE CONDITION].freeze
|
|
12
|
+
|
|
13
|
+
def self.call(results)
|
|
14
|
+
new(results).call
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(results)
|
|
18
|
+
@results = results
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
rows = sorted_rows
|
|
23
|
+
widths = column_widths(rows)
|
|
24
|
+
table_lines = [format_row(HEADERS, widths), format_row(separator_cells(widths), widths)]
|
|
25
|
+
rows.each { |row| table_lines << format_row(row, widths) }
|
|
26
|
+
"#{table_lines.join("\n")}\n\n#{summary_line}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def sorted_rows
|
|
32
|
+
@results.sort_by { |r| [r["role"], r["model"], r["action"]] }
|
|
33
|
+
.map { |r| [r["role"], r["action"], r["model"], r["allowed"].to_s, r["confidence"], condition_cell(r)] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def condition_cell(result)
|
|
37
|
+
result["condition"].nil? ? "-" : TerminalSafe.sanitize(result["condition"])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def separator_cells(widths)
|
|
41
|
+
widths.map { |w| "-" * w }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def column_widths(rows)
|
|
45
|
+
HEADERS.each_index.map { |i| ([HEADERS[i].length] + rows.map { |r| r[i].to_s.length }).max }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def format_row(cells, widths)
|
|
49
|
+
cells.each_with_index.map { |cell, i| cell.to_s.ljust(widths[i]) }.join(" ").rstrip
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def summary_line
|
|
53
|
+
resolved = @results.count { |r| r["confidence"] == "resolved" }
|
|
54
|
+
total = @results.size
|
|
55
|
+
"#{resolved}/#{total} resolved (#{coverage_pct(resolved, total)}%)"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def coverage_pct(resolved, total)
|
|
59
|
+
return 0 if total.zero?
|
|
60
|
+
|
|
61
|
+
((resolved.to_f / total) * 100).round(1)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyAbilityGraph
|
|
4
|
+
# Strips control chars (e.g. ANSI escapes) before untrusted values hit the terminal.
|
|
5
|
+
module TerminalSafe
|
|
6
|
+
CONTROL_CHARS = /[\p{Cc}&&[^\n]]/
|
|
7
|
+
|
|
8
|
+
def self.sanitize(value)
|
|
9
|
+
value.to_s.gsub(CONTROL_CHARS, "")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ruby_ability_graph/version"
|
|
4
|
+
require_relative "ruby_ability_graph/role_stand_in"
|
|
5
|
+
require_relative "ruby_ability_graph/rule_classifier"
|
|
6
|
+
require_relative "ruby_ability_graph/enumerator"
|
|
7
|
+
require_relative "ruby_ability_graph/harness"
|
|
8
|
+
require_relative "ruby_ability_graph/inspector"
|
|
9
|
+
require_relative "ruby_ability_graph/terminal_safe"
|
|
10
|
+
require_relative "ruby_ability_graph/table_formatter"
|
|
11
|
+
require_relative "ruby_ability_graph/policy_checker"
|
|
12
|
+
require_relative "ruby_ability_graph/html_report"
|
|
13
|
+
require_relative "ruby_ability_graph/scan_presenter"
|
|
14
|
+
require_relative "ruby_ability_graph/scan_options"
|
|
15
|
+
require_relative "ruby_ability_graph/cli"
|
|
16
|
+
|
|
17
|
+
module RubyAbilityGraph
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_ability_graph
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jessica Grider
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
description: |
|
|
27
|
+
ruby_ability_graph loads a Rails app's CanCanCan Ability class in isolation,
|
|
28
|
+
enumerates roles x actions x models, and reports resolved permissions --
|
|
29
|
+
distinguishing patterns it can fully resolve from ones it honestly flags as
|
|
30
|
+
unsupported rather than guessing.
|
|
31
|
+
executables:
|
|
32
|
+
- ruby-ability-graph
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- exe/ruby-ability-graph
|
|
39
|
+
- lib/ruby_ability_graph.rb
|
|
40
|
+
- lib/ruby_ability_graph/cli.rb
|
|
41
|
+
- lib/ruby_ability_graph/enumerator.rb
|
|
42
|
+
- lib/ruby_ability_graph/harness.rb
|
|
43
|
+
- lib/ruby_ability_graph/html_report.rb
|
|
44
|
+
- lib/ruby_ability_graph/inspector.rb
|
|
45
|
+
- lib/ruby_ability_graph/policy_checker.rb
|
|
46
|
+
- lib/ruby_ability_graph/role_stand_in.rb
|
|
47
|
+
- lib/ruby_ability_graph/rule_classifier.rb
|
|
48
|
+
- lib/ruby_ability_graph/scan_options.rb
|
|
49
|
+
- lib/ruby_ability_graph/scan_presenter.rb
|
|
50
|
+
- lib/ruby_ability_graph/table_formatter.rb
|
|
51
|
+
- lib/ruby_ability_graph/terminal_safe.rb
|
|
52
|
+
- lib/ruby_ability_graph/version.rb
|
|
53
|
+
homepage: https://github.com/m1gd0n-dev/ruby-ability-graph
|
|
54
|
+
licenses:
|
|
55
|
+
- AGPL-3.0-or-later
|
|
56
|
+
metadata:
|
|
57
|
+
homepage_uri: https://github.com/m1gd0n-dev/ruby-ability-graph
|
|
58
|
+
source_code_uri: https://github.com/m1gd0n-dev/ruby-ability-graph
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '4.0'
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 4.0.16
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Maps CanCanCan authorization rules into a visual, queryable 'who can access
|
|
77
|
+
what' model.
|
|
78
|
+
test_files: []
|