axm 0.1.2 → 1.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b9cf51eb8d1e42d2ee6112d4bf1bc8d38408a56581f0ed0fcaafcb4358692a7
4
- data.tar.gz: 48a58959848c13ff47d37dd9eb9cd104291bd66e422fc00ba8b3bfbdd3eea3b7
3
+ metadata.gz: 729a2e7795a94f807dc732928f6e704f42d4e93fdb712bde4083a04410fb1aa5
4
+ data.tar.gz: 8256543f1d81885b9ee2f6bae17f74506efaef554796b9ac2e03a415a3d50954
5
5
  SHA512:
6
- metadata.gz: bd809519957cc388dff7f4c62ce718de7f0c3990c85056a24964de019b2c77338db9078c56b488a583e289ecde0378f40e965682bcd11beaa329bbab625dccbb
7
- data.tar.gz: 8d4db509e738fe69a17265e718f8371a7c8c2d1e9a1ad7ca3c8471a5eebec62262c5d7f071c5040614fefc1568b75b47f59cd1bc331dae063b9f4ae5073812d1
6
+ metadata.gz: f3ae6ad5f92c66c84284011b9bb3ac2e5f3297df2662783807bf0893ec46efd337490185b6871a131b8a12e2fbfa2b59d581e213921510fadf16be5532ec563f
7
+ data.tar.gz: 5bcb9240a6aae5f17e9e872c0bf5a5796c8b9b0665e583a9063f6d42dafd59161656c1645ea8fa35deba31e8ae899d13993b031c61bddb5c193f7f5381b09bed
data/.rubocop.yml CHANGED
@@ -11,9 +11,3 @@ Layout/LineLength:
11
11
 
12
12
  Style/Documentation:
13
13
  Enabled: false
14
-
15
- Style/StringLiterals:
16
- EnforcedStyle: double_quotes
17
-
18
- Style/StringLiteralsInInterpolation:
19
- EnforcedStyle: double_quotes
data/CHANGELOG.md CHANGED
@@ -1,4 +1,19 @@
1
- ## [Unreleased]
1
+ ## 1.0.1 - 2025-11-13
2
+
3
+ - Add OpenSSL as a dev dependency (<https://github.com/nick-f/axm/pull/26>)
4
+ - Store the access token in memory (<https://github.com/nick-f/axm/pull/25>)
5
+
6
+ ## 1.0.0 - 2025-11-08
7
+
8
+ ### Added
9
+
10
+ - List all MDM servers in an organisation
11
+ - Show all device IDs assigned to a specific MDM server
12
+ - Show which MDM server a device is assigned to
13
+ - Show the ID of the MDM server a device is assigned to
14
+ - Show information about the MDM server a device is assigned to
15
+ - Assign and unassign devices to MDM servers
16
+ - Show AppleCare coverage information for devices
2
17
 
3
18
  ## 0.1.2 - 2025-07-14
4
19
 
data/README.md CHANGED
@@ -58,7 +58,13 @@ key_id = Secret.read('key_id')
58
58
  client = Axm::Client.new(private_key:, client_id:, key_id:)
59
59
  ```
60
60
 
61
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
61
+ To install this gem onto your local machine, run `bundle exec rake install`.
62
+
63
+ ## Releasing
64
+
65
+ 1. Update the version number in [lib/axm/version.rb](./lib/axm/version.rb)
66
+ 2. Add release notes to [CHANGELOG.md](CHANGELOG.md)
67
+ 3. Run the "Release" Action
62
68
 
63
69
  ## Contributing
64
70
 
@@ -70,4 +76,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
70
76
 
71
77
  ## Code of Conduct
72
78
 
73
- Everyone interacting in the Axm project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nick-f/axm/blob/main/CODE_OF_CONDUCT.md).
79
+ Everyone interacting with the AXM codebase and issue tracker is expected to follow the [code of conduct](https://github.com/nick-f/axm/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axm
4
+ class Client
5
+ module MdmServers
6
+ # Retrieves a list of MDM servers associated with the organization.
7
+ #
8
+ # @param options [Hash] Optional query parameters to filter fields or paginate results.
9
+ # - fields: (Array) Array of fields to include in the response.
10
+ # - limit: (Integer) Maximum number of devices to return per page (default: 100, maximum: 1000).
11
+ # @return [Array<Hash>] An array of MDM servers.
12
+ #
13
+ # See: https://developer.apple.com/documentation/applebusinessmanagerapi/get-mdm-servers
14
+ # See: https://developer.apple.com/documentation/appleschoolmanagerapi/get-mdm-servers
15
+ def list_mdm_servers(options = {})
16
+ get('v1/mdmServers', options)
17
+ end
18
+
19
+ # Retrieves a list of IDs for the devices assigned to a specific MDM server.
20
+ #
21
+ # @param mdm_server_id [String] The unique identifier of the MDM server.
22
+ # @param options [Hash] Optional query parameters to paginate results or increase number of returned results.
23
+ # - limit: (Integer) Maximum number of devices to return per page (default: 100, maximum: 1000).
24
+ # @return [Array<Hash>] An array of device IDs assigned to the specified MDM server.
25
+ #
26
+ # See: https://developer.apple.com/documentation/applebusinessmanagerapi/get-all-device-ids-for-a-mdmserver
27
+ # See: https://developer.apple.com/documentation/appleschoolmanagerapi/get-all-device-ids-for-a-mdmserver
28
+ def devices_assigned_to_mdm_server(mdm_server_id, options = {})
29
+ get("v1/mdmServers/#{mdm_server_id}/relationships/devices", options)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axm
4
+ class Client
5
+ module OrganizationDeviceActivities
6
+ # Get information for an organization device activity that a device management action, such as assign or unassign, creates.
7
+ #
8
+ # @param options [Hash] Optional query parameters to filter returned attributes.
9
+ # - fields: (Array) Array of fields to include in the response.
10
+ # @return [<Hash>] A single organization device activity resource.
11
+ #
12
+ # See: https://developer.apple.com/documentation/applebusinessmanagerapi/get-orgdeviceactivity-information
13
+ # See: https://developer.apple.com/documentation/appleschoolmanagerapi/get-orgdeviceactivity-information
14
+ def org_device_activity(activity_id, options = {})
15
+ get("v1/orgDeviceActivities/#{activity_id}", options)
16
+ end
17
+
18
+ # Assigns a device to an MDM server.
19
+ #
20
+ # @param device_ids [Array<String>] Array of device IDs to be assigned.
21
+ # @param mdm_server_id [String] The unique identifier of the MDM server to which the device will be assigned.
22
+ # @return [Hash, Integer] The response from the POST method, containing details of the assignment and status code.
23
+ def assign(device_id, mdm_server_id)
24
+ assignment_change(device_id, mdm_server_id, 'ASSIGN_DEVICES')
25
+ end
26
+
27
+ # Unassigns a device from an MDM server.
28
+ #
29
+ # @param device_ids [Array<String>] Array of device IDs to be unassigned.
30
+ # @param mdm_server_id [String] The unique identifier of the MDM server to which the device will be assigned.
31
+ # @return [Hash, Integer] The response from the POST method, containing details of the assignment and status code.
32
+ def unassign(device_id, mdm_server_id)
33
+ assignment_change(device_id, mdm_server_id, 'UNASSIGN_DEVICES')
34
+ end
35
+
36
+ private
37
+
38
+ # Sends a request to change the assignment of a device to an MDM server.
39
+ #
40
+ # @param device_ids [Array<String>] Array of IDs of devices to be assigned or unassigned.
41
+ # @param mdm_server_id [String] The unique identifier of the MDM server.
42
+ # @param activity_type [String] The type of activity being performed ("ASSIGN_DEVICES" or "UNASSIGN_DEVICES").
43
+ # @return [Hash, Integer] The response from the POST request, containing details of the operation and status code.
44
+ # rubocop:disable Metrics/MethodLength
45
+ def assignment_change(device_ids, mdm_server_id, activity_type)
46
+ devices = device_ids.map do |device_id|
47
+ {
48
+ type: 'orgDevices',
49
+ id: device_id
50
+ }
51
+ end
52
+
53
+ request_body = {
54
+ data: {
55
+ type: 'orgDeviceActivities',
56
+ attributes: {
57
+ activityType: activity_type
58
+ },
59
+ relationships: {
60
+ mdmServer: {
61
+ data: {
62
+ type: 'mdmServers',
63
+ id: mdm_server_id
64
+ }
65
+ },
66
+ devices: {
67
+ data: devices
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ post('v1/orgDeviceActivities', request_body)
74
+ end
75
+ # rubocop:enable Metrics/MethodLength
76
+ end
77
+ end
78
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Axm
2
4
  class Client
3
5
  module OrganizationDevices
@@ -11,7 +13,7 @@ module Axm
11
13
  # See: https://developer.apple.com/documentation/applebusinessmanagerapi/get-org-devices
12
14
  # See: https://developer.apple.com/documentation/appleschoolmanagerapi/get-org-devices
13
15
  def list_org_devices(options = {})
14
- get("v1/orgDevices", options)
16
+ get('v1/orgDevices', options)
15
17
  end
16
18
 
17
19
  # Retrieves information about a specific device in the organization.
@@ -25,6 +27,50 @@ module Axm
25
27
  def device(id, options = {})
26
28
  get("/v1/orgDevices/#{id}", options)
27
29
  end
30
+
31
+ # Fetch the assigned device management service ID for a device.
32
+ #
33
+ # @param device_id [String] The unique identifier of the device.
34
+ # @param options [Hash] Optional query parameters to filter or paginate results.
35
+ # @return [<Hash>] Identifier of the device's assigned MDM server.
36
+ #
37
+ # See: https://developer.apple.com/documentation/applebusinessmanagerapi/get-the-assigned-server-id-for-an-orgdevice
38
+ # See: https://developer.apple.com/documentation/appleschoolmanagerapi/get-the-assigned-server-id-for-an-orgdevice
39
+ def assigned_mdm_server_id(device_id, options = {})
40
+ get("v1/orgDevices/#{device_id}/relationships/assignedServer", options)
41
+ end
42
+
43
+ # Fetch the assigned device management service information for a device.
44
+ #
45
+ # @param device_id [String] The unique identifier of the device.
46
+ # @param options [Hash] Optional query parameters to filter or paginate results.
47
+ # - id: (String) The unique identifier of the device.
48
+ # - fields: (Array) Array of fields to include in the response.
49
+ # @return [<Hash>] A hash containing the identifier of the device's assigned MDM server.
50
+ #
51
+ # See: https://developer.apple.com/documentation/applebusinessmanagerapi/get-the-assigned-server-information-for-an-orgdevice
52
+ # See: https://developer.apple.com/documentation/appleschoolmanagerapi/get-the-assigned-server-information-for-an-orgdevice
53
+ def assigned_mdm_server(device_id, options = {})
54
+ options[:fields_key] = 'mdmServers'
55
+
56
+ get("v1/orgDevices/#{device_id}/assignedServer", options)
57
+ end
58
+
59
+ # Fetch the AppleCare coverage information for a device.
60
+ #
61
+ # @param device_id [String] The unique identifier of the device.
62
+ # @param options [Hash] Optional query parameters to filter or paginate results.
63
+ # - fields: (Array) Array of fields to include in the response.
64
+ # - limit: (Integer) The number of included related resources to return (default: 100, maximum: 1000).
65
+ # @return [<Hash>] A hash containing the identifier of the device's assigned MDM server.
66
+ #
67
+ # See: https://developer.apple.com/documentation/applebusinessmanagerapi/get-all-apple-care-coverage-for-an-orgdevice
68
+ # See: https://developer.apple.com/documentation/appleschoolmanagerapi/get-all-apple-care-coverage-for-an-orgdevice
69
+ def applecare_coverage(device_id, options = {})
70
+ options[:fields_key] = 'appleCareCoverage'
71
+
72
+ get("v1/orgDevices/#{device_id}/appleCareCoverage", options)
73
+ end
28
74
  end
29
75
  end
30
76
  end
data/lib/axm/client.rb CHANGED
@@ -1,13 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'jwt'
3
5
  require 'net/http'
4
6
  require 'securerandom'
5
7
  require 'time'
6
8
 
9
+ require 'axm/client/mdm_servers'
10
+ require 'axm/client/organization_device_activities'
7
11
  require 'axm/client/organization_devices'
8
12
 
9
13
  module Axm
14
+ # rubocop: disable Metrics/ClassLength
10
15
  class Client
16
+ include MdmServers
17
+ include OrganizationDeviceActivities
11
18
  include OrganizationDevices
12
19
 
13
20
  # Initializes a new instance of the AXM client.
@@ -54,13 +61,14 @@ module Axm
54
61
  end
55
62
  end
56
63
 
64
+ # rubocop: disable Metrics/MethodLength
57
65
  def client_assertion
58
66
  @client_assertion ||= begin
59
67
  audience = 'https://account.apple.com/auth/oauth2/v2/token'
60
68
  algo = 'ES256'
61
69
 
62
70
  issued_at_timestamp = Time.now.utc.to_i
63
- expiration_timestamp = issued_at_timestamp + 86_400 * 180 # 180 days
71
+ expiration_timestamp = issued_at_timestamp + (86_400 * 180) # 180 days
64
72
 
65
73
  payload = {
66
74
  sub: @client_id,
@@ -74,34 +82,23 @@ module Axm
74
82
  JWT.encode(payload, @private_key, algo, kid: @key_id)
75
83
  end
76
84
  end
85
+ # rubocop: enable Metrics/MethodLength
77
86
 
78
87
  def access_token
79
- cached_access_token = JSON.parse(Secret.read('stub_access_token')) if File.exist?('secrets/stub_access_token')
80
-
81
- if cached_access_token
82
- token_expiration = Time.parse(cached_access_token['expires_at'])
88
+ if @cached_access_token
89
+ token_expiration = Time.parse(@cached_access_token['expires_at'])
83
90
  token_expired = Time.now.utc >= token_expiration
84
91
 
85
- return cached_access_token unless token_expired
92
+ return @cached_access_token unless token_expired
86
93
  end
87
94
 
88
- params = {
89
- grant_type: 'client_credentials',
90
- client_id: @client_id,
91
- client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
92
- client_assertion: client_assertion,
93
- scope: "#{scope}.api"
94
- }
95
-
96
- response = post('https://account.apple.com/auth/oauth2/v2/token', params)
95
+ response = exchange_access_token_request
97
96
 
98
97
  response_body = response.first if response.last.to_i == 200
99
98
 
100
- token = response_body.merge!({ 'expires_at' => Time.now.utc + response_body['expires_in'] })
101
-
102
- Secret.write('stub_access_token', token.to_json)
99
+ @cached_access_token = response_body.merge({ 'expires_at' => Time.now.utc + response_body['expires_in'] })
103
100
 
104
- response_body
101
+ @cached_access_token
105
102
  end
106
103
 
107
104
  # Sends a GET request to the specified API endpoint.
@@ -111,13 +108,18 @@ module Axm
111
108
  # - :paginate [Boolean] Whether to paginate through all results (unused).
112
109
  # - :fields [Array<String>] Optional fields to include as fields[orgDevices].
113
110
  # @return [Hash] The parsed JSON response.
111
+ # rubocop: disable Metrics/MethodLength, Metrics/AbcSize
114
112
  def get(path, options = {})
115
113
  options = options.dup
116
114
 
117
- endpoint = path.split('/').last
115
+ # API endpoints are prefixed with 'v1', so we can extract the endpoint from the path.
116
+ endpoint = path.split('/')[1]
117
+
118
+ # For the cases where the fields_key is different to the path component, it can be overriden as an option.
119
+ fields_key = options.delete(:fields_key) || endpoint
118
120
 
119
121
  fields = options.delete(:fields)
120
- options["fields[#{endpoint}]"] = fields.join(',') if fields
122
+ options["fields[#{fields_key}]"] = fields.join(',') if fields
121
123
 
122
124
  uri = URI("https://#{api_domain}/#{path}")
123
125
  uri.query = URI.encode_www_form(options) unless options.empty?
@@ -132,21 +134,30 @@ module Axm
132
134
 
133
135
  JSON.parse(res.body)
134
136
  end
137
+ # rubocop: enable Metrics/MethodLength, Metrics/AbcSize
135
138
 
136
- # Sends a POST request to the specified URI with given parameters.
139
+ # Sends a POST request to exchange the credentials for an access token.
137
140
  #
138
- # @param uri [String, URI] The endpoint URI.
139
- # @param params [Hash] Parameters to include in the request body.
140
- # @return [Net::HTTPResponse] The HTTP response object.
141
- def post(uri, params = {})
142
- uri = URI(uri) if uri.is_a?(String)
141
+ # @return [Net::HTTPResponse, integer] The HTTP response object and status code.
142
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
143
+ def exchange_access_token_request
144
+ uri = URI('https://account.apple.com/auth/oauth2/v2/token')
145
+
146
+ request_body = {
147
+ grant_type: 'client_credentials',
148
+ client_id: @client_id,
149
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
150
+ client_assertion: client_assertion,
151
+ scope: "#{scope}.api"
152
+ }
153
+
143
154
  http = Net::HTTP.new(uri.host, uri.port)
144
155
  http.use_ssl = uri.scheme == 'https'
145
156
 
146
157
  request = Net::HTTP::Post.new(uri)
147
158
  request['Host'] = uri.host
148
159
  request['Content-Type'] = 'application/x-www-form-urlencoded'
149
- request.body = URI.encode_www_form(params) unless params.empty?
160
+ request.body = URI.encode_www_form(request_body)
150
161
 
151
162
  response = http.request(request)
152
163
 
@@ -155,8 +166,45 @@ module Axm
155
166
  response_json = JSON.parse(response.body)
156
167
 
157
168
  raise 'Invalid request' if response_json['error'] == 'invalid_request'
169
+ raise 'Invalid client ID or key ID' if response_json['error'] == 'invalid_client'
158
170
 
159
171
  [response_json, response.code]
160
172
  end
173
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
174
+
175
+ # Sends a POST request to the specified URI with given parameters.
176
+ #
177
+ # @param uri [String, URI] The endpoint URI.
178
+ # @param request_body [Hash] Parameters to include in the request body.
179
+ # @return [Hash, Integer] The HTTP response object and status code.
180
+ def post(path, request_body = {})
181
+ uri = URI("https://#{api_domain}/#{path}")
182
+
183
+ http = Net::HTTP.new(uri.host, uri.port, use_ssl: true)
184
+
185
+ request = add_post_headers(Net::HTTP::Post.new(uri))
186
+
187
+ request.body = request_body.to_json unless request_body.empty?
188
+
189
+ response = http.request(request)
190
+
191
+ response_body = JSON.parse(response.body)
192
+
193
+ [response_body, response.code]
194
+ end
195
+
196
+ private
197
+
198
+ # Adds necessary headers to a POST request.
199
+ #
200
+ # @return [Net::HTTP::Post] The modified request with added headers.
201
+ def add_post_headers(request)
202
+ request['Host'] = uri.host
203
+ request['Content-Type'] = 'application/json'
204
+ request['Authorization'] = "Bearer #{access_token['access_token']}"
205
+
206
+ request
207
+ end
161
208
  end
209
+ # rubocop: enable Metrics/ClassLength
162
210
  end
data/lib/axm/secret.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Secret
2
4
  def self.read(filename)
3
5
  File.read("secrets/#{filename}").strip
data/lib/axm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Axm
4
- VERSION = "0.1.2"
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/axm.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "axm/client"
4
- require_relative "axm/secret"
5
- require_relative "axm/version"
3
+ require_relative 'axm/client'
4
+ require_relative 'axm/secret'
5
+ require_relative 'axm/version'
6
6
 
7
7
  module Axm
8
8
  class Error < StandardError; end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: axm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - nick-f
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-07-14 00:00:00.000000000 Z
10
+ date: 2025-11-13 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: jwt
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '3.1'
27
- description:
28
26
  email:
29
27
  - nick@nickf.dev
30
28
  executables: []
@@ -40,6 +38,8 @@ files:
40
38
  - Rakefile
41
39
  - lib/axm.rb
42
40
  - lib/axm/client.rb
41
+ - lib/axm/client/mdm_servers.rb
42
+ - lib/axm/client/organization_device_activities.rb
43
43
  - lib/axm/client/organization_devices.rb
44
44
  - lib/axm/secret.rb
45
45
  - lib/axm/version.rb
@@ -51,7 +51,7 @@ licenses:
51
51
  metadata:
52
52
  homepage_uri: https://github.com/nick-f/axm
53
53
  source_code_uri: https://github.com/nick-f/axm
54
- post_install_message:
54
+ rubygems_mfa_required: 'true'
55
55
  rdoc_options: []
56
56
  require_paths:
57
57
  - lib
@@ -66,8 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  requirements: []
69
- rubygems_version: 3.4.19
70
- signing_key:
69
+ rubygems_version: 3.6.5
71
70
  specification_version: 4
72
71
  summary: Gem to interact with the Apple Business Manager/Apple School Manager API
73
72
  test_files: []