globus_client 0.3.1 → 0.4.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/.autoupdate/postupdate +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +5 -5
- data/api_test.rb +8 -8
- data/globus_client.gemspec +2 -2
- data/lib/globus_client/authenticator.rb +41 -0
- data/lib/globus_client/endpoint.rb +160 -0
- data/lib/globus_client/identity.rb +53 -0
- data/lib/globus_client/unexpected_response.rb +48 -0
- data/lib/globus_client/version.rb +5 -0
- data/lib/globus_client.rb +73 -1
- metadata +6 -8
- data/lib/globus/client/authenticator.rb +0 -43
- data/lib/globus/client/endpoint.rb +0 -162
- data/lib/globus/client/identity.rb +0 -55
- data/lib/globus/client/unexpected_response.rb +0 -50
- data/lib/globus/client/version.rb +0 -7
- data/lib/globus/client.rb +0 -78
- data/sig/globus_client.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a9de93796688ebf5d55d620e0cb290ed7e6b86f67ed1212e7847281ea5ac44d
|
4
|
+
data.tar.gz: b298b6a3744471cb4199e9c9f4ae2c69ece63dd9a3898dc058844d6bad22279c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad0fe7f847270e9534458577dda9a75dad2e83298f0d8232ee8ccd7eca6e127a7290db944ac4dac31cad2df7b42f3e2b7ecc4f273a75059c1c39de2cdf820b09
|
7
|
+
data.tar.gz: 3488ec985fee144991bbe90c29345a7717cae4e371a843bdb0947a6042128391bf7442e522c03494340acfb1ce31b61dc23ca7a30f05bedc667fc459bcfeaea8
|
data/.autoupdate/postupdate
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# This script is called by our weekly dependency update job in Jenkins after updating Ruby and other deps
|
4
4
|
|
5
|
-
# Switch to Ruby 3.1 for
|
5
|
+
# Switch to Ruby 3.1 for GlobusClient (3.0 is default in Jenkinsfile)
|
6
6
|
rvm use 3.1.2@globus_client --create &&
|
7
7
|
gem install bundler &&
|
8
8
|
bundle install --gemfile Gemfile
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
[](https://codeclimate.com/github/sul-dlss/globus_client)
|
4
4
|
[](https://codeclimate.com/github/sul-dlss/globus_client/test_coverage)
|
5
5
|
|
6
|
-
#
|
6
|
+
# GlobusClient
|
7
7
|
|
8
|
-
|
8
|
+
GlobusClient is a Ruby gem that acts as a client to the RESTful HTTP APIs provided by the [Globus service](https://docs.globus.org/api/).
|
9
9
|
|
10
10
|
## Installation
|
11
11
|
|
@@ -25,7 +25,7 @@ For one-off requests:
|
|
25
25
|
require 'globus_client'
|
26
26
|
|
27
27
|
# NOTE: The settings below live in the consumer, not in the gem.
|
28
|
-
client =
|
28
|
+
client = GlobusClient.configure(
|
29
29
|
client_id: Settings.globus.client_id,
|
30
30
|
client_secret: Settings.globus.client_secret,
|
31
31
|
uploads_directory: Settings.globus.uploads_directory,
|
@@ -43,7 +43,7 @@ to be sure configuration has already occurred, e.g.:
|
|
43
43
|
|
44
44
|
```ruby
|
45
45
|
# config/initializers/globus_client.rb
|
46
|
-
|
46
|
+
GlobusClient.configure(
|
47
47
|
client_id: Settings.globus.client_id,
|
48
48
|
client_secret: Settings.globus.client_secret,
|
49
49
|
uploads_directory: Settings.globus.uploads_directory,
|
@@ -53,7 +53,7 @@ Globus::Client.configure(
|
|
53
53
|
# app/services/my_globus_service.rb
|
54
54
|
# ...
|
55
55
|
def create_user_directory
|
56
|
-
|
56
|
+
GlobusClient.mkdir(user_id: 'mjgiarlo', work_id: 1234, work_version: 1)
|
57
57
|
end
|
58
58
|
# ...
|
59
59
|
```
|
data/api_test.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
require "bundler/setup"
|
5
5
|
require "globus_client"
|
6
6
|
|
7
|
-
|
7
|
+
GlobusClient.configure(
|
8
8
|
client_id: ENV["GLOBUS_CLIENT_ID"],
|
9
9
|
client_secret: ENV["GLOBUS_CLIENT_SECRET"],
|
10
10
|
uploads_directory: ENV["GLOBUS_UPLOADS_DIRECTORY"],
|
@@ -14,21 +14,21 @@ Globus::Client.configure(
|
|
14
14
|
user_id, work_id, work_version = *ARGV
|
15
15
|
|
16
16
|
# Test public API methods here.
|
17
|
-
|
17
|
+
GlobusClient.mkdir(user_id:, work_id:, work_version:)
|
18
18
|
|
19
|
-
user_exists =
|
19
|
+
user_exists = GlobusClient.user_exists?(user_id)
|
20
20
|
|
21
21
|
# Not part of the public API but this allows us to test access changes
|
22
|
-
before_permissions =
|
22
|
+
before_permissions = GlobusClient::Endpoint.new(GlobusClient.config, user_id: user_id, work_id: work_id, work_version: work_version).send(:access_rule)["permissions"]
|
23
23
|
|
24
|
-
files_count =
|
24
|
+
files_count = GlobusClient.file_count(user_id:, work_id:, work_version:)
|
25
25
|
|
26
|
-
total_size =
|
26
|
+
total_size = GlobusClient.total_size(user_id:, work_id:, work_version:)
|
27
27
|
|
28
|
-
|
28
|
+
GlobusClient.disallow_writes(user_id:, work_id:, work_version:)
|
29
29
|
|
30
30
|
# Not part of the public API but this allows us to test access changes
|
31
|
-
after_permissions =
|
31
|
+
after_permissions = GlobusClient::Endpoint.new(GlobusClient.config, user_id: user_id, work_id: work_id, work_version: work_version).send(:access_rule)["permissions"]
|
32
32
|
|
33
33
|
puts "User #{user_id} exists: #{user_exists}"
|
34
34
|
puts "Initial directory permissions: #{before_permissions}"
|
data/globus_client.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
lib = File.expand_path("lib", __dir__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require "
|
5
|
+
require "globus_client/version"
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = "globus_client"
|
9
|
-
spec.version =
|
9
|
+
spec.version = GlobusClient::VERSION
|
10
10
|
spec.authors = ["Aaron Collier", "Laura Wrubel", "Mike Giarlo"]
|
11
11
|
spec.email = ["aaron.collier@stanford.edu", "lwrubel@stanford.edu", "mjgiarlo@stanford.edu"]
|
12
12
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class GlobusClient
|
4
|
+
# The namespace for the "login" command
|
5
|
+
class Authenticator
|
6
|
+
def self.token(client_id, client_secret, auth_url)
|
7
|
+
new(client_id, client_secret, auth_url).token
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(client_id, client_secret, auth_url)
|
11
|
+
@client_id = client_id
|
12
|
+
@client_secret = client_secret
|
13
|
+
@auth_url = auth_url
|
14
|
+
end
|
15
|
+
|
16
|
+
# Request an access_token
|
17
|
+
def token
|
18
|
+
response = connection.post("/v2/oauth2/token", form_data)
|
19
|
+
|
20
|
+
JSON.parse(response.body)["access_token"]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :client_id, :client_secret, :auth_url
|
26
|
+
|
27
|
+
def connection
|
28
|
+
Faraday.new(url: auth_url)
|
29
|
+
end
|
30
|
+
|
31
|
+
def form_data
|
32
|
+
{
|
33
|
+
client_id:,
|
34
|
+
client_secret:,
|
35
|
+
encoding: "form",
|
36
|
+
grant_type: "client_credentials",
|
37
|
+
scope: "urn:globus:auth:scope:transfer.api.globus.org:all"
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class GlobusClient
|
4
|
+
# The namespace for endpoint API operations
|
5
|
+
class Endpoint
|
6
|
+
# @param config [#token, #uploads_directory, #transfer_endpoint_id, #transfer_url, #auth_url] configuration for the gem
|
7
|
+
# @param user_id [String] conventionally, we use the SUNet ID, not an email address
|
8
|
+
# @param work_id [#to_s] the identifier of the work (e.g., an H2 work)
|
9
|
+
# @param work_version [#to_s] the version of the work (e.g., an H2 version)
|
10
|
+
def initialize(config, user_id:, work_id:, work_version:)
|
11
|
+
@config = config
|
12
|
+
@user_id = user_id
|
13
|
+
@work_id = work_id
|
14
|
+
@work_version = work_version
|
15
|
+
end
|
16
|
+
|
17
|
+
def file_count
|
18
|
+
objects["total"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def total_size
|
22
|
+
files.sum { |file| file["size"] }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create a directory https://docs.globus.org/api/transfer/file_operations/#make_directory
|
26
|
+
def mkdir
|
27
|
+
# transfer API does not support recursive directory creation
|
28
|
+
paths.each do |path|
|
29
|
+
response = connection.post("#{transfer_path}/mkdir") do |req|
|
30
|
+
req.headers["Content-Type"] = "application/json"
|
31
|
+
req.body = {
|
32
|
+
DATA_TYPE: "mkdir",
|
33
|
+
path:
|
34
|
+
}.to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
next if response.success?
|
38
|
+
|
39
|
+
# Ignore error if directory already exists
|
40
|
+
if response.status == 502
|
41
|
+
error = JSON.parse(response.body)
|
42
|
+
next if error["code"] == "ExternalError.MkdirFailed.Exists"
|
43
|
+
end
|
44
|
+
|
45
|
+
UnexpectedResponse.call(response)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Assign a user read/write permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
|
50
|
+
def allow_writes
|
51
|
+
access_request(permissions: "rw")
|
52
|
+
end
|
53
|
+
|
54
|
+
# Assign a user read-only permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
|
55
|
+
def disallow_writes
|
56
|
+
access_request(permissions: "r")
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :config, :user_id, :work_id, :work_version
|
62
|
+
|
63
|
+
def connection
|
64
|
+
# Transfer API connection
|
65
|
+
Faraday.new(
|
66
|
+
url: config.transfer_url,
|
67
|
+
headers: {Authorization: "Bearer #{config.token}"}
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def user
|
72
|
+
Identity.new(config).get_identity_id(user_id)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Builds up a path from a list of path elements. E.g., input would look like:
|
76
|
+
# ["mjgiarlo", "work123", "version1"]
|
77
|
+
# And this method returns:
|
78
|
+
# ["/uploads/mjgiarlo/", "/uploads/mjgiarlo/work123/", "/uploads/mjgiarlo/work123/version1/"]
|
79
|
+
def paths
|
80
|
+
path_segments.map.with_index do |_segment, index|
|
81
|
+
File.join(config.uploads_directory, path_segments.slice(..index)).concat("/")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @see #paths
|
86
|
+
def full_path
|
87
|
+
paths.last
|
88
|
+
end
|
89
|
+
|
90
|
+
def path_segments
|
91
|
+
[user_id, "work#{work_id}", "version#{work_version}"]
|
92
|
+
end
|
93
|
+
|
94
|
+
def objects
|
95
|
+
# List files at an endpoint https://docs.globus.org/api/transfer/file_operations/#list_directory_contents
|
96
|
+
response = connection.get("#{transfer_path}/ls?path=#{full_path}")
|
97
|
+
return JSON.parse(response.body) if response.success?
|
98
|
+
|
99
|
+
UnexpectedResponse.call(response)
|
100
|
+
end
|
101
|
+
|
102
|
+
def files
|
103
|
+
objects["DATA"].select { |object| object["DATA_TYPE"] == "file" }
|
104
|
+
end
|
105
|
+
|
106
|
+
def access_request(permissions:)
|
107
|
+
response = if access_rule_id
|
108
|
+
connection.put("#{access_path}/#{access_rule_id}") do |req|
|
109
|
+
req.body = {
|
110
|
+
DATA_TYPE: "access",
|
111
|
+
permissions:
|
112
|
+
}.to_json
|
113
|
+
req.headers["Content-Type"] = "application/json"
|
114
|
+
end
|
115
|
+
else
|
116
|
+
connection.post(access_path) do |req|
|
117
|
+
req.body = {
|
118
|
+
DATA_TYPE: "access",
|
119
|
+
principal_type: "identity",
|
120
|
+
principal: user,
|
121
|
+
path: full_path,
|
122
|
+
permissions:,
|
123
|
+
notify_email: "#{user_id}@stanford.edu"
|
124
|
+
}.to_json
|
125
|
+
req.headers["Content-Type"] = "application/json"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
return response if response.success?
|
130
|
+
|
131
|
+
UnexpectedResponse.call(response)
|
132
|
+
end
|
133
|
+
|
134
|
+
def access_rule
|
135
|
+
response = connection.get(access_list_path) do |req|
|
136
|
+
req.headers["Content-Type"] = "application/json"
|
137
|
+
end
|
138
|
+
|
139
|
+
JSON
|
140
|
+
.parse(response.body)["DATA"]
|
141
|
+
.find { |acl| acl["path"] == full_path }
|
142
|
+
end
|
143
|
+
|
144
|
+
def access_rule_id
|
145
|
+
access_rule&.fetch("id")
|
146
|
+
end
|
147
|
+
|
148
|
+
def transfer_path
|
149
|
+
"/v0.10/operation/endpoint/#{config.transfer_endpoint_id}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def access_path
|
153
|
+
"/v0.10/endpoint/#{config.transfer_endpoint_id}/access"
|
154
|
+
end
|
155
|
+
|
156
|
+
def access_list_path
|
157
|
+
"/v0.10/endpoint/#{config.transfer_endpoint_id}/access_list"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class GlobusClient
|
4
|
+
# Lookup of a Globus identity ID
|
5
|
+
class Identity
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_identity_id(sunetid)
|
11
|
+
@email = "#{sunetid}@stanford.edu"
|
12
|
+
|
13
|
+
response = lookup_identity
|
14
|
+
UnexpectedResponse.call(response) unless response.success?
|
15
|
+
|
16
|
+
data = JSON.parse(response.body)
|
17
|
+
extract_id(data)
|
18
|
+
end
|
19
|
+
|
20
|
+
def exists?(sunetid)
|
21
|
+
get_identity_id(sunetid)
|
22
|
+
true
|
23
|
+
rescue
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :config
|
30
|
+
|
31
|
+
def connection
|
32
|
+
Faraday.new(url: config.auth_url)
|
33
|
+
end
|
34
|
+
|
35
|
+
def lookup_identity
|
36
|
+
id_endpoint = "/v2/api/identities"
|
37
|
+
connection.get(id_endpoint) do |req|
|
38
|
+
req.params["usernames"] = @email
|
39
|
+
req.headers["Authorization"] = "Bearer #{config.token}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_id(data)
|
44
|
+
identities = data["identities"]
|
45
|
+
# Select identity with "used" or "private" status
|
46
|
+
matching_users = identities.select { |id| id["username"] == @email }
|
47
|
+
active_users = matching_users.select { |user| (user["status"] == "used" || user["status"] == "private") }
|
48
|
+
raise "No matching active Globus user found for #{@email}." if active_users.empty?
|
49
|
+
|
50
|
+
active_users.first["id"]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class GlobusClient
|
4
|
+
# Handles unexpected responses when communicating with Globus
|
5
|
+
class UnexpectedResponse
|
6
|
+
# Error raised when the Globus Auth or Transfer API returns a 400 error
|
7
|
+
class BadRequestError < StandardError; end
|
8
|
+
|
9
|
+
# Error raised by the Globus Auth API returns a 401 Unauthorized
|
10
|
+
class UnauthorizedError < StandardError; end
|
11
|
+
|
12
|
+
# Error raised when the Globus Auth or Transfer API returns a 403 Forbidden
|
13
|
+
class ForbiddenError < StandardError; end
|
14
|
+
|
15
|
+
# Error raised when the Globus Auth or Transfer API returns a 404 NotFound
|
16
|
+
class ResourceNotFound < StandardError; end
|
17
|
+
|
18
|
+
# Error raised when the Globus Transfer API returns a 502 Bad Gateway
|
19
|
+
class EndpointError < StandardError; end
|
20
|
+
|
21
|
+
# Error raised when the remote server returns a 503 Bad Gateway
|
22
|
+
class ServiceUnavailable < StandardError; end
|
23
|
+
|
24
|
+
# @param [Faraday::Response] response
|
25
|
+
# https://docs.globus.org/api/transfer/file_operations/#common_errors
|
26
|
+
# https://docs.globus.org/api/transfer/file_operations/#errors
|
27
|
+
# https://docs.globus.org/api/transfer/acl/#common_errors
|
28
|
+
# https://docs.globus.org/api/auth/reference/
|
29
|
+
def self.call(response)
|
30
|
+
case response.status
|
31
|
+
when 400
|
32
|
+
raise BadRequestError, "Invalid path or another error with the request: #{response.body}"
|
33
|
+
when 401
|
34
|
+
raise UnauthorizedError, "There was a problem with the access token: #{response.body} "
|
35
|
+
when 403
|
36
|
+
raise ForbiddenError, "The operation requires privileges which the client does not have: #{response.body}"
|
37
|
+
when 404
|
38
|
+
raise ResourceNotFound, "Endpoint ID not found or resource does not exist: #{response.body}"
|
39
|
+
when 502
|
40
|
+
raise EndpointError, "Other error with endpoint: #{response.body}"
|
41
|
+
when 503
|
42
|
+
raise ServiceUnavailable, "The service is down for maintenance."
|
43
|
+
else
|
44
|
+
raise StandardError, "Unexpected response: #{response.status} #{response.body}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/globus_client.rb
CHANGED
@@ -1 +1,73 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module/delegation"
|
4
|
+
require "faraday"
|
5
|
+
require "ostruct"
|
6
|
+
require "singleton"
|
7
|
+
require "zeitwerk"
|
8
|
+
|
9
|
+
# Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
|
10
|
+
Zeitwerk::Loader.for_gem.setup
|
11
|
+
|
12
|
+
# Client for interacting with the Globus API
|
13
|
+
class GlobusClient
|
14
|
+
include Singleton
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# @param client_id [String] the client identifier registered with Globus
|
18
|
+
# @param client_secret [String] the client secret to authenticate with Globus
|
19
|
+
# @param uploads_directory [String] where to upload files
|
20
|
+
# @param transfer_endpoint_id [String] the transfer API endpoint ID supplied by Globus
|
21
|
+
# @param transfer_url [String] the transfer API URL
|
22
|
+
# @param auth_url [String] the authentication API URL
|
23
|
+
def configure(client_id:, client_secret:, uploads_directory:, transfer_endpoint_id:, transfer_url: default_transfer_url, auth_url: default_auth_url)
|
24
|
+
instance.config = OpenStruct.new(
|
25
|
+
token: Authenticator.token(client_id, client_secret, auth_url),
|
26
|
+
uploads_directory:,
|
27
|
+
transfer_endpoint_id:,
|
28
|
+
transfer_url:,
|
29
|
+
auth_url:
|
30
|
+
)
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
delegate :config, :disallow_writes, :file_count, :mkdir, :total_size, :user_exists?, to: :instance
|
36
|
+
|
37
|
+
def default_transfer_url
|
38
|
+
"https://transfer.api.globusonline.org"
|
39
|
+
end
|
40
|
+
|
41
|
+
def default_auth_url
|
42
|
+
"https://auth.globus.org"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_accessor :config
|
47
|
+
|
48
|
+
def mkdir(...)
|
49
|
+
endpoint = Endpoint.new(config, ...)
|
50
|
+
endpoint.mkdir
|
51
|
+
endpoint.allow_writes
|
52
|
+
end
|
53
|
+
|
54
|
+
def disallow_writes(...)
|
55
|
+
endpoint = Endpoint.new(config, ...)
|
56
|
+
endpoint.disallow_writes
|
57
|
+
end
|
58
|
+
|
59
|
+
def file_count(...)
|
60
|
+
endpoint = Endpoint.new(config, ...)
|
61
|
+
endpoint.file_count
|
62
|
+
end
|
63
|
+
|
64
|
+
def total_size(...)
|
65
|
+
endpoint = Endpoint.new(config, ...)
|
66
|
+
endpoint.total_size
|
67
|
+
end
|
68
|
+
|
69
|
+
def user_exists?(...)
|
70
|
+
identity = Identity.new(config)
|
71
|
+
identity.exists?(...)
|
72
|
+
end
|
73
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: globus_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Collier
|
@@ -166,14 +166,12 @@ files:
|
|
166
166
|
- Rakefile
|
167
167
|
- api_test.rb
|
168
168
|
- globus_client.gemspec
|
169
|
-
- lib/globus/client.rb
|
170
|
-
- lib/globus/client/authenticator.rb
|
171
|
-
- lib/globus/client/endpoint.rb
|
172
|
-
- lib/globus/client/identity.rb
|
173
|
-
- lib/globus/client/unexpected_response.rb
|
174
|
-
- lib/globus/client/version.rb
|
175
169
|
- lib/globus_client.rb
|
176
|
-
-
|
170
|
+
- lib/globus_client/authenticator.rb
|
171
|
+
- lib/globus_client/endpoint.rb
|
172
|
+
- lib/globus_client/identity.rb
|
173
|
+
- lib/globus_client/unexpected_response.rb
|
174
|
+
- lib/globus_client/version.rb
|
177
175
|
homepage: https://github.com/sul-dlss/globus_client
|
178
176
|
licenses: []
|
179
177
|
metadata:
|
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Globus
|
4
|
-
class Client
|
5
|
-
# The namespace for the "login" command
|
6
|
-
class Authenticator
|
7
|
-
def self.token(client_id, client_secret, auth_url)
|
8
|
-
new(client_id, client_secret, auth_url).token
|
9
|
-
end
|
10
|
-
|
11
|
-
def initialize(client_id, client_secret, auth_url)
|
12
|
-
@client_id = client_id
|
13
|
-
@client_secret = client_secret
|
14
|
-
@auth_url = auth_url
|
15
|
-
end
|
16
|
-
|
17
|
-
# Request an access_token
|
18
|
-
def token
|
19
|
-
response = connection.post("/v2/oauth2/token", form_data)
|
20
|
-
|
21
|
-
JSON.parse(response.body)["access_token"]
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
attr_reader :client_id, :client_secret, :auth_url
|
27
|
-
|
28
|
-
def connection
|
29
|
-
Faraday.new(url: auth_url)
|
30
|
-
end
|
31
|
-
|
32
|
-
def form_data
|
33
|
-
{
|
34
|
-
client_id:,
|
35
|
-
client_secret:,
|
36
|
-
encoding: "form",
|
37
|
-
grant_type: "client_credentials",
|
38
|
-
scope: "urn:globus:auth:scope:transfer.api.globus.org:all"
|
39
|
-
}
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -1,162 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Globus
|
4
|
-
class Client
|
5
|
-
# The namespace for endpoint API operations
|
6
|
-
class Endpoint
|
7
|
-
# @param config [#token, #uploads_directory, #transfer_endpoint_id, #transfer_url, #auth_url] configuration for the gem
|
8
|
-
# @param user_id [String] conventionally, we use the SUNet ID, not an email address
|
9
|
-
# @param work_id [#to_s] the identifier of the work (e.g., an H2 work)
|
10
|
-
# @param work_version [#to_s] the version of the work (e.g., an H2 version)
|
11
|
-
def initialize(config, user_id:, work_id:, work_version:)
|
12
|
-
@config = config
|
13
|
-
@user_id = user_id
|
14
|
-
@work_id = work_id
|
15
|
-
@work_version = work_version
|
16
|
-
end
|
17
|
-
|
18
|
-
def file_count
|
19
|
-
objects["total"]
|
20
|
-
end
|
21
|
-
|
22
|
-
def total_size
|
23
|
-
files.sum { |file| file["size"] }
|
24
|
-
end
|
25
|
-
|
26
|
-
# Create a directory https://docs.globus.org/api/transfer/file_operations/#make_directory
|
27
|
-
def mkdir
|
28
|
-
# transfer API does not support recursive directory creation
|
29
|
-
paths.each do |path|
|
30
|
-
response = connection.post("#{transfer_path}/mkdir") do |req|
|
31
|
-
req.headers["Content-Type"] = "application/json"
|
32
|
-
req.body = {
|
33
|
-
DATA_TYPE: "mkdir",
|
34
|
-
path:
|
35
|
-
}.to_json
|
36
|
-
end
|
37
|
-
|
38
|
-
next if response.success?
|
39
|
-
|
40
|
-
# Ignore error if directory already exists
|
41
|
-
if response.status == 502
|
42
|
-
error = JSON.parse(response.body)
|
43
|
-
next if error["code"] == "ExternalError.MkdirFailed.Exists"
|
44
|
-
end
|
45
|
-
|
46
|
-
UnexpectedResponse.call(response)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Assign a user read/write permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
|
51
|
-
def allow_writes
|
52
|
-
access_request(permissions: "rw")
|
53
|
-
end
|
54
|
-
|
55
|
-
# Assign a user read-only permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
|
56
|
-
def disallow_writes
|
57
|
-
access_request(permissions: "r")
|
58
|
-
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
attr_reader :config, :user_id, :work_id, :work_version
|
63
|
-
|
64
|
-
def connection
|
65
|
-
# Transfer API connection
|
66
|
-
Faraday.new(
|
67
|
-
url: config.transfer_url,
|
68
|
-
headers: {Authorization: "Bearer #{config.token}"}
|
69
|
-
)
|
70
|
-
end
|
71
|
-
|
72
|
-
def user
|
73
|
-
Identity.new(config).get_identity_id(user_id)
|
74
|
-
end
|
75
|
-
|
76
|
-
# Builds up a path from a list of path elements. E.g., input would look like:
|
77
|
-
# ["mjgiarlo", "work123", "version1"]
|
78
|
-
# And this method returns:
|
79
|
-
# ["/uploads/mjgiarlo/", "/uploads/mjgiarlo/work123/", "/uploads/mjgiarlo/work123/version1/"]
|
80
|
-
def paths
|
81
|
-
path_segments.map.with_index do |_segment, index|
|
82
|
-
File.join(config.uploads_directory, path_segments.slice(..index)).concat("/")
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# @see #paths
|
87
|
-
def full_path
|
88
|
-
paths.last
|
89
|
-
end
|
90
|
-
|
91
|
-
def path_segments
|
92
|
-
[user_id, "work#{work_id}", "version#{work_version}"]
|
93
|
-
end
|
94
|
-
|
95
|
-
def objects
|
96
|
-
# List files at an endpoint https://docs.globus.org/api/transfer/file_operations/#list_directory_contents
|
97
|
-
response = connection.get("#{transfer_path}/ls?path=#{full_path}")
|
98
|
-
return JSON.parse(response.body) if response.success?
|
99
|
-
|
100
|
-
UnexpectedResponse.call(response)
|
101
|
-
end
|
102
|
-
|
103
|
-
def files
|
104
|
-
objects["DATA"].select { |object| object["DATA_TYPE"] == "file" }
|
105
|
-
end
|
106
|
-
|
107
|
-
def access_request(permissions:)
|
108
|
-
response = if access_rule_id
|
109
|
-
connection.put("#{access_path}/#{access_rule_id}") do |req|
|
110
|
-
req.body = {
|
111
|
-
DATA_TYPE: "access",
|
112
|
-
permissions:
|
113
|
-
}.to_json
|
114
|
-
req.headers["Content-Type"] = "application/json"
|
115
|
-
end
|
116
|
-
else
|
117
|
-
connection.post(access_path) do |req|
|
118
|
-
req.body = {
|
119
|
-
DATA_TYPE: "access",
|
120
|
-
principal_type: "identity",
|
121
|
-
principal: user,
|
122
|
-
path: full_path,
|
123
|
-
permissions:,
|
124
|
-
notify_email: "#{user_id}@stanford.edu"
|
125
|
-
}.to_json
|
126
|
-
req.headers["Content-Type"] = "application/json"
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
return response if response.success?
|
131
|
-
|
132
|
-
UnexpectedResponse.call(response)
|
133
|
-
end
|
134
|
-
|
135
|
-
def access_rule
|
136
|
-
response = connection.get(access_list_path) do |req|
|
137
|
-
req.headers["Content-Type"] = "application/json"
|
138
|
-
end
|
139
|
-
|
140
|
-
JSON
|
141
|
-
.parse(response.body)["DATA"]
|
142
|
-
.find { |acl| acl["path"] == full_path }
|
143
|
-
end
|
144
|
-
|
145
|
-
def access_rule_id
|
146
|
-
access_rule&.fetch("id")
|
147
|
-
end
|
148
|
-
|
149
|
-
def transfer_path
|
150
|
-
"/v0.10/operation/endpoint/#{config.transfer_endpoint_id}"
|
151
|
-
end
|
152
|
-
|
153
|
-
def access_path
|
154
|
-
"/v0.10/endpoint/#{config.transfer_endpoint_id}/access"
|
155
|
-
end
|
156
|
-
|
157
|
-
def access_list_path
|
158
|
-
"/v0.10/endpoint/#{config.transfer_endpoint_id}/access_list"
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
@@ -1,55 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Globus
|
4
|
-
class Client
|
5
|
-
# Lookup of a Globus identity ID
|
6
|
-
class Identity
|
7
|
-
def initialize(config)
|
8
|
-
@config = config
|
9
|
-
end
|
10
|
-
|
11
|
-
def get_identity_id(sunetid)
|
12
|
-
@email = "#{sunetid}@stanford.edu"
|
13
|
-
|
14
|
-
response = lookup_identity
|
15
|
-
UnexpectedResponse.call(response) unless response.success?
|
16
|
-
|
17
|
-
data = JSON.parse(response.body)
|
18
|
-
extract_id(data)
|
19
|
-
end
|
20
|
-
|
21
|
-
def exists?(sunetid)
|
22
|
-
get_identity_id(sunetid)
|
23
|
-
true
|
24
|
-
rescue
|
25
|
-
false
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
attr_reader :config
|
31
|
-
|
32
|
-
def connection
|
33
|
-
Faraday.new(url: config.auth_url)
|
34
|
-
end
|
35
|
-
|
36
|
-
def lookup_identity
|
37
|
-
id_endpoint = "/v2/api/identities"
|
38
|
-
connection.get(id_endpoint) do |req|
|
39
|
-
req.params["usernames"] = @email
|
40
|
-
req.headers["Authorization"] = "Bearer #{config.token}"
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def extract_id(data)
|
45
|
-
identities = data["identities"]
|
46
|
-
# Select identity with "used" or "private" status
|
47
|
-
matching_users = identities.select { |id| id["username"] == @email }
|
48
|
-
active_users = matching_users.select { |user| (user["status"] == "used" || user["status"] == "private") }
|
49
|
-
raise "No matching active Globus user found for #{@email}." if active_users.empty?
|
50
|
-
|
51
|
-
active_users.first["id"]
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
@@ -1,50 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Globus
|
4
|
-
class Client
|
5
|
-
# Handles unexpected responses when communicating with Globus
|
6
|
-
class UnexpectedResponse
|
7
|
-
# Error raised when the Globus Auth or Transfer API returns a 400 error
|
8
|
-
class BadRequestError < StandardError; end
|
9
|
-
|
10
|
-
# Error raised by the Globus Auth API returns a 401 Unauthorized
|
11
|
-
class UnauthorizedError < StandardError; end
|
12
|
-
|
13
|
-
# Error raised when the Globus Auth or Transfer API returns a 403 Forbidden
|
14
|
-
class ForbiddenError < StandardError; end
|
15
|
-
|
16
|
-
# Error raised when the Globus Auth or Transfer API returns a 404 NotFound
|
17
|
-
class ResourceNotFound < StandardError; end
|
18
|
-
|
19
|
-
# Error raised when the Globus Transfer API returns a 502 Bad Gateway
|
20
|
-
class EndpointError < StandardError; end
|
21
|
-
|
22
|
-
# Error raised when the remote server returns a 503 Bad Gateway
|
23
|
-
class ServiceUnavailable < StandardError; end
|
24
|
-
|
25
|
-
# @param [Faraday::Response] response
|
26
|
-
# https://docs.globus.org/api/transfer/file_operations/#common_errors
|
27
|
-
# https://docs.globus.org/api/transfer/file_operations/#errors
|
28
|
-
# https://docs.globus.org/api/transfer/acl/#common_errors
|
29
|
-
# https://docs.globus.org/api/auth/reference/
|
30
|
-
def self.call(response)
|
31
|
-
case response.status
|
32
|
-
when 400
|
33
|
-
raise BadRequestError, "Invalid path or another error with the request: #{response.body}"
|
34
|
-
when 401
|
35
|
-
raise UnauthorizedError, "There was a problem with the access token: #{response.body} "
|
36
|
-
when 403
|
37
|
-
raise ForbiddenError, "The operation requires privileges which the client does not have: #{response.body}"
|
38
|
-
when 404
|
39
|
-
raise ResourceNotFound, "Endpoint ID not found or resource does not exist: #{response.body}"
|
40
|
-
when 502
|
41
|
-
raise EndpointError, "Other error with endpoint: #{response.body}"
|
42
|
-
when 503
|
43
|
-
raise ServiceUnavailable, "The service is down for maintenance."
|
44
|
-
else
|
45
|
-
raise StandardError, "Unexpected response: #{response.status} #{response.body}"
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
data/lib/globus/client.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_support/core_ext/module/delegation"
|
4
|
-
require "faraday"
|
5
|
-
require "ostruct"
|
6
|
-
require "singleton"
|
7
|
-
require "zeitwerk"
|
8
|
-
|
9
|
-
# Load the gem's internal dependencies
|
10
|
-
loader = Zeitwerk::Loader.new
|
11
|
-
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
12
|
-
loader.push_dir(File.absolute_path("#{__FILE__}/../.."))
|
13
|
-
loader.setup
|
14
|
-
|
15
|
-
module Globus
|
16
|
-
# Client for interacting with the Globus API
|
17
|
-
class Client
|
18
|
-
include Singleton
|
19
|
-
|
20
|
-
class << self
|
21
|
-
# @param client_id [String] the client identifier registered with Globus
|
22
|
-
# @param client_secret [String] the client secret to authenticate with Globus
|
23
|
-
# @param uploads_directory [String] where to upload files
|
24
|
-
# @param transfer_endpoint_id [String] the transfer API endpoint ID supplied by Globus
|
25
|
-
# @param transfer_url [String] the transfer API URL
|
26
|
-
# @param auth_url [String] the authentication API URL
|
27
|
-
def configure(client_id:, client_secret:, uploads_directory:, transfer_endpoint_id:, transfer_url: default_transfer_url, auth_url: default_auth_url)
|
28
|
-
instance.config = OpenStruct.new(
|
29
|
-
token: Globus::Client::Authenticator.token(client_id, client_secret, auth_url),
|
30
|
-
uploads_directory:,
|
31
|
-
transfer_endpoint_id:,
|
32
|
-
transfer_url:,
|
33
|
-
auth_url:
|
34
|
-
)
|
35
|
-
|
36
|
-
self
|
37
|
-
end
|
38
|
-
|
39
|
-
delegate :config, :disallow_writes, :file_count, :mkdir, :total_size, :user_exists?, to: :instance
|
40
|
-
|
41
|
-
def default_transfer_url
|
42
|
-
"https://transfer.api.globusonline.org"
|
43
|
-
end
|
44
|
-
|
45
|
-
def default_auth_url
|
46
|
-
"https://auth.globus.org"
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
attr_accessor :config
|
51
|
-
|
52
|
-
def mkdir(...)
|
53
|
-
endpoint = Globus::Client::Endpoint.new(config, ...)
|
54
|
-
endpoint.mkdir
|
55
|
-
endpoint.allow_writes
|
56
|
-
end
|
57
|
-
|
58
|
-
def disallow_writes(...)
|
59
|
-
endpoint = Globus::Client::Endpoint.new(config, ...)
|
60
|
-
endpoint.disallow_writes
|
61
|
-
end
|
62
|
-
|
63
|
-
def file_count(...)
|
64
|
-
endpoint = Globus::Client::Endpoint.new(config, ...)
|
65
|
-
endpoint.file_count
|
66
|
-
end
|
67
|
-
|
68
|
-
def total_size(...)
|
69
|
-
endpoint = Globus::Client::Endpoint.new(config, ...)
|
70
|
-
endpoint.total_size
|
71
|
-
end
|
72
|
-
|
73
|
-
def user_exists?(...)
|
74
|
-
identity = Globus::Client::Identity.new(config)
|
75
|
-
identity.exists?(...)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
data/sig/globus_client.rbs
DELETED