Skip to content

Commit 45bb274

Browse files
authored
Add command whitelist/blacklist support (#133)
* Stub in whitelist * Add blacklist/whitelist checking with test * Implement blacklist/whitelist checking * Remove :reload on requires and add whitelist/blacklist docs * Add test for no whitelist or blacklist
1 parent 7f753b4 commit 45bb274

File tree

9 files changed

+172
-45
lines changed

9 files changed

+172
-45
lines changed

config/config.sample.edn

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@
5656
:embedded {:enabled "false"}
5757
;; Whether to enable having a fallback command. Default is true.
5858
:fallback {:enabled "true"}
59+
60+
;; Whitelists and blackists: these can be used to enable/disable
61+
;; specific commands. Only one of these must be specified. If both
62+
;; are specified, it is considered an error and will crash Yetibot
63+
;; on startup. By default there is no whitelist or blacklist.
64+
;;
65+
;; Whitelist: when whitelist is specified, all commands are disabled
66+
;; except those present in the `whitelist` collection. Example:
67+
;;
68+
;; :whitelist ["echo" "list"]
69+
;;
70+
;; Blacklist: when blacklist is specified, all commands are enabled
71+
;; except those present in the `blacklist` collection. Example:
72+
;;
73+
;; :blackist ["echo" "list"]
5974
}
6075
;; the default command to fall back to if no other commands match
6176
:default {:command "giphy"}

config/profiles.sample.clj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@
2222
;; Whether to enable having a fallback command. Default is true.
2323
:yetibot-command-fallback-enabled "true"
2424

25+
;; Whitelists and blackists: these can be used to enable/disable specific
26+
;; commands. Only one of these must be specified. If both are specified, it
27+
;; is considered an error and will crash Yetibot on startup. By default there
28+
;; is no whitelist or blacklist.
29+
;;
30+
;; Whitelist: when whitelist is specified, all commands are disabled except
31+
;; those present in the `whitelist` collection. Example:
32+
;;
33+
;; :yetibot-command-whitelist-0 "echo"
34+
;; :yetibot-command-whitelist-1 "list"
35+
;;
36+
;; Blacklist: when blacklist is specified, all commands are enabled except
37+
;; those present in the `blacklist` collection. Example:
38+
;;
39+
;; :yetibot-command-blacklist-0 "echo"
40+
;; :yetibot-command-blacklist-1 "list"
41+
2542
;; Yetibot needs a Postgres instance to run against.
2643
:yetibot-db-url "postgresql://localhost:5432/yetibot"
2744
:yetibot-db-table-prefix "yetibot_"

config/sample.env

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@ YETIBOT_COMMAND_EMBEDDED_ENABLED="false"
1111
# Whether to enable having a fallback command. Default is true.
1212
YETIBOT_COMMAND_FALLBACK_ENABLED="true"
1313

14+
# Whitelists and blackists: these can be used to enable/disable specific
15+
# commands. Only one of these must be specified. If both are specified, it
16+
# is considered an error and will crash Yetibot on startup. By default there
17+
# is no whitelist or blacklist.
18+
#
19+
# Whitelist: when whitelist is specified, all commands are disabled except
20+
# those present in the `whitelist` collection. Example:
21+
22+
# YETIBOT_COMMAND_WHITELIST_0="echo"
23+
# YETIBOT_COMMAND_WHITELIST_1="list"
24+
25+
# Blacklist: when blacklist is specified, all commands are enabled except
26+
# those present in the `blacklist` collection. Example:
27+
#
28+
# :yetibot-command-blacklist-0 "echo"
29+
# :yetibot-command-blacklist-1 "list"
30+
1431
# Yetibot needs a Postgres instance to run against.
1532
YETIBOT_DB_URL="postgresql://localhost:5432/yetibot"
1633
YETIBOT_DB_TABLE_PREFIX="yetibot_"

src/yetibot/core/hooks.clj

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
(ns yetibot.core.hooks
22
(:require
3-
[yetibot.core.models.admin :as admin]
4-
[clojure.set :refer [difference intersection]]
5-
[taoensso.timbre :refer [color-str trace debug info warn error]]
6-
[yetibot.core.handler]
7-
[clojure.string :as s]
8-
[metrics.timers :as timers]
9-
[yetibot.core.models.channel :as c]
10-
[yetibot.core.interpreter :refer [handle-cmd]]
11-
[yetibot.core.models.help :as help]
12-
[robert.hooke :as rh]
13-
[clojure.stacktrace :as st]))
3+
[yetibot.core.models.admin :as admin]
4+
[clojure.set :refer [intersection]]
5+
[taoensso.timbre :refer [color-str trace debug info warn error]]
6+
[yetibot.core.handler]
7+
[clojure.string :as s]
8+
[metrics.timers :as timers]
9+
[yetibot.core.models.channel :as c]
10+
[yetibot.core.interpreter :refer [handle-cmd]]
11+
[yetibot.core.models.help :as help]
12+
[robert.hooke :as rh]
13+
[yetibot.core.util.command :refer [command-enabled?]]))
1414

1515
(def ^:private Pattern java.util.regex.Pattern)
1616

@@ -80,25 +80,28 @@
8080
;; ensure the user is allowed to run this command
8181
(if (and admin-only-command? (not user-is-admin?))
8282
{:result/error (format
83-
"Only admins are allowed to execute %s commands" cmd)}
83+
"Only admins are allowed to execute %s commands" cmd)}
8484
;; find the top level command and its corresponding sub-cmds
8585
(if-let [[cmd-re sub-cmds] (find-sub-cmds cmd)]
8686
;; Now try to find a matching sub-commands
8787
(if-let [[match sub-fn] (match-sub-cmds args sub-cmds)]
88+
;; TODO check if command is whitelisted or blacklisted
8889
;; extract category settings
89-
(let [disabled-cats (if settings (settings c/cat-settings-key) #{})
90-
fn-cats (set (:yb/cat (meta sub-fn)))]
91-
(if-let [matched-disabled-cats (seq (intersection disabled-cats fn-cats))]
92-
(str
93-
(s/join ", " (map name matched-disabled-cats))
94-
" commands are disabled in this channel🖐")
95-
(timers/time!
96-
(timers/timer ["yetibot" cmd (str (:name (meta sub-fn)))])
97-
(sub-fn (merge extra {:cmd cmd :args args :match match})))))
90+
(if (command-enabled? cmd)
91+
(let [disabled-cats (if settings (settings c/cat-settings-key) #{})
92+
fn-cats (set (:yb/cat (meta sub-fn)))]
93+
(if-let [matched-disabled-cats (seq (intersection disabled-cats fn-cats))]
94+
(str
95+
(s/join ", " (map name matched-disabled-cats))
96+
" commands are disabled in this channel🖐")
97+
(timers/time!
98+
(timers/timer ["yetibot" cmd (str (:name (meta sub-fn)))])
99+
(sub-fn (merge extra {:cmd cmd :args args :match match})))))
100+
{:result/error (format "`%s` is disabled in this Yetibot" cmd)})
98101
;; couldn't find any sub commands so default to help.
99102
(:value
100-
(yetibot.core.handler/handle-unparsed-expr
101-
(str "help " (get @re-prefix->topic (str cmd-re))))))
103+
(yetibot.core.handler/handle-unparsed-expr
104+
(str "help " (get @re-prefix->topic (str cmd-re))))))
102105
(callback cmd-with-args extra)))))
103106

104107
;; Hook the actual handle-cmd called during interpretation.

src/yetibot/core/loader.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
(defn load-ns [arg]
3434
(debug "Loading" arg)
35-
(try (require arg :reload)
35+
(try (require arg)
3636
arg
3737
(catch Exception e
3838
(warn "WARNING: problem requiring" arg "hook:" (.getMessage e))

src/yetibot/core/util/command.clj

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,60 @@
55
[yetibot.core.models.help :as help]
66
[yetibot.core.parser :refer [parser]]))
77

8+
(s/def ::whitelist-config (s/coll-of string?))
9+
(s/def ::blacklist-config (s/coll-of string?))
10+
11+
(defn pattern-config-set
12+
[spec path]
13+
(set
14+
(->>
15+
(get-config spec path)
16+
:value
17+
(map re-pattern))))
18+
19+
(defn whitelist []
20+
(pattern-config-set ::whitelist-config [:command :whitelist]))
21+
22+
(defn blacklist []
23+
(pattern-config-set ::blacklist-config [:command :blacklist]))
24+
25+
(defn throw-config-error! []
26+
(throw
27+
(ex-info
28+
"Invalid configuration: whitelist and blacklist cannot both be specified"
29+
{:whitelist whitelist
30+
:blacklist blacklist})))
31+
32+
;; check config and error on startup if invalid
33+
(when (and (seq (whitelist)) (seq (blacklist)))
34+
(throw-config-error!))
35+
36+
(defn any-match? [patterns s]
37+
(some #(re-find % s) patterns))
38+
39+
(defn command-enabled?
40+
"Given a command prefix, determine whether or not it is enabled.
41+
42+
Users can specify either a whitelist collection of command patterns or a
43+
blacklist collection of patterns, but not both.
44+
45+
If a whitelist is specified, all commands are disabled *except* those in the
46+
whitelist.
47+
48+
If a blacklist is specified, all commands are enabled *except* those in the
49+
blacklist."
50+
[command]
51+
(boolean
52+
(cond
53+
;; blow up if both
54+
(and (seq (whitelist)) (seq (blacklist))) (throw-config-error!)
55+
;; whitelist checking
56+
(seq (whitelist)) (any-match? (whitelist) command)
57+
;; blacklist checking
58+
(seq (blacklist)) (not (any-match? (blacklist) command))
59+
;; neither blacklist nor whitelist are configured
60+
:else true)))
61+
862
(defn error?
963
"Determine whether a value is an error map"
1064
[x]

src/yetibot/core/webapp/route_loader.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
;; TODO: remove dupe between loader ns and this
1212
(defn load-ns [arg]
1313
(info "Loading" arg)
14-
(try (require arg :reload)
14+
(try (require arg)
1515
arg
1616
(catch Exception e
1717
(warn "WARNING: problem requiring" arg (.getMessage e))
Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
(ns yetibot.core.test.util.command
22
(:require
3-
;; yetibot.core.commands.echo
4-
[clojure.test :refer :all]
5-
[yetibot.core.util.command :refer :all]))
3+
yetibot.core.commands.echo
4+
[midje.sweet :refer [fact facts => against-background]]
5+
[yetibot.core.util.command :refer [whitelist blacklist
6+
embedded-cmds command-enabled?]]))
67

7-
;; embedded commands
8+
(facts "About embedded commands"
9+
(fact "Embedded commands that aren't actually known commands are not parsed"
10+
(embedded-cmds "`these` are the `invalid embedded commands`") => empty?)
11+
(fact
12+
"Known embedded commands are properly extracted"
13+
(embedded-cmds "`echo your temp:` wonder what the `temp 98101` is")
14+
=> [[:expr
15+
[:cmd
16+
[:words "echo" [:space " "] "your" [:space " "] "temp:"]]]]))
817

9-
(deftest test-embedded-cmds
10-
(testing
11-
"Embedded commands that aren't actually known commands are not parsed"
12-
(is
13-
(empty?
14-
(embedded-cmds "`these` are the `invalid embedded commands`"))))
18+
(facts "About whitelists and blacklists"
19+
(against-background
20+
[(whitelist) => #{#"list"} (blacklist) => #{}]
21+
(fact "echo is not whitelisted"
22+
(command-enabled? "echo") => false)
23+
(fact "list is whitelisted"
24+
(command-enabled? "list") => true))
1525

16-
(testing
17-
"Known embedded commands are properly extracted"
18-
(is
19-
(=
20-
;; temp shouldn't be included because it's not a command/alias in the
21-
;; test env
22-
(embedded-cmds "`echo your temp:` wonder what the `temp 98101` is")
23-
[[:expr
24-
[:cmd
25-
[:words "echo" [:space " "] "your" [:space " "] "temp:"]]]]))))
26+
(against-background
27+
[(whitelist) => #{} (blacklist) => #{#"list"}]
28+
(fact "echo is not blacklisted"
29+
(command-enabled? "echo") => true)
30+
(fact "list is blacklisted"
31+
(command-enabled? "list") => false))
32+
33+
(against-background
34+
[(whitelist) => #{} (blacklist) => #{}]
35+
(fact "echo is enabled by default"
36+
(command-enabled? "echo") => true)
37+
(fact "list is enabled by default"
38+
(command-enabled? "list") => true)))

tests.edn

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#kaocha/v1
2+
{:tests [{:id :unit
3+
:type :kaocha.type/midje
4+
;; OPTIONAL. default value is "test"
5+
:test-paths ["test"]
6+
;; OPTIONAL. default value is ".*-test"
7+
:ns-patterns [".*"]
8+
}]}

0 commit comments

Comments
 (0)