slack_ruby_bot_authorization 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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,9 @@
1
+ module SlackRubyBotAuthorization
2
+ class DenialProcArityException < StandardError; end
3
+
4
+ class UnknownBuiltinCommandException < StandardError; end
5
+
6
+ class UnknownRoleException < StandardError; end
7
+
8
+ class ArrayArgumentException < StandardError; end
9
+ 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
@@ -0,0 +1,3 @@
1
+ module SlackRubyBotAuthorization
2
+ VERSION = '0.9.0'.freeze
3
+ 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: []