chook 1.0.0.b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +174 -0
  3. data/README.md +259 -0
  4. data/bin/chook-server +28 -0
  5. data/data/sample_handlers/RestAPIOperation-executable +91 -0
  6. data/data/sample_handlers/RestAPIOperation.rb +45 -0
  7. data/data/sample_handlers/SmartGroupComputerMembershipChange-executable +47 -0
  8. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +33 -0
  9. data/data/sample_jsons/ComputerAdded.json +27 -0
  10. data/data/sample_jsons/ComputerCheckIn.json +27 -0
  11. data/data/sample_jsons/ComputerInventoryCompleted.json +27 -0
  12. data/data/sample_jsons/ComputerPolicyFinished.json +27 -0
  13. data/data/sample_jsons/ComputerPushCapabilityChanged.json +27 -0
  14. data/data/sample_jsons/JSSShutdown.json +14 -0
  15. data/data/sample_jsons/JSSStartup.json +14 -0
  16. data/data/sample_jsons/MobileDeviceCheckIn.json +26 -0
  17. data/data/sample_jsons/MobileDeviceCommandCompleted.json +26 -0
  18. data/data/sample_jsons/MobileDeviceEnrolled.json +26 -0
  19. data/data/sample_jsons/MobileDevicePushSent.json +26 -0
  20. data/data/sample_jsons/MobileDeviceUnEnrolled.json +26 -0
  21. data/data/sample_jsons/PatchSoftwareTitleUpdated.json +14 -0
  22. data/data/sample_jsons/PushSent.json +11 -0
  23. data/data/sample_jsons/README +4 -0
  24. data/data/sample_jsons/RestAPIOperation.json +15 -0
  25. data/data/sample_jsons/SCEPChallenge.json +10 -0
  26. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +13 -0
  27. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +13 -0
  28. data/lib/chook.rb +38 -0
  29. data/lib/chook/configuration.rb +198 -0
  30. data/lib/chook/event.rb +153 -0
  31. data/lib/chook/event/handled_event.rb +154 -0
  32. data/lib/chook/event/handled_event/handlers.rb +206 -0
  33. data/lib/chook/event/test_event.rb +140 -0
  34. data/lib/chook/event_handling.rb +40 -0
  35. data/lib/chook/event_testing.rb +43 -0
  36. data/lib/chook/foundation.rb +33 -0
  37. data/lib/chook/handled_events.rb +33 -0
  38. data/lib/chook/handled_subjects.rb +33 -0
  39. data/lib/chook/procs.rb +46 -0
  40. data/lib/chook/server.rb +121 -0
  41. data/lib/chook/server/routes.rb +27 -0
  42. data/lib/chook/server/routes/handle_webhook_event.rb +39 -0
  43. data/lib/chook/server/routes/home.rb +37 -0
  44. data/lib/chook/subject.rb +143 -0
  45. data/lib/chook/subject/computer.rb +121 -0
  46. data/lib/chook/subject/handled_subject.rb +84 -0
  47. data/lib/chook/subject/jss.rb +56 -0
  48. data/lib/chook/subject/mobile_device.rb +115 -0
  49. data/lib/chook/subject/patch_software_title_update.rb +55 -0
  50. data/lib/chook/subject/push.rb +38 -0
  51. data/lib/chook/subject/randomizers.rb +506 -0
  52. data/lib/chook/subject/rest_api_operation.rb +62 -0
  53. data/lib/chook/subject/samplers.rb +360 -0
  54. data/lib/chook/subject/scep_challenge.rb +32 -0
  55. data/lib/chook/subject/smart_group.rb +50 -0
  56. data/lib/chook/subject/test_subject.rb +195 -0
  57. data/lib/chook/subject/validators.rb +117 -0
  58. data/lib/chook/test_events.rb +33 -0
  59. data/lib/chook/test_subjects.rb +33 -0
  60. data/lib/chook/version.rb +32 -0
  61. metadata +129 -0
@@ -0,0 +1,154 @@
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
+ require 'chook/event/handled_event/handlers'
27
+
28
+ #
29
+ module Chook
30
+
31
+ # Load sample JSON files, one per event type
32
+ @sample_jsons = {}
33
+ base_dir = Pathname.new(__FILE__)
34
+ data_dir = base_dir.parent.parent.parent.parent
35
+ sample_json_dir = Pathname.new(data_dir.to_s + '/data/sample_jsons')
36
+ sample_json_dir.children.each do |jf|
37
+ event = jf.basename.to_s.chomp(jf.extname).to_sym
38
+ @sample_jsons[event] = jf.read
39
+ end
40
+
41
+ def self.sample_jsons
42
+ @sample_jsons
43
+ end
44
+
45
+ # An event that has been recieved and needs to be handled.
46
+ #
47
+ # This is the parent class to all of the classes in the
48
+ # Chook::HandledEvents module, which are dynamically defined when this
49
+ # file is loaded.
50
+ #
51
+ # All constants, methods, and attributes that are common to HandledEvent
52
+ # classes are defined here, including the interaction with the Handlers
53
+ # module.
54
+ #
55
+ # Subclasses are automatically generated from the keys and values of
56
+ # Chook::Event::EVENTS
57
+ #
58
+ # Each subclass will have a constant SUBJECT_CLASS containing the
59
+ # class of their #subject attribute.
60
+ #
61
+ class HandledEvent < Chook::Event
62
+
63
+ #### Class Methods
64
+
65
+ # Given some raw_json from the jss, create and return the correct
66
+ # HandledEvent subclass
67
+
68
+ # For each event type in Chook::Event::EVENTS
69
+ # generate a class for it, set its SUBJECT_CLASS constant
70
+ # and add it to the HandledEvents module.
71
+ #
72
+ # @return [void]
73
+ #
74
+ def self.generate_classes
75
+ Chook::Event::EVENTS.each do |class_name, subject|
76
+ next if Chook::HandledEvents.const_defined? class_name
77
+
78
+ # make the new HandledEvent subclass
79
+ the_class = Class.new(Chook::HandledEvent)
80
+
81
+ # Set its EVENT_NAME constant, which is used
82
+ # for finding it's handlers, among other things.
83
+ the_class.const_set Chook::Event::EVENT_NAME_CONST, class_name
84
+
85
+ # Set its SUBJECT_CLASS constant to the appropriate
86
+ # class in the HandledSubjects module.
87
+ the_class.const_set Chook::Event::SUBJECT_CLASS_CONST, Chook::HandledSubjects.const_get(subject)
88
+
89
+ # Add the new class to the HandledEvents module.
90
+ Chook::HandledEvents.const_set(class_name, the_class)
91
+ end # each classname, subject
92
+ end # self.generate_classes
93
+
94
+ # Given the raw json from the JSS webhook,
95
+ # create an object of the correct Event subclass
96
+ #
97
+ # @param [String] raw_event_json The JSON http POST content from the JSS
98
+ #
99
+ # @return [JSSWebHooks::Event subclass] the Event subclass matching the event
100
+ #
101
+ def self.parse_event(raw_event_json)
102
+ event_json = JSON.parse(raw_event_json, symbolize_names: true)
103
+ event_name = event_json[:webhook][:webhookEvent]
104
+ Chook::HandledEvents.const_get(event_name).new raw_event_json
105
+ end
106
+
107
+ #### Attributes
108
+
109
+ # @return [Array<Proc,Pathname>] the handlers defined for this event.
110
+ # Each is either a proc, in which case it is called with this
111
+ # instance as its sole paramter, or its a Pathname to an executable
112
+ # file, in which case the @raw_json is passed to its stdin.
113
+ # See the Chook::HandledEvent::Handlers module.
114
+ attr_reader :handlers
115
+
116
+ #### Constructor
117
+
118
+ # Handled Events are always built from raw_json.
119
+ #
120
+ def initialize(raw_event_json)
121
+ super raw_json: raw_event_json
122
+ end # init
123
+
124
+ def handle
125
+ handlers = Handlers.handlers[self.class.const_get(Chook::Event::EVENT_NAME_CONST)]
126
+ handlers.each do |handler|
127
+ case handler
128
+ when Pathname
129
+ pipe_to_executable handler
130
+ when Proc
131
+ handle_with_proc handler
132
+ end # case
133
+ end # @handlers.each do |handler|
134
+
135
+ # the handle method should return a string,
136
+ # which is the body of the HTTP result for
137
+ # POSTing the event
138
+ "Processed by #{handlers.count} handlers\n"
139
+ end # def handle
140
+
141
+ # TODO: Add something here that cleans up old threads and forks
142
+ def pipe_to_executable(handler)
143
+ _thread = Thread.new do
144
+ IO.popen([handler.to_s], 'w') { |h| h.puts @raw_json }
145
+ end
146
+ end
147
+
148
+ def handle_with_proc(handler)
149
+ _thread = Thread.new { handler.call self }
150
+ end
151
+
152
+ end # class HandledEvent
153
+
154
+ end # module
@@ -0,0 +1,206 @@
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
+ module Chook
26
+
27
+ # This method is used by the Ruby event-handler files.
28
+ #
29
+ # Loading them should call this method and pass in a block
30
+ # with one parameter: a Chook::HandledEvent subclass instance.
31
+ #
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.
34
+ #
35
+ # NOTE: the files should be read with 'load' not 'require', so that they can
36
+ # be re-loaded as needed
37
+ #
38
+ # @see_also Chook::load_handlers
39
+ #
40
+ # @param [Block] block the block to be used as an event handler
41
+ #
42
+ # @yieldparam [JSS::WebHooks::Event subclass] The event to be handled
43
+ #
44
+ # @return [Proc] the block converted to a Proc
45
+ #
46
+ def self.event_handler(&block)
47
+ HandledEvent::Handlers.loaded_handler = Proc.new(&block)
48
+ end
49
+
50
+ # the server class
51
+ class HandledEvent < Event
52
+
53
+ # The Handlers namespace module
54
+ module Handlers
55
+
56
+ # Module Constants
57
+ ############################
58
+
59
+ DEFAULT_HANDLER_DIR = '/Library/Application Support/Chook'.freeze
60
+
61
+ # Module Instance Variables, & accessors
62
+ ############################
63
+
64
+ # This holds the most recently loaded Proc handler
65
+ # until it can be stored in the @handlers Hash
66
+ @loaded_handler = nil
67
+
68
+ # Getter for @loaded_handler
69
+ #
70
+ # @return [Proc,nil] the most recent Proc loaded from a handler file.
71
+ # destined for storage in @handlers
72
+ #
73
+ def self.loaded_handler
74
+ @loaded_handler
75
+ end
76
+
77
+ # Setter for @loaded_event_handler
78
+ #
79
+ # @param a_proc [Proc] a Proc object for storage in @handlers
80
+ #
81
+ def self.loaded_handler=(a_proc)
82
+ @loaded_handler = a_proc
83
+ end
84
+
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
92
+ #
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
+ def self.handlers
98
+ @handlers
99
+ end
100
+
101
+ # Module Methods
102
+ ############################
103
+
104
+ # Load all the event handlers from the handler_dir or an arbitrary dir.
105
+ #
106
+ # @param from_dir [String, Pathname] directory from which to load the
107
+ # handlers. Defaults to CONFIG.handler_dir or DEFAULT_HANDLER_DIR if
108
+ # config is unset
109
+ #
110
+ # @param reload [Boolean] should we reload handlers if they've already
111
+ # been loaded?
112
+ #
113
+ # @return [void]
114
+ #
115
+ def self.load_handlers(from_dir: Chook::CONFIG.handler_dir, reload: false)
116
+ from_dir ||= DEFAULT_HANDLER_DIR
117
+ if reload
118
+ @handlers_loaded_from = nil
119
+ @handlers = {}
120
+ @loaded_handler = nil
121
+ end
122
+
123
+ handler_dir = Pathname.new(from_dir)
124
+ return unless handler_dir.directory? && handler_dir.readable?
125
+
126
+ handler_dir.children.each do |handler_file|
127
+ load_handler(handler_file) if handler_file.file? && handler_file.readable?
128
+ end
129
+
130
+ @handlers_loaded_from = handler_dir
131
+ @handlers.values.flatten.size
132
+ end # load handlers
133
+
134
+ # Load an event handler from a file.
135
+ # Handler files must begin with the name of the event they handle,
136
+ # e.g. ComputerAdded, followed by: nothing, a dot, a dash, or
137
+ # and underscore. Case doesn't matter.
138
+ # So all of these are OK:
139
+ # ComputerAdded
140
+ # computeradded.sh
141
+ # COMPUTERAdded_notify_team
142
+ # Computeradded-update-ldap
143
+ # There can be as many as desired for each event.
144
+ #
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
157
+ #
158
+ # @return [void]
159
+ #
160
+ def self.load_handler(from_file)
161
+ handler_file = Pathname.new from_file
162
+ event_name = event_name_from_handler_filename(handler_file)
163
+ return unless event_name
164
+
165
+ # create an array for this event's handlers, if needed
166
+ @handlers[event_name] ||= []
167
+
168
+ 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
174
+ return
175
+ end
176
+
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
184
+ else
185
+ puts "===> FAILED loading internal handler file '#{handler_file.basename}'"
186
+ end
187
+ end # self.load_handler(handler_file)
188
+
189
+ # Given a handler filename, return the event name it wants to handle
190
+ #
191
+ # @param [Pathname] filename The filename from which to glean the
192
+ # event name.
193
+ #
194
+ # @return [String,nil] The matching event name or nil if no match
195
+ #
196
+ def self.event_name_from_handler_filename(filename)
197
+ @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
200
+ end
201
+
202
+ end # module Handler
203
+
204
+ end # class handledevent
205
+
206
+ end # module
@@ -0,0 +1,140 @@
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
+ # An event that will be sent to a Webhook Server, simulating
29
+ # one from the JSS.
30
+ #
31
+ # This is the parent class to all of the classes in the
32
+ # Chook::TestEvents module, which are dynamically defined when this
33
+ # file is loaded.
34
+ #
35
+ # All constants, methods, and attributes that are common to TestEvent
36
+ # classes are defined here.
37
+ #
38
+ class TestEvent < Chook::Event
39
+
40
+ EVENT_ATTRIBUTES = %w(webhook_id webhook_name subject).freeze
41
+
42
+ # For each event type in Chook::Event::EVENTS.keys
43
+ # generate a TestEvent class for it, set its SUBJECT_CLASS constant
44
+ # and add it to the TestEvents module.
45
+ #
46
+ # @return [void]
47
+ #
48
+ def self.generate_classes
49
+ Chook::Event::EVENTS.each do |classname, subject|
50
+ next if Chook::TestEvents.const_defined? classname
51
+ # make the new TestEvent subclass
52
+ new_class = Class.new(Chook::TestEvent) do
53
+ # Setters & Getters
54
+ EVENT_ATTRIBUTES.each do |attribute|
55
+ # Getter
56
+ attr_reader attribute
57
+ # Setter
58
+ if attribute == 'subject'
59
+ define_method("#{attribute}=") do |new_val|
60
+ raise "Invalid TestSubject: Chook::TestEvents::#{classname} requires a Chook::TestSubjects::#{EVENTS[classname]}" unless Chook::Validators.send(:valid_test_subject, classname, new_val)
61
+ instance_variable_set(('@' + attribute.to_s), new_val)
62
+ end # end define_method
63
+ else
64
+ define_method("#{attribute}=") do |new_val|
65
+ instance_variable_set(('@' + attribute.to_s), new_val)
66
+ end # end define_method
67
+ end
68
+ end # end EVENT_ATTRIBUTES.each do |attribute|
69
+ end # end new_class
70
+
71
+ # Set its EVENT_NAME constant
72
+ new_class.const_set Chook::TestEvent::EVENT_NAME_CONST, classname
73
+
74
+ # Set its SUBJECT_CLASS constant to the appropriate
75
+ # class in the TestEvents module.
76
+ new_class.const_set Chook::TestEvent::SUBJECT_CLASS_CONST, Chook::TestSubjects.const_get(subject)
77
+
78
+ # Add the new class to the HandledEvents module.
79
+ Chook::TestEvents.const_set(classname, new_class)
80
+ end # each classname, subject
81
+ end # self.generate_classes
82
+
83
+ # json_hash
84
+ # Used by the fire method
85
+ #
86
+ # @return [Hash] A JSON Event payload formatted as a Hash.
87
+ #
88
+ def json_hash
89
+ raw_hash_form = {}
90
+ raw_hash_form['webhook'.to_sym] = { 'webhookEvent'.to_sym => self.class.to_s.split('::')[-1] }
91
+ EVENT_ATTRIBUTES.each do |json_attribute|
92
+ next if json_attribute.include? 'json'
93
+ if json_attribute == 'subject'
94
+ raw_hash_form['event'.to_sym] = instance_variable_get('@' + json_attribute).json_hash
95
+ else
96
+ json_hash_attribute = json_attribute.split('webhook_')[1] || json_attribute
97
+ nested_hash = Hash.new do |hash, key|
98
+ hash[key] = {}
99
+ end # end nested_hash
100
+ nested_hash[json_hash_attribute.to_sym] = instance_variable_get('@' + json_attribute)
101
+ raw_hash_form['webhook'.to_sym][nested_hash.keys[0]] = nested_hash[nested_hash.keys[0]]
102
+ end
103
+ end # end EVENT_ATTRIBUTES.each do |json_attribute|
104
+ raw_hash_form # This is the structural equivalent of the Chook::Event @json_hash form
105
+ end # end json_hash
106
+
107
+ # fire
108
+ #
109
+ # @param [String] server_url The URL of a server that can handle an Event
110
+ # @return [void]
111
+ #
112
+ def fire(server_url)
113
+ raise 'Please provide a destination server URL' unless server_url
114
+ uri = URI.parse(server_url)
115
+ raise 'Please provide a valid destination server URL' if uri.host.nil?
116
+ data = json_hash.to_json # This is the structural equivalent of the Chook::Event @raw_json form
117
+ http_connection = Net::HTTP.new uri.host, uri.port
118
+ http_connection.post(uri, data)
119
+ end # end fire
120
+
121
+ # initialize
122
+ # The optional argument is a Hash with the appropriate keys defiend
123
+ #
124
+ # @param [Hash] event_data nil or Hash
125
+ # @return [TestEvent] A new TestEvents subclass object
126
+ #
127
+ def initialize(event_data = nil)
128
+ if event_data
129
+ event_data.each do |key, value|
130
+ next unless EVENT_ATTRIBUTES.include? key
131
+ instance_variable_set(('@' + key.to_s), value)
132
+ end # event_data.each
133
+ else
134
+ EVENT_ATTRIBUTES.each { |attribute| instance_variable_set(('@' + attribute.to_s), nil) }
135
+ end # end if event_data
136
+ end # end init
137
+
138
+ end # class TestEvent
139
+
140
+ end # module