chook 1.0.1.b1 → 1.1.5b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +56 -0
  3. data/README.md +397 -145
  4. data/bin/chook-server +31 -1
  5. data/data/chook.conf.example +183 -0
  6. data/data/com.pixar.chook-server.plist +20 -0
  7. data/data/sample_handlers/RestAPIOperation.rb +11 -11
  8. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
  9. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
  10. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
  11. data/lib/chook/configuration.rb +27 -8
  12. data/lib/chook/event.rb +6 -1
  13. data/lib/chook/event/handled_event.rb +36 -9
  14. data/lib/chook/event/handled_event/handlers.rb +252 -99
  15. data/lib/chook/event/handled_event_logger.rb +86 -0
  16. data/lib/chook/event_handling.rb +1 -0
  17. data/lib/chook/foundation.rb +3 -0
  18. data/lib/chook/procs.rb +17 -1
  19. data/lib/chook/server.rb +73 -72
  20. data/lib/chook/server/auth.rb +164 -0
  21. data/lib/chook/server/log.rb +215 -0
  22. data/lib/chook/server/public/css/chook.css +133 -0
  23. data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
  24. data/lib/chook/server/public/js/chook.js +126 -0
  25. data/lib/chook/server/public/js/logstream.js +101 -0
  26. data/lib/chook/server/routes.rb +28 -0
  27. data/lib/chook/server/routes/handle_by_name.rb +65 -0
  28. data/lib/chook/server/routes/handle_webhook_event.rb +27 -3
  29. data/lib/chook/server/routes/handlers.rb +52 -0
  30. data/lib/chook/server/routes/home.rb +48 -1
  31. data/lib/chook/server/routes/log.rb +105 -0
  32. data/lib/chook/server/routes/login_logout.rb +48 -0
  33. data/lib/chook/server/views/admin.haml +11 -0
  34. data/lib/chook/server/views/bak.haml +48 -0
  35. data/lib/chook/server/views/config.haml +15 -0
  36. data/lib/chook/server/views/handlers.haml +63 -0
  37. data/lib/chook/server/views/layout.haml +64 -0
  38. data/lib/chook/server/views/logstream.haml +33 -0
  39. data/lib/chook/server/views/sketch_admin +44 -0
  40. data/lib/chook/subject.rb +13 -2
  41. data/lib/chook/subject/dep_device.rb +81 -0
  42. data/lib/chook/subject/policy_finished.rb +43 -0
  43. data/lib/chook/subject/smart_group.rb +6 -0
  44. data/lib/chook/subject/test_subject.rb +2 -2
  45. data/lib/chook/version.rb +1 -1
  46. metadata +78 -17
@@ -1,36 +1,45 @@
1
- ### Copyright 2017 Pixar
2
-
3
- ###
4
- ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
- ### with the following modification; you may not use this file except in
6
- ### compliance with the Apache License and the following modification to it:
7
- ### Section 6. Trademarks. is deleted and replaced with:
8
- ###
9
- ### 6. Trademarks. This License does not grant permission to use the trade
10
- ### names, trademarks, service marks, or product names of the Licensor
11
- ### and its affiliates, except as required to comply with Section 4(c) of
12
- ### the License and to reproduce the content of the NOTICE file.
13
- ###
14
- ### You may obtain a copy of the Apache License at
15
- ###
16
- ### http://www.apache.org/licenses/LICENSE-2.0
17
- ###
18
- ### Unless required by applicable law or agreed to in writing, software
19
- ### distributed under the Apache License with the above modification is
20
- ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
- ### KIND, either express or implied. See the Apache License for the specific
22
- ### language governing permissions and limitations under the Apache License.
23
- ###
24
- ###
1
+ # Copyright 2017 Pixar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "Apache License")
4
+ # with the following modification; you may not use this file except in
5
+ # compliance with the Apache License and the following modification to it:
6
+ # Section 6. Trademarks. is deleted and replaced with:
7
+ #
8
+ # 6. Trademarks. This License does not grant permission to use the trade
9
+ # names, trademarks, service marks, or product names of the Licensor
10
+ # and its affiliates, except as required to comply with Section 4(c) of
11
+ # the License and to reproduce the content of the NOTICE file.
12
+ #
13
+ # You may obtain a copy of the Apache License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the Apache License with the above modification is
19
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20
+ # KIND, either express or implied. See the Apache License for the specific
21
+ # language governing permissions and limitations under the Apache License.
22
+ #
23
+ #
24
+
25
25
  module Chook
26
26
 
27
- # This method is used by the Ruby event-handler files.
27
+ # This method is used by the Ruby 'internal' event-handler files.
28
+ #
29
+ # those handlers are defined by passing a block to this method, like so:
30
+ #
31
+ # Chook.event_handler do |event|
32
+ # # so something with the event
33
+ # end
28
34
  #
29
- # Loading them should call this method and pass in a block
35
+ # Loading them will call this method and pass in a block
30
36
  # with one parameter: a Chook::HandledEvent subclass instance.
31
37
  #
32
- # The block is then converted to a Proc instance in @loaded_event_handler
33
- # and from there can be stored for use by the event identified by the filename.
38
+ # The block is then converted to a #handle method in an anonymous object.
39
+ # The object is stored for use by the event identified by the filename.
40
+ #
41
+ # By storing it as a method in an object, the handlers themselves
42
+ # can use #break or #return to exit (or even #next)
34
43
  #
35
44
  # NOTE: the files should be read with 'load' not 'require', so that they can
36
45
  # be re-loaded as needed
@@ -44,7 +53,13 @@ module Chook
44
53
  # @return [Proc] the block converted to a Proc
45
54
  #
46
55
  def self.event_handler(&block)
47
- HandledEvent::Handlers.loaded_handler = Proc.new(&block)
56
+ obj = Object.new
57
+ obj.define_singleton_method(:handle, &block)
58
+ # Loading the file created the object by calling this method
59
+ # but to access it after loading the file, we need to
60
+ # store it in here:
61
+ HandledEvent::Handlers.loaded_handler = obj
62
+ Chook.logger.debug "Code block for 'Chook.event_handler' loaded into \#handle method of runner-object #{obj.object_id}"
48
63
  end
49
64
 
50
65
  # the server class
@@ -53,56 +68,101 @@ module Chook
53
68
  # The Handlers namespace module
54
69
  module Handlers
55
70
 
56
- # Module Constants
57
- ############################
58
-
59
71
  DEFAULT_HANDLER_DIR = '/Library/Application Support/Chook'.freeze
60
72
 
61
- # Module Instance Variables, & accessors
62
- ############################
73
+ # Handlers that are only called by name using the route:
74
+ # post '/handler/:handler_name'
75
+ # are located in this subdirection of the handler directory
76
+ NAMED_HANDLER_SUBDIR = 'NamedHandlers'.freeze
63
77
 
64
- # This holds the most recently loaded Proc handler
65
- # until it can be stored in the @handlers Hash
66
- @loaded_handler = nil
78
+ # internal handler files must match this regex somewhere
79
+ INTERNAL_HANDLER_BLOCK_START_RE = /Chook.event_handler( ?\{| do) *\|/
67
80
 
68
- # Getter for @loaded_handler
81
+ # self loaded_handler=
69
82
  #
70
- # @return [Proc,nil] the most recent Proc loaded from a handler file.
83
+ # @return [Obj,nil] the most recent Proc loaded from a handler file.
71
84
  # destined for storage in @handlers
72
85
  #
73
86
  def self.loaded_handler
74
87
  @loaded_handler
75
88
  end
76
89
 
77
- # Setter for @loaded_event_handler
90
+ # A holding place for internal handlers as they are loaded
91
+ # before being added to the @handlers Hash
92
+ # see Chook.event_handler(&block)
78
93
  #
79
- # @param a_proc [Proc] a Proc object for storage in @handlers
94
+ # @param a_proc [Object] An object instance with a #handle method
80
95
  #
81
- def self.loaded_handler=(a_proc)
82
- @loaded_handler = a_proc
96
+ def self.loaded_handler=(anon_obj)
97
+ @loaded_handler = anon_obj
83
98
  end
84
99
 
85
- # A hash of loaded handlers.
86
- # Keys are Strings - the name of the events handled
87
- # Values are Arrays of either Procs, or Pathnames to executable files.
88
- # See the .handlers getter Methods
89
- @handlers = {}
90
-
91
- # Getter for @event_handlers
100
+ # Getter for @handlers
101
+ #
102
+ # @return [Hash{String => Array}] a mapping of Event Names as they
103
+ # come from the JSS to an Array of handlers for the event.
104
+ # The handlers are either Pathnames to executable external handlers
105
+ # or Objcts with a #handle method, for internal handlers
106
+ # (The objects also have a #handler_file attribute that is the Pathname)
92
107
  #
93
- # @return [Hash{String => Array}] a mapping of Event Names as the come from
94
- # the JSS to an Array of handlers for the event. The handlers are either
95
- # Proc objects to call from within ruby, or Pathnames to executable files
96
- # which will take raw JSON on stdin.
97
108
  def self.handlers
98
- @handlers
109
+ @handlers ||= {}
99
110
  end
100
111
 
101
- # Module Methods
102
- ############################
112
+ # Handlers can check Chook::HandledEvent::Handlers.reloading?
113
+ # and do stuff if desired.
114
+ def self.reloading?
115
+ @reloading
116
+ end
117
+
118
+ # getter for @named_handlers
119
+ # These handlers are called by name via the route
120
+ # " post '/handler/:handler_name'"
121
+ #
122
+ # They are not tied to any event type by their filenames
123
+ # its up to the writers of the handlers to make sure
124
+ # the webhook that calls them is sending the correct event
125
+ # type.
126
+ #
127
+ # The data structure of @named_handlers is a
128
+ # Hash of Strings to Pathnames or Anon Objects:
129
+ # {
130
+ # handler_filename => Pathname or Obj,
131
+ # handler_filename => Pathname or Obj,
132
+ # handler_filename => Pathname or Obj
133
+ # }
134
+ #
135
+ # @return [Hash {String => Pathname, Proc}]
136
+ def self.named_handlers
137
+ @named_handlers ||= {}
138
+ end
139
+
140
+ # the Pathname objects for all loaded handlers
141
+ #
142
+ # @return [Array<Pathname>]
143
+ #
144
+ def self.all_handler_paths
145
+ hndlrs = named_handlers.values
146
+ hndlrs += handlers.values.flatten
147
+ hndlrs.map do |hndlr|
148
+ hndlr.is_a?(Pathname) ? hndlr : hndlr.handler_file
149
+ end
150
+ end
103
151
 
104
152
  # Load all the event handlers from the handler_dir or an arbitrary dir.
105
153
  #
154
+ #
155
+ # Handler files must be either:
156
+ # - An executable file, which will have the raw JSON from the JSS piped
157
+ # to it's stdin when executed
158
+ # or
159
+ # - A non-executable file of ruby code like this:
160
+ # Chook.event_handler do |event|
161
+ # # your code goes here.
162
+ # end
163
+ #
164
+ # (see the Chook README for details about writing the ruby handlers)
165
+ #
106
166
  # @param from_dir [String, Pathname] directory from which to load the
107
167
  # handlers. Defaults to CONFIG.handler_dir or DEFAULT_HANDLER_DIR if
108
168
  # config is unset
@@ -112,27 +172,49 @@ module Chook
112
172
  #
113
173
  # @return [void]
114
174
  #
115
- def self.load_handlers(from_dir: Chook::CONFIG.handler_dir, reload: false)
175
+ def self.load_handlers(from_dir: Chook.config.handler_dir, reload: false)
176
+ # use default if needed
116
177
  from_dir ||= DEFAULT_HANDLER_DIR
178
+ handler_dir = Pathname.new(from_dir)
179
+ named_handler_dir = handler_dir + NAMED_HANDLER_SUBDIR
180
+ load_type = 'Loading'
181
+
117
182
  if reload
118
- @handlers_loaded_from = nil
183
+ @reloading = true
119
184
  @handlers = {}
185
+ @named_handlers = {}
120
186
  @loaded_handler = nil
187
+ load_type = 'Re-loading'
121
188
  end
122
189
 
123
- handler_dir = Pathname.new(from_dir)
124
- return unless handler_dir.directory? && handler_dir.readable?
190
+ # General Handlers
191
+ Chook.logger.info "#{load_type} general handlers from directory: #{handler_dir}"
192
+ if handler_dir.directory? && handler_dir.readable?
193
+ handler_dir.children.each do |handler_file|
194
+ load_general_handler(handler_file) if handler_file.file? && handler_file.readable?
195
+ end
196
+ Chook.logger.info handlers.empty? ? 'No general handlers found' : "Loaded #{handlers.values.flatten.size} general handlers for #{handlers.keys.size} event triggers"
197
+ else
198
+ Chook.logger.error "General handler directory '#{from_dir}' not a readable directory. No general handlers loaded. "
199
+ end
125
200
 
126
- handler_dir.children.each do |handler_file|
127
- load_handler(handler_file) if handler_file.file? && handler_file.readable?
201
+ # Named Handlers
202
+ Chook.logger.info "#{load_type} named handlers from directory: #{named_handler_dir}"
203
+ if named_handler_dir.directory? && named_handler_dir.readable?
204
+ named_handler_dir.children.each do |handler_file|
205
+ load_named_handler(handler_file) if handler_file.file? && handler_file.readable?
206
+ end
207
+ Chook.logger.info "Loaded #{named_handlers.size} named handlers"
208
+ else
209
+ Chook.logger.error "Named handler directory '#{named_handler_dir}' not a readable directory. No named handlers loaded. "
128
210
  end
129
211
 
130
- @handlers_loaded_from = handler_dir
131
- @handlers.values.flatten.size
212
+ @reloading = false
132
213
  end # load handlers
133
214
 
134
- # Load an event handler from a file.
135
- # Handler files must begin with the name of the event they handle,
215
+ # Load a general event handler from a file.
216
+ #
217
+ # General Handler files must begin with the name of the event they handle,
136
218
  # e.g. ComputerAdded, followed by: nothing, a dot, a dash, or
137
219
  # and underscore. Case doesn't matter.
138
220
  # So all of these are OK:
@@ -142,49 +224,113 @@ module Chook
142
224
  # Computeradded-update-ldap
143
225
  # There can be as many as desired for each event.
144
226
  #
145
- # Each must be either:
146
- # - An executable file, which will have the raw JSON from the JSS piped
147
- # to it's stdin when executed
148
- # or
149
- # - A non-executable file of ruby code like this:
150
- # Chook.event_handler do |event|
151
- # # your code goes here.
152
- # end
153
- #
154
- # (see the Chook README for details about writing the ruby handlers)
155
- #
156
- # @param from_file [Pathname] the file from which to load the handler
227
+ # @param handler_file [Pathname] the file from which to load the handler
157
228
  #
158
229
  # @return [void]
159
230
  #
160
- def self.load_handler(from_file)
161
- handler_file = Pathname.new from_file
231
+ def self.load_general_handler(handler_file)
232
+ Chook.logger.debug "Starting load of general handler file '#{handler_file.basename}'"
233
+
162
234
  event_name = event_name_from_handler_filename(handler_file)
163
- return unless event_name
235
+ unless event_name
236
+ Chook.logger.debug "Ignoring general handler file '#{handler_file.basename}': Filename doesn't start with event name"
237
+ return
238
+ end
164
239
 
165
240
  # create an array for this event's handlers, if needed
166
- @handlers[event_name] ||= []
241
+ handlers[event_name] ||= []
167
242
 
243
+ # external? if so, its executable and we only care about its pathname
168
244
  if handler_file.executable?
169
- # store as a Pathname, we'll pipe JSON to it
170
- unless @handlers[event_name].include? handler_file
171
- @handlers[event_name] << handler_file
172
- puts "===> Loaded executable handler file '#{handler_file.basename}'"
173
- end
245
+ Chook.logger.info "Loading external general handler file '#{handler_file.basename}' for #{event_name} events"
246
+ handlers[event_name] << handler_file
174
247
  return
175
248
  end
176
249
 
177
- # load the file. If written correctly, it will
178
- # put a Proc into @loaded_handler
179
- load handler_file.to_s
180
- if @loaded_handler
181
- @handlers[event_name] << @loaded_handler
182
- puts "===> Loaded internal handler file '#{handler_file.basename}'"
183
- @loaded_handler = nil
250
+ # Internal, we store an object with a .handle method
251
+ Chook.logger.info "Loading internal general handler file '#{handler_file.basename}' for #{event_name} events"
252
+ load_internal_handler handler_file
253
+ handlers[event_name] << @loaded_handler if @loaded_handler
254
+
255
+ end # self.load_general_handler(handler_file)
256
+
257
+ # Load a named event handler from a file.
258
+ #
259
+ # Named Handler files can have any name, as they are called directly
260
+ # from a Jamf webhook via URL.
261
+ #
262
+ # @param handler_file [Pathname] the file from which to load the handler
263
+ #
264
+ # @return [void]
265
+ #
266
+ def self.load_named_handler(handler_file)
267
+ Chook.logger.debug "Starting load of named handler file '#{handler_file.basename}'"
268
+
269
+ # external? if so, its executable and we only care about its pathname
270
+ if handler_file.executable?
271
+ Chook.logger.info "Loading external named handler file '#{handler_file.basename}'"
272
+ named_handlers[handler_file.basename.to_s] = handler_file
273
+ return
274
+ end
275
+
276
+ # Internal, we store an object with a .handle method
277
+ Chook.logger.info "Loading internal named handler file '#{handler_file.basename}'"
278
+ load_internal_handler handler_file
279
+ named_handlers[handler_file.basename.to_s] = @loaded_handler if @loaded_handler
280
+ end # self.load_general_handler(handler_file)
281
+
282
+ # if the given file is executable, store it's path as a handler for the event
283
+ #
284
+ # @return [Boolean] did we load an external handler?
285
+ #
286
+ def self.load_external_handler(handler_file, event_name, named)
287
+ return false unless handler_file.executable?
288
+
289
+ say_named = named ? 'named ' : ''
290
+ Chook.logger.info "Loading #{say_named}external handler file '#{handler_file.basename}' for #{event_name} events"
291
+
292
+ if named
293
+ named_handlers[event_name][handler_file.basename.to_s] = handler_file
184
294
  else
185
- puts "===> FAILED loading internal handler file '#{handler_file.basename}'"
295
+ # store the Pathname, we'll pipe JSON to it
296
+ handlers[event_name] << handler_file
297
+ end
298
+
299
+ true
300
+ end
301
+
302
+ # if a given path is not executable, try to load it as an internal handler
303
+ #
304
+ # @param handler_file[Pathname] the handler file
305
+ #
306
+ # @return [Object] and anonymous object that has a .handle method
307
+ #
308
+ def self.load_internal_handler(handler_file)
309
+ # load the file. If written correctly, it will
310
+ # put an anon. Object with a #handle method into @loaded_handler
311
+ unless handler_file.read =~ INTERNAL_HANDLER_BLOCK_START_RE
312
+ Chook.logger.error "Internal handler file '#{handler_file}' missing event_handler block"
313
+ return nil
314
+ end
315
+
316
+ # reset @loaded_handler - the `load` call will refill it
317
+ # see Chook.event_handler
318
+ @loaded_handler = nil
319
+
320
+ begin
321
+ load handler_file.to_s
322
+ raise '@loaded handler nil after loading file' unless @loaded_handler
323
+ rescue => e
324
+ Chook.logger.error "FAILED loading internal handler file '#{handler_file}': #{e}"
325
+ return
186
326
  end
187
- end # self.load_handler(handler_file)
327
+
328
+ # add a method to the object to get its Pathname
329
+ @loaded_handler.define_singleton_method(:handler_file) { handler_file }
330
+
331
+ # return it
332
+ @loaded_handler
333
+ end
188
334
 
189
335
  # Given a handler filename, return the event name it wants to handle
190
336
  #
@@ -194,9 +340,16 @@ module Chook
194
340
  # @return [String,nil] The matching event name or nil if no match
195
341
  #
196
342
  def self.event_name_from_handler_filename(filename)
343
+ filename = filename.basename
197
344
  @event_names ||= Chook::Event::EVENTS.keys
198
- desired_event_name = filename.basename.to_s.split(/\.|-|_/).first
199
- @event_names.select { |n| desired_event_name.casecmp(n).zero? }.first
345
+ desired_event_name = filename.to_s.split(/\.|-|_/).first
346
+ ename = @event_names.select { |n| desired_event_name.casecmp(n).zero? }.first
347
+ if ename
348
+ Chook.logger.debug "Found event name '#{ename}' at start of filename '#{filename}'"
349
+ else
350
+ Chook.logger.debug "No known event name at start of filename '#{filename}'"
351
+ end
352
+ ename
200
353
  end
201
354
 
202
355
  end # module Handler
@@ -0,0 +1,86 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ module Chook
27
+
28
+ # a simple object embedded in a Handled Event that
29
+ # allows a standardize way to note event-related log entries
30
+ # with the event object_id.
31
+ #
32
+ # Every Handled Event has one of these instances exposed in it's
33
+ # #logger attribute, and usable from within 'internal' handlers
34
+ #
35
+ # Here's an example.
36
+ #
37
+ # Say you have a ComputerSmartGroupMembershipChanged event
38
+ #
39
+ # calling `event.logger.info "foobar"` will generate the log message:
40
+ #
41
+ # Event 1234567: foobar
42
+ #
43
+ class HandledEventLogger
44
+
45
+ def initialize(event)
46
+ @event = event
47
+ end
48
+
49
+ def event_message(msg)
50
+ "Event #{@event.id}: #{msg}"
51
+ end
52
+
53
+ def debug(msg)
54
+ Chook::Server::Log.logger.debug event_message(msg)
55
+ end
56
+
57
+ def info(msg)
58
+ Chook::Server::Log.logger.info event_message(msg)
59
+ end
60
+
61
+ def warn(msg)
62
+ Chook::Server::Log.logger.warn event_message(msg)
63
+ end
64
+
65
+ def error(msg)
66
+ Chook::Server::Log.logger.error event_message(msg)
67
+ end
68
+
69
+ def fatal(msg)
70
+ Chook::Server::Log.logger.fatal event_message(msg)
71
+ end
72
+
73
+ def unknown(msg)
74
+ Chook::Server::Log.logger.unknown event_message(msg)
75
+ end
76
+
77
+ # log an exception - multiple log lines
78
+ # the first being the error message the rest being indented backtrace
79
+ def log_exception(exception)
80
+ error "#{exception.class}: #{exception}"
81
+ exception.backtrace.each { |l| error "..#{l}" }
82
+ end
83
+
84
+ end # class HandledEventLogger
85
+
86
+ end # module