end_point_blank 0.6.1 → 0.13.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +653 -0
  3. data/README.md +424 -19
  4. data/end_point_blank.gemspec +5 -3
  5. data/lib/end_point_blank/access_tokens.rb +244 -25
  6. data/lib/end_point_blank/authorization.rb +105 -20
  7. data/lib/end_point_blank/commands/authentication_cache.rb +141 -19
  8. data/lib/end_point_blank/commands/basic_authenticate.rb +66 -2
  9. data/lib/end_point_blank/commands/bearer_generate.rb +36 -0
  10. data/lib/end_point_blank/commands/endpoint_authorize.rb +46 -1
  11. data/lib/end_point_blank/commands/endpoint_update.rb +2 -2
  12. data/lib/end_point_blank/commands/generate_access_token.rb +241 -8
  13. data/lib/end_point_blank/commands/http.rb +20 -1
  14. data/lib/end_point_blank/configuration.rb +111 -4
  15. data/lib/end_point_blank/configuration_error.rb +18 -0
  16. data/lib/end_point_blank/management/client.rb +228 -0
  17. data/lib/end_point_blank/management/configuration.rb +56 -0
  18. data/lib/end_point_blank/management/error.rb +134 -0
  19. data/lib/end_point_blank/management/error_codes.rb +115 -0
  20. data/lib/end_point_blank/management/idempotency_key.rb +33 -0
  21. data/lib/end_point_blank/management/page.rb +55 -0
  22. data/lib/end_point_blank/management/resources/api_packages.rb +104 -0
  23. data/lib/end_point_blank/management/resources/applications.rb +167 -0
  24. data/lib/end_point_blank/management/resources/base.rb +117 -0
  25. data/lib/end_point_blank/management/resources/clients.rb +152 -0
  26. data/lib/end_point_blank/management/retry_policy.rb +65 -0
  27. data/lib/end_point_blank/management/transport.rb +167 -0
  28. data/lib/end_point_blank/management/url_path.rb +18 -0
  29. data/lib/end_point_blank/management.rb +52 -0
  30. data/lib/end_point_blank/rails/authenticated.rb +62 -7
  31. data/lib/end_point_blank/rails/authorized.rb +9 -13
  32. data/lib/end_point_blank/target_url.rb +57 -0
  33. data/lib/end_point_blank/token_unavailable_error.rb +102 -0
  34. data/lib/end_point_blank/unauthorized_error.rb +81 -1
  35. data/lib/end_point_blank/version.rb +1 -1
  36. data/lib/end_point_blank/writers/delayed_writer.rb +131 -21
  37. data/lib/end_point_blank/writers/direct_writer.rb +1 -1
  38. data/lib/end_point_blank/writers/exception_writer.rb +11 -2
  39. data/lib/end_point_blank/writers/log_writer.rb +1 -1
  40. data/lib/end_point_blank/writers/request_writer.rb +1 -0
  41. data/lib/end_point_blank/writers/response_writer.rb +1 -0
  42. data/lib/end_point_blank/writers/shared.rb +35 -4
  43. data/lib/end_point_blank.rb +240 -2
  44. metadata +29 -10
  45. data/lib/end_point_blank/loggers/logger.rb +0 -30
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ module Resources
8
+ # +/api_packages+: your organization's API packages, and what each one
9
+ # publishes (+/api_packages/:id/endpoints+).
10
+ #
11
+ # A package: <tt>{"id", "name", "organization_id", "inserted_at",
12
+ # "updated_at"}</tt>.
13
+ class ApiPackages < Base
14
+ # GET /api_packages. @return [Page]
15
+ def list(limit: nil, after: nil)
16
+ list_page(path("api_packages"), limit, after)
17
+ end
18
+
19
+ # Every API package, across pages. @return [Enumerator, nil]
20
+ def each(limit: nil, &block)
21
+ each_item(path("api_packages"), limit, &block)
22
+ end
23
+
24
+ # GET /api_packages/:id. @return [Hash]
25
+ def get(id)
26
+ get_data(path("api_packages", id))
27
+ end
28
+
29
+ # POST /api_packages. @return [Hash] the new package
30
+ def create(name:, idempotency_key: nil)
31
+ post_data(path("api_packages"), { name: name }, idempotency_key)
32
+ end
33
+
34
+ # PATCH /api_packages/:id (rename). @return [Hash]
35
+ def update(id, name:)
36
+ patch_data(path("api_packages", id), { name: name })
37
+ end
38
+
39
+ # DELETE /api_packages/:id. Refused with +api_package_assigned+ while
40
+ # a client holds it. @return [Hash] <tt>{"id", "deleted"}</tt>
41
+ def delete(id)
42
+ delete_data(path("api_packages", id))
43
+ end
44
+
45
+ # GET /api_packages/:id/endpoints: what the package publishes, one
46
+ # entry per application endpoint (or whole application) and
47
+ # environment. An entry: <tt>{"id", "api_package_id",
48
+ # "application_id", "endpoint_id", "all_endpoints", "endpoint",
49
+ # "environment_id", "inserted_at"}</tt>. @return [Page]
50
+ def list_endpoints(api_package_id, limit: nil, after: nil)
51
+ list_page(path("api_packages", api_package_id, "endpoints"), limit, after)
52
+ end
53
+
54
+ # Every entry of the package, across pages. @return [Enumerator, nil]
55
+ def each_endpoint(api_package_id, limit: nil, &block)
56
+ each_item(path("api_packages", api_package_id, "endpoints"), limit, &block)
57
+ end
58
+
59
+ # POST /api_packages/:id/endpoints. Leave +endpoint_id+ nil to publish
60
+ # every endpoint of the application.
61
+ #
62
+ # Answers the whole body, not only +data+: <tt>{"data" => entry,
63
+ # "warnings" => [...]}</tt>. Each warning (code
64
+ # +assignment_derives_nothing+) names a client assignment of the
65
+ # package that derives no grant.
66
+ #
67
+ # @return [Hash] <tt>{"data", "warnings"}</tt>
68
+ def add_endpoint(api_package_id, application_id:, environment_id:, endpoint_id: nil, idempotency_key: nil)
69
+ body = compact(application_id: application_id, endpoint_id: endpoint_id, environment_id: environment_id)
70
+ @transport.request("POST", path("api_packages", api_package_id, "endpoints"),
71
+ body: body, idempotency_key: idempotency_key)
72
+ end
73
+
74
+ # DELETE /api_packages/:id/endpoints/:access_id. Answers the whole
75
+ # body, as {#add_endpoint} does.
76
+ #
77
+ # @return [Hash] <tt>{"data" => {"id", "deleted"}, "warnings" => [...]}</tt>
78
+ def remove_endpoint(api_package_id, access_id)
79
+ @transport.request("DELETE", path("api_packages", api_package_id, "endpoints", access_id))
80
+ end
81
+ end
82
+
83
+ # +GET /endpoints+: the endpoints of your organization's applications,
84
+ # to find the ids {ApiPackages#add_endpoint} takes.
85
+ #
86
+ # An endpoint: <tt>{"id", "application_id", "path", "action", "public",
87
+ # "inserted_at", "updated_at"}</tt>.
88
+ class Endpoints < Base
89
+ # @param application_id [String, nil] only this application's endpoints
90
+ # @param version [String, nil] only endpoints deployed in an
91
+ # application version of this name
92
+ # @return [Page]
93
+ def list(application_id: nil, version: nil, limit: nil, after: nil)
94
+ list_page(path("endpoints"), limit, after, application_id: application_id, version: version)
95
+ end
96
+
97
+ # Every matching endpoint, across pages. @return [Enumerator, nil]
98
+ def each(application_id: nil, version: nil, limit: nil, &block)
99
+ each_item(path("endpoints"), limit, { application_id: application_id, version: version }, &block)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ module Resources
8
+ # +/applications+, and each application's environments
9
+ # (+/applications/:application_id/environments+, an "application
10
+ # environment": the application placed in an environment at a base URL).
11
+ #
12
+ # An application: <tt>{"id", "name", "public", "organization_group_id",
13
+ # "synced_at", "inserted_at", "updated_at"}</tt>. An application
14
+ # environment: <tt>{"id", "application_id", "environment_id",
15
+ # "base_url", ...}</tt>.
16
+ class Applications < Base
17
+ # @return [Page]
18
+ def list(limit: nil, after: nil)
19
+ list_page(path("applications"), limit, after)
20
+ end
21
+
22
+ # @return [Enumerator, nil]
23
+ def each(limit: nil, &block)
24
+ each_item(path("applications"), limit, &block)
25
+ end
26
+
27
+ # @return [Hash]
28
+ def get(id)
29
+ get_data(path("applications", id))
30
+ end
31
+
32
+ # POST /applications.
33
+ #
34
+ # @param environment_base_urls [Hash] environment id => base URL; at
35
+ # least one is required
36
+ # @return [Hash] the new application
37
+ def create(name:, environment_base_urls:, public: nil, organization_group_id: nil, idempotency_key: nil)
38
+ body = compact(name: name, environment_base_urls: environment_base_urls, public: public,
39
+ organization_group_id: organization_group_id)
40
+ post_data(path("applications"), body, idempotency_key)
41
+ end
42
+
43
+ # PATCH /applications/:id. Only the arguments given are sent.
44
+ # @return [Hash]
45
+ def update(id, name: nil, public: nil)
46
+ patch_data(path("applications", id), compact(name: name, public: public))
47
+ end
48
+
49
+ # DELETE /applications/:id. Refused with +has_dependents+ while grants
50
+ # or credentials depend on it. @return [Hash] <tt>{"id", "deleted"}</tt>
51
+ def delete(id)
52
+ delete_data(path("applications", id))
53
+ end
54
+
55
+ # GET /applications/:application_id/environments. @return [Page]
56
+ def list_environments(application_id, limit: nil, after: nil)
57
+ list_page(path("applications", application_id, "environments"), limit, after)
58
+ end
59
+
60
+ # @return [Enumerator, nil]
61
+ def each_environment(application_id, limit: nil, &block)
62
+ each_item(path("applications", application_id, "environments"), limit, &block)
63
+ end
64
+
65
+ # POST /applications/:application_id/environments: place the
66
+ # application in an environment at +base_url+.
67
+ # @return [Hash] the application environment
68
+ def add_environment(application_id, environment_id:, base_url:, idempotency_key: nil)
69
+ post_data(path("applications", application_id, "environments"),
70
+ { environment_id: environment_id, base_url: base_url }, idempotency_key)
71
+ end
72
+
73
+ # DELETE /applications/:application_id/environments/:id, where +id+ is
74
+ # the application environment's id. @return [Hash] <tt>{"id", "deleted"}</tt>
75
+ def remove_environment(application_id, id)
76
+ delete_data(path("applications", application_id, "environments", id))
77
+ end
78
+ end
79
+
80
+ # +/environments+: deployment environments.
81
+ #
82
+ # An environment: <tt>{"id", "name", "domain", "is_default", ...}</tt>.
83
+ class Environments < Base
84
+ # @return [Page]
85
+ def list(limit: nil, after: nil)
86
+ list_page(path("environments"), limit, after)
87
+ end
88
+
89
+ # @return [Enumerator, nil]
90
+ def each(limit: nil, &block)
91
+ each_item(path("environments"), limit, &block)
92
+ end
93
+
94
+ # @return [Hash]
95
+ def get(id)
96
+ get_data(path("environments", id))
97
+ end
98
+
99
+ # POST /environments. @return [Hash] the new environment
100
+ def create(name:, domain:, is_default: nil, idempotency_key: nil)
101
+ post_data(path("environments"), compact(name: name, domain: domain, is_default: is_default),
102
+ idempotency_key)
103
+ end
104
+
105
+ # PATCH /environments/:id. Only the arguments given are sent. The
106
+ # production environment answers +protected+. @return [Hash]
107
+ def update(id, name: nil, domain: nil, is_default: nil)
108
+ patch_data(path("environments", id), compact(name: name, domain: domain, is_default: is_default))
109
+ end
110
+
111
+ # DELETE /environments/:id. @return [Hash] <tt>{"id", "deleted"}</tt>
112
+ def delete(id)
113
+ delete_data(path("environments", id))
114
+ end
115
+ end
116
+
117
+ # +/credentials+: runtime client credentials (the client_id and
118
+ # client_secret an SDK authenticates with).
119
+ #
120
+ # A credential: <tt>{"id", "client_id", "secret_last_4",
121
+ # "application_environment_id", "application_id", "environment",
122
+ # "previous_secret", "expired_at", "created_at", "updated_at"}</tt>.
123
+ # {#create} and {#rotate} add +client_secret+, the only time the secret
124
+ # is ever returned: store it then. This SDK never logs it.
125
+ class Credentials < Base
126
+ # @param application_environment_id [String, nil] only this
127
+ # application environment's credentials
128
+ # @return [Page] metadata only, no secrets
129
+ def list(application_environment_id: nil, limit: nil, after: nil)
130
+ list_page(path("credentials"), limit, after, application_environment_id: application_environment_id)
131
+ end
132
+
133
+ # @return [Enumerator, nil]
134
+ def each(application_environment_id: nil, limit: nil, &block)
135
+ each_item(path("credentials"), limit, { application_environment_id: application_environment_id }, &block)
136
+ end
137
+
138
+ # @return [Hash] metadata only, no secret
139
+ def get(id)
140
+ get_data(path("credentials", id))
141
+ end
142
+
143
+ # POST /credentials. A retry of a create that succeeded but whose
144
+ # answer was lost raises +idempotency_replay_unavailable+: list the
145
+ # application environment's credentials instead.
146
+ # @return [Hash] the credential, with its one-time +client_secret+
147
+ def create(application_environment_id:, idempotency_key: nil)
148
+ post_data(path("credentials"), { application_environment_id: application_environment_id }, idempotency_key)
149
+ end
150
+
151
+ # POST /credentials/:id/rotate: a new secret; the old one keeps
152
+ # working for the grace window (see +previous_secret+).
153
+ # @return [Hash] the credential, with its new one-time +client_secret+
154
+ def rotate(id, idempotency_key: nil)
155
+ post_data(path("credentials", id, "rotate"), nil, idempotency_key)
156
+ end
157
+
158
+ # DELETE /credentials/:id: revokes the credential on intake, then
159
+ # removes it. @return [Hash] <tt>{"id", "deleted"}</tt>
160
+ def delete(id)
161
+ delete_data(path("credentials", id))
162
+ end
163
+ alias revoke delete
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../page"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ # The management API's resources, each reached from a {Client} (or a
8
+ # {ManagedClient} view) rather than built directly.
9
+ #
10
+ # Every method answers what the API sent, decoded from JSON into Hashes
11
+ # with String keys: a single resource's +data+, a {Page} for a list, and
12
+ # <tt>{"id" => ..., "deleted" => true}</tt> for a delete. Optional keyword
13
+ # arguments left nil are not sent.
14
+ module Resources
15
+ # Shared plumbing: paths, list pages and auto-paging.
16
+ class Base
17
+ MAX_LIMIT = 100
18
+
19
+ def initialize(transport, prefix = "")
20
+ @transport = transport
21
+ @prefix = prefix
22
+ end
23
+
24
+ def inspect
25
+ "#<#{self.class.name} prefix=#{@prefix.inspect}>"
26
+ end
27
+
28
+ # One path segment, percent-escaped. An empty id is refused, so it can
29
+ # never turn a "get one" into a "list all"; so is an id of dots only
30
+ # (".", ".."), a dot-segment that any proxy or URL normalizer may
31
+ # resolve into a different route (DELETE /clients/c1/grants/.. into
32
+ # DELETE /clients/c1). Percent-encoding the dots would not help:
33
+ # normalizers decode %2E first. Real ids are UUIDs.
34
+ def self.escape(segment)
35
+ value = segment.to_s
36
+ raise ArgumentError, "an id must be a non-empty String" if value.empty?
37
+ raise ArgumentError, "an id must not be only dots" if value.each_char.all?(".")
38
+
39
+ value.b.gsub(/[^A-Za-z0-9\-._~]/n) { |char| format("%%%02X", char.ord) }
40
+ end
41
+
42
+ private
43
+
44
+ # "/segment/escaped-id/..." under this resource's prefix. Every
45
+ # argument is one path segment (see {.escape}).
46
+ def path(*segments)
47
+ @prefix + segments.map { |segment| "/#{escape(segment)}" }.join
48
+ end
49
+
50
+ def escape(segment)
51
+ self.class.escape(segment)
52
+ end
53
+
54
+ def get_data(path, query = nil)
55
+ data(@transport.request("GET", path, query: query))
56
+ end
57
+
58
+ def post_data(path, body, idempotency_key)
59
+ data(@transport.request("POST", path, body: body, idempotency_key: idempotency_key))
60
+ end
61
+
62
+ def patch_data(path, body)
63
+ data(@transport.request("PATCH", path, body: body))
64
+ end
65
+
66
+ def delete_data(path)
67
+ data(@transport.request("DELETE", path))
68
+ end
69
+
70
+ def data(body)
71
+ body.is_a?(Hash) && body.key?("data") ? body["data"] : body
72
+ end
73
+
74
+ # One page of a list. +filters+ with a nil value are not sent.
75
+ def list_page(path, limit, after, filters = {})
76
+ check_limit(limit)
77
+ query = compact(filters.merge(limit: limit, after: after))
78
+ Page.from_body(@transport.request("GET", path, query: query))
79
+ end
80
+
81
+ # Every item of a list, fetching pages as it goes. With a block,
82
+ # yields each item and answers nil; without one, answers an
83
+ # Enumerator (lazy: no request is made until it is iterated).
84
+ def each_item(path, limit, filters = {}, &block)
85
+ check_limit(limit)
86
+ enumerator = Enumerator.new { |yielder| each_page_item(yielder, path, limit, filters) }
87
+ return enumerator unless block
88
+
89
+ enumerator.each(&block)
90
+ nil
91
+ end
92
+
93
+ def each_page_item(yielder, path, limit, filters)
94
+ after = nil
95
+ loop do
96
+ page = list_page(path, limit, after, filters)
97
+ page.each { |item| yielder << item }
98
+ break unless page.next_page?
99
+
100
+ after = page.next_cursor
101
+ end
102
+ end
103
+
104
+ def check_limit(limit)
105
+ return if limit.nil?
106
+ return if limit.is_a?(Integer) && limit.between?(1, MAX_LIMIT)
107
+
108
+ raise ArgumentError, "limit must be an Integer from 1 to #{MAX_LIMIT}, got #{limit.inspect}"
109
+ end
110
+
111
+ def compact(hash)
112
+ hash.reject { |_key, value| value.nil? }
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ module Resources
8
+ # +/clients+: the organizations you serve as their provider, as client
9
+ # invites (pending and accepted), and managed clients you run for your
10
+ # customers.
11
+ #
12
+ # A client: <tt>{"id", "name", "status" ("pending"/"accepted"),
13
+ # "invite_code" (write keys, while pending), "accepted_at", "managed",
14
+ # "claimed_at", "client_organization", "inserted_at", "updated_at"}</tt>;
15
+ # {#get} adds +contacts+ and +pre_assignments+.
16
+ class Clients < Base
17
+ # GET /clients. @return [Page]
18
+ def list(limit: nil, after: nil)
19
+ list_page(path("clients"), limit, after)
20
+ end
21
+
22
+ # Every client, across pages. @return [Enumerator, nil]
23
+ def each(limit: nil, &block)
24
+ each_item(path("clients"), limit, &block)
25
+ end
26
+
27
+ # GET /clients/:id. @return [Hash]
28
+ def get(id)
29
+ get_data(path("clients", id))
30
+ end
31
+
32
+ # POST /clients.
33
+ #
34
+ # @param contacts [Array<Hash>, nil] your contacts to share, each
35
+ # <tt>{email:, first_name:, last_name:, title: nil, phone_number: nil}</tt>
36
+ # @param packages [Array<Hash>, nil] API packages to assign when the
37
+ # client accepts, each <tt>{api_package_id:, environment_id:}</tt>
38
+ # @param grants [Array<Hash>, nil] direct grants to make when it
39
+ # accepts, each <tt>{target_application_id:, environment_id:,
40
+ # target_endpoint_id: nil}</tt>
41
+ # @param managed [Boolean, nil] true creates a managed client (no
42
+ # packages or grants with it; assign those afterwards)
43
+ # @return [Hash] the new client; +invite_code+ is what the client
44
+ # accepts with
45
+ # rubocop:disable Metrics/ParameterLists
46
+ def create(name:, contacts: nil, packages: nil, grants: nil, managed: nil, idempotency_key: nil)
47
+ body = compact(name: name, contacts: contacts, packages: packages, grants: grants, managed: managed)
48
+ post_data(path("clients"), body, idempotency_key)
49
+ end
50
+ # rubocop:enable Metrics/ParameterLists
51
+
52
+ # Invites a client: {#create} without +managed+.
53
+ def invite(name:, contacts: nil, packages: nil, grants: nil, idempotency_key: nil)
54
+ create(name: name, contacts: contacts, packages: packages, grants: grants,
55
+ idempotency_key: idempotency_key)
56
+ end
57
+
58
+ # Creates a managed client: {#create} with <tt>managed: true</tt>. Set
59
+ # it up with {Client#for_managed_client}.
60
+ def create_managed(name:, contacts: nil, idempotency_key: nil)
61
+ create(name: name, contacts: contacts, managed: true, idempotency_key: idempotency_key)
62
+ end
63
+
64
+ # DELETE /clients/:id. Refused with +managed_client_has_credentials+
65
+ # for a managed client that still holds credentials.
66
+ # @return [Hash] <tt>{"id", "deleted"}</tt>
67
+ def delete(id)
68
+ delete_data(path("clients", id))
69
+ end
70
+
71
+ # POST /clients/:client_id/claim_invites: emails your customer an
72
+ # invite to claim a managed client.
73
+ # @return [Hash] <tt>{"client_id", "email", "sent_at", "expires_at"}</tt>
74
+ def claim_invite(client_id, email:, idempotency_key: nil)
75
+ post_data(path("clients", client_id, "claim_invites"), { email: email }, idempotency_key)
76
+ end
77
+ end
78
+
79
+ # +/clients/:client_id/packages+: the API packages a client holds from
80
+ # you, or (on a pending invite) is set up to get when it accepts.
81
+ #
82
+ # An assignment: <tt>{"id", "api_package_id", "api_package_name",
83
+ # "environment_id", "status" ("active"/"pending"/"refused"), "refusal",
84
+ # "inserted_at", "updated_at"}</tt>.
85
+ class PackageAssignments < Base
86
+ # @return [Page]
87
+ def list(client_id, limit: nil, after: nil)
88
+ list_page(path("clients", client_id, "packages"), limit, after)
89
+ end
90
+
91
+ # @return [Enumerator, nil]
92
+ def each(client_id, limit: nil, &block)
93
+ each_item(path("clients", client_id, "packages"), limit, &block)
94
+ end
95
+
96
+ # POST /clients/:client_id/packages. Refused with e.g.
97
+ # +nothing_published_in_environment+ or +already_assigned+.
98
+ # @return [Hash] the assignment
99
+ def create(client_id, api_package_id:, environment_id:, idempotency_key: nil)
100
+ post_data(path("clients", client_id, "packages"),
101
+ { api_package_id: api_package_id, environment_id: environment_id }, idempotency_key)
102
+ end
103
+ alias assign create
104
+
105
+ # PATCH /clients/:client_id/packages/:id: move the assignment to
106
+ # another environment. @return [Hash]
107
+ def update(client_id, id, environment_id:)
108
+ patch_data(path("clients", client_id, "packages", id), { environment_id: environment_id })
109
+ end
110
+
111
+ # DELETE /clients/:client_id/packages/:id. @return [Hash] <tt>{"id", "deleted"}</tt>
112
+ def delete(client_id, id)
113
+ delete_data(path("clients", client_id, "packages", id))
114
+ end
115
+ alias unassign delete
116
+ end
117
+
118
+ # +/clients/:client_id/grants+: the grants a client holds directly from
119
+ # you (package-derived grants are managed through {PackageAssignments}).
120
+ #
121
+ # A grant: <tt>{"id", "client_organization_id", "target_application_id",
122
+ # "target_endpoint_id", "all_endpoints", "environment_id",
123
+ # "also_granted_by_api_package_ids", "synced_at", "status", "refusal",
124
+ # "inserted_at", "updated_at"}</tt>.
125
+ class Grants < Base
126
+ # @return [Page]
127
+ def list(client_id, limit: nil, after: nil)
128
+ list_page(path("clients", client_id, "grants"), limit, after)
129
+ end
130
+
131
+ # @return [Enumerator, nil]
132
+ def each(client_id, limit: nil, &block)
133
+ each_item(path("clients", client_id, "grants"), limit, &block)
134
+ end
135
+
136
+ # POST /clients/:client_id/grants. Leave +target_endpoint_id+ nil to
137
+ # grant every endpoint of the application. @return [Hash] the grant
138
+ def create(client_id, target_application_id:, environment_id:, target_endpoint_id: nil, idempotency_key: nil)
139
+ body = compact(target_application_id: target_application_id, target_endpoint_id: target_endpoint_id,
140
+ environment_id: environment_id)
141
+ post_data(path("clients", client_id, "grants"), body, idempotency_key)
142
+ end
143
+
144
+ # DELETE /clients/:client_id/grants/:id. @return [Hash] <tt>{"id", "deleted"}</tt>
145
+ def delete(client_id, id)
146
+ delete_data(path("clients", client_id, "grants", id))
147
+ end
148
+ alias revoke delete
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module EndPointBlank
6
+ module Management
7
+ # Decides whether a failed management API request is sent again, and
8
+ # after how long. See {Transport} for the rules.
9
+ class RetryPolicy
10
+ RETRYABLE_STATUSES = [500, 502, 503, 504].freeze
11
+
12
+ # GET and DELETE are idempotent; POST always carries an Idempotency-Key.
13
+ # PATCH is neither, so a 5xx or a lost answer to one is never re-sent.
14
+ RETRYABLE_METHODS = %w[GET DELETE POST].freeze
15
+
16
+ # Wait before retrying a 409 in progress that has no Retry-After header.
17
+ IN_PROGRESS_WAIT = 1
18
+ # Wait before retrying a 429 that has no Retry-After header (app_portal
19
+ # always sends one; a proxy's 429 may not).
20
+ RATE_LIMITED_WAIT = 1
21
+ # First backoff for a 5xx or a request with no answer; doubles per attempt.
22
+ BACKOFF_BASE = 0.5
23
+
24
+ attr_reader :max_retries, :max_retry_wait
25
+
26
+ def initialize(max_retries:, max_retry_wait:)
27
+ @max_retries = max_retries
28
+ @max_retry_wait = max_retry_wait
29
+ end
30
+
31
+ # Seconds to wait before attempt number +attempt+ + 1 (0 is the first
32
+ # retry), or nil when +error+ must be raised now.
33
+ def delay(method, error, attempt)
34
+ return nil if attempt >= max_retries
35
+
36
+ seconds = wanted_wait(method, error, attempt)
37
+ # A wait longer than max_retry_wait is not waited out.
38
+ seconds && seconds <= max_retry_wait ? seconds : nil
39
+ end
40
+
41
+ private
42
+
43
+ def wanted_wait(method, error, attempt)
44
+ return error.retry_after || RATE_LIMITED_WAIT if error.status == 429
45
+ return in_progress_wait(method, error) if error.code?(ErrorCodes::IDEMPOTENCY_REQUEST_IN_PROGRESS)
46
+ return nil unless server_side_failure?(error) && RETRYABLE_METHODS.include?(method)
47
+
48
+ error.retry_after || backoff(attempt)
49
+ end
50
+
51
+ # Only a POST carries the key the first request is still running under.
52
+ def in_progress_wait(method, error)
53
+ method == "POST" ? error.retry_after || IN_PROGRESS_WAIT : nil
54
+ end
55
+
56
+ def server_side_failure?(error)
57
+ error.code?(ErrorCodes::CONNECTION_ERROR) || RETRYABLE_STATUSES.include?(error.status)
58
+ end
59
+
60
+ def backoff(attempt)
61
+ [BACKOFF_BASE * (2**attempt), max_retry_wait].min
62
+ end
63
+ end
64
+ end
65
+ end