accessly 0.0.1
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/.editorconfig +16 -0
- data/.gitignore +54 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +128 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/accessly.gemspec +32 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/accessly.rb +23 -0
- data/lib/accessly/base.rb +33 -0
- data/lib/accessly/models/permitted_action.rb +7 -0
- data/lib/accessly/models/permitted_action_on_object.rb +8 -0
- data/lib/accessly/permission/grant.rb +95 -0
- data/lib/accessly/permission/revoke.rb +87 -0
- data/lib/accessly/permitted_actions/base.rb +40 -0
- data/lib/accessly/permitted_actions/on_object_query.rb +65 -0
- data/lib/accessly/permitted_actions/query.rb +42 -0
- data/lib/accessly/policy/base.rb +269 -0
- data/lib/accessly/query.rb +122 -0
- data/lib/accessly/query_builder.rb +24 -0
- data/lib/accessly/version.rb +3 -0
- data/lib/generators/accessly/install/USAGE +5 -0
- data/lib/generators/accessly/install/install_generator.rb +51 -0
- data/lib/generators/accessly/install/templates/db/migrate/create_permitted_action_on_objects.rb +15 -0
- data/lib/generators/accessly/install/templates/db/migrate/create_permitted_actions.rb +13 -0
- metadata +176 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require "accessly/base"
|
|
2
|
+
|
|
3
|
+
module Accessly
|
|
4
|
+
# Accessly::Query is the interface that hides the implementation
|
|
5
|
+
# of the data layer. Ask Accessly::Query whether an actor
|
|
6
|
+
# has permission on a record, ask it for a list of permitted records for the record
|
|
7
|
+
# type, and ask it whether an actor has a general permission not
|
|
8
|
+
# related to any certain record or record type.
|
|
9
|
+
class Query < Base
|
|
10
|
+
|
|
11
|
+
# Create an instance of Accessly::Query.
|
|
12
|
+
# Lookups are cached in inherited object(s) to prevent redundant calls to the database.
|
|
13
|
+
# Pass in a Hash or ActiveRecord::Base for actors if the actor(s)
|
|
14
|
+
# inherit some permissions from other actors in the system. This may happen
|
|
15
|
+
# when you have a user in one or more groups or organizations with their own
|
|
16
|
+
# access control permissions.
|
|
17
|
+
#
|
|
18
|
+
# @param actors [Hash, ActiveRecord::Base] The actor(s) we're checking permission(s)
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# # Create a new object with a single actor
|
|
22
|
+
# Accessly::Query.new(user)
|
|
23
|
+
# @example
|
|
24
|
+
# # Create a new object with multiple actors
|
|
25
|
+
# Accessly::Query.new(User => user.id, Group => [1,2], Organization => Organization.where(user_id: user.id).pluck(:id))
|
|
26
|
+
def initialize(actors)
|
|
27
|
+
super(actors)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check whether an actor has a given permission.
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
# @overload can?(action_id, namespace)
|
|
33
|
+
# Ask whether the actor has permission to perform action_id
|
|
34
|
+
# in the given namespace. Multiple actions can have the same id
|
|
35
|
+
# as long as their namespace is different. The namespace can be
|
|
36
|
+
# any String. We recommend using namespace to group a class of
|
|
37
|
+
# permissions, such as to group parts of a particular feature
|
|
38
|
+
# in your application.
|
|
39
|
+
#
|
|
40
|
+
# @param action_id [Integer, Array<Integer>] The action or actions we're checking whether the actor has. If this is an array, then the check is ORed.
|
|
41
|
+
# @param namespace [String] The namespace of the given action_id.
|
|
42
|
+
# @return [Boolean] Returns true if actor has been granted the permission, false otherwise.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# # Can the user perform the action with id 3 for posts?
|
|
46
|
+
# Accessly.can?(user, 3, "posts")
|
|
47
|
+
# @example
|
|
48
|
+
# # Can the user perform the action with id 5 for Posts?
|
|
49
|
+
# Accessly::Query.new(user).can?(5, Post)
|
|
50
|
+
# @example
|
|
51
|
+
# # Can the sets of actors perform the action with id 5 for Posts?
|
|
52
|
+
# Accessly::Query.new(User => user.id, Group => [1,2]).can?(5, Post)
|
|
53
|
+
# @example
|
|
54
|
+
# # Can the user on segment 1 perform the action with id 5 for Posts
|
|
55
|
+
# Accessly::Query.new(user).on_segment(1).can?(5, Post)
|
|
56
|
+
# @example
|
|
57
|
+
# # Can the sets of actors on segment 1 perform the action with id 5 for Posts
|
|
58
|
+
# Accessly::Query.new(User => user.id, Group => [1,2]).on_segment(1).can?(5, Post)
|
|
59
|
+
#
|
|
60
|
+
# @overload can?(action_id, object_type, object_id)
|
|
61
|
+
# Ask whether the actor has permission to perform action_id
|
|
62
|
+
# on a given record.
|
|
63
|
+
#
|
|
64
|
+
# @param action_id [Integer, Array<Integer>] The action or actions we're checking whether the actor has. If this is an array, then the check is ORed.
|
|
65
|
+
# @param object_type [ActiveRecord::Base] The ActiveRecord model which we're checking for permission on.
|
|
66
|
+
# @param object_id [Integer] The id of the ActiveRecord object which we're checking for permission on.
|
|
67
|
+
# @return [Boolean] Returns true if actor has been granted the permission on the specified record, false otherwise.
|
|
68
|
+
#
|
|
69
|
+
# @example
|
|
70
|
+
# # Can the user perform the action with id 5 for the Post with id 7?
|
|
71
|
+
# Accessly::Query.new(user).can?(5, Post, 7)
|
|
72
|
+
# @example
|
|
73
|
+
# # Can the sets of actors perform the action with id 5 for the Post with id 7?
|
|
74
|
+
# Accessly::Query.new(User => user.id, Group => [1,2]).can?(5, Post, 7)
|
|
75
|
+
# @example
|
|
76
|
+
# # Can the user on segment 1 perform the action with id 5 for the Post with id 7?
|
|
77
|
+
# Accessly::Query.new(user).on_segment(1).can?(5, Post, 7)
|
|
78
|
+
# @example
|
|
79
|
+
# # Can the sets of actors on segment 1 perform the action with id 5 for the Post with id 7?
|
|
80
|
+
# Accessly::Query.new(User => user.id, Group => [1,2]).on_segment(1).can?(5, Post, 7)
|
|
81
|
+
def can?(action_id, object_type, object_id = nil)
|
|
82
|
+
if object_id.nil?
|
|
83
|
+
permitted_action_query.can?(action_id, object_type)
|
|
84
|
+
else
|
|
85
|
+
permitted_action_on_object_query.can?(action_id, object_type, object_id)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns an ActiveRecord::Relation of ids in the namespace for
|
|
90
|
+
# which the actor has permission to perform action_id.
|
|
91
|
+
#
|
|
92
|
+
# @param action_id [Integer] The action we're checking on the actor in the namespace.
|
|
93
|
+
# @param namespace [String] The namespace to check actor permissions.
|
|
94
|
+
# @return [ActiveRecord::Relation]
|
|
95
|
+
#
|
|
96
|
+
# @example
|
|
97
|
+
# # Give me the list of Post ids on which the user has permission to perform action_id 3
|
|
98
|
+
# Accessly::Query.new(user).list(3, Post)
|
|
99
|
+
# @example
|
|
100
|
+
# # Give me the list of Post ids on which the user has permission to perform action_id 3 on segment 1
|
|
101
|
+
# Accessly::Query.new(user).on_segment(1).list(3, Post)
|
|
102
|
+
# @example
|
|
103
|
+
# # Give me the list of Post ids on which the user and its groups has permission to perform action_id 3
|
|
104
|
+
# Accessly::Query.new(User => user.id, Group => [1,2]).list(3, Post)
|
|
105
|
+
# @example
|
|
106
|
+
# # Give me the list of Post ids on which the user and its groups has permission to perform action_id 3 on segment 1
|
|
107
|
+
# Accessly::Query.new(User => user.id, Group => [1,2]).on_segment(1).list(3, Post)
|
|
108
|
+
def list(action_id, namespace)
|
|
109
|
+
permitted_action_on_object_query.list(action_id, namespace)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def permitted_action_query
|
|
115
|
+
@_permitted_action_query ||= Accessly::PermittedActions::Query.new(@actors, @segment_id)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def permitted_action_on_object_query
|
|
119
|
+
@_permitted_action_on_object_query ||= Accessly::PermittedActions::OnObjectQuery.new(@actors, @segment_id)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Accessly
|
|
2
|
+
class QueryBuilder
|
|
3
|
+
|
|
4
|
+
# Builds a query with a series of actors 'OR' together
|
|
5
|
+
#
|
|
6
|
+
# Use like this:
|
|
7
|
+
# `Accessly::QueryBuilder.with_actors(PermittedActionOnObject, {User => 1, Group => [2,3]})`
|
|
8
|
+
#
|
|
9
|
+
# @param query [ActiveRecord::Relation] The relation on which to append the where clause
|
|
10
|
+
# @param actors [Hash] A hash of actors where the key is the object/classname and the value is an Integer or array of Integers
|
|
11
|
+
# @return [ActiveRecord::Relation]
|
|
12
|
+
def self.with_actors(query, actors)
|
|
13
|
+
result_query = nil
|
|
14
|
+
actors.each do |key, value|
|
|
15
|
+
result_query = if result_query.nil?
|
|
16
|
+
query.where(actor_type: String(key), actor_id: value)
|
|
17
|
+
else
|
|
18
|
+
result_query.or(query.where(actor_type: String(key), actor_id: value))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
result_query
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module Accessly
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
include Rails::Generators::Migration
|
|
8
|
+
source_root File.expand_path("../templates", __FILE__)
|
|
9
|
+
|
|
10
|
+
def create_accessly_migration
|
|
11
|
+
copy_migration "create_permitted_actions.rb"
|
|
12
|
+
copy_migration "create_permitted_action_on_objects.rb"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def copy_migration(migration_name, config = {})
|
|
18
|
+
unless migration_exists?(migration_name)
|
|
19
|
+
migration_template(
|
|
20
|
+
"db/migrate/#{migration_name}",
|
|
21
|
+
"db/migrate/#{migration_name}",
|
|
22
|
+
config.merge(migration_version: migration_version),
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def migration_exists?(name)
|
|
28
|
+
existing_migrations.include?(name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def existing_migrations
|
|
32
|
+
@existing_migrations ||= Dir.glob("db/migrate/*.rb").map do |file|
|
|
33
|
+
migration_name_without_timestamp(file)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def migration_name_without_timestamp(file)
|
|
38
|
+
file.sub(%r{^.*(db/migrate/)(?:\d+_)?}, '')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# necessary to generate timestamps when using `create_migration`
|
|
42
|
+
def self.next_migration_number(dir)
|
|
43
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def migration_version
|
|
47
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/generators/accessly/install/templates/db/migrate/create_permitted_action_on_objects.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreatePermittedActionOnObjects < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :accessly_permitted_action_on_objects, force: true, id: :uuid do |t|
|
|
4
|
+
t.column :segment_id, :integer, default: -1
|
|
5
|
+
t.column :action, :integer, null: false
|
|
6
|
+
t.column :actor_id, :integer, null: false
|
|
7
|
+
t.column :actor_type, :string, null: false
|
|
8
|
+
t.column :object_type, :string, null: false
|
|
9
|
+
t.column :object_id, :integer, null: false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index(:accessly_permitted_action_on_objects, [:segment_id, :actor_type, :actor_id, :object_type, :object_id, :action], unique: true, name: "acessly_paoo_uniq_table_idx")
|
|
13
|
+
add_index(:accessly_permitted_action_on_objects, [:segment_id, :object_type, :object_id, :action], name: "acessly_paoo_on_object_idx")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreatePermittedActions < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :accessly_permitted_actions, force: true, id: :uuid do |t|
|
|
4
|
+
t.column :segment_id, :integer, default: -1
|
|
5
|
+
t.column :action, :integer, null: false
|
|
6
|
+
t.column :actor_id, :integer, null: false
|
|
7
|
+
t.column :actor_type, :string, null: false
|
|
8
|
+
t.column :object_type, :string, null: false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index(:accessly_permitted_actions, [:segment_id, :actor_type, :actor_id, :object_type, :action], unique: true, name: "acessly_pa_uniq_table_idx")
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: accessly
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aaron Milam
|
|
8
|
+
- Eddie Hourigan
|
|
9
|
+
- Ross Reinhardt
|
|
10
|
+
autorequire:
|
|
11
|
+
bindir: exe
|
|
12
|
+
cert_chain: []
|
|
13
|
+
date: 2018-03-28 00:00:00.000000000 Z
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: activerecord
|
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
|
18
|
+
requirements:
|
|
19
|
+
- - "~>"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '5.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '5.0'
|
|
29
|
+
- !ruby/object:Gem::Dependency
|
|
30
|
+
name: bundler
|
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
|
32
|
+
requirements:
|
|
33
|
+
- - "~>"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '1.16'
|
|
36
|
+
type: :development
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - "~>"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.16'
|
|
43
|
+
- !ruby/object:Gem::Dependency
|
|
44
|
+
name: rake
|
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - "~>"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '10.0'
|
|
50
|
+
type: :development
|
|
51
|
+
prerelease: false
|
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - "~>"
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '10.0'
|
|
57
|
+
- !ruby/object:Gem::Dependency
|
|
58
|
+
name: minitest
|
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - "~>"
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '5.0'
|
|
64
|
+
type: :development
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - "~>"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '5.0'
|
|
71
|
+
- !ruby/object:Gem::Dependency
|
|
72
|
+
name: database_cleaner
|
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - "~>"
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '1.5'
|
|
78
|
+
type: :development
|
|
79
|
+
prerelease: false
|
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - "~>"
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '1.5'
|
|
85
|
+
- !ruby/object:Gem::Dependency
|
|
86
|
+
name: sqlite3
|
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - "~>"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '1.3'
|
|
92
|
+
type: :development
|
|
93
|
+
prerelease: false
|
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - "~>"
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '1.3'
|
|
99
|
+
- !ruby/object:Gem::Dependency
|
|
100
|
+
name: rails
|
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - "~>"
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '5.0'
|
|
106
|
+
type: :development
|
|
107
|
+
prerelease: false
|
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - "~>"
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '5.0'
|
|
113
|
+
description: Use the policy pattern to define access control mechanisms in Rails.
|
|
114
|
+
Store user-level, group-level, or org-level permission on any given record or concept
|
|
115
|
+
in the database with ultra-fast lookups.
|
|
116
|
+
email:
|
|
117
|
+
- devops@lessonly.com
|
|
118
|
+
executables: []
|
|
119
|
+
extensions: []
|
|
120
|
+
extra_rdoc_files: []
|
|
121
|
+
files:
|
|
122
|
+
- ".editorconfig"
|
|
123
|
+
- ".gitignore"
|
|
124
|
+
- ".ruby-version"
|
|
125
|
+
- ".tool-versions"
|
|
126
|
+
- Gemfile
|
|
127
|
+
- Gemfile.lock
|
|
128
|
+
- LICENSE
|
|
129
|
+
- LICENSE.txt
|
|
130
|
+
- README.md
|
|
131
|
+
- Rakefile
|
|
132
|
+
- accessly.gemspec
|
|
133
|
+
- bin/console
|
|
134
|
+
- bin/setup
|
|
135
|
+
- lib/accessly.rb
|
|
136
|
+
- lib/accessly/base.rb
|
|
137
|
+
- lib/accessly/models/permitted_action.rb
|
|
138
|
+
- lib/accessly/models/permitted_action_on_object.rb
|
|
139
|
+
- lib/accessly/permission/grant.rb
|
|
140
|
+
- lib/accessly/permission/revoke.rb
|
|
141
|
+
- lib/accessly/permitted_actions/base.rb
|
|
142
|
+
- lib/accessly/permitted_actions/on_object_query.rb
|
|
143
|
+
- lib/accessly/permitted_actions/query.rb
|
|
144
|
+
- lib/accessly/policy/base.rb
|
|
145
|
+
- lib/accessly/query.rb
|
|
146
|
+
- lib/accessly/query_builder.rb
|
|
147
|
+
- lib/accessly/version.rb
|
|
148
|
+
- lib/generators/accessly/install/USAGE
|
|
149
|
+
- lib/generators/accessly/install/install_generator.rb
|
|
150
|
+
- lib/generators/accessly/install/templates/db/migrate/create_permitted_action_on_objects.rb
|
|
151
|
+
- lib/generators/accessly/install/templates/db/migrate/create_permitted_actions.rb
|
|
152
|
+
homepage: https://github.com/lessonly/accessly
|
|
153
|
+
licenses:
|
|
154
|
+
- MIT
|
|
155
|
+
metadata: {}
|
|
156
|
+
post_install_message:
|
|
157
|
+
rdoc_options: []
|
|
158
|
+
require_paths:
|
|
159
|
+
- lib
|
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - ">="
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: '0'
|
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
|
+
requirements:
|
|
167
|
+
- - ">="
|
|
168
|
+
- !ruby/object:Gem::Version
|
|
169
|
+
version: '0'
|
|
170
|
+
requirements: []
|
|
171
|
+
rubyforge_project:
|
|
172
|
+
rubygems_version: 2.6.13
|
|
173
|
+
signing_key:
|
|
174
|
+
specification_version: 4
|
|
175
|
+
summary: Simplified access control in Rails
|
|
176
|
+
test_files: []
|