jss-api 0.5.5 → 0.5.6

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.
@@ -467,8 +467,15 @@ module JSS
467
467
  @name ||= @main_subset[:name]
468
468
 
469
469
  # many things have a :site
470
- @site = JSS::APIObject.get_name( @main_subset[:site]) if @main_subset[:site]
471
-
470
+ if @main_subset[:site]
471
+ @site = JSS::APIObject.get_name( @main_subset[:site])
472
+ end
473
+
474
+ # many things have a :category
475
+ if @main_subset[:category]
476
+ @category = JSS::APIObject.get_name( @main_subset[:category])
477
+ end
478
+
472
479
  # set empty strings to nil
473
480
  @init_data.jss_nillify! '', :recurse
474
481
 
@@ -146,7 +146,7 @@ module JSS
146
146
  :position => @position,
147
147
  :real_name => @real_name,
148
148
  :room => @room,
149
- :username => @username,
149
+ :username => @username
150
150
  }
151
151
  end
152
152
 
@@ -0,0 +1,261 @@
1
+ ### Copyright 2014 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
+ ###
26
+ module JSS
27
+
28
+ #####################################
29
+ ### Module Variables
30
+ #####################################
31
+
32
+ #####################################
33
+ ### Module Methods
34
+ #####################################
35
+
36
+
37
+ #####################################
38
+ ### Classes
39
+ #####################################
40
+
41
+ ###
42
+ ### An OS X Configuration Profile in the JSS.
43
+ ###
44
+ ### Note that the profile payloads and the profile UUID cannot be edited or updated with this via this class.
45
+ ### Use the web UI.
46
+ ###
47
+ ### @see JSS::APIObject
48
+ ###
49
+ class OSXConfigurationProfile < JSS::APIObject
50
+
51
+ #####################################
52
+ ### Mix-Ins
53
+ #####################################
54
+ include JSS::Updatable
55
+ include JSS::Scopable
56
+ include JSS::SelfServable
57
+
58
+ #####################################
59
+ ### Class Methods
60
+ #####################################
61
+
62
+ #####################################
63
+ ### Class Constants
64
+ #####################################
65
+
66
+ ### The base for REST resources of this class
67
+ RSRC_BASE = "osxconfigurationprofiles"
68
+
69
+ ### the hash key used for the JSON list output of all objects in the JSS
70
+ RSRC_LIST_KEY = :os_x_configuration_profiles
71
+
72
+ ### The hash key used for the JSON object output.
73
+ ### It's also used in various error messages
74
+ RSRC_OBJECT_KEY = :os_x_configuration_profile
75
+
76
+ ### these keys, as well as :id and :name, are present in valid API JSON data for this class
77
+ VALID_DATA_KEYS = [:distribution_method, :scope, :redeploy_on_update]
78
+
79
+ ### Our scopes deal with computers
80
+ SCOPE_TARGET_KEY = :computers
81
+
82
+ ### Our SelfService happens on OSX
83
+ SELF_SERVICE_TARGET = :osx
84
+
85
+ ### Our SelfService deploys profiles
86
+ SELF_SERVICE_PAYLOAD = :profile
87
+
88
+ ### The possible values for the :distribution_method
89
+ DISTRIBUTION_METHODS = ["Install Automatically", "Make Available in Self Service"]
90
+
91
+ SELF_SERVICE_DIST_METHOD = "Make Available in Self Service"
92
+
93
+ ### The possible values for :level
94
+ LEVELS = ["user", "computer"]
95
+
96
+
97
+ #####################################
98
+ ### Attributes
99
+ #####################################
100
+
101
+ ### @return [String] the description of this profile
102
+ attr_reader :description
103
+
104
+ ### @return [String] the distribution_method of this profile
105
+ attr_reader :distribution_method
106
+
107
+ ### @return [Boolean] can the user remove this profile
108
+ attr_reader :user_removable
109
+
110
+ ### @return [String] the level (user/computer) of this profile
111
+ attr_reader :level
112
+
113
+ ### @return [String] the uuid of this profile. NOT Updatable
114
+ attr_reader :uuid
115
+
116
+ ### @return [Boolean] Should this profile be redeployed when an inventory update happens?
117
+ attr_reader :redeploy_on_update
118
+
119
+ ### @return [String] the plist containing the payloads for this profile. NOT Updatable
120
+ attr_reader :payloads
121
+
122
+ #####################################
123
+ ### Constructor
124
+ #####################################
125
+
126
+ ###
127
+ ### See JSS::APIObject#initialize
128
+ ###
129
+ def initialize (args = {})
130
+
131
+ super
132
+
133
+ @description = @main_subset[:description]
134
+ @distribution_method = @main_subset[:distribution_method]
135
+ @user_removable = @main_subset[:user_removable]
136
+ @level = @main_subset[:level]
137
+ @uuid = @main_subset[:uuid]
138
+ @redeploy_on_update = @main_subset[:redeploy_on_update]
139
+ @payloads = @main_subset[:payloads]
140
+
141
+ self.parse_scope
142
+ self.parse_self_service
143
+
144
+ end
145
+
146
+ #####################################
147
+ ### Public Instance Methods
148
+ #####################################
149
+
150
+ ###
151
+ ### @param new_val[String] the new discription
152
+ ###
153
+ ### @return [void]
154
+ ###
155
+ def description= (new_val)
156
+ return nil if @self_service_description == new_val
157
+ @description = new_val.strip!
158
+ @need_to_update = true
159
+ end
160
+
161
+
162
+ ###
163
+ ### @param new_val[String] how should this be distributed to clients?
164
+ ###
165
+ ### @return [void]
166
+ ###
167
+ def distribution_method= (new_val)
168
+ return nil if @distribution_method == new_val
169
+ raise JSS::InvalidDataError, "New value must be one of '#{DISTRIBUTION_METHODS.join("' '")}'" unless DISTRIBUTION_METHODS.include? new_val
170
+ @distribution_method = new_val
171
+ @need_to_update = true
172
+ end
173
+
174
+ ###
175
+ ### @param new_val[Boolean] should the user be able to remove this?
176
+ ###
177
+ ### @return [void]
178
+ ###
179
+ def user_removable= (new_val)
180
+ return nil if @self_service_feature_on_main_page == new_val
181
+ raise JSS::InvalidDataError, "Distribution method must be '#{SELF_SERVICE_DIST_METHOD}' to let the user remove it." unless in_self_service?
182
+ raise JSS::InvalidDataError, "New value must be true or false" unless JSS::TRUE_FALSE.include? new_val
183
+ @user_removable = new_val
184
+ @need_to_update = true
185
+ end
186
+
187
+ ###
188
+ ### @param new_val[String] the new level for this profile (user/computer)
189
+ ###
190
+ ### @return [void]
191
+ ###
192
+ def level= (new_val)
193
+ return nil if @level == new_val
194
+ raise JSS::InvalidDataError, "New value must be one of '#{LEVELS.join("' '")}'" unless LEVELS.include? new_val
195
+ @level = new_val
196
+ @need_to_update = true
197
+ end
198
+
199
+
200
+ ###
201
+ ### @return [Boolean] is this profile available in Self Service?
202
+ ###
203
+ def in_self_service?
204
+ @distribution_method == SELF_SERVICE_DIST_METHOD
205
+ end
206
+
207
+
208
+ ###
209
+ ### @return [Boolean] is this profile removable by the user?
210
+ ###
211
+ def user_removable?
212
+ @user_removable
213
+ end
214
+
215
+
216
+ ###
217
+ ### @return [Hash] The payload plist parsed into a Ruby hash
218
+ ###
219
+ def parsed_payloads
220
+ Plist.parse_xml @payloads
221
+ end
222
+
223
+ ###
224
+ ### @return [Array<Hash>] the individual payloads from the payload Plist
225
+ ###
226
+ def payload_content
227
+ parsed_payloads['PayloadContent']
228
+ end
229
+
230
+ ###
231
+ ### @return [Array<String>] the PayloadType of each payload (e.g. com.apple.caldav.account)
232
+ ###
233
+ def payload_types
234
+ payload_content.map{|p| p['PayloadType'] }
235
+ end
236
+
237
+ #####################################
238
+ ### Private Instance Methods
239
+ #####################################
240
+ private
241
+
242
+ def rest_xml
243
+ doc = REXML::Document.new
244
+
245
+ obj = doc.add_element RSRC_OBJECT_KEY.to_s
246
+ gen = obj.add_element('general')
247
+ gen.add_element('description').text = @description
248
+ gen.add_element('distribution_method').text = @distribution_method
249
+ gen.add_element('user_removable').text = @user_removable
250
+ gen.add_element('level').text = @level
251
+ gen.add_element('redeploy_on_update').text = @redeploy_on_update
252
+
253
+ obj << @scope.scope_xml
254
+ obj << self_service_xml
255
+
256
+ return doc.to_s
257
+ end
258
+
259
+ end # class OSXConfigurationProfile
260
+
261
+ end # module
@@ -0,0 +1,355 @@
1
+ ### Copyright 2014 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
+ ###
26
+ module JSS
27
+
28
+ #####################################
29
+ ### Module Variables
30
+ #####################################
31
+
32
+ #####################################
33
+ ### Module Methods
34
+ #####################################
35
+
36
+ #####################################
37
+ ### Sub-Modules
38
+ #####################################
39
+
40
+ ### A mix-in module for handling Self Service data for objects in the JSS.
41
+ ###
42
+ ### The JSS objects that have Self Service data return it in a :self_service subset,
43
+ ### which all have similar data, a hash with at least these keys:
44
+ ### - :self_service_description
45
+ ### - :self_service_icon
46
+ ###
47
+ ### Most also have:
48
+ ### - :feature_on_main_page
49
+ ### - :self_service_categories
50
+ ###
51
+ ### iOS Profiles in self service have this key:
52
+ ### - :security
53
+ ###
54
+ ### Additionally, items that apper in OS X SlfSvc have these keys:
55
+ ### - :install_button_text
56
+ ### - :force_users_to_view_description
57
+ ###
58
+ ### See the attribute definitions for details of these values and structures.
59
+ ###
60
+ ### Including this module in an {APIObject} subclass and calling {#parse_self_service} in the
61
+ ### subclass's constructor will give it matching attributes with 'self_service_'
62
+ ### appended if needed, e.g. {#self_service_feature_on_main_page}
63
+ ###
64
+ ### If the subclass is creatable or updatable, calling {#self_service_xml} returns
65
+ ### a REXML element representing the Self Service subset, to be included with the
66
+ ### #rest_xml output of the subclass.
67
+ ###
68
+ ### Classes including this module *must*:
69
+ ###
70
+ ### - Define the constant SELF_SERVICE_TARGET which contains either :osx or :ios
71
+ ### - Define the constant SELF_SERVICE_PAYLOAD which contains one of :policy, :profile, or :app
72
+ ### - Call {#parse_self_service} in the subclass's constructor after calling super
73
+ ### - Include the result of {#self_service_xml} in their #rest_xml output
74
+ ### - Define the method #in_self_service? which returns a Boolean indicating that the item is
75
+ ### available in self service. Different API objects indicate this in different ways.
76
+ ### - Define the method #user_removable? which returns Boolean indicating that the item (a profile)
77
+ ### can be removed by the user in SSvc. OS X profiles store this in the :user_removable key of the
78
+ ### :general subset as a boolean, whereas iOS profiles stor it in :security as one of 3 strings
79
+ ###
80
+ ###
81
+ ### Notes:
82
+ ### - Self service icons cannot be modified via this code. Use the Web UI.
83
+ ### - There an API bug in handling categories, and all but the last one are ommitted. Until this is fixed, categories
84
+ ### cannot be saved via this code since that would cause data-loss when more than one category is applied.
85
+ ###
86
+ module SelfServable
87
+
88
+ #####################################
89
+ ### Constants
90
+ #####################################
91
+
92
+ SELF_SERVABLE = true
93
+
94
+ IOS_PROFILE_REMOVAL_OPTIONS = ["Always", "With Authorization", "Never"]
95
+
96
+ #####################################
97
+ ### Variables
98
+ #####################################
99
+
100
+
101
+ #####################################
102
+ ### Attribtues
103
+ #####################################
104
+
105
+
106
+ ### @return [String] The verbage that appears in SelfSvc for this item
107
+ attr_reader :self_service_description
108
+
109
+ ### @return [Hash] The icon that appears in SelfSvc for this item
110
+ ###
111
+ ### The Hash contains these keys with info about the icon:
112
+ ### - :filename => [String] The name of the image file uploaded to the JSS
113
+ ### - :uri => [String] the URI for retriving the icon
114
+ ### - :id => [Integer] the JSS id number for the icon (not all SSvc items have this)
115
+ ### - :data => [String] the icon image encoded as Base64 (not all SSvc items have this)
116
+ ###
117
+ attr_reader :self_service_icon
118
+
119
+ ### @return [Boolean] Should this item feature on the main page of SSvc?
120
+ attr_reader :self_service_feature_on_main_page
121
+
122
+ ### @return [Array<Hash>] The categories in which this item should appear in SSvc
123
+ ###
124
+ ### Each Hash has these keys about the category
125
+ ### - :id => [Integer] the JSS id of the category
126
+ ### - :name => [String] the name of the category
127
+ ### - :display_in => [Boolean] should the item be displayed in this category in SSvc? (OSX SSvc only)
128
+ ### - :feature_in => [Boolean] should the item be featured in this category in SSVC? (OSX SSvc only)
129
+ ###
130
+ ### NOTE: as of Casper 9.61 there's a bug in the JSON output from the API, and only the last
131
+ ### category is returned, if more than one are set.
132
+ ###
133
+ attr_reader :self_service_categories
134
+
135
+ ### @return [Hash] The security settings for iOS profiles in SSvc
136
+ ###
137
+ ### The keys are
138
+ ### - :removal_disallowed => [String] one of the items in IOS_PROFILE_REMOVAL_OPTIONS
139
+ ### - :password => [String] if :removal_disallowed is "With Authorization", this contains the passwd (in plaintext)
140
+ ### needed to remove the profile.
141
+ ###
142
+ ### NOTE that the key should be called :removal_allowed, since 'Never' means it can't be removed.
143
+ ###
144
+ attr_reader :self_service_security
145
+
146
+ ### @return [String] The text label on the install button in SSvc (OSX SSvc only)
147
+ attr_reader :self_service_install_button_text
148
+
149
+ ### @return [Boolean] Should an extra window appear before the user can install the item? (OSX SSvc only)
150
+ attr_reader :self_service_force_users_to_view_description
151
+
152
+
153
+ #####################################
154
+ ### Mixed-in Instance Methods
155
+ #####################################
156
+
157
+ ###
158
+ ### Call this during initialization of
159
+ ### objects that have a self_service subset
160
+ ### and the self_service attributes will be populated
161
+ ### (as primary attributes) from @init_data
162
+ ###
163
+ ### @return [void]
164
+ ###
165
+ def parse_self_service
166
+ @init_data[:self_service] ||= {}
167
+ ss_data = @init_data[:self_service]
168
+
169
+ @self_service_description = ss_data[:self_service_description]
170
+ @self_service_icon = ss_data[:self_service_icon]
171
+
172
+ @self_service_feature_on_main_page = ss_data[:feature_on_main_page]
173
+
174
+ # TEMPORARY - until JAMF fixes the category data in JSON
175
+ @self_service_categories = [
176
+ ss_data[:self_service_categories][:category]
177
+ ]
178
+
179
+ # make this an empty hash if needed
180
+ @self_service_security = ss_data[:security] || {}
181
+
182
+ # if this is an osx profile, set @self_service_security[:removal_disallowed] to "Always" or "Never"
183
+ # to indicate the boolean :user_removable
184
+ if @init_data[:general].keys.include? :user_removable
185
+ @self_service_security[:removal_disallowed] = @init_data[:general][:user_removable] ? "Always" : "Never"
186
+ end
187
+
188
+ @self_service_install_button_text = ss_data[:install_button_text]
189
+ @self_service_force_users_to_view_description = ss_data[:force_users_to_view_description]
190
+
191
+ end
192
+
193
+
194
+ ###
195
+ ###
196
+ ### Setters
197
+ ###
198
+
199
+ ###
200
+ ### @param new_val[String] the new discription
201
+ ###
202
+ ### @return [void]
203
+ ###
204
+ def self_service_description= (new_val)
205
+ return nil if @self_service_description == new_val
206
+ @self_service_description = new_val.strip!
207
+ @need_to_update = true
208
+ end
209
+
210
+ ###
211
+ ### @param new_val[String] the new install button text
212
+ ###
213
+ ### @return [void]
214
+ ###
215
+ def self_service_install_button_text= (new_val)
216
+ return nil if @self_service_install_button_text == new_val
217
+ raise JSS::InvalidDataError, "Only OS X Self Service Items can have custom button text" unless self.class::SELF_SERVICE_TARGET == :osx
218
+ @self_service_install_button_text = new_val.strip
219
+ @need_to_update = true
220
+ end
221
+
222
+ ###
223
+ ### @param new_val[Boolean] should this appear on the main SelfSvc page?
224
+ ###
225
+ ### @return [void]
226
+ ###
227
+ def self_service_feature_on_main_page= (new_val)
228
+ return nil if @self_service_feature_on_main_page == new_val
229
+ raise JSS::InvalidDataError, "New value must be true or false" unless JSS::TRUE_FALSE.include? new_val
230
+ @self_service_feature_on_main_page = new_val
231
+ @need_to_update = true
232
+ end
233
+
234
+ ###
235
+ ### @param new_val[Boolean] should this appear on the main SelfSvc page?
236
+ ###
237
+ ### @return [void]
238
+ ###
239
+ def self_service_force_users_to_view_description= (new_val)
240
+ return nil if @self_service_force_users_to_view_description == new_val
241
+ raise JSS::InvalidDataError, "Only OS X Self Service Items can force users to view description" unless self.class::SELF_SERVICE_TARGET == :osx
242
+ raise JSS::InvalidDataError, "New value must be true or false" unless JSS::TRUE_FALSE.include? new_val
243
+ @self_service_force_users_to_view_description = new_val
244
+ @need_to_update = true
245
+ end
246
+
247
+ ###
248
+ ### Add or change one of the categories for this item in SSvc.
249
+ ###
250
+ ### @param new_cat[String] the name of a category for this item in SelfSvc
251
+ ###
252
+ ### @param display_in[Boolean] should this item appear in the SelfSvc page for the new category?
253
+ ###
254
+ ### @param feature_in[Boolean] should this item be featured in the SelfSvc page for the new category?
255
+ ###
256
+ ### @return [void]
257
+ ###
258
+ def add_self_service_category (new_cat, display_in = true, feature_in = false)
259
+ new_cat.strip!
260
+ raise JSS::NoSuchItemError, "No category '#{new_cat}' in the JSS" unless JSS::Category.all_names(:refresh).include? new_cat
261
+ raise JSS::InvalidDataError, "display_in must be true or false" unless JSS::TRUE_FALSE.include? display_in
262
+ raise JSS::InvalidDataError, "feature_in must be true or false" unless JSS::TRUE_FALSE.include? feature_in
263
+
264
+ new_data = {:name => new_cat, :display_in => display_in, :feature_in => feature_in }
265
+
266
+ # see if this category is already among our categories.
267
+ idx = @self_service_categories.index{|c| c[new_cat]}
268
+
269
+ if idx
270
+ @self_service_categories[idx] = new_data
271
+ else
272
+ @self_service_categories << new_data
273
+ end
274
+
275
+ @need_to_update = true
276
+ end
277
+
278
+ ###
279
+ ### Remove a category from those for this item in SSvc
280
+ ###
281
+ ### @param cat[String] the name of the category to remove
282
+ ###
283
+ ### @return [void]
284
+ ###
285
+ def remove_self_service_category= (cat)
286
+ return nil unless @self_service_categories.map{|c| c[:name]}.include? cat
287
+ @self_service_categories.reject!{|c| c[:name]}
288
+ @need_to_update = true
289
+ end
290
+
291
+ ###
292
+ ### Set whether or when the user can remove a profile installed with SSvc
293
+ ###
294
+ ### @param new_val[String] one of the values in PROFILE_REMOVAL_OPTIONS, or true or false
295
+ ###
296
+ ### @return [void]
297
+ ###
298
+ def profile_can_be_removed (new_val)
299
+
300
+ new_val = "Always" if new_val === true
301
+ new_val = "Never" if new_val === false
302
+
303
+ return nil if new_val == @self_service_security[:removal_disallowed]
304
+ raise JSS::InvalidDataError, "" unless IOS_PROFILE_REMOVAL_OPTIONS.include? new_val
305
+
306
+ @self_service_security[:removal_disallowed] = new_val
307
+ end
308
+
309
+
310
+ ###
311
+ ### @api private
312
+ ###
313
+ ### Return a REXML <location> element to be
314
+ ### included in the rest_xml of
315
+ ### objects that have a Location subset
316
+ ###
317
+ ### @return [REXML::Element]
318
+ ###
319
+ def self_service_xml
320
+
321
+ ssvc = REXML::Element.new('self_service')
322
+
323
+ return ssvc unless self.in_self_service?
324
+
325
+ ssvc.add_element('self_service_description').text = @self_service_description
326
+ ssvc.add_element('feature_on_main_page').text = @self_service_feature_on_main_page
327
+
328
+ ### TEMPORARY - re-enable this when the category bug is fixed.
329
+
330
+ # cats = ssvc.add_element('self_service_categories')
331
+ # @self_service_categories.each do |cat|
332
+ # catelem = cats.add_element('category')
333
+ # catelem.add_element('name').text = cat[:name]
334
+ # catelem.add_element('display_in').text = cat[:display_in] if cat.keys.include? :display_in
335
+ # catelem.add_element('feature_in').text = cat[:feature_in] if cat.keys.include? :feature_in
336
+ # end
337
+
338
+ unless @self_service_security.empty?
339
+ sec = ssvc.add_element('security')
340
+ sec.add_element('removal_disallowed').text = @self_service_security[:removal_disallowed] if @self_service_security[:removal_disallowed]
341
+ sec.add_element('password').text = @self_service_security[:password] if @self_service_security[:password]
342
+ end
343
+
344
+ ssvc.add_element('install_button_text').text = @self_service_install_button_text if @self_service_install_button_text
345
+ ssvc.add_element('force_users_to_view_description').text = @self_service_force_users_to_view_description unless @self_service_force_users_to_view_description.nil?
346
+
347
+ return ssvc
348
+ end
349
+
350
+ ### aliases
351
+ alias change_self_service_category add_self_service_category
352
+
353
+ end # module SelfServable
354
+
355
+ end # module JSS