halo_msp_api 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 98f866a474dd58a86344301d4265af815a4d6582cabb6de0dfe5a94d572116d8
4
+ data.tar.gz: 450ae6c2367fd9acb743fb6b81a183f6218de528868de4dafc63c8893f18cffb
5
+ SHA512:
6
+ metadata.gz: b16bbea790fd1fbc1daf00ba83665261735569d9b0ab4d6fc0ce29562fdad32008220d950e59614712d74a43f476e452ab1e851c5336ea2639362096563c8d47
7
+ data.tar.gz: 1b09d2963bed4af41b7c0e680cf814cb05981c8379b333ed67316a1bf589cd6f3da08f6ec134ded8dfa6c6a029dac038ce8e61764b5be28a3ab442f2783cd13d
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-07-10
9
+
10
+ ### Added
11
+
12
+ - Initial gem structure and foundation
13
+ - Core API client implementation
14
+ - Resource-based API organization
15
+ - Authentication and configuration system
16
+ - Basic test coverage
17
+ - Documentation and examples
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in halo_api.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ group :development, :test do
11
+ gem "rspec", "~> 3.0"
12
+ gem "rubocop", "~> 1.21"
13
+ gem "yard", "~> 0.9"
14
+ gem "webmock", "~> 3.0"
15
+ gem "vcr", "~> 6.0"
16
+ gem "pry", "~> 0.14"
17
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Evo Security
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # HaloMspApi Ruby Gem
2
+
3
+ A comprehensive Ruby API wrapper for the Halo ITSM, HaloPSA and HaloCRM REST API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'halo_msp_api'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install halo_msp_api
20
+
21
+ ## Configuration
22
+
23
+ Configure the gem with your Halo API credentials:
24
+
25
+ ```ruby
26
+ HaloMspApi.configure do |config|
27
+ config.base_url = "https://your-halo-instance.haloitsm.com/api"
28
+ config.client_id = "your_client_id"
29
+ config.client_secret = "your_client_secret"
30
+ config.tenant = "your_tenant" # Optional
31
+ config.timeout = 30 # Optional, default is 30 seconds
32
+ config.retries = 3 # Optional, default is 3 retries
33
+ end
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Basic Usage
39
+
40
+ ```ruby
41
+ # Initialize client
42
+ client = HaloMspApi.client
43
+
44
+ # Or create a new client with custom configuration
45
+ config = HaloMspApi::Configuration.new
46
+ config.base_url = "https://your-instance.haloitsm.com/api"
47
+ config.client_id = "your_client_id"
48
+ config.client_secret = "your_client_secret"
49
+ client = HaloApi::Client.new(config)
50
+ ```
51
+
52
+ ### Working with Tickets
53
+
54
+ ```ruby
55
+ # List all tickets
56
+ tickets = client.tickets.list
57
+
58
+ # Get a specific ticket
59
+ ticket = client.tickets.get(123)
60
+
61
+ # Create a new ticket
62
+ new_ticket = client.tickets.create({
63
+ summary: "New ticket",
64
+ details: "Ticket description",
65
+ tickettype_id: 1,
66
+ client_id: 1
67
+ })
68
+
69
+ # Update a ticket
70
+ client.tickets.update(123, { summary: "Updated summary" })
71
+
72
+ # Delete a ticket
73
+ client.tickets.delete(123)
74
+ ```
75
+
76
+ ### Working with Users
77
+
78
+ ```ruby
79
+ # List all users
80
+ users = client.users.list
81
+
82
+ # Get current user
83
+ current_user = client.users.me
84
+
85
+ # Create a new user
86
+ new_user = client.users.create({
87
+ name: "John Doe",
88
+ emailaddress: "john@example.com"
89
+ })
90
+ ```
91
+
92
+ ### Working with Assets
93
+
94
+ ```ruby
95
+ # List all assets
96
+ assets = client.assets.list
97
+
98
+ # Get a specific asset
99
+ asset = client.assets.get(123)
100
+
101
+ # Create a new asset
102
+ new_asset = client.assets.create({
103
+ inventory_number: "ASSET001",
104
+ assettype_id: 1
105
+ })
106
+ ```
107
+
108
+ ### Working with Clients
109
+
110
+ ```ruby
111
+ # List all clients
112
+ clients = client.clients.list
113
+
114
+ # Get a specific client
115
+ client_record = client.clients.get(123)
116
+
117
+ # Create a new client
118
+ new_client = client.clients.create({
119
+ name: "ACME Corp",
120
+ website: "https://acme.com"
121
+ })
122
+ ```
123
+
124
+ ### Working with Invoices
125
+
126
+ ```ruby
127
+ # List all invoices
128
+ invoices = client.invoices.list
129
+
130
+ # Get a specific invoice
131
+ invoice = client.invoices.get(123)
132
+
133
+ # Create a new invoice
134
+ new_invoice = client.invoices.create({
135
+ client_id: 1,
136
+ invoicedate: "2023-01-01"
137
+ })
138
+
139
+ # Get invoice PDF
140
+ pdf_data = client.invoices.pdf(123)
141
+ ```
142
+
143
+ ### Working with Reports
144
+
145
+ ```ruby
146
+ # List all reports
147
+ reports = client.reports.list
148
+
149
+ # Get a specific report
150
+ report = client.reports.get(123)
151
+
152
+ # Get report data
153
+ report_data = client.reports.data("published_report_id")
154
+ ```
155
+
156
+ ### Working with Integrations
157
+
158
+ ```ruby
159
+ # Get Azure AD data
160
+ azure_data = client.integrations.get_azure_ad
161
+
162
+ # Get Slack data
163
+ slack_data = client.integrations.get_slack
164
+
165
+ # Import Jira data
166
+ client.integrations.import_jira(jira_data)
167
+ ```
168
+
169
+ ## API Resources
170
+
171
+ The gem provides access to the following Halo API resources:
172
+
173
+ ### Core Business Objects
174
+ - **Actions** - `client.actions` - Ticket actions and reactions
175
+ - **Agents** - `client.agents` - Agent management, check-ins, presence
176
+ - **Assets** - `client.assets` - Asset management, groups, software, types
177
+ - **Tickets** - `client.tickets` - Comprehensive ticket management
178
+ - **Users** - `client.users` - User management, preferences, roles
179
+ - **Clients** - `client.clients` - Client management, contracts, prepayments
180
+ - **Organisations** - `client.organisations` - Organization management
181
+
182
+ ### Financial Management
183
+ - **Invoices** - `client.invoices` - Invoice management, payments, recurring invoices
184
+ - **Purchase Orders** - `client.purchase_orders` - PO management, approvals, lines
185
+ - **Quotations** - `client.quotations` - Quote management, approvals
186
+ - **Sales Orders** - `client.sales_orders` - Sales order management
187
+ - **Suppliers** - `client.suppliers` - Supplier and contract management
188
+
189
+ ### Service Management
190
+ - **Appointments** - `client.appointments` - Scheduling and availability
191
+ - **Services** - `client.services` - Service management, availability, categories
192
+ - **SLAs** - `client.slas` - SLA policies and targets
193
+
194
+ ### Knowledge & Communication
195
+ - **Knowledge Base** - `client.knowledge_base` - KB articles, categories, keywords
196
+ - **Webhooks** - `client.webhooks` - Webhook management and subscriptions
197
+
198
+ ### Reporting & Analytics
199
+ - **Reports** - `client.reports` - Reporting, bookmarks, repositories
200
+
201
+ ### Integration Services
202
+ - **Integrations** - `client.integrations` - Third-party integration support (Azure, Slack, Jira, etc.)
203
+
204
+ ## Error Handling
205
+
206
+ The gem provides specific error classes for different types of API errors:
207
+
208
+ ```ruby
209
+ begin
210
+ ticket = client.tickets.get(999999)
211
+ rescue HaloApi::NotFoundError
212
+ puts "Ticket not found"
213
+ rescue HaloApi::AuthenticationError
214
+ puts "Authentication failed"
215
+ rescue HaloApi::AuthorizationError
216
+ puts "Access forbidden"
217
+ rescue HaloApi::ValidationError => e
218
+ puts "Validation error: #{e.message}"
219
+ rescue HaloApi::RateLimitError
220
+ puts "Rate limit exceeded"
221
+ rescue HaloApi::ServerError
222
+ puts "Server error"
223
+ rescue HaloApi::APIError => e
224
+ puts "API error: #{e.message} (Status: #{e.status_code})"
225
+ end
226
+ ```
227
+
228
+ ## Contributing
229
+
230
+ Bug reports and pull requests are welcome on GitHub at https://github.com/evosecurity/HaloAPI-Ruby.
231
+
232
+ ## License
233
+
234
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
235
+
236
+ ## Development
237
+
238
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
239
+
240
+ 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).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "halo_msp_api"
6
+
7
+ # Configure the HaloMspApi gem
8
+ HaloMspApi.configure do |config|
9
+ config.base_url = ENV["HALO_BASE_URL"] || "https://your-instance.haloitsm.com/api"
10
+ config.client_id = ENV["HALO_CLIENT_ID"] || "your_client_id"
11
+ config.client_secret = ENV["HALO_CLIENT_SECRET"] || "your_client_secret"
12
+ config.tenant = ENV["HALO_TENANT"] # Optional
13
+ config.timeout = 30
14
+ config.retries = 3
15
+ end
16
+
17
+ # Get a client instance
18
+ client = HaloMspApi.client
19
+
20
+ begin
21
+ puts "=== HaloApi Ruby Gem Example Usage ==="
22
+ puts
23
+
24
+ # Example 1: List tickets
25
+ puts "1. Fetching tickets..."
26
+ tickets = client.tickets.list(count: 5)
27
+ puts "Found #{tickets.dig('tickets')&.length || 0} tickets"
28
+ puts
29
+
30
+ # Example 2: Get current user
31
+ puts "2. Fetching current user..."
32
+ current_user = client.users.me
33
+ puts "Current user: #{current_user.dig('name') || 'Unknown'}"
34
+ puts
35
+
36
+ # Example 3: List clients
37
+ puts "3. Fetching clients..."
38
+ clients = client.clients.list(count: 5)
39
+ puts "Found #{clients.dig('clients')&.length || 0} clients"
40
+ puts
41
+
42
+ # Example 4: List assets
43
+ puts "4. Fetching assets..."
44
+ assets = client.assets.list(count: 5)
45
+ puts "Found #{assets.dig('assets')&.length || 0} assets"
46
+ puts
47
+
48
+ # Example 5: List invoices
49
+ puts "5. Fetching invoices..."
50
+ invoices = client.invoices.list(count: 5)
51
+ puts "Found #{invoices.dig('invoices')&.length || 0} invoices"
52
+ puts
53
+
54
+ # Example 6: Get reports
55
+ puts "6. Fetching reports..."
56
+ reports = client.reports.list(count: 5)
57
+ puts "Found #{reports.dig('reports')&.length || 0} reports"
58
+ puts
59
+
60
+ puts "=== Example completed successfully! ==="
61
+
62
+ rescue HaloMspApi::AuthenticationError => e
63
+ puts "Authentication failed: #{e.message}"
64
+ puts "Please check your client_id and client_secret"
65
+ rescue HaloMspApi::AuthorizationError => e
66
+ puts "Authorization failed: #{e.message}"
67
+ puts "Please check your permissions"
68
+ rescue HaloMspApi::APIError => e
69
+ puts "API Error: #{e.message}"
70
+ puts "Status Code: #{e.status_code}" if e.respond_to?(:status_code)
71
+ rescue StandardError => e
72
+ puts "Unexpected error: #{e.message}"
73
+ puts e.backtrace.first(5)
74
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "json"
6
+
7
+ module HaloMspApi
8
+ class Client
9
+ attr_reader :configuration, :connection
10
+
11
+ def initialize(configuration = nil)
12
+ @configuration = configuration || HaloApi.configuration
13
+ raise ConfigurationError, "Configuration is required" unless @configuration&.valid?
14
+
15
+ @connection = build_connection
16
+ @access_token = nil
17
+ @token_expires_at = nil
18
+ end
19
+
20
+ # Resource accessors
21
+ def actions
22
+ @actions ||= Resources::Actions.new(self)
23
+ end
24
+
25
+ def agents
26
+ @agents ||= Resources::Agents.new(self)
27
+ end
28
+
29
+ def appointments
30
+ @appointments ||= Resources::Appointments.new(self)
31
+ end
32
+
33
+ def assets
34
+ @assets ||= Resources::Assets.new(self)
35
+ end
36
+
37
+ def clients
38
+ @clients ||= Resources::Clients.new(self)
39
+ end
40
+
41
+ def integrations
42
+ @integrations ||= Resources::Integrations.new(self)
43
+ end
44
+
45
+ def invoices
46
+ @invoices ||= Resources::Invoices.new(self)
47
+ end
48
+
49
+ def knowledge_base
50
+ @knowledge_base ||= Resources::KnowledgeBase.new(self)
51
+ end
52
+
53
+ def organisations
54
+ @organisations ||= Resources::Organisations.new(self)
55
+ end
56
+
57
+ def purchase_orders
58
+ @purchase_orders ||= Resources::PurchaseOrders.new(self)
59
+ end
60
+
61
+ def quotations
62
+ @quotations ||= Resources::Quotations.new(self)
63
+ end
64
+
65
+ def reports
66
+ @reports ||= Resources::Reports.new(self)
67
+ end
68
+
69
+ def services
70
+ @services ||= Resources::Services.new(self)
71
+ end
72
+
73
+ def tickets
74
+ @tickets ||= Resources::Tickets.new(self)
75
+ end
76
+
77
+ def users
78
+ @users ||= Resources::Users.new(self)
79
+ end
80
+
81
+ def sales_orders
82
+ @sales_orders ||= Resources::SalesOrders.new(self)
83
+ end
84
+
85
+ def slas
86
+ @slas ||= Resources::Slas.new(self)
87
+ end
88
+
89
+ def suppliers
90
+ @suppliers ||= Resources::Suppliers.new(self)
91
+ end
92
+
93
+ def webhooks
94
+ @webhooks ||= Resources::Webhooks.new(self)
95
+ end
96
+
97
+ # HTTP methods
98
+ def get(path, params = {})
99
+ request(:get, path, params)
100
+ end
101
+
102
+ def post(path, body = {})
103
+ request(:post, path, body)
104
+ end
105
+
106
+ def put(path, body = {})
107
+ request(:put, path, body)
108
+ end
109
+
110
+ def patch(path, body = {})
111
+ request(:patch, path, body)
112
+ end
113
+
114
+ def delete(path)
115
+ request(:delete, path)
116
+ end
117
+
118
+ private
119
+
120
+ def request(method, path, data = {})
121
+ ensure_authenticated!
122
+
123
+ response = connection.send(method) do |req|
124
+ req.url path
125
+ req.headers["Authorization"] = "Bearer #{@access_token}"
126
+ req.headers["Content-Type"] = "application/json"
127
+
128
+ if %i[post put patch].include?(method) && !data.empty?
129
+ req.body = data.to_json
130
+ elsif method == :get && !data.empty?
131
+ req.params = data
132
+ end
133
+ end
134
+
135
+ handle_response(response)
136
+ rescue Faraday::TimeoutError
137
+ raise TimeoutError, "Request timed out"
138
+ rescue Faraday::ConnectionFailed
139
+ raise ConnectionError, "Connection failed"
140
+ end
141
+
142
+ def handle_response(response)
143
+ case response.status
144
+ when 200..299
145
+ parse_response(response)
146
+ when 401
147
+ raise AuthenticationError, "Authentication failed"
148
+ when 403
149
+ raise AuthorizationError, "Access forbidden"
150
+ when 404
151
+ raise NotFoundError, "Resource not found"
152
+ when 422
153
+ raise ValidationError, "Validation error: #{response.body}"
154
+ when 429
155
+ raise RateLimitError, "Rate limit exceeded"
156
+ when 500..599
157
+ raise ServerError, "Server error: #{response.status}"
158
+ else
159
+ raise APIError.new("Unexpected response", status_code: response.status, response_body: response.body)
160
+ end
161
+ end
162
+
163
+ def parse_response(response)
164
+ return nil if response.body.nil? || response.body.empty?
165
+
166
+ JSON.parse(response.body)
167
+ rescue JSON::ParserError
168
+ response.body
169
+ end
170
+
171
+ def ensure_authenticated!
172
+ return if token_valid?
173
+
174
+ authenticate!
175
+ end
176
+
177
+ def token_valid?
178
+ @access_token && @token_expires_at && Time.now < @token_expires_at
179
+ end
180
+
181
+ def authenticate!
182
+ auth_params = {
183
+ grant_type: "client_credentials",
184
+ client_id: configuration.client_id,
185
+ client_secret: configuration.client_secret,
186
+ scope: "all"
187
+ }
188
+
189
+ # Include tenant if configured (required for multi-tenant instances)
190
+ auth_params[:tenant] = configuration.tenant if configuration.tenant
191
+
192
+ auth_response = connection.post("/auth/token") do |req|
193
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
194
+ req.body = URI.encode_www_form(auth_params)
195
+ end
196
+
197
+ if auth_response.status == 200
198
+ token_data = JSON.parse(auth_response.body)
199
+ @access_token = token_data["access_token"]
200
+ @token_expires_at = Time.now + token_data["expires_in"].to_i
201
+ else
202
+ raise AuthenticationError, "Failed to authenticate: #{auth_response.body}"
203
+ end
204
+ rescue JSON::ParserError
205
+ raise AuthenticationError, "Invalid authentication response"
206
+ end
207
+
208
+ def build_connection
209
+ Faraday.new(url: configuration.base_url) do |conn|
210
+ conn.request :retry, max: configuration.retries
211
+ conn.options.timeout = configuration.timeout
212
+ conn.adapter Faraday.default_adapter
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaloMspApi
4
+ class Configuration
5
+ attr_accessor :base_url, :client_id, :client_secret, :tenant, :timeout, :retries
6
+
7
+ def initialize
8
+ @base_url = nil
9
+ @client_id = nil
10
+ @client_secret = nil
11
+ @tenant = nil
12
+ @timeout = 30
13
+ @retries = 3
14
+ end
15
+
16
+ def valid?
17
+ !base_url.nil? && !client_id.nil? && !client_secret.nil?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaloMspApi
4
+ class Error < StandardError; end
5
+
6
+ class ConfigurationError < Error; end
7
+ class AuthenticationError < Error; end
8
+ class AuthorizationError < Error; end
9
+ class NotFoundError < Error; end
10
+ class ValidationError < Error; end
11
+ class RateLimitError < Error; end
12
+ class ServerError < Error; end
13
+ class TimeoutError < Error; end
14
+ class ConnectionError < Error; end
15
+
16
+ class APIError < Error
17
+ attr_reader :status_code, :response_body
18
+
19
+ def initialize(message, status_code: nil, response_body: nil)
20
+ super(message)
21
+ @status_code = status_code
22
+ @response_body = response_body
23
+ end
24
+ end
25
+ end