conjur-api 4.11.2 → 4.12.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 +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +5 -0
- data/lib/conjur/api/roles.rb +33 -1
- data/lib/conjur/graph.rb +194 -0
- data/lib/conjur-api/version.rb +1 -1
- data/spec/api/graph_spec.rb +102 -0
- data/spec/api/roles_spec.rb +74 -3
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4158ca28a27fd311f677a695b2f551e9f0f7984
|
4
|
+
data.tar.gz: 4acd4da34ae508e0d91260d37ea93f48b20dc315
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b8f8e9e4833429a08b95c04d002c3163e99dd5af85127418ec50b742d7f8ab3da41149bb607816bf44af3b66a1f6c168277f8c81459505dcf617076f137f5d3e
|
7
|
+
data.tar.gz: 184f448ab7eee155ff9b52bcb23b71bbda1e739bf714f04578c90ba69d0049d4359ff5bb7c91a2f9bde99755347e7741bc86fdc8ec84da2ff5a0406f4cf2cf5b
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/lib/conjur/api/roles.rb
CHANGED
@@ -19,9 +19,32 @@
|
|
19
19
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
20
|
#
|
21
21
|
require 'conjur/role'
|
22
|
+
require 'conjur/graph'
|
22
23
|
|
23
24
|
module Conjur
|
24
25
|
class API
|
26
|
+
##
|
27
|
+
# Fetch a digraph (or a forest of digraphs) representing of
|
28
|
+
# role memberships related transitively to any of a list of roles.
|
29
|
+
#
|
30
|
+
# @param [Array<Conjur::Role, String>] roles the digraph (or forest thereof) of
|
31
|
+
# the ancestors and descendants of these roles or role ids will be returned
|
32
|
+
# @param [Hash] options options determining the graph returned
|
33
|
+
# @option opts [Boolean] :ancestors Whether to return ancestors of the given roles (true by default)
|
34
|
+
# @option opts [Boolean] :descendants Whether to return descendants of the given roles (true by default)
|
35
|
+
# @option opts [Conjur::Role, String] :as_role Only roles visible to this role will be included in the graph
|
36
|
+
# @return [Conjur::Graph] An object representing the role memberships digraph
|
37
|
+
def role_graph roles, options = {}
|
38
|
+
roles.map!{|r| normalize_roleid(r) }
|
39
|
+
options[:as_role] = normalize_roleid(options[:as_role]) if options.include?(:as_role)
|
40
|
+
options.reverse_merge! as_role: normalize_roleid(current_role), descendants: true, ancestors: true
|
41
|
+
|
42
|
+
query = {from_role: options.delete(:as_role)}
|
43
|
+
.merge(options.slice(:ancestors, :descendants))
|
44
|
+
.merge(roles: roles).to_query
|
45
|
+
Conjur::Graph.new RestClient::Resource.new(Conjur::Authz::API.host, credentials)["#{Conjur.account}/roles?#{query}"].get
|
46
|
+
end
|
47
|
+
|
25
48
|
def create_role(role, options = {})
|
26
49
|
role(role).tap do |r|
|
27
50
|
r.create(options)
|
@@ -39,7 +62,7 @@ module Conjur
|
|
39
62
|
def role_from_username username
|
40
63
|
role(role_name_from_username username)
|
41
64
|
end
|
42
|
-
|
65
|
+
|
43
66
|
def role_name_from_username username = self.username
|
44
67
|
tokens = username.split('/')
|
45
68
|
if tokens.size == 1
|
@@ -48,5 +71,14 @@ module Conjur
|
|
48
71
|
[ tokens[0], tokens[1..-1].join('/') ].join(':')
|
49
72
|
end
|
50
73
|
end
|
74
|
+
|
75
|
+
private
|
76
|
+
def normalize_roleid role
|
77
|
+
case role
|
78
|
+
when String then role
|
79
|
+
when Role then role.roleid
|
80
|
+
else raise "Can't normalize #{role}@#{role.class}"
|
81
|
+
end
|
82
|
+
end
|
51
83
|
end
|
52
84
|
end
|
data/lib/conjur/graph.rb
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2015 Conjur Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
module Conjur
|
22
|
+
class Graph
|
23
|
+
|
24
|
+
include Enumerable
|
25
|
+
|
26
|
+
# @!attribute r edges
|
27
|
+
# @return [Array<Conjur::Graph::Edge>] the edges of this graph
|
28
|
+
attr_reader :edges
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
def initialize val
|
32
|
+
@edges = case val
|
33
|
+
when String then JSON.parse(val)['graph']
|
34
|
+
when Hash then val['graph']
|
35
|
+
when Array then val
|
36
|
+
when Graph then val.edges
|
37
|
+
else raise ArgumentError, "don't know how to turn #{val}:#{val.class} into a Graph"
|
38
|
+
end.map{|pair| Edge.new(*pair) }.freeze
|
39
|
+
@next_node_id = 0
|
40
|
+
@node_ids = Hash.new{ |h,k| h[k] = next_node_id }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Enumerates the edges of this graph.
|
44
|
+
# @yieldparam [Conjur::Graph::Edge] each edge of the graph
|
45
|
+
# @return edge [Conjur::Graph] this graph
|
46
|
+
def each_edge
|
47
|
+
return enum_for(__method__) unless block_given?
|
48
|
+
edges.each{|e| yield e}
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
alias each each_edge
|
53
|
+
|
54
|
+
# Enumerates the vertices (roles) of this graph
|
55
|
+
# @yieldparam vertex [Conjur::Role] each vertex in this graph
|
56
|
+
# @return [Conjur::Graph] this graph
|
57
|
+
def each_vertex
|
58
|
+
return enum_for(__method__) unless block_given?
|
59
|
+
vertices.each{|v| yield v}
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def to_json short = false
|
64
|
+
as_json(short).to_json
|
65
|
+
end
|
66
|
+
|
67
|
+
def as_json short = false
|
68
|
+
edges = self.edges.map{|e| e.as_json(short)}
|
69
|
+
short ? edges : {'graph' => edges}
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param [String, NilClass] name to assign to the graph. Usually this can be omitted unless you
|
73
|
+
# are writing multiple graphs to a single file. Must be in the ID format specified by
|
74
|
+
# http://www.graphviz.org/content/dot-language
|
75
|
+
#
|
76
|
+
# @return [String] the dot format (used by graphvis, among others) representation of this graph.
|
77
|
+
def to_dot name = nil
|
78
|
+
dot = "digraph #{name || ''} {"
|
79
|
+
vertices.each do |v|
|
80
|
+
dot << "\n\t" << dot_node(v)
|
81
|
+
end
|
82
|
+
edges.each do |e|
|
83
|
+
dot << "\n\t" << dot_edge(e)
|
84
|
+
end
|
85
|
+
dot << "\n}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def vertices
|
89
|
+
@vertices ||= edges.inject([]) {|a, pair| a.concat pair.to_a }.uniq
|
90
|
+
end
|
91
|
+
|
92
|
+
alias roles vertices
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def node_id_for role
|
97
|
+
role = role.id if role.respond_to?(:id)
|
98
|
+
@node_ids[role]
|
99
|
+
end
|
100
|
+
|
101
|
+
def next_node_id
|
102
|
+
id = @next_node_id
|
103
|
+
@next_node_id += 1
|
104
|
+
"node_#{id}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def node_label_for role
|
108
|
+
role = role.id if role.respond_to? :id
|
109
|
+
if single_account?
|
110
|
+
role = role.split(':', 2).last
|
111
|
+
if single_kind?
|
112
|
+
role = role.split(':', 2).last
|
113
|
+
end
|
114
|
+
end
|
115
|
+
role
|
116
|
+
end
|
117
|
+
|
118
|
+
def single_account?
|
119
|
+
if @single_account.nil?
|
120
|
+
@single_account = roles.map do |role|
|
121
|
+
role = role.id if role.respond_to?(:id)
|
122
|
+
role.split(':').first
|
123
|
+
end.uniq.size == 1
|
124
|
+
end
|
125
|
+
@single_account
|
126
|
+
end
|
127
|
+
|
128
|
+
def single_kind?
|
129
|
+
if @single_kind.nil?
|
130
|
+
return @single_kind = false unless single_account?
|
131
|
+
@single_kind = roles.map do |role|
|
132
|
+
role = role.id if role.respond_to?(:id)
|
133
|
+
role.split(':')[1]
|
134
|
+
end.uniq.size == 1
|
135
|
+
end
|
136
|
+
@single_kind
|
137
|
+
end
|
138
|
+
|
139
|
+
def dot_node v
|
140
|
+
id = node_id_for v
|
141
|
+
label = node_label_for v
|
142
|
+
"#{id} [label=\"#{label}\"]"
|
143
|
+
end
|
144
|
+
|
145
|
+
def dot_edge e
|
146
|
+
parent_id = node_id_for(e.parent)
|
147
|
+
child_id = node_id_for(e.child)
|
148
|
+
"#{parent_id} -> #{child_id}"
|
149
|
+
end
|
150
|
+
|
151
|
+
# an edge consisting of a parent & child, both of which are Conjur::Role instances
|
152
|
+
class Edge
|
153
|
+
attr_reader :parent
|
154
|
+
attr_reader :child
|
155
|
+
|
156
|
+
def initialize parent, child
|
157
|
+
@parent = parent
|
158
|
+
@child = child
|
159
|
+
end
|
160
|
+
|
161
|
+
def to_json short = false
|
162
|
+
as_json(short).to_json
|
163
|
+
end
|
164
|
+
|
165
|
+
def as_json short = false
|
166
|
+
short ? to_a : to_h
|
167
|
+
end
|
168
|
+
|
169
|
+
def to_h
|
170
|
+
# return string keys to make testing less brittle
|
171
|
+
{'parent' => @parent, 'child' => @child}
|
172
|
+
end
|
173
|
+
|
174
|
+
def to_a
|
175
|
+
[@parent, @child]
|
176
|
+
end
|
177
|
+
|
178
|
+
def to_s
|
179
|
+
"<Edge #{parent.id} --> #{child.id}>"
|
180
|
+
end
|
181
|
+
|
182
|
+
def hash
|
183
|
+
@hash ||= to_a.map(&:to_s).hash
|
184
|
+
end
|
185
|
+
|
186
|
+
def == other
|
187
|
+
other.kind_of?(self.class) and other.parent == parent and other.child == child
|
188
|
+
end
|
189
|
+
|
190
|
+
alias eql? ==
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
data/lib/conjur-api/version.rb
CHANGED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Conjur::Graph do
|
4
|
+
let(:edges){ [
|
5
|
+
[ 'a', 'b' ],
|
6
|
+
[ 'a', 'c'],
|
7
|
+
[ 'c', 'd'],
|
8
|
+
['b', 'd'],
|
9
|
+
[ 'd', 'e'],
|
10
|
+
# make two connected components
|
11
|
+
['o', 'q'],
|
12
|
+
['x', 'o']
|
13
|
+
]}
|
14
|
+
let(:hash_graph){ {'graph' => edges} }
|
15
|
+
let(:json_graph){ hash_graph.to_json }
|
16
|
+
let(:short_json_graph){ edges.to_json }
|
17
|
+
let(:long_edges){ edges.map{|e| {'parent' => e[0], 'child' => e[1]}} }
|
18
|
+
let(:long_hash_graph){ {'graph' => long_edges} }
|
19
|
+
let(:long_json_graph){ long_hash_graph.to_json }
|
20
|
+
let(:edge_objects){ edges.map{|e| Conjur::Graph::Edge.new(*e) }}
|
21
|
+
|
22
|
+
describe "json methods" do
|
23
|
+
subject{described_class.new edges}
|
24
|
+
it "converts to long json correctly" do
|
25
|
+
expect(subject.to_json).to eq(long_json_graph)
|
26
|
+
expect(subject.as_json).to eq(long_hash_graph)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "converts to short json correctly" do
|
30
|
+
expect(subject.to_json(true)).to eq(short_json_graph)
|
31
|
+
expect(subject.as_json(true)).to eq(edges)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#vertices" do
|
36
|
+
subject{Conjur::Graph.new(edges).vertices.to_set}
|
37
|
+
it "contains all unique members of edges" do
|
38
|
+
expect(subject.to_set).to eq(edges.flatten.uniq.to_set)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#to_dot" do
|
43
|
+
let(:name){ nil }
|
44
|
+
subject{ Conjur::Graph.new(edges).to_dot(name) }
|
45
|
+
before do
|
46
|
+
File.write('/tmp/conjur-graph-spec.dot', subject)
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:role_to_node_id) do
|
50
|
+
{}.tap do |h|
|
51
|
+
edges.flatten.uniq.each do |v|
|
52
|
+
expect(subject =~ /^\s*([a-z][0-9a-z_\-]*)\s*\[label\="(#{v})"\]/i).to be_truthy
|
53
|
+
h[$2] = $1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when given a name" do
|
59
|
+
let(:name){ 'foo' }
|
60
|
+
it "names the digraph" do
|
61
|
+
expect(subject).to match(/\A\s*digraph\s+foo\s*\{/)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "defines all the vertices in the graph" do
|
66
|
+
edges.flatten.uniq.each do |v|
|
67
|
+
expect(subject).to match(/^\s*[a-z][0-9a-z_\-]*\s*\[label\="#{v}"\]/i)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it "defines all the edges in the graph" do
|
72
|
+
edges.each do |e|
|
73
|
+
parent_id = role_to_node_id[e[0]]
|
74
|
+
child_id = role_to_node_id[e[1]]
|
75
|
+
expect(subject).to match(/^\s*#{parent_id}\s*\->\s*#{child_id}/)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "Graph.new" do
|
81
|
+
let(:arg){ edges }
|
82
|
+
subject{ described_class.new arg }
|
83
|
+
def self.it_accepts_the_argument
|
84
|
+
it "accepts the argument" do
|
85
|
+
expect(subject.edges.to_set).to eq(edge_objects.to_set)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
describe "given an array of edges" do
|
89
|
+
it_accepts_the_argument
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "given a hash of {'graph' => <array of edges>}" do
|
93
|
+
let(:arg){ hash_graph }
|
94
|
+
it_accepts_the_argument
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "given a JSON string" do
|
98
|
+
let(:arg){ json_graph }
|
99
|
+
it_accepts_the_argument
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/spec/api/roles_spec.rb
CHANGED
@@ -1,14 +1,85 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Conjur::API, api: :dummy do
|
4
|
+
subject { api }
|
5
|
+
|
6
|
+
describe 'role_graph' do
|
7
|
+
let(:roles){ [ 'acct:user:alice', 'acct:user:bob', 'acct:user:eve' ] }
|
8
|
+
let(:options){ {} }
|
9
|
+
let(:current_role){ 'some-role' }
|
10
|
+
let(:graph){
|
11
|
+
[
|
12
|
+
[ 'acct:user:alice', 'acct:user:eve' ],
|
13
|
+
[ 'acct:user:bob', 'acct:user:eve']
|
14
|
+
]
|
15
|
+
}
|
16
|
+
let(:response){ {
|
17
|
+
graph: graph
|
18
|
+
}.to_json }
|
19
|
+
|
20
|
+
let(:graph_edges){
|
21
|
+
graph.map{|e| Conjur::Graph::Edge.new *e}
|
22
|
+
}
|
23
|
+
|
24
|
+
before do
|
25
|
+
allow(api).to receive(:current_role).and_return current_role
|
26
|
+
end
|
27
|
+
|
28
|
+
subject{ api.role_graph roles, options }
|
29
|
+
|
30
|
+
def role_graph_url_for roles, options, current_role
|
31
|
+
qs = options.reverse_merge(ancestors: true, descendants: true)
|
32
|
+
.merge(from_role: current_role, roles: roles).slice(:from_role, :ancestors, :descendants, :roles).to_query
|
33
|
+
"http://authz.example.com/#{account}/roles?#{qs}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def expect_request_with_params params={}
|
37
|
+
expect(RestClient::Request).to receive(:execute)
|
38
|
+
.with(headers: credentials[:headers], method: :get, url: role_graph_url_for(roles, options, current_role))
|
39
|
+
.and_return(response)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "gets /roles with the correct params" do
|
43
|
+
expect_request_with_params ancestors: true, descendants: true, from_role: current_role
|
44
|
+
subject
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when options[:ancestors] and options[:descendants] are false" do
|
48
|
+
let(:options){ { ancestors: false, descendants: false } }
|
49
|
+
it "gets /roles with the correct params" do
|
50
|
+
expect_request_with_params ancestors: false, descendants: false, from_role: current_role
|
51
|
+
subject
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "when given options[:as_role] = 'foo'" do
|
56
|
+
it "sets the from_role param to 'foo'" do
|
57
|
+
expect_request_with_params from_role: 'foo'
|
58
|
+
subject
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "the result" do
|
63
|
+
it "is a Conjur::Graph" do
|
64
|
+
expect_request_with_params
|
65
|
+
expect(subject).to be_kind_of(Conjur::Graph)
|
66
|
+
end
|
67
|
+
it "has the right edges" do
|
68
|
+
expect_request_with_params
|
69
|
+
expect(subject.edges.to_set).to eq(graph_edges.to_set)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
4
75
|
describe '#role_name_from_username' do
|
5
|
-
|
76
|
+
|
6
77
|
before {
|
7
78
|
allow(api).to receive(:username) { username }
|
8
79
|
}
|
9
80
|
context "username is" do
|
10
|
-
[
|
11
|
-
[ 'the-user', 'user:the-user' ],
|
81
|
+
[
|
82
|
+
[ 'the-user', 'user:the-user' ],
|
12
83
|
[ 'host/the-host', 'host:the-host' ],
|
13
84
|
[ 'host/a/quite/long/host/name', 'host:a/quite/long/host/name' ],
|
14
85
|
[ 'newkind/host/name', 'newkind:host/name' ],
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: conjur-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rafal Rzepecki
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-01-
|
12
|
+
date: 2015-01-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rest-client
|
@@ -232,6 +232,7 @@ files:
|
|
232
232
|
- lib/conjur/escape.rb
|
233
233
|
- lib/conjur/event_source.rb
|
234
234
|
- lib/conjur/exists.rb
|
235
|
+
- lib/conjur/graph.rb
|
235
236
|
- lib/conjur/group.rb
|
236
237
|
- lib/conjur/has_attributes.rb
|
237
238
|
- lib/conjur/has_id.rb
|
@@ -254,6 +255,7 @@ files:
|
|
254
255
|
- lib/conjur/variable.rb
|
255
256
|
- reqspeed.rb
|
256
257
|
- spec/api/authn_spec.rb
|
258
|
+
- spec/api/graph_spec.rb
|
257
259
|
- spec/api/groups_spec.rb
|
258
260
|
- spec/api/hosts_spec.rb
|
259
261
|
- spec/api/layer_spec.rb
|
@@ -317,6 +319,7 @@ test_files:
|
|
317
319
|
- features/ping_as_server.feature
|
318
320
|
- features/ping_as_user.feature
|
319
321
|
- spec/api/authn_spec.rb
|
322
|
+
- spec/api/graph_spec.rb
|
320
323
|
- spec/api/groups_spec.rb
|
321
324
|
- spec/api/hosts_spec.rb
|
322
325
|
- spec/api/layer_spec.rb
|