chook 1.0.1.b2 → 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
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