chook 1.0.1.b2 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +56 -0
  3. data/README.md +363 -127
  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 +260 -98
  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/version.rb +1 -1
  45. metadata +79 -19
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ Process.setproctitle('chook')
4
+
3
5
  ### Copyright 2017 Pixar
4
6
 
5
7
  ###
@@ -24,5 +26,33 @@
24
26
  ### language governing permissions and limitations under the Apache License.
25
27
  ###
26
28
  ###
29
+
30
+ require 'getoptlong'
31
+
32
+ # The CLI options for GetoptLong
33
+ OPTS = GetoptLong.new(
34
+ ['--log', '-l', GetoptLong::REQUIRED_ARGUMENT],
35
+ ['--dev', '-d', GetoptLong::NO_ARGUMENT]
36
+ )
37
+
38
+ env = :production
39
+ log_level = nil
40
+
41
+ OPTS.each do |opt, arg|
42
+ case opt
43
+ when '--log'
44
+ log_level = arg
45
+ when '--dev'
46
+ env = :development
47
+ end # case
48
+ end # opts.each
49
+
50
+ ENV['APP_ENV'] = env.to_s
51
+
27
52
  require 'chook/server'
28
- Chook::Server.run!
53
+ begin
54
+ Chook::Server.run! log_level: log_level
55
+ rescue => e
56
+ Chook.logger.fatal e.to_s
57
+ e.backtrace.each { |line| Chook.logger.fatal "..#{line}" }
58
+ end
@@ -0,0 +1,183 @@
1
+ #################
2
+ # This sample config file uses default settings.
3
+ #
4
+ # Each setting is defined on one line like so:
5
+ #
6
+ # key: value
7
+ #
8
+ # Values may have spaces.
9
+ #
10
+ # Blank lines and those starting with # are ignored.
11
+
12
+ #################
13
+ # The port used by the server.
14
+ # Default: 443 when use_ssl is true, 80 otherwise
15
+ #
16
+ port: 80
17
+
18
+ #################
19
+ # By default incoming webhooks are handled in parallel
20
+ # If you have problems (e.g. a handler isn't thread-safe) try setting this
21
+ # to false and hooks will be handled one at a time, in the order received.
22
+ # Default: true
23
+ #
24
+ concurrency: true
25
+
26
+ #################
27
+ # The directory holding the internal and external handlers.
28
+ # Their filenames must start with a known webhook name, and
29
+ # their executability determines internal (not executable)
30
+ # versus external (executable). See README.md for details.
31
+ # Default: /Library/Application Support/Chook
32
+ #
33
+ handler_dir: /Library/Application Support/Chook
34
+
35
+ #################
36
+ # Should the server use SSL (https)? Ignored unless the engine is 'thin'
37
+ # Default: false
38
+ #
39
+ use_ssl: false
40
+
41
+ #################
42
+ # When using SSL, the path to the SSL certificate to use, in PEM format
43
+ # Required if use_ssl == true and engine == thin
44
+ # Default: none
45
+ #
46
+ ssl_cert_path:
47
+
48
+ #################
49
+ # When using SSL, the path to the private key for the SSL certificate, in PEM format
50
+ # Required if use_ssl == true and engine == thin
51
+ # Default: none
52
+ #
53
+ ssl_private_key_path:
54
+
55
+ #################
56
+ # The path to the file used for chook server logging
57
+ # Default: /var/log/chook-server.log
58
+ #
59
+ log_file: /var/log/chook-server.log
60
+
61
+ #################
62
+ # The detail level for the log. One of:
63
+ # fatal (only fatal errors logged), error, warn, info, or debug (everything logged)
64
+ # Default: info
65
+ #
66
+ log_level: info
67
+
68
+ #################
69
+ # How many old log files to keep when rotating?
70
+ # Set to 0, or don't set at all, to disable auto-rotating
71
+ # of log files.
72
+ # Default: 10
73
+ #
74
+ logs_to_keep: 10
75
+
76
+ #################
77
+ # The log file rotates automatically when it reaches this size in megabytes.
78
+ # Default: 10
79
+ #
80
+ log_max_megs: 10
81
+
82
+ #################
83
+ # Any value here will turn on 'HTTP Basic Authentication' for webhooks,
84
+ # and this will be the username required for a Jamf Pro server to send
85
+ # webhooks to the Chook server.
86
+ #
87
+ # Leaving this empty will allow receiving of webooks with no authentication
88
+ #
89
+ # Default: none
90
+ #
91
+ webhooks_user:
92
+
93
+ #################
94
+ # When 'HTTP Basic Authentication' is enabled by setting webhooks_user, this
95
+ # tells chook how to learn the password for that user:
96
+ #
97
+ # - If its a path to a file, the file contains the password and nothing else.
98
+ # The file must be owned by the user running the chook server, and must have
99
+ # mode 0600.
100
+ #
101
+ # - If it ends with a pipe character (|), everything execpt the pipe is considered
102
+ # to be a shell command, executable by the user running the chook server.
103
+ # The standard-output of the command will be the password.
104
+ #
105
+ # Default: none
106
+ #
107
+ webhooks_user_pw:
108
+
109
+ #################
110
+ # Any value here will require authentication to view the admin web page.
111
+ #
112
+ # Leaving this empty will allow anyone to see the admin web page.
113
+ #
114
+ # If the value here is 'use_jamf' then anyone can log in with their
115
+ # Jamf Pro credentials, and the admin_pw setting is ignored
116
+ # (see the jamf_* settings below)
117
+ #
118
+ # If the value is anything else, it is the username required to
119
+ # log in to the admin web page.
120
+ #
121
+ # Default: none
122
+ #
123
+ admin_user:
124
+
125
+ #################
126
+ # When admin page authentication is enabled by setting admin_user, this
127
+ # tells chook how to learn the password for that user:
128
+ #
129
+ # - if the admin_user is 'use_jamf' then this is ignored, see the jamf_* settings below
130
+ #
131
+ # - If its a path to a file, the file contains the password and nothing else.
132
+ # The file must be owned by the user running the chook server, and must have
133
+ # mode 0600.
134
+ #
135
+ # - If it ends with a pipe character (|), everything execpt the pipe is considered
136
+ # to be a shell command, executable by the user running the chook server.
137
+ # The standard-output of the command will be the password.
138
+ #
139
+ # Default: none
140
+ #
141
+ admin_pw:
142
+
143
+ #################
144
+ # When admin page authentication is enabled by setting admin_user,
145
+ # this is how many seconds before the browser session expires and the
146
+ # admin must log in again.
147
+ #
148
+ # Default: 86400 (24 hours)
149
+ #
150
+ admin_session_expires: 86400
151
+
152
+ #################
153
+ # When the admin_user is 'use_jamf', this is the hostname of the
154
+ # Jamf Pro server to use for authentication
155
+ #
156
+ # Default: none, but /etc/ruby-jss.conf will be honored
157
+ #
158
+ jamf_server:
159
+
160
+ #################
161
+ # When the admin_user is 'use_jamf', this is the port of the
162
+ # Jamf Pro server to use for Authentication
163
+ #
164
+ # Default: none, but /etc/ruby-jss.conf will be honored
165
+ #
166
+ jamf_port:
167
+
168
+ #################
169
+ # When the admin_user is 'use_jamf', should SSL be used when
170
+ # connection to the jamf_server? true or false
171
+ #
172
+ # Default: none, but /etc/ruby-jss.conf will be honored
173
+ #
174
+ jamf_use_ssl:
175
+
176
+ #################
177
+ # When the admin_user is 'use_jamf', and jamf_use_ssl is true,
178
+ # should SSL certificates from the jamf server be
179
+ # validated?
180
+ #
181
+ # Default: none, but /etc/ruby-jss.conf will be honored
182
+ #
183
+ jamf_verify_cert:
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.pixar.chook-server</string>
7
+ <key>StandardOutPath</key>
8
+ <string>/var/log/chook-server.log</string>
9
+ <key>StandardErrorPath</key>
10
+ <string>/var/log/chook-server.log</string>
11
+ <key>ProgramArguments</key>
12
+ <array>
13
+ <string>/usr/local/pixar/bin/chook-server</string>
14
+ </array>
15
+ <key>RunAtLoad</key>
16
+ <true/>
17
+ <key>KeepAlive</key>
18
+ <true/>
19
+ </dict>
20
+ </plist>
@@ -23,23 +23,23 @@
23
23
  ###
24
24
  ###
25
25
 
26
- Chook.event_handler do |event|
27
- eobject = event.event_object
28
- whook = event.webhook
26
+ # this module is just a namespace so we don't conflict with other handlers
27
+ module APIOpHandler
29
28
 
30
29
  REPORT_ACTIONS = {
31
30
  'PUT' => 'update',
32
31
  'POST' => 'create',
33
32
  'DELETE' => 'delete'
34
- }.freeze unless defined? REPORT_ACTIONS
33
+ }.freeze
34
+
35
+ end
36
+
37
+ Chook.event_handler do |event|
38
+ event.logger.debug "This is a handler-level debug message for event #{event.object_id}"
35
39
 
36
- action = REPORT_ACTIONS[eobject.restAPIOperationType]
40
+ action = APIOpHandler::REPORT_ACTIONS[event.subject.restAPIOperationType]
37
41
 
38
- return nil unless action
42
+ break unless action
39
43
 
40
- puts <<-ENDMSG
41
- The JSS WebHook named '#{whook.name}' was just triggered.
42
- It indicates that Casper user '#{eobject.authorizedUsername}' just used the JSS API to #{action}
43
- the JSS #{eobject.objectTypeName} named '#{eobject.objectName}' (id #{eobject.objectID})
44
- ENDMSG
44
+ event.logger.info "API #{action}: #{event.subject.objectTypeName} '#{event.subject.objectName}' (id #{event.subject.objectID})"
45
45
  end
@@ -23,11 +23,8 @@
23
23
  ###
24
24
  ###
25
25
 
26
- HANDLER_NAME = File.basename __FILE__
27
- CHANGELOG = '/tmp/smart-computer-group-changes.log'.freeze
28
-
29
26
  Chook.event_handler do |event|
30
- now = Time.now.strftime '%Y-%m-%d %H:%M:%S'
31
- msg = "#{now} #{HANDLER_NAME}: Smart Computer Group '#{event.event_object.name}' changed.\n"
32
- open(CHANGELOG, 'a') { |file| file.write msg }
27
+ event.logger.debug "Computer Smart Group Changed: #{event.subject.name}"
28
+ event.logger.debug " Additions: #{event.subject.groupAddedDevicesIds.join ', '}"
29
+ event.logger.debug " Removals: #{event.subject.groupRemovedDevicesIds.join ', '}"
33
30
  end
@@ -8,6 +8,8 @@
8
8
  "name": "OddComputers",
9
9
  "smartGroup": true,
10
10
  "jssid": 95,
11
- "computer": true
11
+ "computer": true,
12
+ "groupAddedDevicesIds": [1,2,3,4,5],
13
+ "groupRemovedDevicesIds": [6,7,8,9,10]
12
14
  }
13
15
  }
@@ -8,6 +8,8 @@
8
8
  "name": "OddMobileDevs",
9
9
  "smartGroup": true,
10
10
  "jssid": 99,
11
- "computer": false
11
+ "computer": false,
12
+ "groupAddedDevicesIds": [1,2,3,4,5],
13
+ "groupRemovedDevicesIds": [6,7,8,9,10]
12
14
  }
13
15
  }
@@ -35,16 +35,33 @@ module Chook
35
35
  # The location of the default config file
36
36
  DEFAULT_CONF_FILE = Pathname.new '/etc/chook.conf'
37
37
 
38
- # The attribute keys we maintain, and the type they should be stored as
38
+ SAMPLE_CONF_FILE = Pathname.new(__FILE__).parent.parent.parent + 'data/chook.conf.example'
39
+
40
+ # The attribute keys we maintain, and how they should be converted to
41
+ # the value used by chook internally.
42
+ #
43
+ # For descriptions of the keys, see data/chook.conf.example
44
+ #
39
45
  CONF_KEYS = {
40
- server_port: :to_i,
41
- server_engine: :to_sym,
42
- handler_dir: nil,
46
+ port: :to_i,
47
+ concurrency: Chook::Procs::STRING_TO_BOOLEAN,
48
+ handler_dir: Chook::Procs::STRING_TO_PATHNAME,
43
49
  use_ssl: Chook::Procs::STRING_TO_BOOLEAN,
44
- ssl_private_key_path: Chook::Procs::STRING_TO_PATHNAME,
45
- ssl_private_key_pw_path: nil,
46
50
  ssl_cert_path: Chook::Procs::STRING_TO_PATHNAME,
47
- ssl_cert_name: nil
51
+ ssl_private_key_path: Chook::Procs::STRING_TO_PATHNAME,
52
+ log_file: Chook::Procs::STRING_TO_PATHNAME,
53
+ log_level: Chook::Procs::STRING_TO_LOG_LEVEL,
54
+ log_max_megs: :to_i,
55
+ logs_to_keep: :to_i,
56
+ webhooks_user: nil,
57
+ webhooks_user_pw: nil,
58
+ admin_user: nil,
59
+ admin_pw: nil,
60
+ admin_session_expires: :to_i,
61
+ jamf_server: nil,
62
+ jamf_port: :to_i,
63
+ jamf_use_ssl: Chook::Procs::STRING_TO_BOOLEAN,
64
+ jamf_verify_cert: Chook::Procs::STRING_TO_BOOLEAN
48
65
  }.freeze
49
66
 
50
67
  # Class Variables
@@ -193,6 +210,8 @@ module Chook
193
210
  end # class Configuration
194
211
 
195
212
  # The single instance of Configuration
196
- CONFIG = Chook::Configuration.instance
213
+ def self.config
214
+ Chook::Configuration.instance
215
+ end
197
216
 
198
217
  end # module
@@ -65,8 +65,9 @@ module Chook
65
65
  'ComputerAdded' => Chook::Subject::COMPUTER,
66
66
  'ComputerCheckIn' => Chook::Subject::COMPUTER,
67
67
  'ComputerInventoryCompleted' => Chook::Subject::COMPUTER,
68
- 'ComputerPolicyFinished' => Chook::Subject::COMPUTER,
68
+ 'ComputerPolicyFinished' => Chook::Subject::POLICY_FINISHED,
69
69
  'ComputerPushCapabilityChanged' => Chook::Subject::COMPUTER,
70
+ 'DeviceAddedToDEP' => Chook::Subject::DEP_DEVICE,
70
71
  'JSSShutdown' => Chook::Subject::JAMF_SOFTWARE_SERVER,
71
72
  'JSSStartup' => Chook::Subject::JAMF_SOFTWARE_SERVER,
72
73
  'MobileDeviceCheckIn' => Chook::Subject::MOBILE_DEVICE,
@@ -94,6 +95,9 @@ module Chook
94
95
 
95
96
  #### Attrbutes common to all events
96
97
 
98
+ # @return [String] A unique identifier for this chook event
99
+ attr_reader :id
100
+
97
101
  # @return [Integer] The webhook id in the JSS that caused this event
98
102
  attr_reader :webhook_id
99
103
 
@@ -128,6 +132,7 @@ module Chook
128
132
  # event. Any not provided will be nil.
129
133
  #
130
134
  def initialize(**args)
135
+ @id = "#{Time.now.to_i}-#{SecureRandom.urlsafe_base64 8}"
131
136
  if args[:raw_json]
132
137
  @raw_json = args[:raw_json]
133
138
  @parsed_json = JSON.parse @raw_json, symbolize_names: true
@@ -25,7 +25,6 @@
25
25
 
26
26
  require 'chook/event/handled_event/handlers'
27
27
 
28
- #
29
28
  module Chook
30
29
 
31
30
  # Load sample JSON files, one per event type
@@ -62,9 +61,6 @@ module Chook
62
61
 
63
62
  #### Class Methods
64
63
 
65
- # Given some raw_json from the jss, create and return the correct
66
- # HandledEvent subclass
67
-
68
64
  # For each event type in Chook::Event::EVENTS
69
65
  # generate a class for it, set its SUBJECT_CLASS constant
70
66
  # and add it to the HandledEvents module.
@@ -99,6 +95,7 @@ module Chook
99
95
  # @return [JSSWebHooks::Event subclass] the Event subclass matching the event
100
96
  #
101
97
  def self.parse_event(raw_event_json)
98
+ return nil if raw_event_json.to_s.empty?
102
99
  event_json = JSON.parse(raw_event_json, symbolize_names: true)
103
100
  event_name = event_json[:webhook][:webhookEvent]
104
101
  Chook::HandledEvents.const_get(event_name).new raw_event_json
@@ -121,13 +118,21 @@ module Chook
121
118
  super raw_json: raw_event_json
122
119
  end # init
123
120
 
121
+ def event_class_name
122
+ self.class.const_get(Chook::Event::EVENT_NAME_CONST)
123
+ end
124
+
125
+ # Run all the general handlers for this event class
126
+ #
124
127
  def handle
125
- handlers = Handlers.handlers[self.class.const_get(Chook::Event::EVENT_NAME_CONST)]
128
+ handlers = Handlers.handlers[event_class_name]
129
+ return "No handlers loaded for #{event_class_name} events" unless handlers.is_a? Array
130
+
126
131
  handlers.each do |handler|
127
132
  case handler
128
133
  when Pathname
129
134
  pipe_to_executable handler
130
- when Proc
135
+ when Object
131
136
  handle_with_proc handler
132
137
  end # case
133
138
  end # @handlers.each do |handler|
@@ -135,18 +140,40 @@ module Chook
135
140
  # the handle method should return a string,
136
141
  # which is the body of the HTTP result for
137
142
  # POSTing the event
138
- "Processed by #{handlers.count} handlers\n"
143
+ "Processed by #{handlers.count} general handlers"
139
144
  end # def handle
140
145
 
141
- # TODO: Add something here that cleans up old threads and forks
146
+ # run a single handler specified by filename
147
+ #
148
+ def handle_by_name(handler_to_run)
149
+ handler = Handlers.named_handlers[handler_to_run]
150
+ return "No named handler '#{handler_to_run}'" unless handler
151
+
152
+ if handler.is_a? Pathname
153
+ pipe_to_executable handler
154
+ else
155
+ handle_with_proc handler
156
+ end # if
157
+ "Processed by named handler '#{handler_to_run}'"
158
+ end
159
+
160
+ # TODO: these threads will die midstream when the server stops.
161
+ # Find a way to .join them or otherwise clean them up.
162
+
142
163
  def pipe_to_executable(handler)
164
+ logger.debug "EXTERNAL: Sending JSON to stdin of '#{handler.basename}'"
143
165
  _thread = Thread.new do
144
166
  IO.popen([handler.to_s], 'w') { |h| h.puts @raw_json }
145
167
  end
146
168
  end
147
169
 
148
170
  def handle_with_proc(handler)
149
- _thread = Thread.new { handler.call self }
171
+ logger.debug "INTERNAL: Running Handler defined in #{handler.handler_file}"
172
+ _thread = Thread.new { handler.handle self }
173
+ end
174
+
175
+ def logger
176
+ @logger ||= Chook::HandledEventLogger.new self
150
177
  end
151
178
 
152
179
  end # class HandledEvent