conjur-api 2.4.0 → 2.5.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.
- data/.gitignore +2 -0
- data/LICENSE +1 -1
- data/Rakefile +3 -1
- data/conjur-api.gemspec +3 -1
- data/lib/conjur-api/version.rb +1 -1
- data/lib/conjur/has_id.rb +1 -1
- data/lib/conjur/log.rb +6 -26
- data/lib/conjur/resource.rb +10 -1
- data/manual/asset/about.markdown +12 -0
- data/manual/asset/members.add.markdown +52 -0
- data/manual/asset/show.markdown +50 -0
- data/manual/group/about.markdown +6 -0
- data/manual/group/create.markdown +20 -0
- data/manual/host/about.markdown +23 -0
- data/manual/host/create.markdown +34 -0
- data/manual/host/enroll.markdown +21 -0
- data/manual/resource/about.markdown +11 -0
- data/manual/resource/create.markdown +29 -0
- data/manual/resource/deny.markdown +23 -0
- data/manual/resource/permit.markdown +35 -0
- data/manual/role/about.markdown +10 -0
- data/manual/role/members.markdown +40 -0
- data/manual/role/memberships.markdown +26 -0
- data/spec/api/authn_spec.rb +49 -0
- data/spec/api/groups_spec.rb +24 -0
- data/spec/api/hosts_spec.rb +29 -0
- data/spec/api/resources_spec.rb +19 -0
- data/spec/api/secrets_spec.rb +16 -0
- data/spec/api/users_spec.rb +16 -0
- data/spec/api/variables_spec.rb +14 -0
- data/spec/cas_rest_client.rb +17 -0
- data/spec/io_helper.rb +18 -0
- data/spec/lib/build_from_response_spec.rb +49 -0
- data/spec/lib/host_spec.rb +12 -8
- data/spec/lib/log_source_spec.rb +13 -0
- data/spec/lib/log_spec.rb +42 -0
- data/spec/lib/resource_spec.rb +98 -5
- data/spec/lib/role_grant_spec.rb +12 -0
- data/spec/lib/role_spec.rb +83 -3
- data/spec/lib/standard_methods_spec.rb +66 -0
- data/spec/lib/user_spec.rb +2 -1
- data/spec/spec_helper.rb +27 -0
- data/spec/standard_methods_helper.rb +30 -0
- data/spec/variable_spec.rb +41 -0
- metadata +71 -8
- data/.rvmrc +0 -1
@@ -0,0 +1,10 @@
|
|
1
|
+
Role
|
2
|
+
====
|
3
|
+
|
4
|
+
A role is a target to which permissions are assigned. Permitting an action on a resource by a role enables the role to perform
|
5
|
+
the specified action.
|
6
|
+
|
7
|
+
In addition, roles can be "granted to" other roles. When role "A" is granted to role "B", role "B" gains the ability to perform
|
8
|
+
all the actions permitted to "A". In addition, a role can be granted with "grant option". When role "A" is granted to role "B" with
|
9
|
+
grant option, role "B" can in turn grant role "A" to other roles.
|
10
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
Role#members
|
2
|
+
============
|
3
|
+
|
4
|
+
Lists the roles that have been the recipient of a role grant. The creator of the role is always a role member.
|
5
|
+
|
6
|
+
If role "A" is created by user "U" and then granted to roles "B" and "C", then the members of role "A" are [ "U", "B", "C" ].
|
7
|
+
|
8
|
+
Examples
|
9
|
+
--------
|
10
|
+
|
11
|
+
### Command Line
|
12
|
+
|
13
|
+
#### Add a role member and list the members
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ conjur role:create test:`conjur id:create`
|
17
|
+
Created https://authz-v3-conjur.herokuapp.com/sandbox/roles/test/2y1300
|
18
|
+
$ conjur role:members test:2y1300
|
19
|
+
[
|
20
|
+
"sandbox:user:kgilpin"
|
21
|
+
]
|
22
|
+
$ conjur host:create
|
23
|
+
{
|
24
|
+
"id": "w43699",
|
25
|
+
<snip>
|
26
|
+
}
|
27
|
+
$ conjur role:grant_to test:2y1300 host:w43699
|
28
|
+
Role granted
|
29
|
+
$ conjur role:members test:2y1300
|
30
|
+
[
|
31
|
+
"sandbox:user:kgilpin",
|
32
|
+
"sandbox:host:w43699"
|
33
|
+
]
|
34
|
+
```
|
35
|
+
|
36
|
+
#### The role argument is optional and defaults to the current role
|
37
|
+
|
38
|
+
```bash
|
39
|
+
$ conjur role:members
|
40
|
+
```
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Role#memberships
|
2
|
+
================
|
3
|
+
|
4
|
+
List the roles that a role has been granted, transitively.
|
5
|
+
|
6
|
+
If role "A" is granted to role "B" which is granted to role "C", then the memberships of role "C" are [ "C", "B", "A" ].
|
7
|
+
|
8
|
+
Examples
|
9
|
+
--------
|
10
|
+
|
11
|
+
### Command Line
|
12
|
+
|
13
|
+
```bash
|
14
|
+
$ ns=`conjur id:create`
|
15
|
+
$ conjur role:create test:$ns/A
|
16
|
+
$ conjur role:create test:$ns/B
|
17
|
+
$ conjur role:create test:$ns/C
|
18
|
+
$ conjur role:grant_to test:$ns/A test:$ns/B
|
19
|
+
$ conjur role:grant_to test:$ns/B test:$ns/C
|
20
|
+
$ conjur role:memberships test:$ns/C
|
21
|
+
[
|
22
|
+
"sandbox:test:dy7mg0/C",
|
23
|
+
"sandbox:test:dy7mg0/B",
|
24
|
+
"sandbox:test:dy7mg0/A"
|
25
|
+
]
|
26
|
+
```
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cas_rest_client'
|
3
|
+
|
4
|
+
describe Conjur::API do
|
5
|
+
let(:host) { 'http://authn.example.com' }
|
6
|
+
let(:user) { 'kmitnick' }
|
7
|
+
let(:password) { 'sikret' }
|
8
|
+
|
9
|
+
before do
|
10
|
+
Conjur::Authn::API.stub host: host
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "::login" do
|
14
|
+
it "gets /users/login" do
|
15
|
+
RestClient::Request.should_receive(:execute).with(
|
16
|
+
method: :get, url: "http://authn.example.com/users/login", user: user,
|
17
|
+
password: password, headers: {}
|
18
|
+
).and_return(response = double)
|
19
|
+
Conjur::API::login(user, password).should == response
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "::login_cas" do
|
24
|
+
let(:response) { "response body" }
|
25
|
+
let(:cas_uri) { 'http://cas.example.com' }
|
26
|
+
|
27
|
+
it "uses CasRestClient to authenticate" do
|
28
|
+
stub_const 'CasRestClient', MockCasRestClient.new(double("response", body: response))
|
29
|
+
Conjur::API.login_cas(user, password, cas_uri).should == response
|
30
|
+
CasRestClient.options.should == {
|
31
|
+
username: user,
|
32
|
+
password: password,
|
33
|
+
uri: "http://cas.example.com/v1/tickets",
|
34
|
+
use_cookies: false
|
35
|
+
}
|
36
|
+
CasRestClient.url.should == "http://authn.example.com/users/login"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "::authenticate" do
|
41
|
+
it "posts the password and dejsons the result" do
|
42
|
+
RestClient::Request.should_receive(:execute).with(
|
43
|
+
method: :post, url: "http://authn.example.com/users/#{user}/authenticate",
|
44
|
+
payload: password, headers: { content_type: 'text/plain' }
|
45
|
+
).and_return '{ "response": "foo"}'
|
46
|
+
Conjur::API.authenticate(user, password).should == { 'response' => 'foo' }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'standard_methods_helper'
|
3
|
+
|
4
|
+
describe Conjur::API, api: :dummy do
|
5
|
+
subject { api }
|
6
|
+
|
7
|
+
describe '#groups' do
|
8
|
+
it_should_behave_like 'standard_list with', :group, :options do
|
9
|
+
let(:invoke) { subject.groups :options }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#create_group' do
|
14
|
+
it_should_behave_like 'standard_create with', :group, :id, :options do
|
15
|
+
let(:invoke) { subject.create_group :id, :options }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#group' do
|
20
|
+
it_should_behave_like 'standard_show with', :group, :id do
|
21
|
+
let(:invoke) { subject.group :id }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'standard_methods_helper'
|
3
|
+
|
4
|
+
describe Conjur::API, api: :dummy do
|
5
|
+
describe '::enroll_host' do
|
6
|
+
it "uses Net::HTTP to get something" do
|
7
|
+
response = double "response",
|
8
|
+
code: '200', body: 'foobar'
|
9
|
+
response.stub(:[]).with('Content-Type').and_return 'text/whatever'
|
10
|
+
|
11
|
+
url = URI.parse "http://example.com"
|
12
|
+
Net::HTTP.stub(:get_response).with(url).and_return response
|
13
|
+
|
14
|
+
Conjur::API.enroll_host("http://example.com").should == ['text/whatever', 'foobar']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#create_host' do
|
19
|
+
it_should_behave_like "standard_create with", :host, nil, :options do
|
20
|
+
let(:invoke) { subject.create_host :options }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#host' do
|
25
|
+
it_should_behave_like "standard_show with", :host, :id do
|
26
|
+
let(:invoke) { subject.host :id }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Conjur::API, api: :dummy do
|
4
|
+
describe '#create_resource' do
|
5
|
+
it "passes to resource#create" do
|
6
|
+
api.stub(:resource).with(:id).and_return(resource = double)
|
7
|
+
resource.should_receive :create
|
8
|
+
|
9
|
+
api.create_resource(:id).should == resource
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#resource' do
|
14
|
+
it "builds a path and creates a resource from it" do
|
15
|
+
res = api.resource "some-account:a-kind:the-id"
|
16
|
+
res.url.should == "#{authz_host}/some-account/resources/a-kind/the-id"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'standard_methods_helper'
|
3
|
+
|
4
|
+
describe Conjur::API, api: :dummy do
|
5
|
+
describe '#create_secret' do
|
6
|
+
it_should_behave_like 'standard_create with', :secret, nil, value: 'val' do
|
7
|
+
let(:invoke) { api.create_secret 'val' }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#secret' do
|
12
|
+
it_should_behave_like 'standard_show with', :secret, :id do
|
13
|
+
let(:invoke) { api.secret :id }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'standard_methods_helper'
|
3
|
+
|
4
|
+
describe Conjur::API, api: :dummy do
|
5
|
+
describe '#create_user' do
|
6
|
+
it_should_behave_like 'standard_create with', :user, nil, login: 'login', other: true do
|
7
|
+
let(:invoke) { api.create_user 'login', other: true }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#user' do
|
12
|
+
it_should_behave_like 'standard_show with', :user, :login do
|
13
|
+
let(:invoke) { api.user :login }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'standard_methods_helper'
|
3
|
+
|
4
|
+
describe Conjur::API, api: :dummy do
|
5
|
+
describe '#create_variable' do
|
6
|
+
let(:invoke) { api.create_variable :type, :kind, other: true }
|
7
|
+
it_should_behave_like 'standard_create with', :variable, nil, mime_type: :type, kind: :kind, other: true
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#variable' do
|
11
|
+
let(:invoke) { api.variable :id }
|
12
|
+
it_should_behave_like 'standard_show with', :variable, :id
|
13
|
+
end
|
14
|
+
end
|
data/spec/io_helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Conjur::BuildFromResponse do
|
4
|
+
describe "::build_from_response", logging: :temp do
|
5
|
+
let(:location) { "http://example.com" }
|
6
|
+
let(:attrs) {{ 'some' => 'foo', 'other' => 'bar' }}
|
7
|
+
let(:response) do
|
8
|
+
double "response", headers: { location: location }, body: attrs.to_json
|
9
|
+
end
|
10
|
+
subject { double "class", name: 'some' }
|
11
|
+
let(:constructed) { double "object" }
|
12
|
+
let(:credentials) { "whatever" }
|
13
|
+
|
14
|
+
before do
|
15
|
+
subject.extend Conjur::BuildFromResponse
|
16
|
+
subject.should_receive(:new).with(location, credentials).and_return constructed
|
17
|
+
constructed.should_receive(:attributes=).with attrs
|
18
|
+
|
19
|
+
constructed.extend Conjur::LogSource
|
20
|
+
constructed.stub username: 'whatever'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "passes the location credentials and attributes" do
|
24
|
+
subject.build_from_response response, credentials
|
25
|
+
end
|
26
|
+
|
27
|
+
context "with a resource(-ish) class" do
|
28
|
+
before do
|
29
|
+
constructed.stub resource_kind: 'chunky', resource_id: 'bacon'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "logs creation correctly" do
|
33
|
+
subject.build_from_response response, credentials
|
34
|
+
log.should =~ /Created chunky bacon/
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a id(-ish) class" do
|
39
|
+
before do
|
40
|
+
constructed.stub id: 'bacon'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "logs creation correctly" do
|
44
|
+
subject.build_from_response response, credentials
|
45
|
+
log.should =~ /Created some bacon/
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/spec/lib/host_spec.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Conjur::Host do
|
4
|
-
|
5
|
-
let(:api_key) { 'the-api-key' }
|
6
|
-
let(:credentials) { { user: login, password: api_key } }
|
7
|
-
let(:account) { 'test-account' }
|
3
|
+
describe Conjur::Host, api: :dummy do
|
4
|
+
subject { Conjur::Host.new 'http://example.com/hosts/my/hostname', nil }
|
8
5
|
|
9
|
-
|
6
|
+
its(:resource) { should be }
|
7
|
+
its(:login) { should == 'host/my/hostname' }
|
10
8
|
|
11
|
-
|
9
|
+
let(:api_key) { 'theapikey' }
|
10
|
+
before { subject.attributes = { 'api_key' => api_key } }
|
11
|
+
its(:api_key) { should == api_key }
|
12
12
|
|
13
|
-
|
13
|
+
it "fetches enrollment_url" do
|
14
|
+
stub_request(:head, "http://example.com/hosts/my/hostname/enrollment_url").
|
15
|
+
to_return(:status => 200, :headers => {location: 'foo'})
|
16
|
+
subject.enrollment_url.should == 'foo'
|
17
|
+
end
|
14
18
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'io_helper'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
describe Conjur do
|
6
|
+
describe '::log=' do
|
7
|
+
before { @old_log = Conjur.log }
|
8
|
+
let(:log) { double 'log' }
|
9
|
+
it "creates the log with given type and makes it available" do
|
10
|
+
Conjur.stub(:create_log).with(:param).and_return log
|
11
|
+
Conjur::log = :param
|
12
|
+
Conjur::log.should == log
|
13
|
+
end
|
14
|
+
after { Conjur.class_variable_set :@@log, @old_log }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '::create_log' do
|
18
|
+
let(:log) { Conjur::create_log param }
|
19
|
+
context "with 'stdout'" do
|
20
|
+
let(:param) { 'stdout' }
|
21
|
+
it "creates something which writes to STDOUT" do
|
22
|
+
STDOUT.grab { log << "foo" }.should == 'foo'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "with 'stderr'" do
|
27
|
+
let(:param) { 'stderr' }
|
28
|
+
it "creates something which writes to STDERR" do
|
29
|
+
STDERR.grab { log << "foo" }.should == 'foo'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "with a filename" do
|
34
|
+
let(:tempfile) { Tempfile.new 'spec' }
|
35
|
+
let(:param) { tempfile.path }
|
36
|
+
it "creates something which writes to the file" do
|
37
|
+
log << "foo"
|
38
|
+
tempfile.read.should == "foo"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/spec/lib/resource_spec.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Conjur::Resource do
|
3
|
+
describe Conjur::Resource, api: :dummy, logging: :temp do
|
4
4
|
let(:account) { "the-account" }
|
5
5
|
let(:uuid) { "ddd1f59a-494d-48fb-b045-0374c4a6eef9" }
|
6
|
-
|
6
|
+
|
7
7
|
context "identifier" do
|
8
8
|
include Conjur::Escape
|
9
9
|
let(:resource) { Conjur::Resource.new("#{Conjur::Authz::API.host}/#{account}/resources/#{kind}/#{path_escape identifier}") }
|
10
|
-
|
10
|
+
|
11
11
|
context "Object with an #id" do
|
12
12
|
let(:kind) { "host" }
|
13
13
|
let(:identifier) do
|
@@ -17,7 +17,7 @@ describe Conjur::Resource do
|
|
17
17
|
resource.identifier.should == "foobar"
|
18
18
|
end
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
[ [ "foo", "bar/baz" ], [ "f:o", "bar" ], [ "@f", "bar.baz" ], [ "@f", "bar baz" ], [ "@f", "@:bar/baz" ] ].each do |p|
|
22
22
|
context "of /#{p[0]}/#{p[1]}" do
|
23
23
|
let(:kind) { p[0] }
|
@@ -33,4 +33,97 @@ describe Conjur::Resource do
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
|
+
let(:uri) { "#{authz_host}/some-account/resources/the-kind/resource-id" }
|
38
|
+
subject { Conjur::Resource.new uri }
|
39
|
+
|
40
|
+
describe '#create' do
|
41
|
+
it "simply puts" do
|
42
|
+
RestClient::Request.should_receive(:execute).with(
|
43
|
+
method: :put,
|
44
|
+
url: uri,
|
45
|
+
payload: {},
|
46
|
+
headers: {}
|
47
|
+
).and_return "new resource"
|
48
|
+
subject.create.should == "new resource"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#permitted_roles' do
|
53
|
+
it 'gets the list from /roles/allowed_to' do
|
54
|
+
RestClient::Request.should_receive(:execute).with(
|
55
|
+
method: :get,
|
56
|
+
url: "http://authz.example.com/some-account/roles/allowed_to/nuke/the-kind/resource-id",
|
57
|
+
headers: {}
|
58
|
+
).and_return '["foo", "bar"]'
|
59
|
+
|
60
|
+
subject.permitted_roles("nuke").should == ['foo', 'bar']
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#give_to' do
|
65
|
+
it "puts the owner field" do
|
66
|
+
RestClient::Request.should_receive(:execute).with(
|
67
|
+
method: :put,
|
68
|
+
url: uri,
|
69
|
+
payload: {owner: 'new-owner' },
|
70
|
+
headers: {}
|
71
|
+
)
|
72
|
+
|
73
|
+
subject.give_to 'new-owner'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#delete' do
|
78
|
+
it 'simply deletes' do
|
79
|
+
RestClient::Request.should_receive(:execute).with(
|
80
|
+
method: :delete,
|
81
|
+
url: uri,
|
82
|
+
headers: {}
|
83
|
+
)
|
84
|
+
|
85
|
+
subject.delete
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#permit' do
|
90
|
+
it 'posts permit for every privilege' do
|
91
|
+
privileges = [:nuke, :fry]
|
92
|
+
privileges.each do |p|
|
93
|
+
RestClient::Request.should_receive(:execute).with(
|
94
|
+
method: :post,
|
95
|
+
url: uri + "/?permit&privilege=#{p}&role=dr-strangelove",
|
96
|
+
headers: {},
|
97
|
+
payload: {}
|
98
|
+
)
|
99
|
+
end
|
100
|
+
subject.permit privileges, "dr-strangelove"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#deny' do
|
105
|
+
it 'posts deny for every privilege' do
|
106
|
+
privileges = [:nuke, :fry]
|
107
|
+
privileges.each do |p|
|
108
|
+
RestClient::Request.should_receive(:execute).with(
|
109
|
+
method: :post,
|
110
|
+
url: uri + "/?deny&privilege=#{p}&role=james-bond",
|
111
|
+
headers: {},
|
112
|
+
payload: {}
|
113
|
+
)
|
114
|
+
end
|
115
|
+
subject.deny privileges, "james-bond"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '#permitted?' do
|
120
|
+
it 'gets the ?permitted? action' do
|
121
|
+
RestClient::Request.should_receive(:execute).with(
|
122
|
+
method: :get,
|
123
|
+
url: uri + "/?check&privilege=fry",
|
124
|
+
headers: {}
|
125
|
+
)
|
126
|
+
subject.permitted? 'fry'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|