slack_ruby_bot_authorization 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +105 -0
- data/lib/slack_ruby_bot_authorization.rb +24 -0
- data/lib/slack_ruby_bot_authorization/authorization.rb +76 -0
- data/lib/slack_ruby_bot_authorization/command.rb +49 -0
- data/lib/slack_ruby_bot_authorization/command_audit.rb +39 -0
- data/lib/slack_ruby_bot_authorization/commands.rb +85 -0
- data/lib/slack_ruby_bot_authorization/exceptions.rb +9 -0
- data/lib/slack_ruby_bot_authorization/role.rb +149 -0
- data/lib/slack_ruby_bot_authorization/roles.rb +183 -0
- data/lib/slack_ruby_bot_authorization/utils.rb +38 -0
- data/lib/slack_ruby_bot_authorization/version.rb +3 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cda3bcf95b6ed548330977fcd75d1f0700c3345f
|
4
|
+
data.tar.gz: ef911e017f94f8ea0cd4de2317def95ed1831418
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 992d41ff6a2f4fe8629262989e2a991b2fbe68018d057b31de543622d58700774f327f0abe2984549a3a1c78a587732a0aab377fb43cbce87c58f485aa5f2049
|
7
|
+
data.tar.gz: 5bffc40043108f6e21dfab9ad0664c2dc55525cbcfe6b55d8912cbb5bff193dbdf546021b951fec2e6336ac4e7b20c52a2d7b943e4f8141ee547da28b30cceb3
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Stax Logistics
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# slack-ruby-bot-authorization
|
2
|
+
An authorization framework for slack-ruby-bot
|
3
|
+
|
4
|
+
# How to Use It
|
5
|
+
Many bots are created by subclassing `SlackRubyBot::Bot` or `SlackRubyBot::Commands::Base` and adding commands (or using the cool new MVC features in `slack-ruby-bot`). This framework works similarly by allowing inclusion of a module into the subclass to add some authorization features.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class MyBot < SlackRubyBot::Bot
|
9
|
+
extend SlackRubyBotAuthorization
|
10
|
+
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
Including the module adds several important methods to the subclass that can be used to register roles, users, and commands. Additionally, we can customize the "denial" behavior when a user runs a command for which they are not authorized.
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
class MyBot < SlackRubyBot::Bot
|
18
|
+
extend SlackRubyBotAuthorization
|
19
|
+
|
20
|
+
add_default_denial_handler(Proc.new do |client, data, match|
|
21
|
+
# Called when a command is not permitted to user/role and a specific
|
22
|
+
# denial Proc hasn't already been registered
|
23
|
+
client.say(channel: data.channel, text: "Sorry, but you have been denied!")
|
24
|
+
end
|
25
|
+
)
|
26
|
+
|
27
|
+
add_users_to_role('admin','abc', 'def', 'ghi', 'jkl')
|
28
|
+
|
29
|
+
role = add_commands_to_role('admin','update inventory', 'remove user', 'add user', 'run report')
|
30
|
+
|
31
|
+
role.add_denial_handler(
|
32
|
+
Proc.new do |client, data, match|
|
33
|
+
client.say(...)
|
34
|
+
# returning true means the default_denial should also run
|
35
|
+
# return false would indicate only this proc should run and skip
|
36
|
+
# the default
|
37
|
+
true
|
38
|
+
end
|
39
|
+
)
|
40
|
+
|
41
|
+
remove_users_from_role('admin', 'abc')
|
42
|
+
remove_commands_from_role('admin', 'run report')
|
43
|
+
remove_denial_handler_from_command('update inventory') # default, if set, will still run
|
44
|
+
|
45
|
+
admin_role = new_role('admin') do |role|
|
46
|
+
role.enable_slack_builtin_commands('hi') # defaults to all unless specifically listed
|
47
|
+
role.add_users('user1', 'user2', 'user3')
|
48
|
+
role.add_commands('cmd1', 'cmd2', 'cmd3', 'cmd4')
|
49
|
+
role.add_default_denial_handler(proc { |a, b, c| nil } )
|
50
|
+
end
|
51
|
+
|
52
|
+
admin_role.remove_users('user1', 'user2')
|
53
|
+
admin_role.remove_commands('cmd1')
|
54
|
+
command = admin_role.find_command('cmd1')
|
55
|
+
command.add_denial_handler(
|
56
|
+
proc { |a, b, c| p a, b, c; return false }
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate an array of commands that are in a
|
61
|
+
# `SlackRubyBot::Commands::Base` subclass but
|
62
|
+
# that have not been authorized via the auth framework
|
63
|
+
missing = MyBot.unauthorized_commands
|
64
|
+
unless missing.empty?
|
65
|
+
raise 'Some commands have not been added to any role! ' + missing.inspect
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generate an array of commands that were authorized
|
69
|
+
# in the framework but do NOT exist in the
|
70
|
+
# bot subclasses.
|
71
|
+
missing = MyBot.missing_commands
|
72
|
+
unless missing.empty?
|
73
|
+
raise 'Some authorized commands do not exist in the bot! ' + missing.inspect
|
74
|
+
end
|
75
|
+
```
|
76
|
+
In situations where multiple strings can trigger a command, each string should be added to the role. For example, a command may respond to three different strings such as 'add', 'create new', and 'append to collection'. While they all execute the same business logic, from the perspective of the authorization framework they are different commands. Permission to run all 3 should be granted to the roles that need to execute any of them.
|
77
|
+
|
78
|
+
Adding a `denial` proc via `add_denial_handler_to_command` multiple times results in the last operation overwriting any previous operations. The `default_denial` proc can still be scheduled to run if the command-specific proc returns true. If it returns false, the default denial proc is skipped.
|
79
|
+
|
80
|
+
|
81
|
+
# Defaults
|
82
|
+
The system works off the assumption that "everything is prohibited unless expressly allowed." That is, it's a whitelist. Only those users (and roles) that are specifically registered to run commands may run them. All others attempting to run those commands will be denied. This is the most secure approach and is highly recommended.
|
83
|
+
|
84
|
+
Note that this behavior extends to builtin commands such as 'about', 'help', and 'hi.' If not explicitly allowed, these commands will be denied.
|
85
|
+
|
86
|
+
# How to Subvert the Whitelist Behavior
|
87
|
+
Programmers who are writing their own code can obviously overrule the defaults. The included module contains a method named `permitted?` which overrides the default method of the same name in `SlackRubyBot::Commands::Base`. This method may also be overridden by the subclass which would wipe out the whitelist behavior.
|
88
|
+
|
89
|
+
This is not recommended.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class MyBot < SlackRubyBot::Bot
|
93
|
+
include SlackRubyBotAuthorization
|
94
|
+
|
95
|
+
# Permit any user to run any command
|
96
|
+
def permitted?(*args)
|
97
|
+
return true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
# Warning
|
103
|
+
The API is subject to change without warning until version 1.0 is released. This project will follow semantic versioning.
|
104
|
+
|
105
|
+
(c) Copyright 2017 Stax Logistics LLC
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require_relative 'slack_ruby_bot_authorization/version'
|
6
|
+
require_relative 'slack_ruby_bot_authorization/exceptions'
|
7
|
+
require_relative 'slack_ruby_bot_authorization/utils'
|
8
|
+
require_relative 'slack_ruby_bot_authorization/command_audit'
|
9
|
+
require_relative 'slack_ruby_bot_authorization/command'
|
10
|
+
require_relative 'slack_ruby_bot_authorization/commands'
|
11
|
+
require_relative 'slack_ruby_bot_authorization/role'
|
12
|
+
require_relative 'slack_ruby_bot_authorization/roles'
|
13
|
+
require_relative 'slack_ruby_bot_authorization/authorization'
|
14
|
+
|
15
|
+
# The main entry point for users of his library. This module
|
16
|
+
# should be included into any Bot class that requires
|
17
|
+
# authorization for running commands.
|
18
|
+
module SlackRubyBotAuthorization
|
19
|
+
include Authorization
|
20
|
+
|
21
|
+
def self.extended(othermod)
|
22
|
+
othermod.reset!
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module SlackRubyBotAuthorization
|
2
|
+
# All of the real logic lives in the Roles class. This module
|
3
|
+
# exists to make it easy to +include+ the functionality in a
|
4
|
+
# class.
|
5
|
+
module Authorization
|
6
|
+
include Utils
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@roles,
|
10
|
+
:new_role,
|
11
|
+
:role_names,
|
12
|
+
:user_names,
|
13
|
+
:command_names,
|
14
|
+
:find_role,
|
15
|
+
:add_users_to_role,
|
16
|
+
:add_commands_to_role,
|
17
|
+
:remove_users_from_role,
|
18
|
+
:remove_commands_from_role,
|
19
|
+
:add_default_denial_handler,
|
20
|
+
:default_denial_handler,
|
21
|
+
:find_command,
|
22
|
+
:call_default_denial_handler,
|
23
|
+
:unauthorized_commands,
|
24
|
+
:missing_bot_commands
|
25
|
+
|
26
|
+
def reset!
|
27
|
+
@roles = Roles.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Override the default method from the slack-ruby-bot
|
31
|
+
# gem and hook in our standard logic.
|
32
|
+
def permitted?(client, data, match)
|
33
|
+
user, command = extract_details(data, match)
|
34
|
+
return true if command.nil? || user.nil?
|
35
|
+
final_permission?(client, data, match, user, command)
|
36
|
+
end
|
37
|
+
|
38
|
+
# If we get to here, then we know the SlackRubyBot::Bot
|
39
|
+
# has executed a command for a user. We run through the
|
40
|
+
# following logic:
|
41
|
+
#
|
42
|
+
# * Try to match the +user+ and +command+ to an existing
|
43
|
+
# role
|
44
|
+
# * If a role is found, then the user has permission to
|
45
|
+
# execute the given command
|
46
|
+
# * If a role is not found, find the associated Command
|
47
|
+
# and run its denial handler.
|
48
|
+
# * If Command denial handler returns true, run the global
|
49
|
+
# denial handler.
|
50
|
+
# * Return false
|
51
|
+
def final_permission?(client, data, match, user, command_string)
|
52
|
+
command = @roles.find_command(command_string)
|
53
|
+
role = @roles.find_for(user, command_string)
|
54
|
+
|
55
|
+
# found a user/command match, so this should be permitted
|
56
|
+
if role
|
57
|
+
command.record_allowance(user)
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
|
61
|
+
process_denial(client, data, match, user, command)
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
def process_denial(client, data, match, user, command)
|
66
|
+
# If we made it here, then we didn't find a role that
|
67
|
+
# allows a user to execute the given command. Lookup
|
68
|
+
# the command and execute its denial handler if one
|
69
|
+
# exists. If that handler returns true, also run the
|
70
|
+
# default denial handler.
|
71
|
+
command.record_denial(user)
|
72
|
+
return unless command.call_denial_handler(client, data, match)
|
73
|
+
@roles.call_default_denial_handler(client, data, match)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module SlackRubyBotAuthorization
|
2
|
+
# Store the command name and keep track of any
|
3
|
+
# denial handlers for this command.
|
4
|
+
class Command
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
attr_reader :name, :denial_handler
|
8
|
+
|
9
|
+
def initialize(name)
|
10
|
+
@name = normalize_string(name)
|
11
|
+
@denial_handler = EmptyDenialProc
|
12
|
+
@audit = CommandAudit.new(@name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_denial_handler(aproc)
|
16
|
+
unless aproc.arity == 3
|
17
|
+
raise(DenialProcArityException, 'Must accept 3 arguments')
|
18
|
+
end
|
19
|
+
|
20
|
+
@denial_handler = aproc
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_denial_handler
|
24
|
+
@denial_handler = Utils::EmptyDenialProc
|
25
|
+
end
|
26
|
+
|
27
|
+
def call_denial_handler(client, data, match)
|
28
|
+
@denial_handler.call(client, data, match)
|
29
|
+
end
|
30
|
+
|
31
|
+
def record_allowance(user)
|
32
|
+
@audit.allow(user)
|
33
|
+
end
|
34
|
+
|
35
|
+
def record_denial(user)
|
36
|
+
@audit.deny(user)
|
37
|
+
end
|
38
|
+
|
39
|
+
def stats_for(user)
|
40
|
+
@audit.stats_for(user)
|
41
|
+
end
|
42
|
+
|
43
|
+
def print_stats
|
44
|
+
@audit.inspect
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
DefaultCommand = Command.new('default-not-a-command')
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SlackRubyBotAuthorization
|
2
|
+
# Tracks the number of times per user that a command
|
3
|
+
# has been allowed to run or denied to run.
|
4
|
+
#
|
5
|
+
# Pretty prints the structures so it's easy to read.
|
6
|
+
class CommandAudit
|
7
|
+
include Utils
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(command_name)
|
11
|
+
@name = command_name
|
12
|
+
@map = Hash.new { |h, k| h[k] = OpenStruct.new(allowed: 0, denied: 0) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def allow(user)
|
16
|
+
stats_for(user).allowed += 1
|
17
|
+
end
|
18
|
+
|
19
|
+
def deny(user)
|
20
|
+
stats_for(user).denied += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def stats_for(user)
|
24
|
+
@map[normalize_string(user)]
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
len = 15
|
29
|
+
string = "#{name}\n"
|
30
|
+
@map.keys.each do |user|
|
31
|
+
string << user.to_s.ljust(len)
|
32
|
+
string << ":allowed => #{stats_for(user).allowed}\n"
|
33
|
+
string << ' ' * len
|
34
|
+
string << ":denied => #{stats_for(user).denied}\n"
|
35
|
+
end
|
36
|
+
string
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Commands stores a list of all commands along with
|
2
|
+
# 1. A defaut denial handler if no command can be
|
3
|
+
# matched.
|
4
|
+
# 2. Each command tracks its own denial handler so we can
|
5
|
+
# allow for specific behavior when a command request is
|
6
|
+
# denied.
|
7
|
+
#
|
8
|
+
module SlackRubyBotAuthorization
|
9
|
+
# Tracks all Command instances and maintains the global
|
10
|
+
# default denial handler.
|
11
|
+
class Commands
|
12
|
+
include Utils
|
13
|
+
|
14
|
+
attr_accessor :default_denial_handler
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
reset!
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset!
|
21
|
+
@commands = {}
|
22
|
+
@default_denial_handler = EmptyDenialProc
|
23
|
+
end
|
24
|
+
|
25
|
+
def new_command(command_name)
|
26
|
+
command_name = normalize_string(command_name)
|
27
|
+
@commands[command_name] = Command.new(command_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_or_make(*command_strings)
|
31
|
+
set = Set.new
|
32
|
+
command_strings.each do |command_string|
|
33
|
+
command = find(command_string, false)
|
34
|
+
|
35
|
+
# when not found, make a new Command instance
|
36
|
+
set << (command || new_command(command_string))
|
37
|
+
end
|
38
|
+
set.to_a
|
39
|
+
end
|
40
|
+
|
41
|
+
def find(command_string, default = true)
|
42
|
+
command = @commands[normalize_string(command_string)]
|
43
|
+
default ? (command || DefaultCommand) : command
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns list of all commands that were ever registered. This
|
47
|
+
# will even return commands that are no longer associated with
|
48
|
+
# a role, so the output from this command will be a superset
|
49
|
+
# of the Roles#command_names method.
|
50
|
+
def command_names
|
51
|
+
@commands.values.map(&:name).sort
|
52
|
+
end
|
53
|
+
|
54
|
+
# Default denial proc called when a Command doesn't have a specific
|
55
|
+
# denial handler set.
|
56
|
+
#
|
57
|
+
# When the specific Command has its own handler, that handler may
|
58
|
+
# return true to allow the default handler to also run. To prevent
|
59
|
+
# the default handler from running, the Command-specific denial
|
60
|
+
# handler should return false.
|
61
|
+
def add_default_denial_handler(aproc)
|
62
|
+
unless aproc.arity == 3
|
63
|
+
raise(DenialProcArityException, 'Must accept 3 arguments')
|
64
|
+
end
|
65
|
+
|
66
|
+
@default_denial_handler = aproc
|
67
|
+
end
|
68
|
+
|
69
|
+
def remove_denial_handler_from_command(command_string)
|
70
|
+
command = find(command_string)
|
71
|
+
unless command
|
72
|
+
raise(
|
73
|
+
UnknownCommandException,
|
74
|
+
"No command named '#{command_string}'"
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
command.remove_denial_handler
|
79
|
+
end
|
80
|
+
|
81
|
+
def call_denial_handler(client, data, match)
|
82
|
+
@default_denial_handler.call(client, data, match)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module SlackRubyBotAuthorization
|
2
|
+
BUILTIN_COMMANDS = %w[about help hi].freeze
|
3
|
+
|
4
|
+
# Implement the Null Pattern for the Role class. Other code
|
5
|
+
# will search for a matching Role instance. When not found,
|
6
|
+
# we want to return an instance of NullRole so that all
|
7
|
+
# method calls succeed (unless someone passes a proc with
|
8
|
+
# the wrong arity) and we can avoid checking for nil elsewhere.
|
9
|
+
class NullRole
|
10
|
+
attr_reader :name, :denial_handler
|
11
|
+
|
12
|
+
def initialize(_name, &_blk)
|
13
|
+
@name = :default
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_users(*_users)
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_commands(*_commands)
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_denial_handler(aproc)
|
25
|
+
return if aproc.arity == 3
|
26
|
+
raise(DenialProcArityException, 'Must accept 3 arguments')
|
27
|
+
end
|
28
|
+
|
29
|
+
def enable_slack_builtin_commands(*commands)
|
30
|
+
missing = commands.none? do |command|
|
31
|
+
BUILTIN_COMMANDS.include?(command.downcase)
|
32
|
+
end
|
33
|
+
|
34
|
+
return unless missing
|
35
|
+
|
36
|
+
message = "Unknown command '#{command}'; \
|
37
|
+
must be one of #{BUILTIN_COMMANDS.inspect}"
|
38
|
+
|
39
|
+
raise(UnknownBuiltinCommandException, message)
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_users(*_users)
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_commands(*_commands)
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_denial_handler
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def user?(_user)
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def command?(_command)
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
def users
|
63
|
+
[]
|
64
|
+
end
|
65
|
+
|
66
|
+
def commands
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Maps roles to users and commands.
|
72
|
+
class Role < NullRole
|
73
|
+
include Utils
|
74
|
+
|
75
|
+
attr_reader :name, :denial_handler
|
76
|
+
|
77
|
+
def initialize(name, &blk)
|
78
|
+
@name = normalize_string(name)
|
79
|
+
@users = Set.new
|
80
|
+
@commands = Set.new
|
81
|
+
|
82
|
+
return unless block_given?
|
83
|
+
|
84
|
+
# Execute the block depending on how many args were passed
|
85
|
+
# http://graysoftinc.com/ruby-voodoo/dsl-block-styles
|
86
|
+
blk.arity == 1 ? yield(self) : instance_eval(&blk)
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_users(*users)
|
90
|
+
users.each { |user| @users << normalize_string(user) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_commands(*commands)
|
94
|
+
commands.each { |command| @commands << command }
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_denial_handler(aproc)
|
98
|
+
unless aproc.arity == 3
|
99
|
+
raise(DenialProcArityException, 'Must accept 3 arguments')
|
100
|
+
end
|
101
|
+
|
102
|
+
@denial_handler = aproc
|
103
|
+
end
|
104
|
+
|
105
|
+
def enable_slack_builtin_commands(*commands)
|
106
|
+
commands = BUILTIN_COMMANDS if commands.empty?
|
107
|
+
commands.each do |command|
|
108
|
+
raise(UnknownBuiltinCommandException, "Unknown command '#{command}'") \
|
109
|
+
unless BUILTIN_COMMANDS.include?(command.downcase)
|
110
|
+
|
111
|
+
add_commands(command)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def remove_users(*users)
|
116
|
+
users.each { |user| @users.delete(normalize_string(user)) }
|
117
|
+
end
|
118
|
+
|
119
|
+
def remove_commands(*commands)
|
120
|
+
commands.each { |command| @commands.delete(command) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def remove_denial_handler
|
124
|
+
@denial_handler = Utils::EmptyDenialProc
|
125
|
+
end
|
126
|
+
|
127
|
+
def user?(user)
|
128
|
+
@users.include?(normalize_string(user))
|
129
|
+
end
|
130
|
+
|
131
|
+
def command?(command)
|
132
|
+
@commands.include?(command)
|
133
|
+
end
|
134
|
+
|
135
|
+
def users
|
136
|
+
@users.to_a.sort
|
137
|
+
end
|
138
|
+
|
139
|
+
def commands
|
140
|
+
@commands.to_a
|
141
|
+
end
|
142
|
+
|
143
|
+
def command_names
|
144
|
+
commands.map(&:name).sort
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
DefaultRole = NullRole.new('default')
|
149
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
module SlackRubyBotAuthorization
|
2
|
+
# The main API for handling authorization. See documentation
|
3
|
+
# for SlackRubyBotAuthorization top-level module.
|
4
|
+
class Roles
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
attr_reader :commands
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
reset!
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset!
|
14
|
+
@roles = {}
|
15
|
+
@commands = Commands.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_role(role_name, &blk)
|
19
|
+
role_name = normalize_string(role_name)
|
20
|
+
@roles[role_name] = Role.new(role_name, &blk)
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_for(user_string, command_string)
|
24
|
+
command = find_command(command_string)
|
25
|
+
_, found_role = @roles.find do |_name, role|
|
26
|
+
role.user?(user_string) && role.command?(command)
|
27
|
+
end
|
28
|
+
|
29
|
+
found_role
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_role(role_name)
|
33
|
+
@roles[normalize_string(role_name)]
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_command(command_string)
|
37
|
+
@commands.find(command_string)
|
38
|
+
end
|
39
|
+
|
40
|
+
def role_names
|
41
|
+
@roles.values.map(&:name).uniq.sort
|
42
|
+
end
|
43
|
+
|
44
|
+
def user_names
|
45
|
+
@roles.values.inject([]) { |a, e| a + e.users }.uniq.sort
|
46
|
+
end
|
47
|
+
|
48
|
+
def command_names
|
49
|
+
@roles.values.inject([]) { |a, e| a + e.command_names }.uniq.sort
|
50
|
+
end
|
51
|
+
|
52
|
+
# Default denial proc called when a role doesn't have a specific denial
|
53
|
+
# handler set.
|
54
|
+
#
|
55
|
+
# When the specific role has its own handler, that handler may return true
|
56
|
+
# to allow the default handler to also run. To prevent the default handler
|
57
|
+
# from running, the role-specific denial handler should return false.
|
58
|
+
def add_default_denial_handler(aproc)
|
59
|
+
@commands.add_default_denial_handler(aproc)
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_denial_handler
|
63
|
+
@commands.default_denial_handler
|
64
|
+
end
|
65
|
+
|
66
|
+
def call_default_denial_handler(client, data, match)
|
67
|
+
default_denial_handler.call(client, data, match)
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_users_to_role(role_name, *users)
|
71
|
+
if users.any? { |user| user.is_a?(Array) }
|
72
|
+
raise(ArrayArgumentException, '+users+ argument should not be an array!\
|
73
|
+
Try prepending argument with splat \'*\'')
|
74
|
+
end
|
75
|
+
|
76
|
+
role = find_role(role_name) || new_role(role_name)
|
77
|
+
role.add_users(*users)
|
78
|
+
role
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_commands_to_role(role_name, *commands)
|
82
|
+
if commands.any? { |command| command.is_a?(Array) }
|
83
|
+
raise(ArrayArgumentException, '+commands+ argument should not\
|
84
|
+
be an array! Try prepending argument with splat \'*\'')
|
85
|
+
end
|
86
|
+
|
87
|
+
role = find_role(role_name) || new_role(role_name)
|
88
|
+
commands = @commands.find_or_make(*commands)
|
89
|
+
role.add_commands(*commands)
|
90
|
+
role
|
91
|
+
end
|
92
|
+
|
93
|
+
def remove_users_from_role(role_name, *users)
|
94
|
+
role = find_role(role_name)
|
95
|
+
raise(UnknownRoleException, "No role named '#{role_name}'") unless role
|
96
|
+
|
97
|
+
role.remove_users(*users)
|
98
|
+
@roles.delete(role.name) if role.users.empty?
|
99
|
+
role
|
100
|
+
end
|
101
|
+
|
102
|
+
def remove_commands_from_role(role_name, *commands)
|
103
|
+
role = find_role(role_name)
|
104
|
+
raise(UnknownRoleException, "No role named '#{role_name}'") unless role
|
105
|
+
|
106
|
+
commands = @commands.find_or_make(*commands)
|
107
|
+
role.remove_commands(*commands)
|
108
|
+
@roles.delete(role.name) if role.commands.empty?
|
109
|
+
role
|
110
|
+
end
|
111
|
+
|
112
|
+
# Compares all of the commands registered to a role to a list
|
113
|
+
# of all custom commands. Any custom command that has not been
|
114
|
+
# added to a role is listed.
|
115
|
+
#
|
116
|
+
# Use this for detecting possible mispellings between the
|
117
|
+
# Command classes and the authorization setup. Useful for cases
|
118
|
+
# where there are dozens of commands and many roles where a
|
119
|
+
# manual audit would be difficult.
|
120
|
+
def unauthorized_commands
|
121
|
+
missing_commands = []
|
122
|
+
|
123
|
+
command_subclass_routes.each do |route|
|
124
|
+
next if route_matches_any_command?(route, command_names)
|
125
|
+
|
126
|
+
# command did NOT match a route, therefore the command has not
|
127
|
+
# been authorized and is orphaned
|
128
|
+
missing_commands += commands_from_route(route)
|
129
|
+
end
|
130
|
+
missing_commands
|
131
|
+
end
|
132
|
+
|
133
|
+
# The complement to #unauthorized_commands. This returns an array
|
134
|
+
# of command names that have been authorized to a role but do NOT
|
135
|
+
# exist in any bot Command class.
|
136
|
+
def missing_bot_commands
|
137
|
+
missing_commands = []
|
138
|
+
|
139
|
+
command_names.each do |command_symbol|
|
140
|
+
next if command_matches_any_route?(command_subclass_routes, command_symbol)
|
141
|
+
|
142
|
+
# No route found for this command!
|
143
|
+
missing_commands << command_symbol.to_s
|
144
|
+
end
|
145
|
+
missing_commands
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# SlackRubyBot tracks all Command subclasses and their routes. Take
|
151
|
+
# advantage of this to collect an array of all routes.
|
152
|
+
# Only accept routes from child classes by filtering out any classes
|
153
|
+
# from the SlackRubyBot::Commands namespace.
|
154
|
+
def command_subclass_routes
|
155
|
+
klasses = SlackRubyBot::Commands::Base.command_classes.reject do |k|
|
156
|
+
k.name && k.name.start_with?('SlackRubyBot::Commands::')
|
157
|
+
end
|
158
|
+
|
159
|
+
klasses.map do |klass|
|
160
|
+
klass.routes ? klass.routes.keys : []
|
161
|
+
end.flatten
|
162
|
+
end
|
163
|
+
|
164
|
+
def route_matches_any_command?(route, command_strings)
|
165
|
+
command_strings.any? do |string|
|
166
|
+
expression = "botname #{string}"
|
167
|
+
route.match(expression)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def command_matches_any_route?(routes, command_string)
|
172
|
+
routes.any? do |route|
|
173
|
+
expression = "botname #{command_string}"
|
174
|
+
route.match(expression)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def commands_from_route(route)
|
179
|
+
catted_commands = route.to_s.split('<command>')[1].split(')').first
|
180
|
+
catted_commands.split('|')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SlackRubyBotAuthorization
|
2
|
+
# Methods that don't fit in any other class and also may
|
3
|
+
# be needed by multiple classes/modules.
|
4
|
+
module Utils
|
5
|
+
# Used as the default denial handler for Command instances.
|
6
|
+
# When the proc returns true, it allows the global default
|
7
|
+
# handler to also run. We want the empty handler to return
|
8
|
+
# true so that the default (if it exists) will run.
|
9
|
+
EmptyDenialProc = proc do |_client, _data, _match|
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Takes a String or Symbol, manipulates it, and returns a Symbol.
|
14
|
+
def normalize_string(obj)
|
15
|
+
obj.to_s.downcase.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
# Authorization only works when we've been passed a +data+
|
19
|
+
# object with a +user+ field *and* a +match+ object that
|
20
|
+
# contains a +command+ field.
|
21
|
+
#
|
22
|
+
# Extract those items and return them in an array. If those
|
23
|
+
# items don't exist in the form we expect, return nil in
|
24
|
+
# place of those objects.
|
25
|
+
def extract_details(data, match)
|
26
|
+
[extract_user(data), extract_command(match)]
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_user(data)
|
30
|
+
data.respond_to?(:user) ? data.user : nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_command(match)
|
34
|
+
return unless match.is_a?(MatchData) && match.names.include?('command')
|
35
|
+
match[:command].downcase
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slack_ruby_bot_authorization
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chuck Remes
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: slack-ruby-bot
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.10.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.10.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: |2
|
70
|
+
Easy way to build a whitelist authorization mechanism for slack ruby bot
|
71
|
+
commands. Define roles and add slack users and slack commands to those
|
72
|
+
roles. When the bot receives the command, the framework verifies that
|
73
|
+
the given user has permission to execute the command. If denied, a
|
74
|
+
custom handler can be setup to run instead.
|
75
|
+
email: git@chuckremes.com
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- lib/slack_ruby_bot_authorization.rb
|
83
|
+
- lib/slack_ruby_bot_authorization/authorization.rb
|
84
|
+
- lib/slack_ruby_bot_authorization/command.rb
|
85
|
+
- lib/slack_ruby_bot_authorization/command_audit.rb
|
86
|
+
- lib/slack_ruby_bot_authorization/commands.rb
|
87
|
+
- lib/slack_ruby_bot_authorization/exceptions.rb
|
88
|
+
- lib/slack_ruby_bot_authorization/role.rb
|
89
|
+
- lib/slack_ruby_bot_authorization/roles.rb
|
90
|
+
- lib/slack_ruby_bot_authorization/utils.rb
|
91
|
+
- lib/slack_ruby_bot_authorization/version.rb
|
92
|
+
homepage: http://github.com/stax-dog/slack_ruby_bot_authorization
|
93
|
+
licenses:
|
94
|
+
- MIT
|
95
|
+
metadata: {}
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.6.8
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Simple authorization framework for slack ruby bots
|
116
|
+
test_files: []
|