ruby-jss 1.2.3 → 1.2.4a1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jamf.rb +169 -0
  3. data/lib/jamf/api/abstract_classes/collection_resource.rb +422 -0
  4. data/lib/jamf/api/abstract_classes/generic_reference.rb +145 -0
  5. data/lib/jamf/api/abstract_classes/json_object.rb +1074 -0
  6. data/lib/jamf/api/abstract_classes/prestage.rb +219 -0
  7. data/lib/jamf/api/abstract_classes/prestage_skip_setup_items.rb +126 -0
  8. data/lib/jamf/api/abstract_classes/resource.rb +250 -0
  9. data/lib/jamf/api/abstract_classes/singleton_resource.rb +87 -0
  10. data/lib/jamf/api/attribute_classes/ip_address.rb +66 -0
  11. data/lib/jamf/api/attribute_classes/timestamp.rb +144 -0
  12. data/lib/jamf/api/connection.rb +734 -0
  13. data/lib/jamf/api/connection/api_error.rb +111 -0
  14. data/lib/jamf/api/connection/api_error_styleguide.rb +96 -0
  15. data/lib/jamf/api/connection/token.rb +220 -0
  16. data/lib/jamf/api/json_objects/account_prefs.rb +79 -0
  17. data/lib/jamf/api/json_objects/android_details.rb +139 -0
  18. data/lib/jamf/api/json_objects/appletv_details.rb +110 -0
  19. data/lib/jamf/api/json_objects/attachment.rb +68 -0
  20. data/lib/jamf/api/json_objects/cellular_network.rb +151 -0
  21. data/lib/jamf/api/json_objects/change_log_entry.rb +77 -0
  22. data/lib/jamf/api/json_objects/computer_prestage_skip_setup_items.rb +67 -0
  23. data/lib/jamf/api/json_objects/country.rb +51 -0
  24. data/lib/jamf/api/json_objects/extension_attribute_value.rb +128 -0
  25. data/lib/jamf/api/json_objects/installed_application.rb +59 -0
  26. data/lib/jamf/api/json_objects/installed_certificate.rb +53 -0
  27. data/lib/jamf/api/json_objects/installed_configuration_profile.rb +67 -0
  28. data/lib/jamf/api/json_objects/installed_ebook.rb +58 -0
  29. data/lib/jamf/api/json_objects/installed_provisioning_profile.rb +59 -0
  30. data/lib/jamf/api/json_objects/inventory_preload_extension_attribute.rb +52 -0
  31. data/lib/jamf/api/json_objects/ios_details.rb +244 -0
  32. data/lib/jamf/api/json_objects/location.rb +95 -0
  33. data/lib/jamf/api/json_objects/md_prestage_name.rb +57 -0
  34. data/lib/jamf/api/json_objects/md_prestage_names.rb +82 -0
  35. data/lib/jamf/api/json_objects/md_prestage_skip_setup_items.rb +165 -0
  36. data/lib/jamf/api/json_objects/mobile_device_details.rb +219 -0
  37. data/lib/jamf/api/json_objects/mobile_device_security.rb +101 -0
  38. data/lib/jamf/api/json_objects/prestage_assignment.rb +61 -0
  39. data/lib/jamf/api/json_objects/prestage_location.rb +104 -0
  40. data/lib/jamf/api/json_objects/prestage_purchasing_data.rb +132 -0
  41. data/lib/jamf/api/json_objects/prestage_scope.rb +54 -0
  42. data/lib/jamf/api/json_objects/prestage_sync_status.rb +63 -0
  43. data/lib/jamf/api/json_objects/purchasing_data.rb +125 -0
  44. data/lib/jamf/api/mixins/abstract.rb +58 -0
  45. data/lib/jamf/api/mixins/bulk_deletable.rb +39 -0
  46. data/lib/jamf/api/mixins/change_log.rb +136 -0
  47. data/lib/jamf/api/mixins/extendable.rb +75 -0
  48. data/lib/jamf/api/mixins/immutable.rb +39 -0
  49. data/lib/jamf/api/mixins/locatable.rb +124 -0
  50. data/lib/jamf/api/mixins/lockable.rb +48 -0
  51. data/lib/jamf/api/mixins/referable.rb +92 -0
  52. data/lib/jamf/api/mixins/searchable.rb +202 -0
  53. data/lib/jamf/api/mixins/uncreatable.rb +40 -0
  54. data/lib/jamf/api/mixins/undeletable.rb +40 -0
  55. data/lib/jamf/api/resources/collection_resources/account.rb +163 -0
  56. data/lib/jamf/api/resources/collection_resources/building.rb +114 -0
  57. data/lib/jamf/api/resources/collection_resources/category.rb +82 -0
  58. data/lib/jamf/api/resources/collection_resources/computer.rb +49 -0
  59. data/lib/jamf/api/resources/collection_resources/computer_prestage.rb +80 -0
  60. data/lib/jamf/api/resources/collection_resources/department.rb +79 -0
  61. data/lib/jamf/api/resources/collection_resources/extension_attribute.rb +45 -0
  62. data/lib/jamf/api/resources/collection_resources/inventory_preload_record.rb +274 -0
  63. data/lib/jamf/api/resources/collection_resources/md_prestage.rb +139 -0
  64. data/lib/jamf/api/resources/collection_resources/mobile_device.rb +315 -0
  65. data/lib/jamf/api/resources/collection_resources/script.rb +190 -0
  66. data/lib/jamf/api/resources/collection_resources/site.rb +77 -0
  67. data/lib/jamf/api/resources/singleton_resources/app_store_country_codes.rb +131 -0
  68. data/lib/jamf/api/resources/singleton_resources/authorization.rb +88 -0
  69. data/lib/jamf/api/resources/singleton_resources/client_checkin_settings.rb +139 -0
  70. data/lib/jamf/api/resources/singleton_resources/reenrollment_settings.rb +95 -0
  71. data/lib/jamf/client.rb +301 -0
  72. data/lib/jamf/client/jamf_binary.rb +132 -0
  73. data/lib/jamf/client/jamf_helper.rb +298 -0
  74. data/lib/jamf/client/management_action.rb +114 -0
  75. data/lib/jamf/compatibility.rb +88 -0
  76. data/lib/jamf/composer.rb +190 -0
  77. data/lib/jamf/configuration.rb +281 -0
  78. data/lib/jamf/exceptions.rb +107 -0
  79. data/lib/jamf/ruby_extensions.rb +36 -0
  80. data/lib/jamf/ruby_extensions/array.rb +35 -0
  81. data/lib/jamf/ruby_extensions/array/predicates.rb +46 -0
  82. data/lib/jamf/ruby_extensions/array/utils.rb +47 -0
  83. data/lib/jamf/ruby_extensions/filetest.rb +32 -0
  84. data/lib/jamf/ruby_extensions/filetest/predicates.rb +46 -0
  85. data/lib/jamf/ruby_extensions/hash.rb +33 -0
  86. data/lib/jamf/ruby_extensions/hash/backports.rb +92 -0
  87. data/lib/jamf/ruby_extensions/ipaddr.rb +37 -0
  88. data/lib/jamf/ruby_extensions/ipaddr/utils.rb +95 -0
  89. data/lib/jamf/ruby_extensions/object.rb +30 -0
  90. data/lib/jamf/ruby_extensions/object/predicates.rb +51 -0
  91. data/lib/jamf/ruby_extensions/pathname.rb +39 -0
  92. data/lib/jamf/ruby_extensions/pathname/predicates.rb +50 -0
  93. data/lib/jamf/ruby_extensions/pathname/utils.rb +75 -0
  94. data/lib/jamf/ruby_extensions/string.rb +35 -0
  95. data/lib/jamf/ruby_extensions/string/backports.rb +66 -0
  96. data/lib/jamf/ruby_extensions/string/conversions.rb +65 -0
  97. data/lib/jamf/ruby_extensions/string/predicates.rb +47 -0
  98. data/lib/jamf/utility.rb +423 -0
  99. data/lib/jamf/validate.rb +224 -0
  100. data/lib/jamf/version.rb +32 -0
  101. data/lib/jpapi.rb +26 -0
  102. data/lib/jss/version.rb +1 -1
  103. metadata +104 -4
@@ -0,0 +1,87 @@
1
+ # Copyright 2019 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
+ # The module
27
+ module Jamf
28
+
29
+ # Classes
30
+ #####################################
31
+
32
+ # A Singleton Resource in the API.
33
+ #
34
+ # See {Jamf::Resource} for details and required constants
35
+ #
36
+ # @abstract
37
+ #
38
+ class SingletonResource < Jamf::Resource
39
+
40
+ extend Jamf::Abstract
41
+
42
+ # Public Class Methods
43
+ #####################################
44
+
45
+ # Return a SingletonResource from the API, from the cache if already cached
46
+ # or retrieving from the API and caching it if neededl
47
+ #
48
+ # @param reload[Boolean] If already cached, re-cache from the API.
49
+ # WARNING: unsaved changes will be lost.
50
+ #
51
+ # @param version[String] the API resource version to use.
52
+ # Defaults to the RSRC_VERSION for the class.
53
+ #
54
+ # @param cnx[Jamf::Connection] The API connection to use
55
+ #
56
+ # @return [Jamf::SingletonResource] The ruby-instance of a Jamf resource
57
+ #
58
+ def self.fetch(reload = false, cnx: Jamf.cnx)
59
+ cnx.singleton_cache[self] = nil if reload
60
+ cached = cnx.singleton_cache[self]
61
+ return cached if cached
62
+
63
+ data = cnx.get "#{self::RSRC_VERSION}/#{self::RSRC_PATH}"
64
+ cnx.singleton_cache[self] = new data, cnx: cnx
65
+ end # fetch
66
+
67
+ def self.flushcache(cnx: Jamf.cnx)
68
+ validate_not_abstract
69
+ cnx.singleton_cache[self] = nil
70
+ end
71
+
72
+ # Instance Methods
73
+ #####################################
74
+
75
+ # only have one path
76
+ def rsrc_path
77
+ self.class.rsrc_path
78
+ end
79
+
80
+ # singltons always exist
81
+ def exist?
82
+ true
83
+ end
84
+
85
+ end # class APIObject
86
+
87
+ end # module JAMF
@@ -0,0 +1,66 @@
1
+ # Copyright 2019 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
+ # The module
27
+ module Jamf
28
+
29
+ # handle nils and IPAddrs
30
+ # def new(an_ip, cnx: nil)
31
+ # return nil if an_ip.nil?
32
+ # an_ip = an_ip.to_s if an_ip.is_a? IPAddr
33
+ # super an_ip, cnx: cnx
34
+ # end
35
+
36
+ # A wrapper for IPAddr - allowing initialize to take an unused cnx:
37
+ # and providing #to_jamf
38
+ #
39
+ class IPAddress < IPAddr
40
+
41
+ # @param an_ip[String,IPAddr]
42
+ #
43
+ # @param cnx [void] unused, but required
44
+ #
45
+ def initialize(an_ip, cnx: nil)
46
+ cnx.to_s # shutup rubocop.
47
+
48
+ if an_ip.nil?
49
+ @empty_ip = true
50
+ return
51
+ end
52
+
53
+ super an_ip
54
+ end
55
+
56
+ # @return [String] the IP formatted for passing to the API as a string.
57
+ #
58
+ def to_jamf
59
+ return Jamf::BLANK if @empty_ip
60
+ to_s
61
+ end
62
+
63
+
64
+ end # class Timestamp
65
+
66
+ end # module
@@ -0,0 +1,144 @@
1
+ # Copyright 2019 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 Jamf
27
+
28
+ # A timestamp as used in the JAMF API JSON data
29
+ #
30
+ # Instantiate with a String in iso6801 format (used in the API JSON for
31
+ # all(?) time/date values), or with a Time or Jamf::Timestamp instance, or with
32
+ # an Integer unix epoch, which is treated Jamf-style if 1_000_000_000_000 or
33
+ # higher
34
+ #
35
+ # To unset a timestamp value, instantiate with nil or an empty string. The
36
+ # Time value will be '1970-01-01 00:00:00 -0000', the unix epoch,
37
+ # and the to_jamf method will return an empty string.
38
+ #
39
+ # NOTE: Passing '1970-01-01 00:00:00 -0000' or the equivalent explicitly
40
+ # will NOT be treated as an empty timestamp, but as that actual value.
41
+ # You must pass nil or an empty string to indicate an empty value
42
+ #
43
+ # TODO: Find out: will an empty string work, e.g. in ext attrs with a DATE
44
+ # value, when used in criteria?
45
+ #
46
+ # This class is a subclass of Time, so all Time methods are available.
47
+ # - use .to_i for a unix epoch in seconds
48
+ # - use .to_f for a unix epoch with fractions
49
+ #
50
+ # Use #to_jamf to get the formated string to use in JSON for sending to the
51
+ # API - it *should* always be in ISO8601 format
52
+ #
53
+ class Timestamp < ::Time
54
+
55
+ # When we are unsetting a timestamp by intializing with nil,
56
+ # we still have to have a time object - so use the unix epoch
57
+ NIL_TIMESTAMP = Time.at 0
58
+
59
+ # Integers with this value or higher are a jamf-style epoch,
60
+ # meaning the first 10 digits are a unix epoch, and the last 3
61
+ # are milliseconds. Integers below this shouldn't appear, but
62
+ # will be treated as a regular unix epoch.
63
+ # (999_999_999_999 = 33658-09-27 01:46:39 UTC)
64
+ J_EPOCH_INT_START = 1_000_000_000_000
65
+
66
+ # Stings containing integers of this length are a jamf-style epoch,
67
+ # meaning the first 10 digits are a unix epoch, and the last 3
68
+ # are milliseconds. This length-test will be valid until the year 2286.
69
+ J_EPOCH_STR_LEN = 13
70
+
71
+ # @param tstamp[String,Integer,Time] A representation of a timestampe
72
+ #
73
+ # @param _args [void] unused, but required for JSONObject init.
74
+ #
75
+ def initialize(tstamp, **_args)
76
+ # use a Time object to parse the input and generate our own
77
+ # object
78
+ time = parse_init_tstamp(tstamp)
79
+
80
+ super(
81
+ time.year,
82
+ time.month,
83
+ time.day,
84
+ time.hour,
85
+ time.min,
86
+ (time.sec + (time.usec/1_000_000.0)).round(3),
87
+ time.utc_offset
88
+ )
89
+ end
90
+
91
+ # @return [Integer] the milliseconds of the Time
92
+ def msec
93
+ return 0 if @empty_timestamp
94
+
95
+ (usec / 1000.0).round
96
+ end
97
+
98
+ # @return [String] the timestamp formatted for passing to the API as a string.
99
+ def to_jamf
100
+ return Jamf::BLANK if @empty_timestamp
101
+
102
+ iso8601
103
+ end
104
+
105
+ def to_jamf_epoch
106
+ (to_f.round(3) * 1000).to_i
107
+ end
108
+
109
+ # Private Instance Methods
110
+ ################################
111
+ private
112
+
113
+ # @param tstamp @see #initialize
114
+ # @return [Time]
115
+ def parse_init_tstamp(tstamp)
116
+ case tstamp
117
+ when Time
118
+ tstamp
119
+
120
+ when Integer
121
+ Time.at real_epoch_from_j_epoch(tstamp)
122
+
123
+ when /^\d+$/
124
+ Time.at real_epoch_from_j_epoch(tstamp.to_i)
125
+
126
+ when Jamf::BLANK, nil
127
+ @empty_timestamp = true
128
+ NIL_TIMESTAMP
129
+
130
+ else
131
+ Time.parse tstamp.to_s
132
+ end # case
133
+ end
134
+
135
+ # convert an integer into a float if needed for parsing
136
+ # @param j_epoch [Integer]
137
+ # @return [Integer, Float]
138
+ def real_epoch_from_j_epoch(j_epoch)
139
+ j_epoch >= J_EPOCH_INT_START ? (j_epoch / 1000.0) : j_epoch
140
+ end
141
+
142
+ end # class Timestamp
143
+
144
+ end # module
@@ -0,0 +1,734 @@
1
+ # Copyright 2019 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
+ require 'faraday' # >= 0.17.0
25
+ require 'faraday_middleware' # >= 0.13.0
26
+
27
+ require 'jamf/api/connection/token'
28
+ require 'jamf/api/connection/api_error'
29
+
30
+ # The module
31
+ module Jamf
32
+
33
+ # Changes from classic Jamf::APIconnection
34
+ # - uses Faraday as the REST engine
35
+ # - accepts a url with connect/initialize
36
+ # - only supports https, no http
37
+ # - no xml
38
+ # - tokens & keep_alive
39
+ # - no object class method wrappers in connection objects,
40
+ # only passing connection objects into the class methods
41
+ #
42
+ class Connection
43
+
44
+ # Class Constants
45
+ #####################################
46
+
47
+ # The start of the path for API resources
48
+ RSRC_BASE = 'uapi'.freeze
49
+
50
+ # The API version must be this or higher
51
+ MIN_API_VERSION = Gem::Version.new('1.0')
52
+
53
+ HTTPS_SCHEME = 'https'.freeze
54
+
55
+ # The https default SSL port, default for Jamf Cloud servers
56
+ HTTPS_SSL_PORT = 443
57
+
58
+ # The Jamf default SSL port, default for on-prem servers
59
+ ON_PREM_SSL_PORT = 8443
60
+
61
+ # if either of these is specified, we'll default to SSL
62
+ SSL_PORTS = [ON_PREM_SSL_PORT, HTTPS_SSL_PORT].freeze
63
+
64
+ # Recognize Jamf Cloud servers
65
+ JAMFCLOUD_DOMAIN = '.jamfcloud.com'.freeze
66
+
67
+ # JamfCloud connections default to 443, not 8443
68
+ JAMFCLOUD_PORT = HTTPS_SSL_PORT
69
+
70
+ # Default open-connection timeout in seconds
71
+ DFT_OPEN_TIMEOUT = 60
72
+
73
+ # Default response timeout in seconds
74
+ DFT_TIMEOUT = 60
75
+
76
+ # The Default SSL Version
77
+ DFT_SSL_VERSION = 'TLSv1_2'.freeze
78
+
79
+ # refresh token if less than this many seconds until
80
+ # expiration. Default is 30 minutes if not specified
81
+ DFT_TOKEN_REFRESH = 60 * 30
82
+
83
+ # pre-existing tokens must have this many seconds before
84
+ # before they expire
85
+ TOKEN_REUSE_MIN_LIFE = 60
86
+
87
+ HTTP_ACCEPT_HEADER = 'Accept'
88
+ HTTP_CONTENT_TYPE_HEADER = 'Content-Type'
89
+
90
+ MIME_JSON = 'application/json'
91
+
92
+ SLASH = '/'.freeze
93
+
94
+ VALID_URL_REGEX = /\A#{URI.regexp(%w[https])}\z/.freeze
95
+
96
+ NOT_CONNECTED = 'Not Connected'.freeze
97
+
98
+ # Only these variables are displayed with PrettyPrint
99
+ # This avoids, especially, the caches, which are available
100
+ # as attr_readers
101
+ PP_VARS = %i[
102
+ @name
103
+ @connected
104
+ @host
105
+ @port
106
+ @user
107
+ @base_url
108
+ @ssl_options
109
+ @open_timeout
110
+ @timeout
111
+ @login_time
112
+ @keep_alive
113
+ @token_refresh
114
+ ].freeze
115
+
116
+ # Attributes
117
+ #####################################
118
+
119
+ # @return [String, nil]
120
+ attr_reader :name
121
+
122
+ # @return [String, nil]
123
+ attr_reader :host
124
+
125
+ # @return [Integer, nil]
126
+ attr_reader :port
127
+
128
+ # @return [String, nil]
129
+ attr_reader :user
130
+
131
+ # @return [Integer, nil]
132
+ attr_reader :timeout
133
+
134
+ # @return [Jamf::Connection::Token, nil]
135
+ attr_reader :token
136
+
137
+ # @return [Integer] Refresh the token this many seconds before it expires
138
+ attr_reader :token_refresh
139
+
140
+ # @return [String, nil]
141
+ attr_reader :base_url
142
+
143
+ # @return [Boolean]
144
+ attr_reader :connected
145
+ alias connected? connected
146
+
147
+ # @return [RestClient::Resource] the underlying rest resource
148
+ attr_reader :rest_cnx
149
+
150
+ # when was this connection logged in?
151
+ attr_reader :login_time
152
+
153
+ # @return [Hash]
154
+ # This Hash holds the most recently fetched instance of a SingletonResource
155
+ # subclass, keyed by the subclass itself.
156
+ #
157
+ # SingletonResource.fetch will return the instance from here, if it exists,
158
+ # unless the first parameter is truthy.
159
+
160
+ attr_reader :singleton_cache
161
+
162
+ # @return [Hash]
163
+ # This Hash holds the most recent API data (an Array of Hashes) for the list
164
+ # of all items in a CollectionResource subclass, keyed by the subclass itself.
165
+ #
166
+ # CollectionResource.all return the appropriate data from here, if it exists,
167
+ #
168
+ # See the CollectionResource.all class method.
169
+ attr_reader :collection_cache
170
+
171
+ # @return [Hash]
172
+ # This hash holds ExtensionAttribute instances, which are used
173
+ # for validating values passed to Extendable.set_ext_attr.
174
+ attr_reader :ext_attr_cache
175
+
176
+ # @return [Faraday::Response] The response object from the last API access.
177
+ attr_reader :last_http_response
178
+
179
+ # Constructor
180
+ #####################################
181
+
182
+ # @see #connect
183
+ def initialize(url = nil, **params)
184
+ @name = params.delete :name
185
+ @name ||= NOT_CONNECTED
186
+ connect(url, params) unless params[:at_load]
187
+ end
188
+
189
+ # Public Instance Methods
190
+ #####################################
191
+
192
+ # Connect this Connection object to an Jamf Pro API
193
+ #
194
+ # The first parameter may be a URL (must be https) from which
195
+ # the host & port will be used, and if present, the user and password
196
+ # E.g.
197
+ # connect 'https://myuser:pass@host.domain.edu:8443'
198
+ #
199
+ # which is the same as:
200
+ # connect host: 'host.domain.edu', port: 8443, user: 'myuser', pw: 'pass'
201
+ #
202
+ # When using a URL, other parameters below may be specified, however
203
+ # host: and port: parameters will be ignored, since they came from the URL,
204
+ # as will user: and :pw, if they are present in the URL. If the URL doesn't
205
+ # contain user and pw, they can be provided via the parameters, or left
206
+ # to default values.
207
+ #
208
+ # ### Passwords
209
+ # The pw: parameter also accepts the symbols :prompt, and :stdin[X]
210
+ #
211
+ # If :prompt, the user is promted on the commandline to enter the password
212
+ # for the :user.
213
+ #
214
+ # If :stdin the password is read from the first line of stdin
215
+ #
216
+ # If :stdinX (where X is an integer) the password is read from the Xth
217
+ # line of stdin.see {Jamf.stdin}
218
+ #
219
+ # If omitted, and running from an interactive terminal, the user is
220
+ # prompted as with :prompt
221
+ #
222
+ # ### Tokens
223
+ # Instead of a user and password, you may specify a valid 'token:', either:
224
+ #
225
+ # A Jamf::Connection::Token object, which can be extracted from an active
226
+ # Jamf::Connection via its #token method
227
+ #
228
+ # or
229
+ #
230
+ # A token string e.g. "eyJhdXR...6EKoo" from any source can also be used.
231
+ #
232
+ #
233
+ # Any values available via Jamf.config will be used if they are not provided
234
+ # in the parameters.
235
+ #
236
+ # @param host: [String] The API server hostname. The param 'server:' is a
237
+ # synonym
238
+ #
239
+ # @param port: [Integer] The API server port. If omitted, the value from
240
+ # Jamf.config will be used. If no config value, defaults to 443 if the
241
+ # host ends with 'jamfcloud.com' or 8443 otherwise
242
+ #
243
+ # @param user: [String] The username for the API connection
244
+ #
245
+ # @param pw: [String, Symbol] The password for the user, :prompt, or :stdin[X]
246
+ #
247
+ # @param token: [Jamf::Connection::Token, String] An existing, valid token.
248
+ # When used, there's no need to provide user: or pw:.
249
+ #
250
+ # @param open_timeout: [Integer] The number of seconds for initial contact
251
+ # with the host.
252
+ #
253
+ # @param timeout: [Integer] The number of seconds for a full response from
254
+ # the host.
255
+ #
256
+ # @param ssl_version: [Symbol] The SSL version, e.g. :TLSv1_2
257
+ #
258
+ # @param verify_cert: [Boolean] Should the SSL certificate be verified?
259
+ # Default is true, should only be set to false if using a on-prem
260
+ # server with a self-signed certificate, which is rare
261
+ #
262
+ def connect(url = nil, **params)
263
+ # This sets all the instance vars to nil, and flushes/creates the caches
264
+ disconnect
265
+
266
+ # parse the url if one was given
267
+ params[:url] = url
268
+ parse_url params if params[:url]
269
+
270
+ # apply defaults from config, client, and then this class.
271
+ apply_connection_defaults params
272
+
273
+ # confirm we know a host, port, user and pw
274
+ verify_basic_params params
275
+ parse_connect_params params
276
+
277
+ @token = acquire_token params
278
+ @login_time = @token.login_time
279
+ @user ||= @token.user
280
+
281
+ # if we're here we have a valid token
282
+ @rest_cnx = create_connection
283
+
284
+ validate_api_version
285
+
286
+ @connected = true
287
+
288
+ @keep_alive = params[:keep_alive].nil? ? false : params[:keep_alive]
289
+ @keep_alive && start_keep_alive
290
+ to_s
291
+ end # connect
292
+
293
+ def disconnect
294
+ # reset everything except the name & timeouts
295
+ @connected = false
296
+ @login_time = nil
297
+ @host = nil
298
+ @port = nil
299
+ @user = nil
300
+ @token = nil
301
+ @base_url = nil
302
+ @rest_cnx = nil
303
+ @ssl_version = nil
304
+ flushcache
305
+ end
306
+
307
+ # Same as disconnect, but invalidates the token
308
+ def logout
309
+ @token.destroy
310
+ disconnect
311
+ end
312
+
313
+ def get(rsrc)
314
+ validate_connected
315
+ resp = @rest_cnx.get rsrc
316
+ @last_http_response = resp
317
+ return resp.body if resp.success?
318
+
319
+ raise Jamf::Connection::APIError.new(resp)
320
+ end
321
+
322
+ # GET a rsrc without doing any JSON parsing, using
323
+ # a temporary Faraday connection object
324
+ def download(rsrc)
325
+ temp_cnx = create_connection(false)
326
+ resp = temp_cnx.get rsrc
327
+ @last_http_response = resp
328
+ return resp.body if resp.success?
329
+
330
+ raise Jamf::Connection::APIError.new(resp)
331
+ end
332
+
333
+ def post(rsrc, data)
334
+ validate_connected
335
+ resp = @rest_cnx.post(rsrc) do |req|
336
+ req.body = data
337
+ end
338
+ @last_http_response = resp
339
+ return resp.body if resp.success?
340
+
341
+ raise Jamf::Connection::APIError.new(resp)
342
+ end
343
+
344
+ def put(rsrc, data)
345
+ validate_connected
346
+ resp = @rest_cnx.put(rsrc) do |req|
347
+ req.body = data
348
+ end
349
+ @last_http_response = resp
350
+ return resp.body if resp.success?
351
+
352
+ raise Jamf::Connection::APIError.new(resp)
353
+ end
354
+
355
+ def patch(rsrc, data)
356
+ validate_connected
357
+ resp = @rest_cnx.patch(rsrc) do |req|
358
+ req.body = data
359
+ end
360
+ @last_http_response = resp
361
+ return resp.body if resp.success?
362
+
363
+ raise Jamf::Connection::APIError.new(resp)
364
+ end
365
+
366
+ def delete(rsrc)
367
+ validate_connected
368
+ resp = @rest_cnx.delete rsrc
369
+ @last_http_response = resp
370
+ return resp.body if resp.success?
371
+
372
+ raise Jamf::Connection::APIError.new(resp)
373
+ end
374
+
375
+ # A useful string about this connection
376
+ #
377
+ # @return [String]
378
+ #
379
+ def to_s
380
+ "Jamf::Connection: https://#{@user}@#{@host}:#{@port}"
381
+ end
382
+
383
+ def keep_alive?
384
+ !@keep_alive_thread.nil?
385
+ end
386
+
387
+ def keep_alive=(bool)
388
+ bool ? start_keep_alive : stop_keep_alive
389
+ end
390
+
391
+ # This should take effect even if we're already running the keep_alive thread
392
+ #
393
+ def token_refresh=(secs)
394
+ raise ArgumentError, 'Value must be an Integer number of seconds' unless secs.is_a? Integer
395
+
396
+ @token_refresh = secs
397
+ end
398
+
399
+ def api_version
400
+ @token.api_version
401
+ end
402
+
403
+ # Flush the collection and/or ea cache for the given class,
404
+ # or all cached data
405
+ # @param klass[Class] the class of cache to flush
406
+ #
407
+ # @return [void]
408
+ #
409
+ def flushcache(klass = nil)
410
+ if klass
411
+ @collection_cache.delete klass
412
+ @singleton_cache.delete klass
413
+ @ext_attr_cache.delete klass
414
+ else
415
+ @collection_cache = {}
416
+ @singleton_cache = {}
417
+ @ext_attr_cache = {}
418
+ end
419
+ end
420
+
421
+ # Remove large cached items from
422
+ # the instance_variables used to create
423
+ # pretty-print (pp) output.
424
+ #
425
+ # @return [Array] the desired instance_variables
426
+ #
427
+ def pretty_print_instance_variables
428
+ PP_VARS
429
+ end
430
+
431
+ # Private Insance Methods
432
+ ####################################
433
+ private
434
+
435
+ # given a token string or a password, get a valid token
436
+ # Token.new will raise an exception if the token string or
437
+ # credentials are invalid
438
+ def token_from(type, data)
439
+ token_params = {
440
+ user: @user,
441
+ base_url: @base_url,
442
+ timeout: @timeout,
443
+ ssl_options: @ssl_options
444
+ }
445
+
446
+ case type
447
+ when :token_string
448
+ token_params[:token_string] = data
449
+ when :pw
450
+ token_params[:pw] = data
451
+ end
452
+ self.class::Token.new token_params
453
+ end
454
+
455
+ # raise exception if not connected
456
+ def validate_connected
457
+ raise Jamf::InvalidConnectionError, 'Not Connected. Use .connect first.' unless connected?
458
+ end
459
+
460
+ # raise exception if API version is too low.
461
+ def validate_api_version
462
+ vers = api_version
463
+ return if Gem::Version.new(vers) >= MIN_API_VERSION
464
+
465
+ raise Jamf::InvalidConnectionError, "API version '#{vers}' too low, must be >= '#{MIN_API_VERSION}'"
466
+ end
467
+
468
+ def parse_url(params)
469
+ url = URI.parse params[:url].to_s
470
+ raise ArgumentError, 'Invalid url, scheme must be https' unless url.scheme == HTTPS_SCHEME
471
+
472
+ params[:user] = url.user
473
+ params[:pw] = url.password
474
+ params[:host] = url.host
475
+ params[:port] = url.port
476
+ end
477
+
478
+ # Apply defaults from the Jamf.config,
479
+ # then from the Jamf::Client,
480
+ # then from the module defaults
481
+ # to the params for the #connect method
482
+ #
483
+ # @param params[Hash] The params for #connect
484
+ #
485
+ # @return [Hash] The params with defaults applied
486
+ #
487
+ def apply_connection_defaults(params)
488
+ apply_defaults_from_config(params)
489
+ # TODO: when clients are moved over
490
+ # apply_defaults_from_client(params)
491
+ apply_module_defaults(params)
492
+
493
+ # if we have a TTY, pw defaults to :prompt
494
+ params[:pw] ||= :prompt if STDIN.tty?
495
+ end
496
+
497
+ # Apply defaults from the Jamf.config
498
+ # to the params for the #connect method
499
+ #
500
+ # @param params[Hash] The params for #connect
501
+ #
502
+ # @return [Hash] The params with defaults applied
503
+ #
504
+ def apply_defaults_from_config(params)
505
+ # settings from config if they aren't in the params
506
+ params[:host] ||= Jamf.config.api_server_name
507
+ params[:port] ||= Jamf.config.api_server_port
508
+ params[:user] ||= Jamf.config.api_username
509
+ params[:timeout] ||= Jamf.config.api_timeout
510
+ params[:open_timeout] ||= Jamf.config.api_timeout_open
511
+ params[:ssl_version] ||= Jamf.config.api_ssl_version
512
+
513
+ # if verify cert was not in the params, get it from the prefs.
514
+ # We can't use ||= because the desired value might be 'false'
515
+ params[:verify_cert] = Jamf.config.api_verify_cert if params[:verify_cert].nil?
516
+ end # apply_defaults_from_config
517
+
518
+ # Apply defaults from the Jamf::Client
519
+ # to the params for the #connect method
520
+ #
521
+ # @param params[Hash] The params for #connect
522
+ #
523
+ # @return [Hash] The params with defaults applied
524
+ #
525
+ def apply_defaults_from_client(params)
526
+ return unless Jamf::Client.installed?
527
+
528
+ # these settings can come from the jamf binary config,
529
+ # if this machine is a Jamf client.
530
+ params[:host] ||= Jamf::Client.jss_server
531
+ params[:port] ||= Jamf::Client.jss_port.to_i
532
+ end
533
+
534
+ # Apply the module defaults to the params for the #connect method
535
+ #
536
+ # @param params[Hash] The params for #connect
537
+ #
538
+ # @return [Hash] The params with defaults applied
539
+ #
540
+ def apply_module_defaults(params)
541
+ params[:port] ||= params[:host].to_s.end_with?(JAMFCLOUD_DOMAIN) ? JAMFCLOUD_PORT : ON_PREM_SSL_PORT
542
+ params[:timeout] ||= DFT_TIMEOUT
543
+ params[:open_timeout] ||= DFT_OPEN_TIMEOUT
544
+ params[:ssl_version] ||= DFT_SSL_VERSION
545
+ end
546
+
547
+ # From whatever was given in params[:pw], figure out the real password
548
+ #
549
+ # @param params[Hash] The params for #connect
550
+ #
551
+ # @return [String] The password for the connection
552
+ #
553
+ def acquire_password(params)
554
+ if params[:pw] == :prompt
555
+ Jamf.prompt_for_password "Enter the password for Jamf user #{params[:user]}@#{params[:host]}:"
556
+ elsif params[:pw].is_a?(Symbol) && params[:pw].to_s.start_with?('stdin')
557
+ params[:pw].to_s =~ /^stdin(\d+)$/
558
+ line = Regexp.last_match(1)
559
+ line ||= 1
560
+ Jamf.stdin line
561
+ else
562
+ params[:pw]
563
+ end
564
+ end
565
+
566
+ # Raise execeptions if we don't have essential data for a new connection
567
+ #
568
+ # @param params[Hash] The params for #connect
569
+ #
570
+ # @return [void]
571
+ #
572
+ def verify_basic_params(params)
573
+ # if given a Token object, it has host, port, user, and base_url
574
+ # and will be verified and parsed
575
+ return if params[:token].is_a? self.class::Token
576
+
577
+ # must have a host, accept :server as well as :host
578
+ params[:host] ||= params[:server]
579
+ raise Jamf::MissingDataError, 'No Jamf :host specified, or in configuration.' unless params[:host]
580
+
581
+ # no need for user or pass if using a token string
582
+ return if params[:token]
583
+
584
+ raise Jamf::MissingDataError, 'No Jamf :user specified, or in configuration.' unless params[:user]
585
+ raise Jamf::MissingDataError, "No :pw specified for user '#{params[:user]}'" unless params[:pw]
586
+ end
587
+
588
+ def parse_connect_params(params)
589
+ @host = params[:host]
590
+ @port = params[:port]
591
+ @port ||= @host.end_with?(JAMFCLOUD_DOMAIN) ? JAMFCLOUD_PORT : ON_PREM_SSL_PORT
592
+ @user = params[:user]
593
+ @token_refresh = params[:token_refresh] || DFT_TOKEN_REFRESH
594
+ @timeout = params[:timeout] || DFT_TIMEOUT
595
+ @open_timeout = params[:open_timeout] || DFT_TIMEOUT
596
+ @base_url = URI.parse "https://#{@host}:#{@port}/#{RSRC_BASE}"
597
+ # ssl opts for faraday
598
+ # TODO: implement all of faraday's options
599
+ @ssl_options = {
600
+ verify: params[:verify_cert],
601
+ version: params[:ssl_version]
602
+ }
603
+ @name = "#{@user}@#{@host}:#{@port}" if @name == NOT_CONNECTED
604
+ end
605
+
606
+ # Get our token either from a passwd or another token or token string
607
+ def acquire_token(params)
608
+ if params[:token]
609
+ if params[:token].is_a? self.class::Token
610
+ verify_token params[:token]
611
+ parse_token params[:token]
612
+ params[:token]
613
+ else
614
+ token_from :token_string, params[:token].to_s
615
+ end
616
+ else
617
+ token_from :pw, acquire_password(params)
618
+ end
619
+ end
620
+
621
+ # Raise execeptions if we were given an unusable token
622
+ #
623
+ # @param params[Hash] The params for #connect
624
+ #
625
+ # @return [void]
626
+ #
627
+ def verify_token(token)
628
+ raise 'Cannot use token: it has expired' if token.expired?
629
+ raise 'Cannot use token: it is invalid' unless token.valid?
630
+ raise "Cannot use token: it expires in less than #{TOKEN_REUSE_MIN_LIFE} seconds" if token.secs_remaining < TOKEN_REUSE_MIN_LIFE
631
+ end
632
+
633
+ def parse_token(token)
634
+ @host = token.host
635
+ @port = token.port
636
+ @user = token.user
637
+ @base_url = token.base_url
638
+ end
639
+
640
+ # create the faraday connection object
641
+ def create_connection(parse_json = true)
642
+ Faraday.new(@base_url, ssl: @ssl_options) do |cnx|
643
+ cnx.headers[HTTP_ACCEPT_HEADER] = MIME_JSON
644
+ cnx.headers[:authorization] = @token.auth_token
645
+ cnx.request :json if parse_json
646
+ cnx.response :json, parser_options: { symbolize_names: true } if parse_json
647
+ cnx.options[:timeout] = @timeout
648
+ cnx.options[:open_timeout] = @open_timeout
649
+ cnx.use Faraday::Adapter::NetHttp
650
+ end
651
+ end
652
+
653
+ # creates a thread that loops forever, sleeping most of the time, but
654
+ # waking up every 60 seconds to see if the token is expiring in the
655
+ # next @token_refresh seconds.
656
+ #
657
+ # If so, the token is refreshed, and we keep looping and sleeping.
658
+ #
659
+ # Sets @keep_alive_thread to the Thread object
660
+ #
661
+ # @return [void]
662
+ #
663
+ def start_keep_alive
664
+ return if @keep_alive_thread
665
+ raise 'Token expired' if @token.expired?
666
+
667
+ @keep_alive_thread =
668
+ Thread.new do
669
+ loop do
670
+ sleep 60
671
+ next if @token.secs_remaining > @token_refresh
672
+
673
+ @token.keep_alive
674
+ end # loop
675
+ end # thread
676
+ end
677
+
678
+ # Kills the @keep_alive_thread, if it exists, and sets
679
+ # @keep_alive_thread to nil
680
+ #
681
+ # @return [void]
682
+ #
683
+ def stop_keep_alive
684
+ return unless @keep_alive_thread
685
+
686
+ @keep_alive_thread.kill
687
+ @keep_alive_thread = nil
688
+ end
689
+
690
+ end # class Connection
691
+
692
+ # Jamf module methods dealing with the active connection
693
+
694
+ # @return [Jamf::Connection] the active connection
695
+ #
696
+ def self.cnx
697
+ @active_connection ||= Connection.new
698
+ end
699
+
700
+ # Create a new Connection object and use it as the active_connection,
701
+ # replacing the current active_connection. If connection options are provided,
702
+ # they are passed to the connect method immediately, otherwise
703
+ # Jamf.cnx.connect must be called before attemting to use the
704
+ # connection.
705
+ #
706
+ # @param (See Jamf::Connection#connect)
707
+ #
708
+ # @return [APIConnection] the new, active connection
709
+ #
710
+ def self.connect(url = nil, **params)
711
+ @active_connection = Connection.new url, params
712
+ @active_connection.to_s
713
+ end
714
+
715
+ # Switch the connection used for all API interactions to the
716
+ # one provided. See {Jamf::APIConnection} for details and examples
717
+ # of using multiple connections
718
+ #
719
+ # @param connection [APIConnection] The APIConnection to use for future
720
+ # API calls. If omitted, use the default connection created when ruby-jss
721
+ # was loaded (which may or may not yet be connected)
722
+ #
723
+ # @return [APIConnection] The connection now being used.
724
+ #
725
+ def self.cnx=(connection)
726
+ raise 'API connections must be instances of Jamf::Connection' unless connection.is_a? Jamf::Connection
727
+
728
+ @active_connection = connection
729
+ end
730
+
731
+ # create the default connection
732
+ connect(at_load: true) unless @active_connection
733
+
734
+ end # module Jamf