op_connect 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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.github/workflows/coverage.yml +27 -0
  5. data/.github/workflows/publish.yml +42 -0
  6. data/.github/workflows/test.yml +27 -0
  7. data/.gitignore +11 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +16 -0
  10. data/Guardfile +23 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +322 -0
  13. data/Rakefile +12 -0
  14. data/bin/console +17 -0
  15. data/bin/setup +8 -0
  16. data/docker-compose.yml +20 -0
  17. data/lib/op_connect/api_request/actor.rb +15 -0
  18. data/lib/op_connect/api_request/resource.rb +14 -0
  19. data/lib/op_connect/api_request.rb +17 -0
  20. data/lib/op_connect/client/files.rb +20 -0
  21. data/lib/op_connect/client/items.rb +34 -0
  22. data/lib/op_connect/client/vaults.rb +15 -0
  23. data/lib/op_connect/client.rb +45 -0
  24. data/lib/op_connect/configurable.rb +45 -0
  25. data/lib/op_connect/connection.rb +103 -0
  26. data/lib/op_connect/default.rb +57 -0
  27. data/lib/op_connect/error.rb +60 -0
  28. data/lib/op_connect/item/field.rb +18 -0
  29. data/lib/op_connect/item/file.rb +16 -0
  30. data/lib/op_connect/item/generator_recipe.rb +12 -0
  31. data/lib/op_connect/item/section.rb +12 -0
  32. data/lib/op_connect/item/url.rb +14 -0
  33. data/lib/op_connect/item.rb +31 -0
  34. data/lib/op_connect/object.rb +21 -0
  35. data/lib/op_connect/response/raise_error.rb +16 -0
  36. data/lib/op_connect/response.rb +5 -0
  37. data/lib/op_connect/server_health/dependency.rb +13 -0
  38. data/lib/op_connect/server_health.rb +13 -0
  39. data/lib/op_connect/vault.rb +16 -0
  40. data/lib/op_connect/version.rb +5 -0
  41. data/lib/op_connect.rb +47 -0
  42. data/op_connect.gemspec +34 -0
  43. metadata +116 -0
@@ -0,0 +1,20 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ op-connect-api:
5
+ image: 1password/connect-api:latest
6
+ ports:
7
+ - 8080:8080
8
+ volumes:
9
+ - ./1password-credentials.json:/home/opuser/.op/1password-credentials.json
10
+ - op-connect-data:/home/opuser/.op/data
11
+ op-connect-sync:
12
+ image: 1password/connect-sync:latest
13
+ ports:
14
+ - 8081:8080
15
+ volumes:
16
+ - ./1password-credentials.json:/home/opuser/.op/1password-credentials.json
17
+ - op-connect-data:/home/opuser/.op/data
18
+
19
+ volumes:
20
+ op-connect-data:
@@ -0,0 +1,15 @@
1
+ module OpConnect
2
+ class APIRequest
3
+ class Actor
4
+ attr_reader :id, :account, :jti, :user_agent, :ip
5
+
6
+ def initialize(options = {})
7
+ @id = options["id"]
8
+ @account = options["account"]
9
+ @jti = options["jti"]
10
+ @user_agent = options["user_agent"]
11
+ @ip = options["ip"]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module OpConnect
2
+ class APIRequest
3
+ class Resource
4
+ attr_reader :type, :vault, :item, :item_version
5
+
6
+ def initialize(options = {})
7
+ @type = options["type"]
8
+ @vault = Object.new(options["vault"])
9
+ @item = Object.new(options["item"])
10
+ @item_version = options["item_version"]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module OpConnect
2
+ class APIRequest
3
+ autoload :Actor, "op_connect/api_request/actor"
4
+ autoload :Resource, "op_connect/api_request/resource"
5
+
6
+ attr_reader :request_id, :timestamp, :action, :result, :actor, :resource
7
+
8
+ def initialize(options = {})
9
+ @request_id = options["request_id"]
10
+ @timestamp = options["timestamp"]
11
+ @action = options["action"]
12
+ @result = options["result"]
13
+ @actor = Actor.new(options["actor"])
14
+ @resource = Resource.new(options["resource"])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module OpConnect
2
+ class Client
3
+ module Files
4
+ def list_files(vault_id:, item_id:, **params)
5
+ get("vaults/#{vault_id}/items/#{item_id}/files", params: params).body.map { |file| Item::File.new(file) }
6
+ end
7
+ alias_method :files, :list_files
8
+
9
+ def get_file(vault_id:, item_id:, id:, **params)
10
+ Item::File.new get("vaults/#{vault_id}/items/#{item_id}/files/#{id}", params: params).body
11
+ end
12
+ alias_method :file, :get_file
13
+
14
+ def get_file_content(vault_id:, item_id:, id:)
15
+ get("vaults/#{vault_id}/items/#{item_id}/files/#{id}/content").body
16
+ end
17
+ alias_method :file_content, :get_file_content
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ module OpConnect
2
+ class Client
3
+ module Items
4
+ def list_items(vault_id:, **params)
5
+ get("vaults/#{vault_id}/items", params: params).body.map { |item| Item.new(item) }
6
+ end
7
+ alias_method :items, :list_items
8
+
9
+ def get_item(vault_id:, id:)
10
+ Item.new get("vaults/#{vault_id}/items/#{id}").body
11
+ end
12
+ alias_method :item, :get_item
13
+
14
+ def create_item(vault_id:, **attributes)
15
+ Item.new post("vaults/#{vault_id}/items", body: attributes).body
16
+ end
17
+
18
+ def replace_item(vault_id:, id:, **attributes)
19
+ Item.new put("vaults/#{vault_id}/items/#{id}", body: attributes).body
20
+ end
21
+
22
+ def delete_item(vault_id:, id:)
23
+ return true if delete("vaults/#{vault_id}/items/#{id}").status == 204
24
+ false
25
+ rescue OpConnect::Error
26
+ false
27
+ end
28
+
29
+ def update_item(vault_id:, id:, **attributes)
30
+ Item.new patch("vaults/#{vault_id}/items/#{id}", body: attributes, headers: {"Content-Type": "applicatoin/json-patch+json"}).body
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ module OpConnect
2
+ class Client
3
+ module Vaults
4
+ def list_vaults(**params)
5
+ get("vaults", params: params).body.map { |vault| Vault.new(vault) }
6
+ end
7
+ alias_method :vaults, :list_vaults
8
+
9
+ def get_vault(id:)
10
+ Vault.new get("vaults/#{id}").body
11
+ end
12
+ alias_method :vault, :get_vault
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ module OpConnect
2
+ class Client
3
+ autoload :Vaults, "op_connect/client/vaults"
4
+ autoload :Items, "op_connect/client/items"
5
+ autoload :Files, "op_connect/client/files"
6
+
7
+ include OpConnect::Configurable
8
+ include OpConnect::Connection
9
+ include OpConnect::Client::Vaults
10
+ include OpConnect::Client::Items
11
+ include OpConnect::Client::Files
12
+
13
+ def initialize(options = {})
14
+ OpConnect::Configurable.keys.each do |key|
15
+ value = options.key?(key) ? options[key] : OpConnect.instance_variable_get(:"@#{key}")
16
+ instance_variable_set(:"@#{key}", value)
17
+ end
18
+ end
19
+
20
+ def inspect
21
+ inspected = super
22
+ inspected.gsub!(@access_token, ("*" * 24).to_s) if @access_token
23
+ inspected
24
+ end
25
+
26
+ def activity(**params)
27
+ get("activity", params: params).body.map { |a| APIRequest.new(a) }
28
+ end
29
+
30
+ def heartbeat
31
+ return true if get("/heartbeat").status == 200
32
+ false
33
+ rescue OpConnect::Error
34
+ false
35
+ end
36
+
37
+ def health
38
+ ServerHealth.new get("/health").body
39
+ end
40
+
41
+ def metrics
42
+ get("/metrics").body
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ module OpConnect
2
+ module Configurable
3
+ attr_accessor :access_token, :adapter, :stubs, :user_agent
4
+ attr_writer :api_endpoint
5
+
6
+ class << self
7
+ def keys
8
+ @keys ||= [
9
+ :access_token,
10
+ :adapter,
11
+ :api_endpoint,
12
+ :stubs,
13
+ :user_agent
14
+ ]
15
+ end
16
+ end
17
+
18
+ def configure
19
+ yield self
20
+ end
21
+
22
+ def reset!
23
+ OpConnect::Configurable.keys.each do |key|
24
+ instance_variable_set(:"@#{key}", OpConnect::Default.options[key])
25
+ end
26
+
27
+ self
28
+ end
29
+ alias_method :setup, :reset!
30
+
31
+ def same_options?(opts)
32
+ opts.hash == options.hash
33
+ end
34
+
35
+ def api_endpoint
36
+ ::File.join(@api_endpoint, "")
37
+ end
38
+
39
+ private
40
+
41
+ def options
42
+ OpConnect::Configurable.keys.map { |key| [key, instance_variable_get(:"@#{key}")] }.to_h
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,103 @@
1
+ module OpConnect
2
+ # Network layer for API clients.
3
+ #
4
+ module Connection
5
+ # Make an HTTP GET request.
6
+ #
7
+ # @param url [String] The path, relative to {#api_endpoint}.
8
+ # @param params [Hash] Query parameters for the request.
9
+ # @param headers [Hash] Header params for the request.
10
+ #
11
+ # @return [Faraday::Response]
12
+ #
13
+ def get(url, params: {}, headers: {})
14
+ request :get, url, params, headers
15
+ end
16
+
17
+ # Make an HTTP POST request.
18
+ #
19
+ # @param url [String] The path, relative to {#api_endpoint}.
20
+ # @param body [Hash] Body params for the request.
21
+ # @param headers [Hash] Header params for the request.
22
+ #
23
+ # @return [Faraday::Response]
24
+ #
25
+ def post(url, body:, headers: {})
26
+ request :post, url, body, headers
27
+ end
28
+
29
+ # Make an HTTP PUT request.
30
+ #
31
+ # @param url [String] The path, relative to {#api_endpoint}.
32
+ # @param body [Hash] Body params for the request.
33
+ # @param headers [Hash] Header params for the request.
34
+ #
35
+ # @return [Faraday::Response]
36
+ #
37
+ def put(url, body:, headers: {})
38
+ request :put, url, body, headers
39
+ end
40
+
41
+ # Make an HTTP PATCH request.
42
+ #
43
+ # @param url [String] The path, relative to {#api_endpoint}.
44
+ # @param body [Hash] Body params for the request.
45
+ # @param headers [Hash] Header params for the request.
46
+ #
47
+ # @return [Faraday::Response]
48
+ #
49
+ def patch(url, body:, headers: {})
50
+ request :patch, url, body, headers
51
+ end
52
+
53
+ # Make an HTTP DELETE request.
54
+ #
55
+ # @param url [String] The path, relative to {#api_endpoint}.
56
+ # @param params [Hash] Query params for the request.
57
+ # @param headers [Hash] Header params for the request.
58
+ #
59
+ # @return [Faraday::Response]
60
+ #
61
+ def delete(url, params: {}, headers: {})
62
+ request :delete, url, params, headers
63
+ end
64
+
65
+ # Connection object for the 1Password Connect API.
66
+ #
67
+ # @return [Faraday::Client]
68
+ #
69
+ def connection
70
+ @connection ||= Faraday.new(api_endpoint) do |http|
71
+ http.headers[:user_agent] = user_agent
72
+
73
+ http.request :authorization, :Bearer, access_token
74
+ http.request :json
75
+
76
+ http.use OpConnect::Response::RaiseError
77
+
78
+ http.response :dates
79
+ http.response :json, content_type: "application/json"
80
+
81
+ http.adapter adapter, @stubs
82
+ end
83
+ end
84
+
85
+ # Response for the last HTTP request.
86
+ #
87
+ # @return [Faraday::Response]
88
+ #
89
+ def last_response
90
+ @last_response if defined? @last_response
91
+ end
92
+
93
+ private
94
+
95
+ def request(method, path, data, headers = {})
96
+ @last_response = response = connection.send(method, path, data, headers)
97
+ response
98
+ rescue OpConnect::Error => error
99
+ @last_response = nil
100
+ raise error
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "op_connect/version"
4
+
5
+ module OpConnect
6
+ # Default configuration options for {Client}
7
+ #
8
+ module Default
9
+ API_ENDPOINT = "http://localhost:8080/v1"
10
+ USER_AGENT = "1Password Connect Ruby SDK #{OpConnect::VERSION}"
11
+
12
+ class << self
13
+ # Configuration options
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ def options
18
+ OpConnect::Configurable.keys.map { |key| [key, send(key)] }.to_h
19
+ end
20
+
21
+ # Default access token from ENV
22
+ #
23
+ # @return [String]
24
+ #
25
+ def access_token
26
+ ENV["OP_CONNECT_ACCESS_TOKEN"]
27
+ end
28
+
29
+ # Default network adapter for Faraday (defaults to :net_http)
30
+ #
31
+ # @return [Symbol]
32
+ #
33
+ def adapter
34
+ Faraday.default_adapter
35
+ end
36
+
37
+ # Default API endpoint from ENV or {API_ENDPOINT}
38
+ #
39
+ # @return [<Type>] <description>
40
+ #
41
+ def api_endpoint
42
+ ENV["OP_CONNECT_API_ENDPOINT"] || API_ENDPOINT
43
+ end
44
+
45
+ def stubs
46
+ end
47
+
48
+ # Default user agent from ENV or {USER_AGENT}
49
+ #
50
+ # @return [<Type>] <description>
51
+ #
52
+ def user_agent
53
+ ENV["OP_CONNECT_USER_AGENT"] || USER_AGENT
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,60 @@
1
+ module OpConnect
2
+ class Error < StandardError
3
+ class << self
4
+ def from_response(response)
5
+ status = response.status
6
+
7
+ if (klass = case status
8
+ when 400 then OpConnect::BadRequest
9
+ when 401 then OpConnect::Unauthorized
10
+ when 403 then OpConnect::Forbidden
11
+ when 404 then OpConnect::NotFound
12
+ when 413 then OpConnect::PayloadTooLarge
13
+ when 400..499 then OpConnect::ClientError
14
+ when 500 then OpConnect::InternalServerError
15
+ when 503 then OpConnect::ServiceUnavailable
16
+ when 500..599 then OpConnect::ServerError
17
+ end)
18
+ klass.new(response)
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize(response = nil)
24
+ @response = response
25
+ super(build_error_message)
26
+ end
27
+
28
+ private
29
+
30
+ def build_error_message
31
+ return nil if @response.nil?
32
+
33
+ message = "#{@response.method.to_s.upcase} "
34
+ message << "#{@response.url}: "
35
+ message << "#{@response.status} - "
36
+ message << @response.body["message"].to_s if @response.body["message"]
37
+ message << "\n\n#{@response.body}\n\n"
38
+
39
+ message
40
+ end
41
+ end
42
+
43
+ class ClientError < Error; end
44
+
45
+ class BadRequest < ClientError; end
46
+
47
+ class Unauthorized < ClientError; end
48
+
49
+ class Forbidden < ClientError; end
50
+
51
+ class NotFound < ClientError; end
52
+
53
+ class PayloadTooLarge < ClientError; end
54
+
55
+ class ServerError < Error; end
56
+
57
+ class InternalServerError < ServerError; end
58
+
59
+ class ServiceUnavailable < ServerError; end
60
+ end
@@ -0,0 +1,18 @@
1
+ module OpConnect
2
+ class Item
3
+ class Field
4
+ attr_reader :purpose, :type, :value, :should_generate, :recipe, :section
5
+
6
+ alias_method :generate?, :should_generate
7
+
8
+ def initialize(options = {})
9
+ @purpose = options["purpose"] if options["purpose"]
10
+ @type = options["type"] if options["type"]
11
+ @value = options["value"]
12
+ @should_generate = options["generate"] || false
13
+ @recipe = GeneratorRecipe.new(options["recipe"])
14
+ @section = Object.new(options["section"])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module OpConnect
2
+ class Item
3
+ class File
4
+ attr_reader :id, :name, :size, :content_path, :content, :section
5
+
6
+ def initialize(options = {})
7
+ @id = options["id"]
8
+ @name = options["name"]
9
+ @size = options["size"]
10
+ @content_path = options["content_path"]
11
+ @content = options["content"]
12
+ @section = Object.new(options["section"])
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module OpConnect
2
+ class Item
3
+ class GeneratorRecipe
4
+ attr_reader :length, :character_sets
5
+
6
+ def initialize(options = {})
7
+ @length = options["length"] if options & ["length"]
8
+ @character_sets = options["characterSets"] if options & ["characterSets"]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module OpConnect
2
+ class Item
3
+ class Section
4
+ attr_reader :id, :label
5
+
6
+ def initialize(options = {})
7
+ @id = options["id"]
8
+ @label = options["label"]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module OpConnect
2
+ class Item
3
+ class URL
4
+ attr_reader :url, :is_primary
5
+
6
+ alias_method :primary?, :is_primary
7
+
8
+ def initialize(options = {})
9
+ @url = options["url"]
10
+ @is_primary = options["primary"] || false
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ module OpConnect
2
+ class Item
3
+ autoload :Field, "op_connect/item/field"
4
+ autoload :File, "op_connect/item/file"
5
+ autoload :GeneratorRecipe, "op_connect/item/generator_recipe"
6
+ autoload :Section, "op_connect/item/section"
7
+ autoload :URL, "op_connect/item/url"
8
+
9
+ attr_reader :id, :title, :vault, :category, :urls, :is_favorite, :tags, :version, :state, :sections, :fields, :files, :created_at, :updated_at, :last_edited_by
10
+
11
+ alias_method :favorite?, :is_favorite
12
+
13
+ def initialize(options = {})
14
+ @id = options["id"]
15
+ @title = options["title"]
16
+ @vault = Object.new(options["vault"])
17
+ @category = options["category"]
18
+ @urls = options["urls"]&.collect! { |url| URL.new(url) }
19
+ @is_favorite = options["favorite"] || false
20
+ @tags = options["tags"]
21
+ @version = options["version"]
22
+ @state = options["state"]
23
+ @sections = options["sections"]&.collect! { |section| Section.new(section) } || []
24
+ @fields = options["fields"]&.collect! { |field| Field.new(field) } || []
25
+ @files = options["files"]&.collect! { |file| File.new(file) } || []
26
+ @created_at = options["createdAt"]
27
+ @updated_at = options["updatedAt"]
28
+ @last_edited_by = options["lastEditedBy"]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ require "ostruct"
2
+
3
+ module OpConnect
4
+ class Object < SimpleDelegator
5
+ def initialize(attributes)
6
+ super to_ostruct(attributes)
7
+ end
8
+
9
+ private
10
+
11
+ def to_ostruct(obj)
12
+ if obj.is_a?(Hash)
13
+ OpenStruct.new(obj.map { |key, value| [key, to_ostruct(value)] }.to_h)
14
+ elsif obj.is_a?(Array)
15
+ obj.map { |o| to_ostruct(o) }
16
+ else
17
+ obj
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module OpConnect
2
+ # Faraday response middleware
3
+ #
4
+ module Response
5
+ # This class raises an OpConnect-flavored exception based on HTTP status
6
+ # codes returned by the API.
7
+ #
8
+ class RaiseError < Faraday::Middleware
9
+ def on_complete(response)
10
+ if (error = OpConnect::Error.from_response(response))
11
+ raise error
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module OpConnect
2
+ module Response
3
+ autoload :RaiseError, "op_connect/response/raise_error"
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ module OpConnect
2
+ class ServerHealth
3
+ class Dependency
4
+ attr_reader :service, :status, :message
5
+
6
+ def initialize(options = {})
7
+ @service = options["service"]
8
+ @status = options["status"]
9
+ @message = options["message"]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module OpConnect
2
+ class ServerHealth
3
+ autoload :Dependency, "op_connect/server_health/dependency"
4
+
5
+ attr_reader :name, :version, :dependencies
6
+
7
+ def initialize(options = {})
8
+ @name = options["name"]
9
+ @version = options["version"]
10
+ @dependencies = options["dependencies"]&.collect! { |dependency| Dependency.new(dependency) } || []
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module OpConnect
2
+ class Vault
3
+ attr_reader :id, :name, :attribute_version, :content_version, :items, :type, :created_at, :updated_at
4
+
5
+ def initialize(options = {})
6
+ @id = options["id"]
7
+ @name = options["name"]
8
+ @attribute_version = options["attributeVersion"]
9
+ @content_version = options["contentVersion"]
10
+ @items = options["items"]
11
+ @type = options["type"]
12
+ @created_at = options["createdAt"]
13
+ @updated_at = options["updatedAt"]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpConnect
4
+ VERSION = "0.1.1"
5
+ end