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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +653 -0
- data/README.md +424 -19
- data/end_point_blank.gemspec +5 -3
- data/lib/end_point_blank/access_tokens.rb +244 -25
- data/lib/end_point_blank/authorization.rb +105 -20
- data/lib/end_point_blank/commands/authentication_cache.rb +141 -19
- data/lib/end_point_blank/commands/basic_authenticate.rb +66 -2
- data/lib/end_point_blank/commands/bearer_generate.rb +36 -0
- data/lib/end_point_blank/commands/endpoint_authorize.rb +46 -1
- data/lib/end_point_blank/commands/endpoint_update.rb +2 -2
- data/lib/end_point_blank/commands/generate_access_token.rb +241 -8
- data/lib/end_point_blank/commands/http.rb +20 -1
- data/lib/end_point_blank/configuration.rb +111 -4
- data/lib/end_point_blank/configuration_error.rb +18 -0
- data/lib/end_point_blank/management/client.rb +228 -0
- data/lib/end_point_blank/management/configuration.rb +56 -0
- data/lib/end_point_blank/management/error.rb +134 -0
- data/lib/end_point_blank/management/error_codes.rb +115 -0
- data/lib/end_point_blank/management/idempotency_key.rb +33 -0
- data/lib/end_point_blank/management/page.rb +55 -0
- data/lib/end_point_blank/management/resources/api_packages.rb +104 -0
- data/lib/end_point_blank/management/resources/applications.rb +167 -0
- data/lib/end_point_blank/management/resources/base.rb +117 -0
- data/lib/end_point_blank/management/resources/clients.rb +152 -0
- data/lib/end_point_blank/management/retry_policy.rb +65 -0
- data/lib/end_point_blank/management/transport.rb +167 -0
- data/lib/end_point_blank/management/url_path.rb +18 -0
- data/lib/end_point_blank/management.rb +52 -0
- data/lib/end_point_blank/rails/authenticated.rb +62 -7
- data/lib/end_point_blank/rails/authorized.rb +9 -13
- data/lib/end_point_blank/target_url.rb +57 -0
- data/lib/end_point_blank/token_unavailable_error.rb +102 -0
- data/lib/end_point_blank/unauthorized_error.rb +81 -1
- data/lib/end_point_blank/version.rb +1 -1
- data/lib/end_point_blank/writers/delayed_writer.rb +131 -21
- data/lib/end_point_blank/writers/direct_writer.rb +1 -1
- data/lib/end_point_blank/writers/exception_writer.rb +11 -2
- data/lib/end_point_blank/writers/log_writer.rb +1 -1
- data/lib/end_point_blank/writers/request_writer.rb +1 -0
- data/lib/end_point_blank/writers/response_writer.rb +1 -0
- data/lib/end_point_blank/writers/shared.rb +35 -4
- data/lib/end_point_blank.rb +240 -2
- metadata +29 -10
- 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
|