lita-activedirectory 1.0.0 → 1.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 +4 -4
- data/README.md +13 -1
- data/Rakefile +1 -1
- data/lib/lita/handlers/activedirectory.rb +46 -0
- data/lib/utils/cratususer.rb +32 -2
- data/lita-activedirectory.gemspec +3 -3
- data/locales/en.yml +14 -0
- data/spec/lita/handlers/activedirectory_spec.rb +96 -13
- data/spec/utils/cratususer_spec.rb +75 -0
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e234fdbf176937e5e6a4ad5bf906999bebcef275
|
4
|
+
data.tar.gz: 86900dd8e120905eb003469ead17984d458221d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98f81d6e24790574f06bbcd2f625dacba7f1a4078e3a2aec1b6f2f458677ae1556fb19668176209a8d27fbbc1d625748a9b94973db71ad36d6169218efcc94b7
|
7
|
+
data.tar.gz: 52fd9a7603db8f5d968c2b3e544a1ebf7900bf702544aaf5626f8a9f0c4f402e69d67692dc7ff60b79ae887908cd6607781863d499eb7cb4659d2a6e12030635
|
data/README.md
CHANGED
@@ -30,7 +30,7 @@ gem "lita-activedirectory"
|
|
30
30
|
|
31
31
|
Requires membership in `ad_admins` authorization group.
|
32
32
|
|
33
|
-
The user account specified in `config.handlers.activedirectory.username` must have permission to write the lockouttime attribute for unlocking to succeed. We leave it up to you to secure this account accordingly.
|
33
|
+
The user account specified in `config.handlers.activedirectory.username` must have permission to write the lockouttime attribute for unlocking to succeed. We leave it up to you to secure this account accordingly.
|
34
34
|
|
35
35
|
### List a User's Group Memberships
|
36
36
|
`<username> groups>`
|
@@ -38,4 +38,16 @@ The user account specified in `config.handlers.activedirectory.username` must ha
|
|
38
38
|
### List a Group's Members
|
39
39
|
`group <groupname> members`
|
40
40
|
|
41
|
+
### Add a User to a Group
|
42
|
+
`add <username> to <groupname>`
|
41
43
|
|
44
|
+
Requires membership in `ad_admins` authorization group.
|
45
|
+
|
46
|
+
The user account specified in `config.handlers.activedirectory.username` must have permission to write the member attribute on groups for the membership change to succeed. We leave it up to you to secure this account accordingly.
|
47
|
+
|
48
|
+
### Remove a User from a Group
|
49
|
+
`remove <username> from <groupname>`
|
50
|
+
|
51
|
+
Requires membership in `ad_admins` authorization group.
|
52
|
+
|
53
|
+
The user account specified in `config.handlers.activedirectory.username` must have permission to write the member attribute on groups for the membership change to succeed. We leave it up to you to secure this account accordingly.
|
data/Rakefile
CHANGED
@@ -39,6 +39,22 @@ module Lita
|
|
39
39
|
help: { t('help.group_members.syntax') => t('help.group_members.desc') }
|
40
40
|
)
|
41
41
|
|
42
|
+
route(
|
43
|
+
/^remove\s+(\S+)\s+from\s+(\S+)$/i,
|
44
|
+
:remove_group_member,
|
45
|
+
command: true,
|
46
|
+
restrict_to: :ad_admins,
|
47
|
+
help: { t('help.remove_member.syntax') => t('help.remove_member.desc') }
|
48
|
+
)
|
49
|
+
|
50
|
+
route(
|
51
|
+
/^add\s+(\S+)\s+to\s+(\S+)$/i,
|
52
|
+
:add_group_member,
|
53
|
+
command: true,
|
54
|
+
restrict_to: :ad_admins,
|
55
|
+
help: { t('help.add_member.syntax') => t('help.add_member.desc') }
|
56
|
+
)
|
57
|
+
|
42
58
|
include ::Utils::Cratususer
|
43
59
|
|
44
60
|
def user_locked?(response)
|
@@ -84,6 +100,36 @@ module Lita
|
|
84
100
|
end
|
85
101
|
end
|
86
102
|
|
103
|
+
def add_group_member(response)
|
104
|
+
user = response.matches[0][0]
|
105
|
+
group = response.matches[0][1]
|
106
|
+
|
107
|
+
response.reply_with_mention(t('replies.add_member.working'))
|
108
|
+
result = add_user_to_group(user, group)
|
109
|
+
response.reply_with_mention(
|
110
|
+
if result.nil?
|
111
|
+
t('replies.add_member.error', user: user, group: group)
|
112
|
+
else
|
113
|
+
t('replies.add_member.success', user: user, group: group)
|
114
|
+
end
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def remove_group_member(response)
|
119
|
+
user = response.matches[0][0]
|
120
|
+
group = response.matches[0][1]
|
121
|
+
|
122
|
+
response.reply_with_mention(t('replies.remove_member.working'))
|
123
|
+
result = remove_user_from_group(user, group)
|
124
|
+
response.reply_with_mention(
|
125
|
+
if result.nil?
|
126
|
+
t('replies.remove_member.error', user: user, group: group)
|
127
|
+
else
|
128
|
+
t('replies.remove_member.success', user: user, group: group)
|
129
|
+
end
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
87
133
|
private
|
88
134
|
|
89
135
|
def handle_user_query(response, user, result)
|
data/lib/utils/cratususer.rb
CHANGED
@@ -49,9 +49,39 @@ module Utils
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
+
def add_user_to_group(username, groupname)
|
53
|
+
cratus_connect
|
54
|
+
begin
|
55
|
+
user = Cratus::User.new(username.to_s)
|
56
|
+
group = Cratus::Group.new(groupname.to_s)
|
57
|
+
raise 'InvalidUser' unless user
|
58
|
+
raise 'InvalidGroup' unless group
|
59
|
+
group.add_user(user)
|
60
|
+
rescue
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove_user_from_group(username, groupname)
|
66
|
+
cratus_connect
|
67
|
+
begin
|
68
|
+
user = Cratus::User.new(username.to_s)
|
69
|
+
group = Cratus::Group.new(groupname.to_s)
|
70
|
+
raise 'InvalidUser' unless user
|
71
|
+
raise 'InvalidGroup' unless group
|
72
|
+
group.remove_user(user)
|
73
|
+
rescue
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
52
78
|
def unlock_user(username)
|
53
|
-
|
54
|
-
|
79
|
+
cratus_connect
|
80
|
+
begin
|
81
|
+
Cratus::User.new(username.to_s).unlock
|
82
|
+
rescue
|
83
|
+
nil
|
84
|
+
end
|
55
85
|
end
|
56
86
|
end
|
57
87
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = 'lita-activedirectory'
|
3
|
-
spec.version = '1.
|
4
|
-
spec.authors = ['Daniel Schaaff']
|
3
|
+
spec.version = '1.1.0'
|
4
|
+
spec.authors = ['Daniel Schaaff', 'Jonathan Gnagy']
|
5
5
|
spec.email = ['dschaaff@knuedge.com']
|
6
6
|
spec.description = 'ldap/active directory instructions for Lita'
|
7
7
|
spec.summary = 'Allow Lita to interact with Active Directory'
|
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.required_ruby_version = '~> 2.2'
|
18
18
|
|
19
19
|
spec.add_runtime_dependency 'lita', '>= 4.7'
|
20
|
-
spec.add_runtime_dependency 'cratus'
|
20
|
+
spec.add_runtime_dependency 'cratus', '~> 0.5'
|
21
21
|
|
22
22
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
23
23
|
spec.add_development_dependency 'pry-byebug'
|
data/locales/en.yml
CHANGED
@@ -15,6 +15,12 @@ en:
|
|
15
15
|
group_members:
|
16
16
|
syntax: group <groupname> members
|
17
17
|
desc: lists all members of the given group
|
18
|
+
add_member:
|
19
|
+
syntax: add <username> to <groupname>
|
20
|
+
desc: add a user to an LDAP group
|
21
|
+
remove_member:
|
22
|
+
syntax: remove <username> from <groupname>
|
23
|
+
desc: remove a user from an LDAP group
|
18
24
|
replies:
|
19
25
|
user_locked?:
|
20
26
|
working: let me check on that
|
@@ -33,3 +39,11 @@ en:
|
|
33
39
|
group_members:
|
34
40
|
working: Give me a second to search
|
35
41
|
error: "That did not work, double check the '%{group}' is a valid group name"
|
42
|
+
add_member:
|
43
|
+
working: I'll get that user added
|
44
|
+
error: "That did not work, double check that '%{user}' and '%{group}' are valid"
|
45
|
+
success: "'%{user}' is now a member of '%{group}'"
|
46
|
+
remove_member:
|
47
|
+
working: Give me just a second to remove that user from the group
|
48
|
+
error: "That did not work, double check that '%{user}' and '%{group}' are valid"
|
49
|
+
success: "'%{user}' is no longer a member of '%{group}'"
|
@@ -18,42 +18,86 @@ describe Lita::Handlers::Activedirectory, lita_handler: true do
|
|
18
18
|
is_expected.to route_command('unlock jdoe').with_authorization_for(:ad_admins).to(:unlock)
|
19
19
|
is_expected.to route_command('jdoe groups').to(:user_groups)
|
20
20
|
is_expected.to route_command('group foo members').to(:group_members)
|
21
|
+
is_expected.to route_command('add foo to bar')
|
22
|
+
.with_authorization_for(:ad_admins).to(:add_group_member)
|
23
|
+
is_expected.to route_command('remove foo from bar')
|
24
|
+
.with_authorization_for(:ad_admins).to(:remove_group_member)
|
21
25
|
end
|
26
|
+
|
27
|
+
let(:fake_group1) do
|
28
|
+
instance_double(
|
29
|
+
'Cratus::Group',
|
30
|
+
name: 'lame_group1',
|
31
|
+
members: [fake_user]
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
let(:fake_group2) do
|
36
|
+
instance_double(
|
37
|
+
'Cratus::Group',
|
38
|
+
name: 'lame_group2'
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:fake_user) do
|
43
|
+
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
44
|
+
allow(Cratus::LDAP).to receive(:connection).and_return(true)
|
45
|
+
instance_double(
|
46
|
+
'Cratus::User',
|
47
|
+
dn: 'cn=fbar,dc=example,dc=com',
|
48
|
+
username: 'fabar',
|
49
|
+
fullname: 'Foo Bar',
|
50
|
+
member_of: [],
|
51
|
+
lockouttime: '0',
|
52
|
+
locked?: false
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
22
56
|
let(:locked_user) do
|
23
|
-
|
57
|
+
instance_double(
|
24
58
|
'Cratus::User',
|
25
59
|
dn: 'cn=jdoe,dc=example,dc=com',
|
60
|
+
member_of: [fake_group1, fake_group2],
|
26
61
|
lockouttime: '124',
|
27
|
-
locked?: true
|
62
|
+
locked?: true,
|
63
|
+
unlock: true
|
28
64
|
)
|
65
|
+
end
|
66
|
+
|
67
|
+
let(:false_user) do
|
29
68
|
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
30
69
|
allow(Cratus::LDAP).to receive(:connection).and_return(true)
|
31
|
-
|
70
|
+
nil
|
32
71
|
end
|
33
72
|
|
34
73
|
let(:unlocked_user) do
|
35
|
-
|
74
|
+
instance_double(
|
36
75
|
'Cratus::User',
|
37
76
|
dn: 'cn=jdoe,dc=example,dc=com',
|
38
|
-
|
77
|
+
username: 'jdoe',
|
78
|
+
member_of: [fake_group1, fake_group2],
|
39
79
|
lockouttime: '0',
|
40
80
|
locked?: false
|
41
81
|
)
|
42
|
-
|
43
|
-
|
44
|
-
|
82
|
+
end
|
83
|
+
|
84
|
+
let(:simple_group) do
|
85
|
+
instance_double(
|
86
|
+
'Cratus::Group',
|
87
|
+
dn: 'cn=testgroup,dc=example,dc=com',
|
88
|
+
add_user: true,
|
89
|
+
remove_user: true
|
90
|
+
)
|
45
91
|
end
|
46
92
|
|
47
93
|
describe '#user_locked?' do
|
48
94
|
it 'lets you know if the user is locked' do
|
49
|
-
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
50
95
|
allow(Cratus::User).to receive(:new).and_return(locked_user)
|
51
96
|
send_command('is jdoe locked?')
|
52
97
|
expect(replies.first).to eq('let me check on that')
|
53
98
|
expect(replies.last).to eq("looks like 'jdoe' is locked")
|
54
99
|
end
|
55
100
|
it 'lets you know if a user is not locked' do
|
56
|
-
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
57
101
|
allow(Cratus::User).to receive(:new).and_return(unlocked_user)
|
58
102
|
send_command('is jdoe locked?')
|
59
103
|
expect(replies.first).to eq('let me check on that')
|
@@ -67,7 +111,6 @@ describe Lita::Handlers::Activedirectory, lita_handler: true do
|
|
67
111
|
end
|
68
112
|
it 'unlocks the user when locked' do
|
69
113
|
allow(Cratus::User).to receive(:new).and_return(locked_user)
|
70
|
-
allow(Cratus::LDAP.connection).to receive(:replace_attribute).and_return(true)
|
71
114
|
send_command('unlock jdoe', as: lita_user)
|
72
115
|
expect(replies.first).to eq('lets see what we can do')
|
73
116
|
expect(replies.last).to eq("'jdoe' has been unlocked")
|
@@ -81,9 +124,39 @@ describe Lita::Handlers::Activedirectory, lita_handler: true do
|
|
81
124
|
end
|
82
125
|
end
|
83
126
|
|
127
|
+
describe '#add_group_member' do
|
128
|
+
before do
|
129
|
+
robot.auth.add_user_to_group!(lita_user, :ad_admins)
|
130
|
+
end
|
131
|
+
it 'adds a user to a group' do
|
132
|
+
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
133
|
+
allow(Cratus::LDAP).to receive(:connection).and_return(true)
|
134
|
+
allow(Cratus::User).to receive(:new).and_return(unlocked_user)
|
135
|
+
allow(Cratus::Group).to receive(:new).and_return(simple_group)
|
136
|
+
send_command('add jdoe to testgroup', as: lita_user)
|
137
|
+
expect(replies.first).to eq("I'll get that user added")
|
138
|
+
expect(replies.last).to eq("'jdoe' is now a member of 'testgroup'")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe '#remove_group_member' do
|
143
|
+
before do
|
144
|
+
robot.auth.add_user_to_group!(lita_user, :ad_admins)
|
145
|
+
end
|
146
|
+
it 'removes a user from a group' do
|
147
|
+
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
148
|
+
allow(Cratus::LDAP).to receive(:connection).and_return(true)
|
149
|
+
allow(Cratus::User).to receive(:new).and_return(unlocked_user)
|
150
|
+
allow(Cratus::Group).to receive(:new).and_return(simple_group)
|
151
|
+
send_command('remove jdoe from testgroup', as: lita_user)
|
152
|
+
expect(replies.first).to eq('Give me just a second to remove that user from the group')
|
153
|
+
expect(replies.last).to eq("'jdoe' is no longer a member of 'testgroup'")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
84
157
|
describe '#user_groups' do
|
85
158
|
it 'should return proper error mesage' do
|
86
|
-
allow(Cratus::User).to receive(:new).and_return(
|
159
|
+
allow(Cratus::User).to receive(:new).and_return(false_user)
|
87
160
|
send_command('fbar groups')
|
88
161
|
expect(replies.first).to eq('Give me a second to search')
|
89
162
|
expect(replies.last)
|
@@ -91,8 +164,18 @@ describe Lita::Handlers::Activedirectory, lita_handler: true do
|
|
91
164
|
end
|
92
165
|
it 'should return group membership' do
|
93
166
|
allow(Cratus::User).to receive(:new).and_return(unlocked_user)
|
94
|
-
send_command('
|
167
|
+
send_command('jdoe groups')
|
168
|
+
expect(replies.first).to eq('Give me a second to search')
|
169
|
+
expect(replies.last).to eq("lame_group1\nlame_group2")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#group_members' do
|
174
|
+
it 'should return members of the group' do
|
175
|
+
allow(Cratus::Group).to receive(:new).and_return(fake_group1)
|
176
|
+
send_command('group fake_group1 members')
|
95
177
|
expect(replies.first).to eq('Give me a second to search')
|
178
|
+
expect(replies.last).to eq('Foo Bar')
|
96
179
|
end
|
97
180
|
end
|
98
181
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Utils::Cratususer do
|
4
|
+
let(:fake_group1) do
|
5
|
+
instance_double(
|
6
|
+
'Cratus::Group',
|
7
|
+
name: 'lame_group1',
|
8
|
+
members: [fake_user2]
|
9
|
+
)
|
10
|
+
end
|
11
|
+
let(:fake_group2) do
|
12
|
+
instance_double(
|
13
|
+
'Cratus::Group',
|
14
|
+
name: 'lame_group2'
|
15
|
+
)
|
16
|
+
end
|
17
|
+
let(:fake_user) do
|
18
|
+
fakeuser = instance_double(
|
19
|
+
'Cratus::User',
|
20
|
+
dn: 'cn=jdoe,dc=example,dc=com',
|
21
|
+
username: 'jdoe',
|
22
|
+
fullname: 'John Doe',
|
23
|
+
member_of: [fake_group1, fake_group2],
|
24
|
+
lockouttime: '0',
|
25
|
+
locked?: false
|
26
|
+
)
|
27
|
+
fakeuser
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:fake_user2) do
|
31
|
+
fakeuser = instance_double(
|
32
|
+
'Cratus::User',
|
33
|
+
dn: 'cn=fbar,dc=example,dc=com',
|
34
|
+
username: 'fabar',
|
35
|
+
fullname: 'Foo Bar',
|
36
|
+
member_of: [],
|
37
|
+
lockouttime: '0',
|
38
|
+
locked?: false
|
39
|
+
)
|
40
|
+
fakeuser
|
41
|
+
end
|
42
|
+
|
43
|
+
subject do
|
44
|
+
# shut up rspec
|
45
|
+
class Dummy
|
46
|
+
include Utils::Cratususer
|
47
|
+
def config
|
48
|
+
conf = OpenStruct.new
|
49
|
+
conf.host = 'localhost'
|
50
|
+
conf
|
51
|
+
end
|
52
|
+
end
|
53
|
+
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
54
|
+
allow(Cratus::LDAP).to receive(:connection).and_return(true)
|
55
|
+
Dummy.new
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#user_groups_query' do
|
59
|
+
it 'should return the group memberships' do
|
60
|
+
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
61
|
+
allow(Cratus::LDAP).to receive(:connection).and_return(true)
|
62
|
+
allow(Cratus::User).to receive(:new).and_return(fake_user)
|
63
|
+
expect(subject.user_groups_query('jdoe')).to eq("lame_group1\nlame_group2")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#group_mem_query' do
|
68
|
+
it 'should return members of the group' do
|
69
|
+
allow(Cratus::LDAP).to receive(:connect).and_return(true)
|
70
|
+
allow(Cratus::LDAP).to receive(:connection).and_return(true)
|
71
|
+
allow(Cratus::Group).to receive(:new).and_return(fake_group1)
|
72
|
+
expect(subject.group_mem_query('foo')).to eq('Foo Bar')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lita-activedirectory
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Schaaff
|
8
|
+
- Jonathan Gnagy
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2017-
|
12
|
+
date: 2017-04-10 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: lita
|
@@ -28,16 +29,16 @@ dependencies:
|
|
28
29
|
name: cratus
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
30
31
|
requirements:
|
31
|
-
- - "
|
32
|
+
- - "~>"
|
32
33
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
+
version: '0.5'
|
34
35
|
type: :runtime
|
35
36
|
prerelease: false
|
36
37
|
version_requirements: !ruby/object:Gem::Requirement
|
37
38
|
requirements:
|
38
|
-
- - "
|
39
|
+
- - "~>"
|
39
40
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
+
version: '0.5'
|
41
42
|
- !ruby/object:Gem::Dependency
|
42
43
|
name: bundler
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -200,6 +201,7 @@ files:
|
|
200
201
|
- locales/en.yml
|
201
202
|
- spec/lita/handlers/activedirectory_spec.rb
|
202
203
|
- spec/spec_helper.rb
|
204
|
+
- spec/utils/cratususer_spec.rb
|
203
205
|
- templates/.gitkeep
|
204
206
|
homepage: https://github.com/knuedge/lita-activedirectory
|
205
207
|
licenses:
|
@@ -222,10 +224,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
222
224
|
version: '0'
|
223
225
|
requirements: []
|
224
226
|
rubyforge_project:
|
225
|
-
rubygems_version: 2.6.
|
227
|
+
rubygems_version: 2.6.8
|
226
228
|
signing_key:
|
227
229
|
specification_version: 4
|
228
230
|
summary: Allow Lita to interact with Active Directory
|
229
231
|
test_files:
|
230
232
|
- spec/lita/handlers/activedirectory_spec.rb
|
231
233
|
- spec/spec_helper.rb
|
234
|
+
- spec/utils/cratususer_spec.rb
|