fortress-sdk-ruby 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f2b2221413093ee926090ed325be2a137e31240ef76d1bd04b9084359a45d9e1
4
+ data.tar.gz: b01bd23f81ed9baded9782cbb5b4ea24d95a17ac62058f6de3124132863c37ea
5
+ SHA512:
6
+ metadata.gz: 9cae26f44e083b8e864717f137ec7c4f1fd09c909a3e2a50d688c6e4173aadb189b8df024b447184c34093fcafb685f61403bb847a4368ae23715a685ec06585
7
+ data.tar.gz: 0c7c28977e775ce3fee1cea2fb4513adbc15a4959c903ca46e906107bb27a1a3e8d88e2a4c661966944b180bc45099053d079c10df21dfdd5e252a314dbbc152
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Fortress
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,69 @@
1
+ # Fortress Ruby SDK
2
+
3
+ Welcome to the Fortress Ruby SDK. This SDK provides a way for you to leverage the power of the Fortress platform in your Ruby applications.
4
+
5
+ ## Installation
6
+
7
+ You can install the SDK using Gem. Simply run the following command:
8
+
9
+ ```bash
10
+ gem install fortress_sdk_ruby
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ Here is a quick example to get you started with the SDK:
16
+
17
+ ```ruby
18
+ require 'fortress_sdk_ruby'
19
+
20
+ # Initialize the client
21
+ client = Fortress::Client.new(api_key, organization_id)
22
+
23
+ # Create a new tenant
24
+ client.create_tenant("tenant_name", "alias")
25
+
26
+ # Connect to the tenant
27
+ conn = client.connect_tenant("tenant_name")
28
+
29
+ conn.exec('CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(50))')
30
+ conn.exec("INSERT INTO users (name) VALUES ('Alice')")
31
+ conn.exec('SELECT * FROM users') do |result|
32
+ result.each do |row|
33
+ print "User: #{row['name']}\n"
34
+ end
35
+ end
36
+
37
+ # Delete the tenant
38
+ client.delete_tenant("tenant_name")
39
+ ```
40
+
41
+ ## Documentation
42
+
43
+ Below is a list of the available functionality in the SDK. Using the SDK you can create a new tenants and point them to existing or new databases. You can also easily route data requests based on tenant names. For more detailed information, please refer to the [Fortress API documentation](https://docs.fortress.build).
44
+
45
+ Database Management:
46
+
47
+ - `create_database(database_name: str, alias: str)`: Creates a new database.
48
+ - `delete_database(database_name: str)`: Deletes to a database.
49
+ - `list_databases()`: Lists all databases.
50
+ - `connect_database(database_id: str)`: Connects to a database and turns into SQL connection.
51
+
52
+ Tenant Management:
53
+
54
+ - `create_tenant(tenant_name: str, alias: str, database_id: str = "")`: Creates a new tenant.
55
+ - `delete_tenant(tenant_name: str)`: Deletes a tenant.
56
+ - `list_tenants()`: Lists all tenants.
57
+ - `connect_tenant(tenant_name: str)`: Connects to a tenant and turns into SQL connection.
58
+
59
+ ## Configuration
60
+
61
+ To use the SDK, generate an API key from the Fortress dashboard to initialize the client. Also, provide the organization ID, which is available under the API Keys page on the platform website.
62
+
63
+ ## License
64
+
65
+ This SDK is licensed under the MIT License.
66
+
67
+ ## Support
68
+
69
+ If you have any questions or need help, don't hesitate to get in touch with our support team at founders@fortress.build.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/examples/test.rb ADDED
@@ -0,0 +1,27 @@
1
+ require_relative 'lib/fortress'
2
+
3
+ # Create a client
4
+ client = Fortress::Fortress.new('orgId', 'apiKey')
5
+
6
+ # Create a database
7
+ id = client.create_database('Client 1')
8
+
9
+ # Create a tenant in that database
10
+ client.create_tenant('client1', 'Client 1', id)
11
+
12
+ # List all tenants
13
+ client.list_tenants.each do |tenant|
14
+ print "Tenant: #{tenant.name} (#{tenant.alias})\n"
15
+ end
16
+
17
+ # Connect to the tenant
18
+ conn = client.connect_tenant('client1')
19
+ conn.exec('CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(50))')
20
+ conn.exec("INSERT INTO users (name) VALUES ('Alice')")
21
+ conn.exec("INSERT INTO users (name) VALUES ('Bob')")
22
+ conn.exec("INSERT INTO users (name) VALUES ('Charlie')")
23
+ conn.exec('SELECT * FROM users') do |result|
24
+ result.each do |row|
25
+ print "User: #{row['name']}\n"
26
+ end
27
+ end
data/lib/crypto.rb ADDED
@@ -0,0 +1,68 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'digest'
4
+ require 'hmac'
5
+ require 'hmac-sha1'
6
+
7
+ module Fortress
8
+ # Crypto provides methods to encrypt and decrypt data using the ECIES algorithm.
9
+ module Crypto
10
+ # Decrypts the ciphertext using the provided private key.
11
+ def self.decrypt(private_key, ciphertext)
12
+ # Format the private key
13
+ formated_private_key = "-----BEGIN EC PRIVATE KEY-----\n#{private_key}\n-----END EC PRIVATE KEY-----"
14
+
15
+ # Load the private key
16
+ private_key = OpenSSL::PKey::EC.new(formated_private_key)
17
+ private_key.check_key
18
+
19
+ # Decode the ciphertext
20
+ ciphertext = Base64.decode64(ciphertext)
21
+
22
+ # Extract the ephemeral public key
23
+ ephemeral_size = ciphertext[0].ord
24
+ ephemeral_public_key = ciphertext[1, ephemeral_size]
25
+
26
+ # Extract the MAC and AES-GCM ciphertext
27
+ sha1_size = 20
28
+ aes_size = 16
29
+ ciphertext = ciphertext[(1 + ephemeral_size)..-1]
30
+
31
+ # Verify the ciphertext length
32
+ raise 'Invalid ciphertext' if ciphertext.length < sha1_size + aes_size
33
+
34
+ # Derive the public key
35
+ eph_pub = OpenSSL::PKey::EC::Point.new(OpenSSL::PKey::EC::Group.new('prime256v1'),
36
+ OpenSSL::BN.new(ephemeral_public_key, 2))
37
+
38
+ # Perform the ECDH key exchange
39
+ shared_key = private_key.dh_compute_key(eph_pub)
40
+
41
+ # Derive the shared key
42
+ shared = Digest::SHA256.digest(shared_key)
43
+
44
+ # Verify the MAC
45
+ tag_start = ciphertext.length - sha1_size
46
+ hmac = HMAC::SHA1.new(shared[16, 16])
47
+ hmac.update(ciphertext[0, tag_start])
48
+ mac = hmac.digest
49
+
50
+ raise 'Invalid MAC' unless mac == ciphertext[tag_start..-1]
51
+
52
+ # Decrypt the ciphertext using AES-CBC
53
+ cipher = OpenSSL::Cipher.new('aes-128-cbc')
54
+ cipher.decrypt
55
+ cipher.key = shared[0, 16]
56
+ cipher.iv = ciphertext[0, aes_size]
57
+ cipher.padding = 0
58
+
59
+ plaintext = cipher.update(ciphertext[aes_size, tag_start - aes_size]) + cipher.final
60
+
61
+ # Remove padding
62
+ padding_length = plaintext[-1].ord
63
+ plaintext = plaintext[0...-padding_length]
64
+
65
+ plaintext.force_encoding('UTF-8')
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fortress
4
+ VERSION = '0.1.1'
5
+ end
data/lib/fortress.rb ADDED
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fortress/version'
4
+ require_relative 'crypto'
5
+ require 'uri'
6
+ require 'net/http'
7
+ require 'json'
8
+ require 'base64'
9
+ require 'pg'
10
+
11
+ # Fortress is a Ruby client for the Fortress API.
12
+ #
13
+ # It provides methods to manage tenants and databases in the Fortress platform.
14
+ #
15
+ # @example
16
+ # fortress = Fortress.new('org_id', 'api_key')
17
+ #
18
+ module Fortress
19
+ class Error < StandardError; end
20
+
21
+ # Define the Tenant struct here
22
+ Tenant = Struct.new(:name, :alias, :database_id, :date_created) do
23
+ def to_json(*_args)
24
+ { id:, alias: self.alias }.to_json
25
+ end
26
+ end
27
+
28
+ # Define the Database struct here
29
+ Database = Struct.new(:id, :alias, :bytes_size, :average_read_iops, :average_write_iops, :date_created) do
30
+ def to_json(*_args)
31
+ { id:, alias: self.alias }.to_json
32
+ end
33
+ end
34
+
35
+ # Client provides methods to interact with the Fortress API.
36
+ # It requires an organization ID and an API key to authenticate requests.
37
+ class Fortress
38
+ BASE_URL = 'https://api.fortress.build'
39
+ attr_reader :org_id, :api_key
40
+
41
+ # Create a new Fortress client.
42
+ # @param org_id [String] The organization ID.
43
+ # @param api_key [String] The API key.
44
+ # @return [Fortress] The Fortress client.
45
+ def initialize(org_id, api_key)
46
+ @org_id = org_id
47
+ @api_key = api_key
48
+ end
49
+
50
+ # Connect to a tenant.
51
+ # @param id [String] The tenant ID.
52
+ # @return [PG::Connection] The connection to the tenant database.
53
+ def connect_tenant(id)
54
+ connection_details = get_connection_uri(id, 'tenant')
55
+ PG::Connection.new(dbname: connection_details.database, user: connection_details.username,
56
+ password: connection_details.password, host: connection_details.url, port: connection_details.port)
57
+ end
58
+
59
+ # Create a tenant in a database.
60
+ # @param id [String] The tenant ID.
61
+ # @param tenant_alias [String] The tenant alias.
62
+ # @param database_id [String] The database ID. (optional)
63
+ # @return [void]
64
+ def create_tenant(id, tenant_alias, database_id = nil)
65
+ endpoint = "#{BASE_URL}/v1/organization/#{@org_id}/tenant/#{id}"
66
+ body = { alias: tenant_alias }
67
+ body[:databaseId] = database_id if database_id
68
+ _ = make_request(:post, endpoint, body)
69
+ end
70
+
71
+ # List all tenants in the organization.
72
+ # @return [Array<Tenant>] The list of tenants.
73
+ def list_tenants
74
+ endpoint = "#{BASE_URL}/v1/organization/#{@org_id}/tenants"
75
+ response = make_request(:get, endpoint)
76
+ tenants = response['tenants']
77
+ tenants.map do |tenant|
78
+ Tenant.new(tenant['name'], tenant['alias'], tenant['databaseId'], tenant['dateCreated'])
79
+ end
80
+ end
81
+
82
+ # Delete a tenant.
83
+ # @param id [String] The tenant ID.
84
+ # @return [void]
85
+ def delete_tenant(id)
86
+ endpoint = "#{BASE_URL}/v1/organization/#{@org_id}/tenant/#{id}"
87
+ _ = make_request(:delete, endpoint)
88
+ end
89
+
90
+ # Connect to a database.
91
+ # @param id [String] The database ID.
92
+ # @return [PG::Connection] The connection to the database.
93
+ def connect_database(id)
94
+ connection_details = get_connection_uri(id, 'database')
95
+ PG::Connection.new(dbname: connection_details.database, user: connection_details.username,
96
+ password: connection_details.password, host: connection_details.url, port: connection_details.port)
97
+ end
98
+
99
+ # Create a database.
100
+ # @param database_alias [String] The database alias.
101
+ # @return [String] The database ID.
102
+ def create_database(database_alias)
103
+ endpoint = "#{BASE_URL}/v1/organization/#{@org_id}/database"
104
+ response = make_request(:post, endpoint, { alias: database_alias })
105
+ response['id']
106
+ end
107
+
108
+ # List all databases in the organization.
109
+ # @return [Array<Database>] The list of databases.
110
+ def list_databases
111
+ endpoint = "#{BASE_URL}/v1/organization/#{@org_id}/databases"
112
+ response = make_request(:get, endpoint)
113
+ databases = response['databases']
114
+ databases.map do |database|
115
+ Database.new(database['id'], database['alias'], database['bytesSize'],
116
+ database['averageReadIOPS'], database['averageWriteIOPS'], database['dateCreated'])
117
+ end
118
+ end
119
+
120
+ # Delete a database.
121
+ # @param id [String] The database ID.
122
+ def delete_database(id)
123
+ endpoint = "#{BASE_URL}/v1/organization/#{@org_id}/database/#{id}"
124
+ _ = make_request(:delete, endpoint)
125
+ end
126
+
127
+ private
128
+
129
+ ConnectionDetails = Struct.new(:database_id, :url, :port, :username, :password, :database) do
130
+ def to_json(*_args)
131
+ { database_id:, url:, port:, username:, password:, database: }.to_json
132
+ end
133
+ end
134
+
135
+ def get_connection_uri(id, type)
136
+ endpoint = "#{BASE_URL}/v1/organization/#{@org_id}/#{type}/#{id}/uri"
137
+
138
+ response = make_request(:get, endpoint)
139
+ connection_details_encrypted = response['connectionDetails']
140
+ connection_details_decrypted = Crypto.decrypt(@api_key, connection_details_encrypted)
141
+ connection_details = JSON.parse(connection_details_decrypted)
142
+ ConnectionDetails.new(connection_details['databaseId'].to_i, connection_details['url'],
143
+ connection_details['port'].to_i, connection_details['username'], connection_details['password'], connection_details['database'])
144
+ end
145
+
146
+ def make_request(method, url, body = nil)
147
+ uri = URI.parse(url)
148
+
149
+ request = Net::HTTP.const_get(method.capitalize).new(uri)
150
+ request['Content-Type'] = 'application/json'
151
+ request['Api-Key'] = @api_key
152
+ http = Net::HTTP.new(uri.host, uri.port)
153
+
154
+ # TODO: Enable SSL
155
+ http.use_ssl = true
156
+
157
+ request.body = body.to_json if body
158
+
159
+ parse_response(http.request(request))
160
+ end
161
+
162
+ def parse_response(response)
163
+ case response
164
+ when Net::HTTPSuccess
165
+ JSON.parse(response.body)
166
+ else
167
+ raise Error, "Request failed with response code #{response.code}: #{response.message}"
168
+ end
169
+ end
170
+ end
171
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fortress-sdk-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Fortress
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-08-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This is the official Ruby SDK for Fortress. It provides a simple way
14
+ to interact with the Fortress API.
15
+ email:
16
+ - founder@fortress.build
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - Rakefile
24
+ - examples/test.rb
25
+ - lib/crypto.rb
26
+ - lib/fortress.rb
27
+ - lib/fortress/version.rb
28
+ homepage: https://fortress.build
29
+ licenses: []
30
+ metadata:
31
+ allowed_push_host: https://rubygems.org
32
+ homepage_uri: https://fortress.build
33
+ source_code_uri: https://github.com/fortress-build/sdk-ruby
34
+ changelog_uri: https://github.com/fortress-build/sdk-python
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.3.7
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: The Fortress SDK for Ruby
54
+ test_files: []