alta_labs 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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +221 -0
- data/lib/alta_labs/auth/cognito.rb +178 -0
- data/lib/alta_labs/auth/srp.rb +134 -0
- data/lib/alta_labs/client.rb +137 -0
- data/lib/alta_labs/configuration.rb +40 -0
- data/lib/alta_labs/error.rb +30 -0
- data/lib/alta_labs/resource.rb +26 -0
- data/lib/alta_labs/resources/account.rb +17 -0
- data/lib/alta_labs/resources/client_device.rb +13 -0
- data/lib/alta_labs/resources/device.rb +41 -0
- data/lib/alta_labs/resources/filter.rb +17 -0
- data/lib/alta_labs/resources/floor_plan.rb +25 -0
- data/lib/alta_labs/resources/group.rb +17 -0
- data/lib/alta_labs/resources/profile.rb +17 -0
- data/lib/alta_labs/resources/site.rb +57 -0
- data/lib/alta_labs/resources/ssid.rb +47 -0
- data/lib/alta_labs/version.rb +3 -0
- data/lib/alta_labs.rb +52 -0
- metadata +195 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ee3307c6dee96fcff9e6ef2a96e283a119b7c1c9d8f67af89f306342c1ffe4a5
|
|
4
|
+
data.tar.gz: 38841e7b4d9ee246c9b0d079e7fa8401abe77d5d3b93af43fd8c32424adb6b6c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4503a64818d9b600bfd87dbf0ea6a34b50022a16d6d4e673a4689630be7f57850f859a2116ee7b7d8b5004c46f5bf56dfa2d785ce119b119586cfaa63597cd1d
|
|
7
|
+
data.tar.gz: 4bcec3a38df3126f40a44485b673b96dd8d7ca3c6105972f2bc6be5e380aa624e69e7e3a1e4997568ce39196d499ee04edc216d659ef4fe7837486afdc4e6250
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - Unreleased
|
|
6
|
+
|
|
7
|
+
- Initial release
|
|
8
|
+
- Cognito SRP authentication (no AWS SDK dependency)
|
|
9
|
+
- Site management (list, get, stats)
|
|
10
|
+
- Device management (list, search)
|
|
11
|
+
- WiFi/SSID management (list, get)
|
|
12
|
+
- Client device listing
|
|
13
|
+
- Account info retrieval
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dale Stevens
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
[](https://rubygems.org/gems/alta_labs)
|
|
2
|
+
[](https://github.com/TwilightCoders/alta_labs/actions)
|
|
3
|
+
[](https://github.com/TwilightCoders/alta_labs)
|
|
4
|
+
|
|
5
|
+
# AltaLabs
|
|
6
|
+
|
|
7
|
+
A Ruby SDK for the [Alta Labs](https://alta.inc) cloud management API. Manage sites, devices, WiFi networks, and more programmatically.
|
|
8
|
+
|
|
9
|
+
> **Note:** Alta Labs does not currently publish public API documentation. This library was built by reverse-engineering the web management portal at `manage.alta.inc`. While functional, the API surface may change without notice.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add to your Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'alta_labs'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
$ gem install alta_labs
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Authentication
|
|
28
|
+
|
|
29
|
+
The SDK authenticates with Alta Labs via AWS Cognito SRP — the same mechanism used by the web portal. No AWS SDK dependency is required; SRP is implemented natively.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'alta_labs'
|
|
33
|
+
|
|
34
|
+
client = AltaLabs::Client.new(
|
|
35
|
+
email: 'you@example.com',
|
|
36
|
+
password: 'your-password'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Authentication happens automatically on the first API call,
|
|
40
|
+
# or you can authenticate explicitly:
|
|
41
|
+
client.authenticate
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can also configure via environment variables or a block:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# Environment variables (ALTA_LABS_EMAIL, ALTA_LABS_PASSWORD)
|
|
48
|
+
client = AltaLabs::Client.new
|
|
49
|
+
|
|
50
|
+
# Block configuration
|
|
51
|
+
AltaLabs.configure do |config|
|
|
52
|
+
config.email = 'you@example.com'
|
|
53
|
+
config.password = 'your-password'
|
|
54
|
+
config.timeout = 60
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Sites
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# List all sites
|
|
62
|
+
sites = client.sites.list
|
|
63
|
+
# => [{"id" => "abc123", "name" => "Main Office", "online" => 5, ...}]
|
|
64
|
+
|
|
65
|
+
# Get site detail
|
|
66
|
+
site = client.sites.find(id: 'abc123')
|
|
67
|
+
# => {"id" => "abc123", "tz" => "America/Denver", "vlans" => [...], ...}
|
|
68
|
+
|
|
69
|
+
# Site audit log
|
|
70
|
+
audit = client.sites.audit(id: 'abc123')
|
|
71
|
+
audit['trail'].each do |entry|
|
|
72
|
+
puts "[#{entry['ts']}] #{entry['action']} #{entry['type']}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Create a site
|
|
76
|
+
client.sites.create(name: 'New Site', type: 'residential')
|
|
77
|
+
|
|
78
|
+
# Rename a site
|
|
79
|
+
client.sites.rename(id: 'abc123', name: 'Updated Name')
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Devices
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# List devices for a site
|
|
86
|
+
devices = client.devices.list(site_id: 'abc123')
|
|
87
|
+
|
|
88
|
+
# Add a device by serial number
|
|
89
|
+
client.devices.add_serial(site_id: 'abc123', serial: 'ALTA-XXXX')
|
|
90
|
+
|
|
91
|
+
# Move a device between sites
|
|
92
|
+
client.devices.move(id: 'device-id', site_id: 'new-site-id')
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### WiFi / SSIDs
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# List SSIDs for a site
|
|
99
|
+
result = client.wifi.list(site_id: 'abc123')
|
|
100
|
+
result['ssids'].each do |ssid|
|
|
101
|
+
puts "#{ssid['ssid']} (#{ssid.dig('config', 'security')})"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get a specific SSID
|
|
105
|
+
ssid = client.wifi.find(id: 'ssid-id')
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Account
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# Get account info (requires access_token)
|
|
112
|
+
info = client.account.info
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Content Filtering
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# Get content filter settings
|
|
119
|
+
filter = client.filters.get_filter(site_id: 'abc123')
|
|
120
|
+
# => {"blockedRegions" => ["CN"], "blockWired" => true, ...}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Profiles
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
profiles = client.profiles.list(site_ids: ['abc123'])
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Floor Plans
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
floors = client.floor_plans.floors(site_id: 'abc123')
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Token Management
|
|
136
|
+
|
|
137
|
+
Tokens are automatically refreshed when they expire. The SDK handles:
|
|
138
|
+
|
|
139
|
+
- Initial SRP authentication with Cognito
|
|
140
|
+
- JWT token storage and expiration tracking
|
|
141
|
+
- Automatic token refresh using the refresh token
|
|
142
|
+
- MFA challenges (raises `AltaLabs::MfaRequiredError` with session data)
|
|
143
|
+
|
|
144
|
+
### MFA Support
|
|
145
|
+
|
|
146
|
+
If MFA is enabled on the account:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
begin
|
|
150
|
+
client.authenticate
|
|
151
|
+
rescue AltaLabs::MfaRequiredError => e
|
|
152
|
+
# Prompt user for MFA code
|
|
153
|
+
client.auth.verify_mfa(
|
|
154
|
+
code: '123456',
|
|
155
|
+
session: e.session,
|
|
156
|
+
challenge_name: e.challenge_name
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Error Handling
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
begin
|
|
165
|
+
client.sites.find(id: 'nonexistent')
|
|
166
|
+
rescue AltaLabs::AuthenticationError => e
|
|
167
|
+
# Invalid credentials or expired session
|
|
168
|
+
rescue AltaLabs::NotFoundError => e
|
|
169
|
+
# Resource not found
|
|
170
|
+
rescue AltaLabs::RateLimitError => e
|
|
171
|
+
# Too many requests
|
|
172
|
+
rescue AltaLabs::ServerError => e
|
|
173
|
+
# Alta Labs server error
|
|
174
|
+
rescue AltaLabs::ApiError => e
|
|
175
|
+
# Generic API error
|
|
176
|
+
puts e.status # HTTP status code
|
|
177
|
+
puts e.body # Response body
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Configuration Options
|
|
182
|
+
|
|
183
|
+
| Option | Default | Description |
|
|
184
|
+
|--------|---------|-------------|
|
|
185
|
+
| `email` | `ENV['ALTA_LABS_EMAIL']` | Account email |
|
|
186
|
+
| `password` | `ENV['ALTA_LABS_PASSWORD']` | Account password |
|
|
187
|
+
| `api_url` | `https://manage.alta.inc` | API base URL |
|
|
188
|
+
| `timeout` | `30` | Request timeout (seconds) |
|
|
189
|
+
| `open_timeout` | `10` | Connection open timeout (seconds) |
|
|
190
|
+
|
|
191
|
+
## Self-Hosted Controller
|
|
192
|
+
|
|
193
|
+
To use with a self-hosted Alta Control instance:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
client = AltaLabs::Client.new(
|
|
197
|
+
email: 'you@example.com',
|
|
198
|
+
password: 'your-password',
|
|
199
|
+
)
|
|
200
|
+
client.config.api_url = 'https://your-controller.local'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
$ bundle install
|
|
207
|
+
$ bundle exec rspec
|
|
208
|
+
$ bin/console
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Contributing
|
|
212
|
+
|
|
213
|
+
1. Fork it
|
|
214
|
+
2. Create your feature branch (`git checkout -b feature/my-feature`)
|
|
215
|
+
3. Commit your changes (`git commit -am 'Add my feature'`)
|
|
216
|
+
4. Push to the branch (`git push origin feature/my-feature`)
|
|
217
|
+
5. Create a Pull Request
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Auth
|
|
3
|
+
# Authenticates with AWS Cognito using the SRP protocol.
|
|
4
|
+
# Returns JWT tokens (id_token, access_token, refresh_token).
|
|
5
|
+
class Cognito
|
|
6
|
+
attr_reader :config, :tokens
|
|
7
|
+
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@config = config
|
|
10
|
+
@tokens = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Perform full SRP authentication flow.
|
|
14
|
+
# @return [Hash] tokens hash with :id_token, :access_token, :refresh_token
|
|
15
|
+
def authenticate(email: config.email, password: config.password)
|
|
16
|
+
raise InvalidCredentialsError, 'Email and password are required' unless email && password
|
|
17
|
+
|
|
18
|
+
srp = SRP.new(config.pool_name)
|
|
19
|
+
|
|
20
|
+
# Step 1: Initiate auth
|
|
21
|
+
challenge = initiate_auth(email, srp.srp_a)
|
|
22
|
+
|
|
23
|
+
case challenge['ChallengeName']
|
|
24
|
+
when 'PASSWORD_VERIFIER'
|
|
25
|
+
respond_to_password_verifier(srp, challenge, email, password)
|
|
26
|
+
when 'SOFTWARE_TOKEN_MFA', 'SMS_MFA'
|
|
27
|
+
raise MfaRequiredError.new(
|
|
28
|
+
session: challenge['Session'],
|
|
29
|
+
challenge_name: challenge['ChallengeName']
|
|
30
|
+
)
|
|
31
|
+
else
|
|
32
|
+
raise AuthenticationError, "Unexpected challenge: #{challenge['ChallengeName']}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Refresh tokens using the refresh_token.
|
|
37
|
+
# @return [Hash] refreshed tokens
|
|
38
|
+
def refresh(refresh_token = nil)
|
|
39
|
+
refresh_token ||= @tokens[:refresh_token]
|
|
40
|
+
raise AuthenticationError, 'No refresh token available' unless refresh_token
|
|
41
|
+
|
|
42
|
+
result = cognito_request('InitiateAuth', {
|
|
43
|
+
'AuthFlow' => 'REFRESH_TOKEN_AUTH',
|
|
44
|
+
'ClientId' => config.client_id,
|
|
45
|
+
'AuthParameters' => {
|
|
46
|
+
'REFRESH_TOKEN' => refresh_token
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
auth_result = result['AuthenticationResult']
|
|
51
|
+
@tokens = {
|
|
52
|
+
id_token: auth_result['IdToken'],
|
|
53
|
+
access_token: auth_result['AccessToken'],
|
|
54
|
+
refresh_token: refresh_token, # Cognito doesn't return a new refresh token
|
|
55
|
+
expires_at: Time.now + auth_result['ExpiresIn'].to_i
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Respond to MFA challenge.
|
|
60
|
+
# @param code [String] the MFA code
|
|
61
|
+
# @param session [String] session from MfaRequiredError
|
|
62
|
+
# @param challenge_name [String] challenge type from MfaRequiredError
|
|
63
|
+
# @return [Hash] tokens
|
|
64
|
+
def verify_mfa(code:, session:, challenge_name: 'SOFTWARE_TOKEN_MFA')
|
|
65
|
+
result = cognito_request('RespondToAuthChallenge', {
|
|
66
|
+
'ChallengeName' => challenge_name,
|
|
67
|
+
'ClientId' => config.client_id,
|
|
68
|
+
'Session' => session,
|
|
69
|
+
'ChallengeResponses' => {
|
|
70
|
+
'USERNAME' => config.email,
|
|
71
|
+
'SOFTWARE_TOKEN_MFA_CODE' => code
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
extract_tokens(result['AuthenticationResult'])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def id_token
|
|
79
|
+
@tokens[:id_token]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def token_expired?
|
|
83
|
+
return true unless @tokens[:expires_at]
|
|
84
|
+
|
|
85
|
+
Time.now >= @tokens[:expires_at] - 60 # 60s buffer
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def initiate_auth(email, srp_a)
|
|
91
|
+
cognito_request('InitiateAuth', {
|
|
92
|
+
'AuthFlow' => 'USER_SRP_AUTH',
|
|
93
|
+
'ClientId' => config.client_id,
|
|
94
|
+
'AuthParameters' => {
|
|
95
|
+
'USERNAME' => email,
|
|
96
|
+
'SRP_A' => srp_a
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def respond_to_password_verifier(srp, challenge, email, password)
|
|
102
|
+
params = challenge['ChallengeParameters']
|
|
103
|
+
timestamp = Time.now.utc.strftime('%a %b %-d %H:%M:%S UTC %Y')
|
|
104
|
+
|
|
105
|
+
signature = srp.compute_claim(
|
|
106
|
+
user_id: params['USER_ID_FOR_SRP'],
|
|
107
|
+
password: password,
|
|
108
|
+
srp_b: params['SRP_B'],
|
|
109
|
+
salt: params['SALT'],
|
|
110
|
+
secret_block: params['SECRET_BLOCK'],
|
|
111
|
+
timestamp: timestamp
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
result = cognito_request('RespondToAuthChallenge', {
|
|
115
|
+
'ChallengeName' => 'PASSWORD_VERIFIER',
|
|
116
|
+
'ClientId' => config.client_id,
|
|
117
|
+
'ChallengeResponses' => {
|
|
118
|
+
'USERNAME' => params['USER_ID_FOR_SRP'],
|
|
119
|
+
'PASSWORD_CLAIM_SECRET_BLOCK' => params['SECRET_BLOCK'],
|
|
120
|
+
'PASSWORD_CLAIM_SIGNATURE' => signature,
|
|
121
|
+
'TIMESTAMP' => timestamp
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
if result['ChallengeName']
|
|
126
|
+
case result['ChallengeName']
|
|
127
|
+
when 'SOFTWARE_TOKEN_MFA', 'SMS_MFA'
|
|
128
|
+
raise MfaRequiredError.new(
|
|
129
|
+
session: result['Session'],
|
|
130
|
+
challenge_name: result['ChallengeName']
|
|
131
|
+
)
|
|
132
|
+
else
|
|
133
|
+
raise AuthenticationError, "Unexpected post-auth challenge: #{result['ChallengeName']}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
extract_tokens(result['AuthenticationResult'])
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def extract_tokens(auth_result)
|
|
141
|
+
raise AuthenticationError, 'No authentication result' unless auth_result
|
|
142
|
+
|
|
143
|
+
@tokens = {
|
|
144
|
+
id_token: auth_result['IdToken'],
|
|
145
|
+
access_token: auth_result['AccessToken'],
|
|
146
|
+
refresh_token: auth_result['RefreshToken'],
|
|
147
|
+
expires_at: Time.now + auth_result['ExpiresIn'].to_i
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def cognito_request(action, body)
|
|
152
|
+
response = cognito_connection.post('') do |req|
|
|
153
|
+
req.headers['X-Amz-Target'] = "AWSCognitoIdentityProviderService.#{action}"
|
|
154
|
+
req.headers['Content-Type'] = 'application/x-amz-json-1.1'
|
|
155
|
+
req.body = body.to_json
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
result = JSON.parse(response.body)
|
|
159
|
+
|
|
160
|
+
if response.status != 200
|
|
161
|
+
error_type = result['__type']&.split('#')&.last || 'Unknown'
|
|
162
|
+
error_msg = result['message'] || result['Message'] || 'Authentication failed'
|
|
163
|
+
raise AuthenticationError, "#{error_type}: #{error_msg}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
result
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def cognito_connection
|
|
170
|
+
@cognito_connection ||= Faraday.new(url: config.cognito_endpoint) do |f|
|
|
171
|
+
f.options.timeout = config.timeout
|
|
172
|
+
f.options.open_timeout = config.open_timeout
|
|
173
|
+
f.adapter Faraday.default_adapter
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Auth
|
|
3
|
+
# Implements the Secure Remote Password (SRP-6a) protocol as used by
|
|
4
|
+
# AWS Cognito. Uses only Ruby stdlib (OpenSSL) -- no AWS SDK required.
|
|
5
|
+
#
|
|
6
|
+
# Matches the amazon-cognito-identity-js reference implementation.
|
|
7
|
+
class SRP
|
|
8
|
+
# Cognito's 3072-bit SRP prime (hex)
|
|
9
|
+
N_HEX = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1' \
|
|
10
|
+
'29024E088A67CC74020BBEA63B139B22514A08798E3404DD' \
|
|
11
|
+
'EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245' \
|
|
12
|
+
'E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' \
|
|
13
|
+
'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D' \
|
|
14
|
+
'C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F' \
|
|
15
|
+
'83655D23DCA3AD961C62F356208552BB9ED529077096966D' \
|
|
16
|
+
'670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' \
|
|
17
|
+
'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9' \
|
|
18
|
+
'DE2BCBF6955817183995497CEA956AE515D2261898FA0510' \
|
|
19
|
+
'15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64' \
|
|
20
|
+
'ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7' \
|
|
21
|
+
'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B' \
|
|
22
|
+
'F12FFA06D98A0864D87602733EC86A64521F2B18177B200CB' \
|
|
23
|
+
'BE117577A615D6C770988C0BAD946E208E24FA074E5AB3143' \
|
|
24
|
+
'DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF'
|
|
25
|
+
|
|
26
|
+
G_HEX = '2'
|
|
27
|
+
INFO_BITS = 'Caldera Derived Key'
|
|
28
|
+
|
|
29
|
+
attr_reader :pool_name, :big_a, :small_a
|
|
30
|
+
|
|
31
|
+
def initialize(pool_name)
|
|
32
|
+
@pool_name = pool_name
|
|
33
|
+
@n = OpenSSL::BN.new(N_HEX, 16)
|
|
34
|
+
@g = OpenSSL::BN.new(G_HEX, 16)
|
|
35
|
+
@k = compute_k
|
|
36
|
+
generate_a
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def srp_a
|
|
40
|
+
@big_a.to_s(16)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Given the server's challenge parameters, compute the password claim signature.
|
|
44
|
+
def compute_claim(user_id:, password:, srp_b:, salt:, secret_block:, timestamp:)
|
|
45
|
+
big_b = OpenSSL::BN.new(srp_b, 16)
|
|
46
|
+
|
|
47
|
+
u = compute_u(big_b)
|
|
48
|
+
raise AuthenticationError, 'SRP safety check: u must not be zero' if u.zero?
|
|
49
|
+
|
|
50
|
+
x = compute_x(salt, user_id, password)
|
|
51
|
+
s = compute_s(big_b, x, u)
|
|
52
|
+
hkdf_key = compute_hkdf(s, u)
|
|
53
|
+
|
|
54
|
+
secret_block_bytes = Base64.decode64(secret_block)
|
|
55
|
+
msg = pool_name.encode('utf-8') +
|
|
56
|
+
user_id.encode('utf-8') +
|
|
57
|
+
secret_block_bytes +
|
|
58
|
+
timestamp.encode('utf-8')
|
|
59
|
+
hmac = OpenSSL::HMAC.digest('SHA256', hkdf_key, msg)
|
|
60
|
+
Base64.strict_encode64(hmac)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def generate_a
|
|
66
|
+
loop do
|
|
67
|
+
@small_a = OpenSSL::BN.new(SecureRandom.hex(128), 16)
|
|
68
|
+
@big_a = @g.mod_exp(@small_a, @n)
|
|
69
|
+
break unless (@big_a % @n).zero?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def compute_k
|
|
74
|
+
hex_hash_bn(pad_hex(@n) + pad_hex(@g))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def compute_u(big_b)
|
|
78
|
+
hex_hash_bn(pad_hex(@big_a) + pad_hex(big_b))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def compute_x(salt_hex, user_id, password)
|
|
82
|
+
# Inner: H(poolName || userId || ":" || password) -> hex
|
|
83
|
+
identity_hex = hex_sha256(pool_name + user_id + ':' + password)
|
|
84
|
+
# Outer: H(pad(salt) || identityHash)
|
|
85
|
+
hex_hash_bn(pad_hex_str(salt_hex) + identity_hex)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def compute_s(big_b, x, u)
|
|
89
|
+
gx = @g.mod_exp(x, @n)
|
|
90
|
+
base = (big_b - @k * gx) % @n
|
|
91
|
+
exp = @small_a + u * x
|
|
92
|
+
base.mod_exp(exp, @n)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def compute_hkdf(s, u)
|
|
96
|
+
ikm = hex_to_bytes(pad_hex(s))
|
|
97
|
+
salt = hex_to_bytes(pad_hex(OpenSSL::BN.new(u.to_s(16), 16)))
|
|
98
|
+
|
|
99
|
+
prk = OpenSSL::HMAC.digest('SHA256', salt, ikm)
|
|
100
|
+
info = INFO_BITS + "\x01"
|
|
101
|
+
hmac = OpenSSL::HMAC.digest('SHA256', prk, info)
|
|
102
|
+
hmac[0, 16]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Cognito-style hex padding: ensure even length, prepend 00 if high bit set.
|
|
106
|
+
# Matches amazon-cognito-identity-js padHex().
|
|
107
|
+
def pad_hex(bn)
|
|
108
|
+
pad_hex_str(bn.to_s(16))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pad_hex_str(hex)
|
|
112
|
+
hex = hex.delete_prefix('-')
|
|
113
|
+
hex = "0#{hex}" if hex.length.odd?
|
|
114
|
+
hex = "00#{hex}" if hex[0]&.match?(/[89a-fA-F]/)
|
|
115
|
+
hex
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def hex_to_bytes(hex)
|
|
119
|
+
[hex].pack('H*')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# SHA256 of a raw string, returned as hex
|
|
123
|
+
def hex_sha256(str)
|
|
124
|
+
OpenSSL::Digest::SHA256.hexdigest(str)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Hash a hex string (converting to bytes first), return as BN
|
|
128
|
+
def hex_hash_bn(hex_str)
|
|
129
|
+
digest = OpenSSL::Digest::SHA256.hexdigest(hex_to_bytes(hex_str))
|
|
130
|
+
OpenSSL::BN.new(digest, 16)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
class Client
|
|
3
|
+
attr_reader :config, :auth
|
|
4
|
+
|
|
5
|
+
def initialize(email: nil, password: nil, config: nil)
|
|
6
|
+
@config = config || Configuration.new
|
|
7
|
+
@config.email = email if email
|
|
8
|
+
@config.password = password if password
|
|
9
|
+
@auth = Auth::Cognito.new(@config)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Authenticate with Cognito. Called automatically on first request if needed.
|
|
13
|
+
def authenticate
|
|
14
|
+
@auth.authenticate
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# GET request — token sent as query parameter.
|
|
19
|
+
def get(path, params = {})
|
|
20
|
+
ensure_authenticated
|
|
21
|
+
params[:token] = @auth.id_token
|
|
22
|
+
|
|
23
|
+
response = connection.get(path, params)
|
|
24
|
+
handle_response(response)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# POST request — token sent in JSON body.
|
|
28
|
+
def post(path, body = {})
|
|
29
|
+
ensure_authenticated
|
|
30
|
+
body[:token] = @auth.id_token
|
|
31
|
+
|
|
32
|
+
response = connection.post(path) do |req|
|
|
33
|
+
req.body = body.to_json
|
|
34
|
+
end
|
|
35
|
+
handle_response(response)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# POST request that also includes the Cognito access_token.
|
|
39
|
+
def post_authenticated(path, body = {})
|
|
40
|
+
ensure_authenticated
|
|
41
|
+
body[:token] = @auth.id_token
|
|
42
|
+
body[:access_token] = @auth.tokens[:access_token]
|
|
43
|
+
|
|
44
|
+
response = connection.post(path) do |req|
|
|
45
|
+
req.body = body.to_json
|
|
46
|
+
end
|
|
47
|
+
handle_response(response)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sites
|
|
51
|
+
@sites ||= Resources::Site.new(self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def devices
|
|
55
|
+
@devices ||= Resources::Device.new(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def wifi
|
|
59
|
+
@wifi ||= Resources::Ssid.new(self)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clients
|
|
63
|
+
@clients ||= Resources::ClientDevice.new(self)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def account
|
|
67
|
+
@account ||= Resources::Account.new(self)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def groups
|
|
71
|
+
@groups ||= Resources::Group.new(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def profiles
|
|
75
|
+
@profiles ||= Resources::Profile.new(self)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def floor_plans
|
|
79
|
+
@floor_plans ||= Resources::FloorPlan.new(self)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def filters
|
|
83
|
+
@filters ||= Resources::Filter.new(self)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def ensure_authenticated
|
|
89
|
+
if @auth.id_token.nil?
|
|
90
|
+
authenticate
|
|
91
|
+
elsif @auth.token_expired?
|
|
92
|
+
@auth.refresh
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def connection
|
|
97
|
+
@connection ||= Faraday.new(url: @config.api_url) do |f|
|
|
98
|
+
f.headers['Content-Type'] = 'application/json'
|
|
99
|
+
f.headers['Accept'] = 'application/json'
|
|
100
|
+
f.request :retry, max: 2, interval: 0.5, backoff_factor: 2,
|
|
101
|
+
exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
|
|
102
|
+
f.options.timeout = @config.timeout
|
|
103
|
+
f.options.open_timeout = @config.open_timeout
|
|
104
|
+
f.adapter Faraday.default_adapter
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_response(response)
|
|
109
|
+
body = parse_body(response)
|
|
110
|
+
|
|
111
|
+
case response.status
|
|
112
|
+
when 200..299
|
|
113
|
+
body
|
|
114
|
+
when 401
|
|
115
|
+
@auth.refresh
|
|
116
|
+
raise AuthenticationError, 'Authentication failed after refresh'
|
|
117
|
+
when 404
|
|
118
|
+
raise NotFoundError.new(status: response.status, body: body)
|
|
119
|
+
when 429
|
|
120
|
+
raise RateLimitError.new(status: response.status, body: body)
|
|
121
|
+
when 500..599
|
|
122
|
+
raise ServerError.new(status: response.status, body: body)
|
|
123
|
+
else
|
|
124
|
+
raise ApiError.new("#{body}", status: response.status, body: body)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_body(response)
|
|
129
|
+
return response.body unless response.body.is_a?(String)
|
|
130
|
+
return nil if response.body.empty?
|
|
131
|
+
|
|
132
|
+
JSON.parse(response.body)
|
|
133
|
+
rescue JSON::ParserError
|
|
134
|
+
response.body
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
class Configuration
|
|
3
|
+
COGNITO_USER_POOL_ID = 'us-east-1_4QbA7N3Uy'
|
|
4
|
+
COGNITO_CLIENT_ID = '24bk8l088t5bf31nuceoqb503q'
|
|
5
|
+
COGNITO_REGION = 'us-east-1'
|
|
6
|
+
DEFAULT_API_URL = 'https://manage.alta.inc'
|
|
7
|
+
|
|
8
|
+
attr_accessor :email, :password, :api_url,
|
|
9
|
+
:user_pool_id, :client_id, :region,
|
|
10
|
+
:log_level, :timeout, :open_timeout
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@email = ENV['ALTA_LABS_EMAIL']
|
|
14
|
+
@password = ENV['ALTA_LABS_PASSWORD']
|
|
15
|
+
@api_url = ENV.fetch('ALTA_LABS_API_URL', DEFAULT_API_URL)
|
|
16
|
+
@user_pool_id = COGNITO_USER_POOL_ID
|
|
17
|
+
@client_id = COGNITO_CLIENT_ID
|
|
18
|
+
@region = COGNITO_REGION
|
|
19
|
+
@log_level = :warn
|
|
20
|
+
@timeout = 30
|
|
21
|
+
@open_timeout = 10
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pool_name
|
|
25
|
+
user_pool_id.split('_').last
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cognito_endpoint
|
|
29
|
+
"https://cognito-idp.#{region}.amazonaws.com/"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.default
|
|
33
|
+
@default ||= new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.reset!
|
|
37
|
+
@default = new
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
class AuthenticationError < Error; end
|
|
5
|
+
class TokenExpiredError < AuthenticationError; end
|
|
6
|
+
class InvalidCredentialsError < AuthenticationError; end
|
|
7
|
+
class MfaRequiredError < AuthenticationError
|
|
8
|
+
attr_reader :session, :challenge_name
|
|
9
|
+
|
|
10
|
+
def initialize(message = 'MFA verification required', session: nil, challenge_name: nil)
|
|
11
|
+
@session = session
|
|
12
|
+
@challenge_name = challenge_name
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class ApiError < Error
|
|
18
|
+
attr_reader :status, :body
|
|
19
|
+
|
|
20
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
21
|
+
@status = status
|
|
22
|
+
@body = body
|
|
23
|
+
super(message || "API error (#{status})")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class NotFoundError < ApiError; end
|
|
28
|
+
class RateLimitError < ApiError; end
|
|
29
|
+
class ServerError < ApiError; end
|
|
30
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
class Resource
|
|
3
|
+
attr_reader :client
|
|
4
|
+
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# GET request — for read operations.
|
|
12
|
+
def get(path, params = {})
|
|
13
|
+
client.get(path, params)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# POST request — for write/mutation operations.
|
|
17
|
+
def post(path, body = {})
|
|
18
|
+
client.post(path, body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# POST with access_token — for account operations.
|
|
22
|
+
def post_authenticated(path, body = {})
|
|
23
|
+
client.post_authenticated(path, body)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class Account < Resource
|
|
4
|
+
def info
|
|
5
|
+
post_authenticated('/api/account/info')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
post_authenticated('/api/account/list')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def update(**params)
|
|
13
|
+
post_authenticated('/api/account/update', params)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class Device < Resource
|
|
4
|
+
def list(site_id:)
|
|
5
|
+
get('/api/device/list', siteid: site_id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def add(site_id:, **params)
|
|
9
|
+
post('/api/device/add', params.merge(siteid: site_id))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_serial(site_id:, serial:, **params)
|
|
13
|
+
post('/api/device/add-serial', params.merge(siteid: site_id, serial: serial))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def edit(id:, **params)
|
|
17
|
+
post('/api/device/edit', params.merge(id: id))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def delete(id:)
|
|
21
|
+
post('/api/device/delete', id: id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def search(query:, **params)
|
|
25
|
+
post('/api/device/search', params.merge(query: query))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def grab(id:)
|
|
29
|
+
post('/api/device/grab', id: id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def release(id:)
|
|
33
|
+
post('/api/device/release', id: id)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def move(id:, site_id:)
|
|
37
|
+
post('/api/device/move', id: id, siteid: site_id)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class Filter < Resource
|
|
4
|
+
def get_filter(site_id:)
|
|
5
|
+
get('/api/filter', siteid: site_id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def set(site_id:, **params)
|
|
9
|
+
post('/api/filter', params.merge(siteid: site_id))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pause(site_id:, **params)
|
|
13
|
+
post('/api/filter/pause', params.merge(siteid: site_id))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class FloorPlan < Resource
|
|
4
|
+
def floors(site_id:)
|
|
5
|
+
get('/api/floor-plan/floors', siteid: site_id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create_floor(**params)
|
|
9
|
+
post('/api/floor-plan/floor', params)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def delete_floor(id:)
|
|
13
|
+
post('/api/floor-plan/delete-floor', id: id)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def devices(site_id:)
|
|
17
|
+
get('/api/floor-plan/devices', siteid: site_id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def walls(site_id:)
|
|
21
|
+
get('/api/floor-plan/walls', siteid: site_id)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class Group < Resource
|
|
4
|
+
def add(site_id:, **params)
|
|
5
|
+
post('/api/group/add', params.merge(siteid: site_id))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def edit(id:, **params)
|
|
9
|
+
post('/api/group/edit', params.merge(id: id))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def delete(id:)
|
|
13
|
+
post('/api/group/delete', id: id)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class Profile < Resource
|
|
4
|
+
def list(site_ids: [])
|
|
5
|
+
post('/api/profile/list', siteids: site_ids)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def edit(id:, **params)
|
|
9
|
+
post('/api/profile/edit', params.merge(id: id))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def delete(id:)
|
|
13
|
+
post('/api/profile/delete', id: id)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class Site < Resource
|
|
4
|
+
def list(timezone: nil)
|
|
5
|
+
params = {}
|
|
6
|
+
params[:tz] = timezone if timezone
|
|
7
|
+
get('/api/sites/list', params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find(id:)
|
|
11
|
+
get('/api/site', id: id)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def stats(id:)
|
|
15
|
+
post('/api/sites/stats', siteid: id)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def audit(id:)
|
|
19
|
+
get('/api/site/audit', id: id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def asn(id:, ip:)
|
|
23
|
+
get('/api/site/asn', id: id, ip: ip)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create(name:, type: nil, icon: nil, location: nil)
|
|
27
|
+
params = { name: name }
|
|
28
|
+
params[:type] = type if type
|
|
29
|
+
params[:icon] = icon if icon
|
|
30
|
+
params[:location] = location if location
|
|
31
|
+
post('/api/sites/new', params)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rename(id:, name:, icon: nil)
|
|
35
|
+
params = { siteid: id, name: name }
|
|
36
|
+
params[:icon] = icon if icon
|
|
37
|
+
post('/api/sites/rename', params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def copy(id:)
|
|
41
|
+
post('/api/sites/copy', siteid: id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def delete(id:)
|
|
45
|
+
post('/api/sites/delete', siteid: id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sync(id:)
|
|
49
|
+
post('/api/site/sync', siteid: id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def remove_user(id:, user_id:)
|
|
53
|
+
post('/api/sites/remove-user', siteid: id, userid: user_id)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module AltaLabs
|
|
2
|
+
module Resources
|
|
3
|
+
class Ssid < Resource
|
|
4
|
+
def list(site_id:)
|
|
5
|
+
get('/api/wifi/ssid/list', siteid: site_id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def find(id:)
|
|
9
|
+
get('/api/wifi/ssid', id: id)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def blob(id:)
|
|
13
|
+
get('/api/wifi/ssid-blob', id: id)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create(**params)
|
|
17
|
+
post('/api/wifi/ssid', params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def delete(id:)
|
|
21
|
+
post('/api/wifi/ssid/delete', id: id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def audit(id:)
|
|
25
|
+
get('/api/wifi/ssid/audit', id: id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sync_template(site_id:)
|
|
29
|
+
post('/api/wifi/ssid-template/sync', siteid: site_id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def approve(site_id:, **params)
|
|
33
|
+
post('/api/wifi/approve', params.merge(siteid: site_id))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def auth_respond(site_id:, event_id:, allow:, timeout: nil)
|
|
37
|
+
params = { siteid: site_id, eventid: event_id, allow: allow }
|
|
38
|
+
params[:timeout] = timeout if timeout
|
|
39
|
+
post('/api/wifi/auth-resp', params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset_voucher(site_id:, **params)
|
|
43
|
+
post('/api/wifi/voucher/reset', params.merge(siteid: site_id))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/alta_labs.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'logger'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'base64'
|
|
6
|
+
require 'pathname'
|
|
7
|
+
require 'time'
|
|
8
|
+
|
|
9
|
+
require 'faraday'
|
|
10
|
+
require 'faraday/retry'
|
|
11
|
+
|
|
12
|
+
require_relative 'alta_labs/version'
|
|
13
|
+
require_relative 'alta_labs/error'
|
|
14
|
+
require_relative 'alta_labs/configuration'
|
|
15
|
+
require_relative 'alta_labs/auth/srp'
|
|
16
|
+
require_relative 'alta_labs/auth/cognito'
|
|
17
|
+
require_relative 'alta_labs/resource'
|
|
18
|
+
require_relative 'alta_labs/resources/account'
|
|
19
|
+
require_relative 'alta_labs/resources/site'
|
|
20
|
+
require_relative 'alta_labs/resources/device'
|
|
21
|
+
require_relative 'alta_labs/resources/ssid'
|
|
22
|
+
require_relative 'alta_labs/resources/client_device'
|
|
23
|
+
require_relative 'alta_labs/resources/group'
|
|
24
|
+
require_relative 'alta_labs/resources/profile'
|
|
25
|
+
require_relative 'alta_labs/resources/floor_plan'
|
|
26
|
+
require_relative 'alta_labs/resources/filter'
|
|
27
|
+
require_relative 'alta_labs/client'
|
|
28
|
+
|
|
29
|
+
module AltaLabs
|
|
30
|
+
class << self
|
|
31
|
+
attr_writer :logger
|
|
32
|
+
|
|
33
|
+
def root(*args)
|
|
34
|
+
(@root ||= Pathname.new(File.expand_path('../', __dir__))).join(*args)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def logger
|
|
38
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
|
39
|
+
log.progname = name
|
|
40
|
+
log.level = Logger::WARN
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def configure
|
|
45
|
+
yield(Configuration.default)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configuration
|
|
49
|
+
Configuration.default
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: alta_labs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dale Stevens
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-31 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: faraday-retry
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: bundler
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '13.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '13.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rspec
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: pry-byebug
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: webmock
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '3.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '3.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: vcr
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '6.0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '6.0'
|
|
139
|
+
description: A Ruby client library for interacting with the Alta Labs cloud management
|
|
140
|
+
platform. Manage sites, devices, WiFi networks, and more programmatically.
|
|
141
|
+
email:
|
|
142
|
+
- dale@twilightcoders.net
|
|
143
|
+
executables: []
|
|
144
|
+
extensions: []
|
|
145
|
+
extra_rdoc_files: []
|
|
146
|
+
files:
|
|
147
|
+
- CHANGELOG.md
|
|
148
|
+
- LICENSE.txt
|
|
149
|
+
- README.md
|
|
150
|
+
- lib/alta_labs.rb
|
|
151
|
+
- lib/alta_labs/auth/cognito.rb
|
|
152
|
+
- lib/alta_labs/auth/srp.rb
|
|
153
|
+
- lib/alta_labs/client.rb
|
|
154
|
+
- lib/alta_labs/configuration.rb
|
|
155
|
+
- lib/alta_labs/error.rb
|
|
156
|
+
- lib/alta_labs/resource.rb
|
|
157
|
+
- lib/alta_labs/resources/account.rb
|
|
158
|
+
- lib/alta_labs/resources/client_device.rb
|
|
159
|
+
- lib/alta_labs/resources/device.rb
|
|
160
|
+
- lib/alta_labs/resources/filter.rb
|
|
161
|
+
- lib/alta_labs/resources/floor_plan.rb
|
|
162
|
+
- lib/alta_labs/resources/group.rb
|
|
163
|
+
- lib/alta_labs/resources/profile.rb
|
|
164
|
+
- lib/alta_labs/resources/site.rb
|
|
165
|
+
- lib/alta_labs/resources/ssid.rb
|
|
166
|
+
- lib/alta_labs/version.rb
|
|
167
|
+
homepage: https://github.com/TwilightCoders/alta_labs
|
|
168
|
+
licenses:
|
|
169
|
+
- MIT
|
|
170
|
+
metadata:
|
|
171
|
+
allowed_push_host: https://rubygems.org
|
|
172
|
+
homepage_uri: https://github.com/TwilightCoders/alta_labs
|
|
173
|
+
source_code_uri: https://github.com/TwilightCoders/alta_labs
|
|
174
|
+
changelog_uri: https://github.com/TwilightCoders/alta_labs/blob/main/CHANGELOG.md
|
|
175
|
+
bug_tracker_uri: https://github.com/TwilightCoders/alta_labs/issues
|
|
176
|
+
post_install_message:
|
|
177
|
+
rdoc_options: []
|
|
178
|
+
require_paths:
|
|
179
|
+
- lib
|
|
180
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - ">="
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: '3.1'
|
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
186
|
+
requirements:
|
|
187
|
+
- - ">="
|
|
188
|
+
- !ruby/object:Gem::Version
|
|
189
|
+
version: '0'
|
|
190
|
+
requirements: []
|
|
191
|
+
rubygems_version: 3.5.22
|
|
192
|
+
signing_key:
|
|
193
|
+
specification_version: 4
|
|
194
|
+
summary: Ruby SDK for the Alta Labs cloud management API
|
|
195
|
+
test_files: []
|