globus_client 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Code Climate](https://codeclimate.com/github/sul-dlss/globus_client/badges/gpa.svg)](https://codeclimate.com/github/sul-dlss/globus_client)
|
4
4
|
[![Code Climate Test Coverage](https://api.codeclimate.com/v1/badges/8e6bf1a5d3fc86a6fbd0/test_coverage)](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