p4_web_api 2014.2.0.pre1
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 +7 -0
- data/bin/p4_web_api +4 -0
- data/lib/p4_web_api.rb +700 -0
- data/lib/p4_web_api/auth.rb +268 -0
- data/lib/p4_web_api/p4_error.rb +36 -0
- data/lib/p4_web_api/p4_util.rb +417 -0
- data/lib/p4_web_api/version.rb +6 -0
- metadata +249 -0
@@ -0,0 +1,268 @@
|
|
1
|
+
# Copyright (c) 2014 Perforce Software, Inc. All rights reserved.
|
2
|
+
# vim:ts=2:sw=2:et:si:ai:
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'json'
|
6
|
+
require 'pathname'
|
7
|
+
require 'securerandom'
|
8
|
+
|
9
|
+
require_relative 'p4_util'
|
10
|
+
|
11
|
+
module P4WebAPI
|
12
|
+
# Sinatra Middleware component to take the HTTP Basic authentication request
|
13
|
+
# and pass that along to the internal P4WebAPI::Auth class methods
|
14
|
+
class AuthMiddleware
|
15
|
+
def initialize(app, options = {})
|
16
|
+
@app = app
|
17
|
+
@unauthenticated_paths = []
|
18
|
+
if options[:unauthenticated_paths]
|
19
|
+
@unauthenticated_paths = options[:unauthenticated_paths]
|
20
|
+
end
|
21
|
+
@settings = options[:settings]
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
return @app.call(env) if unauthenticated_path?(env)
|
26
|
+
|
27
|
+
auth = Rack::Auth::Basic::Request.new(env)
|
28
|
+
if auth.provided? &&
|
29
|
+
auth.basic? &&
|
30
|
+
P4WebAPI::Auth.credentials_valid?(auth.credentials, @settings)
|
31
|
+
env['AUTH_CREDENTIALS'] = auth.credentials
|
32
|
+
return @app.call(env)
|
33
|
+
end
|
34
|
+
|
35
|
+
unauthenticated_error
|
36
|
+
end
|
37
|
+
|
38
|
+
def unauthenticated_path?(env)
|
39
|
+
@unauthenticated_paths.include?(env['PATH_INFO'])
|
40
|
+
end
|
41
|
+
|
42
|
+
def unauthenticated_error
|
43
|
+
[
|
44
|
+
403,
|
45
|
+
{ 'Content-Type' => 'text/plain',
|
46
|
+
'Content-Length' => '0',
|
47
|
+
'WWW-Authenticate' => 'Basic realm="Perforce Web API"' },
|
48
|
+
[]
|
49
|
+
]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# "Session" handling that basically uses Perforce on the backend and stores
|
54
|
+
# the p4 ticket in a GUID file. We then pass the GUID around 'publicly' since
|
55
|
+
# that's really only relevant to this one instance. We can then fetch the
|
56
|
+
# p4 ticket and pass that to other services internally without "exposing" it.
|
57
|
+
module Auth
|
58
|
+
# Double checks that really only the current user can write to the token
|
59
|
+
# directory. If it doesn't exist, we just print warnings to stdout.
|
60
|
+
#
|
61
|
+
# These checks are only intended to work on Linux and OS X.
|
62
|
+
def self.validate_token_dir(token_dir)
|
63
|
+
warn_if_tmp_dir(token_dir)
|
64
|
+
|
65
|
+
return unless File.exist?(token_dir)
|
66
|
+
|
67
|
+
if !File.owned?(token_dir)
|
68
|
+
puts "The current user does not own security token dir: #{token_dir}"
|
69
|
+
else
|
70
|
+
mode = File.stat(token_dir).mode
|
71
|
+
warn_illegal_privileges(mode, token_dir)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# If the login/password combination is valid, we'll create a new session and
|
76
|
+
# return the session ID (which is just a GUID).
|
77
|
+
#
|
78
|
+
# This assumes that the p4 connection already has the user name set.
|
79
|
+
#
|
80
|
+
# If the password is a ticket, that'll be stored as is. If it's a password
|
81
|
+
# (currently determined if it's not a 32 character alphanumeric string)
|
82
|
+
# we'll create a ticket and store that.
|
83
|
+
#
|
84
|
+
# Settings is the Sinatra application configuration.
|
85
|
+
def self.create_session(p4, password, settings)
|
86
|
+
ticket = nil
|
87
|
+
|
88
|
+
if P4Util.p4_ticket?(password)
|
89
|
+
ticket = password
|
90
|
+
else
|
91
|
+
ticket = ticket_from_login(p4)
|
92
|
+
end
|
93
|
+
|
94
|
+
return if ticket.nil?
|
95
|
+
|
96
|
+
results = p4.run_user('-o')
|
97
|
+
|
98
|
+
save_token(user_info(p4, results, ticket), settings)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Saves the information stored in the user_info hash to a new randomly
|
102
|
+
# generated ticket file, and returns that new ticket file's name.
|
103
|
+
#
|
104
|
+
# user_info should have the following values:
|
105
|
+
# * login
|
106
|
+
# * email
|
107
|
+
# * full_name
|
108
|
+
# * ticket
|
109
|
+
def self.save_token(user_info, settings)
|
110
|
+
unless File.directory?(settings.token_path)
|
111
|
+
Pathname.new(settings.token_path).mkpath
|
112
|
+
FileUtils.chmod(0700, settings.token_path)
|
113
|
+
end
|
114
|
+
|
115
|
+
token = SecureRandom.uuid
|
116
|
+
token_path = Pathname.new(settings.token_path) + token
|
117
|
+
|
118
|
+
File.open(token_path, 'w', 0600) do |file|
|
119
|
+
file.write(JSON.generate(user_info))
|
120
|
+
end
|
121
|
+
|
122
|
+
token
|
123
|
+
end
|
124
|
+
|
125
|
+
# Removes the session file
|
126
|
+
#
|
127
|
+
# Assumes blindly that we're supposed to be able to do this. The caller
|
128
|
+
# should ensure that the token is deletable.
|
129
|
+
def self.delete_session(token, settings)
|
130
|
+
token_path = Pathname.new(settings.token_path) + token
|
131
|
+
File.delete(token_path) if File.exist?(token_path)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Takes an array with [login, password], and returns true if the password
|
135
|
+
# is a legal token for that login, or looks like a p4 ticket.
|
136
|
+
def self.credentials_valid?(credentials, settings)
|
137
|
+
login = credentials.first
|
138
|
+
password = credentials.last
|
139
|
+
# Can't really determine much at this point, just return true. If we're
|
140
|
+
# a token, it's a UUID and we'll learn more about that later.
|
141
|
+
return true unless P4Util.uuid?(password)
|
142
|
+
|
143
|
+
user_info = read_token(password, settings)
|
144
|
+
if user_info
|
145
|
+
return user_info['login'] == login
|
146
|
+
else
|
147
|
+
false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.read_token(token, settings)
|
152
|
+
token_path = Pathname.new(settings.token_path) + token
|
153
|
+
if File.exist?(token_path)
|
154
|
+
File.open(token_path, 'r') do |file|
|
155
|
+
return JSON.parse(file.read)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def self.warn_if_tmp_dir(token_dir)
|
164
|
+
if token_dir.start_with?('/tmp')
|
165
|
+
puts "Your token directory is using the default '/tmp' location, "\
|
166
|
+
'please reconfigure to a reliable location'
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.warn_illegal_privileges(mode, token_dir)
|
171
|
+
warn_unless_user_rwx(mode, token_dir)
|
172
|
+
warn_no_group_write(mode, token_dir)
|
173
|
+
warn_no_group_read(mode, token_dir)
|
174
|
+
warn_no_group_execute(mode, token_dir)
|
175
|
+
warn_no_other_write(mode, token_dir)
|
176
|
+
warn_no_other_read(mode, token_dir)
|
177
|
+
warn_no_other_execute(mode, token_dir)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Check owner read/write/execute - should all be there
|
181
|
+
def self.warn_unless_user_rwx(mode, token_dir)
|
182
|
+
unless (mode & 0400 == 0400) &&
|
183
|
+
(mode & 0200 == 0200) &&
|
184
|
+
(mode & 0100 == 0100)
|
185
|
+
puts "The token_path '#{token_dir}' should allow the owner read, "\
|
186
|
+
'write and execute privileges'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.warn_no_group_write(mode, token_dir)
|
191
|
+
if (mode & 0040 == 0040)
|
192
|
+
puts "The token_path '#{token_dir}' should not have group write access"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.warn_no_group_read(mode, token_dir)
|
197
|
+
if (mode & 0020 == 0020)
|
198
|
+
puts "The token_path '#{token_dir}' should not have group read access"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.warn_no_group_execute(mode, token_dir)
|
203
|
+
if (mode & 0010 == 0010)
|
204
|
+
puts "The token_path '#{token_dir}' should not have group "\
|
205
|
+
'execute access'
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.warn_no_other_write(mode, token_dir)
|
210
|
+
if (mode & 0004 == 0004)
|
211
|
+
puts "The token_path '#{token_dir}' should not have other write access"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.warn_no_other_read(mode, token_dir)
|
216
|
+
if (mode & 0002 == 0002)
|
217
|
+
puts "The token_path '#{token_dir}' should not have other read access"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.warn_no_other_execute(mode, token_dir)
|
222
|
+
if (mode & 0001 == 0001)
|
223
|
+
puts "The token_path '#{token_dir}' should not have other "\
|
224
|
+
'execute access'
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# We want special error handling here to return 4xx codes instead of 5xx
|
229
|
+
# in the face of an invalid password. Temporarily drop to lowest
|
230
|
+
# exception level, and just return nil when login doesn't work.
|
231
|
+
def self.ticket_from_login(p4)
|
232
|
+
results = nil
|
233
|
+
p4.at_exception_level(P4::RAISE_NONE) do
|
234
|
+
results = p4.run_login('-p')
|
235
|
+
end
|
236
|
+
|
237
|
+
auth_ok = raise_unless_auth_error(p4)
|
238
|
+
|
239
|
+
if !auth_ok
|
240
|
+
nil
|
241
|
+
else
|
242
|
+
p4.password = results[0]
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def self.raise_unless_auth_error(p4)
|
247
|
+
if P4Util.error?(p4)
|
248
|
+
msg = p4.messages[0]
|
249
|
+
if msg.msgid == 7205 || # invalid user
|
250
|
+
msg.msgid == 7206 # invalid password
|
251
|
+
return false
|
252
|
+
else
|
253
|
+
P4Util.raise_error(p4)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
true
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.user_info(p4, results, ticket)
|
260
|
+
{
|
261
|
+
login: p4.user,
|
262
|
+
email: results[0]['Email'],
|
263
|
+
full_name: results[0]['FullName'],
|
264
|
+
ticket: ticket
|
265
|
+
}
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# vim:ts=2:sw=2:et:si:ai:
|
2
|
+
# The API sends back messages via the p4.messages attribute if there was an
|
3
|
+
# error with the last command. We will often convert that to this exception,
|
4
|
+
# which will be formatted into JSON.
|
5
|
+
require 'P4'
|
6
|
+
|
7
|
+
module P4WebAPI
|
8
|
+
# Any error we get from the Perforce server is generally encapsulated in
|
9
|
+
# a P4Error, which allows us to get some basic diagnostic information from
|
10
|
+
# the Perforce server out to the user.
|
11
|
+
class P4Error < RuntimeError
|
12
|
+
# All error codes must be greater than this number.
|
13
|
+
ERROR_CODE_BASE = 15_360
|
14
|
+
|
15
|
+
attr_accessor :message_code, :message_severity, :message_text
|
16
|
+
|
17
|
+
# Class method to create a valid P4Error object from a simple
|
18
|
+
# string. Used when we get errors for which no P4::Message object
|
19
|
+
# is available.
|
20
|
+
def self.default_error(message_text)
|
21
|
+
P4Error.new(ERROR_CODE_BASE,
|
22
|
+
P4::E_FAILED,
|
23
|
+
message_text)
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(code, severity, text)
|
27
|
+
@message_code = code
|
28
|
+
@message_severity = severity
|
29
|
+
@message_text = text
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
@message_text
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,417 @@
|
|
1
|
+
# Copyright (c) 2014 Perforce Software, Inc. All rights reserved.
|
2
|
+
#
|
3
|
+
# vim:ts=2:sw=2:et:si:ai:
|
4
|
+
#
|
5
|
+
# p4_util.rb
|
6
|
+
#
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'date'
|
10
|
+
require 'ostruct'
|
11
|
+
require 'P4'
|
12
|
+
|
13
|
+
require_relative 'p4_error'
|
14
|
+
|
15
|
+
module P4WebAPI
|
16
|
+
# Namespace for p4ruby conventions used in the P4 Web API.
|
17
|
+
module P4Util
|
18
|
+
PROPERTIES = [
|
19
|
+
:password, :port, :user, :api_level, :charset, :client,
|
20
|
+
:host, :handler, :maxlocktime, :maxresults, :maxscanrows, :prog,
|
21
|
+
:ticketfile
|
22
|
+
]
|
23
|
+
|
24
|
+
# Creates your p4 connection using some common forms.
|
25
|
+
#
|
26
|
+
# If you call open with a block, this will call connect before your block
|
27
|
+
# executes, and disconnect afterwards.
|
28
|
+
#
|
29
|
+
# If you do not call open with a block, it is up to the caller to connect
|
30
|
+
# and disconnect. (It's assumed you are calling w/o a block because you want
|
31
|
+
# to manage when the connection actually needs to happen.)
|
32
|
+
def self.open(options = {})
|
33
|
+
p4 = create_p4(options)
|
34
|
+
|
35
|
+
# Again, if we're calling using the block, we'll connect and disconnect.
|
36
|
+
# Otherwise, just return the created p4 object.
|
37
|
+
if block_given?
|
38
|
+
begin
|
39
|
+
p4.connect
|
40
|
+
yield p4
|
41
|
+
rescue P4Exception
|
42
|
+
raise make_p4_error(p4)
|
43
|
+
end
|
44
|
+
else
|
45
|
+
return p4
|
46
|
+
end
|
47
|
+
ensure
|
48
|
+
p4.disconnect if block_given? && p4 && p4.connected?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check for P4 errors
|
52
|
+
def self.error?(p4)
|
53
|
+
!p4.errors.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Raise an exception if necessary on a P4 Error
|
57
|
+
def self.raise_error(p4)
|
58
|
+
err = p4.messages.find { |m| m.severity > 2 }
|
59
|
+
fail P4WebAPI::P4Error.new(err.msgid, err.severity, err.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns true if the string looks like a Perforce authentication ticket.
|
63
|
+
def self.p4_ticket?(str)
|
64
|
+
/^[a-zA-Z0-9]{32,}$/.match(str) != nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# We only count uuids that were returned via SecureRandom.uuid used to
|
68
|
+
# generate internal security tokens.
|
69
|
+
def self.uuid?(str)
|
70
|
+
/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/
|
71
|
+
.match(str)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.singular(plural)
|
75
|
+
matches = {
|
76
|
+
branches: 'branch',
|
77
|
+
clients: 'client',
|
78
|
+
depots: 'depot',
|
79
|
+
groups: 'group',
|
80
|
+
jobs: 'job',
|
81
|
+
labels: 'label',
|
82
|
+
protects: 'protect',
|
83
|
+
servers: 'server',
|
84
|
+
streams: 'stream',
|
85
|
+
triggers: 'trigger',
|
86
|
+
users: 'user'
|
87
|
+
}
|
88
|
+
matches[plural.to_sym]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Data 'normalization'
|
92
|
+
#
|
93
|
+
# A very annoying aspect of our tagged output is that it often is *slightly*
|
94
|
+
# different between 'plural' and spec forms. This logic is made available
|
95
|
+
# as an option in this API
|
96
|
+
|
97
|
+
BRANCHES_MAP = {
|
98
|
+
'branch' => 'Branch'
|
99
|
+
}
|
100
|
+
BRANCHES_DATES = %w(Update Access)
|
101
|
+
|
102
|
+
CHANGES_MAP = {
|
103
|
+
'path' => 'Path',
|
104
|
+
'change' => 'Change',
|
105
|
+
'time' => 'Date',
|
106
|
+
'client' => 'Client',
|
107
|
+
'user' => 'User',
|
108
|
+
'status' => 'Status',
|
109
|
+
'type' => 'Type',
|
110
|
+
'changeType' => 'Type',
|
111
|
+
'desc' => 'Description'
|
112
|
+
}
|
113
|
+
DESCRIBES_MAP = CHANGES_MAP
|
114
|
+
CHANGES_DATES = ['Date']
|
115
|
+
|
116
|
+
CLIENTS_MAP = {
|
117
|
+
'client' => 'Client'
|
118
|
+
}
|
119
|
+
CLIENTS_DATES = %w(Update Access)
|
120
|
+
|
121
|
+
DEPOTS_MAP = {
|
122
|
+
'name' => 'Depot',
|
123
|
+
'time' => 'Date',
|
124
|
+
'type' => 'Type',
|
125
|
+
'map' => 'Map',
|
126
|
+
'desc' => 'Description'
|
127
|
+
}
|
128
|
+
DEPOTS_DATES = ['Date']
|
129
|
+
|
130
|
+
DIRS_MAP = {
|
131
|
+
'dir' => 'Dir'
|
132
|
+
}
|
133
|
+
|
134
|
+
FILES_MAP = {
|
135
|
+
'depotFile' => 'DepotFile',
|
136
|
+
'rev' => 'Revision',
|
137
|
+
'change' => 'Change',
|
138
|
+
'action' => 'Action',
|
139
|
+
'type' => 'Type',
|
140
|
+
'time' => 'Date'
|
141
|
+
}
|
142
|
+
FILES_DATES = %w(Date)
|
143
|
+
|
144
|
+
GROUPS_MAP = {
|
145
|
+
'group' => 'Group',
|
146
|
+
'maxResults' => 'MaxResults',
|
147
|
+
'maxScanRows' => 'MaxScanRows',
|
148
|
+
'maxLockTime' => 'MaxLockTime',
|
149
|
+
'timeout' => 'Timeout',
|
150
|
+
'desc' => 'Description',
|
151
|
+
'user' => 'User',
|
152
|
+
'isSubGroup' => 'IsSubGroup',
|
153
|
+
'isOwner' => 'IsOwner',
|
154
|
+
'isUser' => 'IsUser',
|
155
|
+
'passTimeout' => 'PasswordTimeout'
|
156
|
+
}
|
157
|
+
|
158
|
+
JOBS_MAP = {}
|
159
|
+
JOBS_DATES = %w(Date)
|
160
|
+
|
161
|
+
LABELS_MAP = {
|
162
|
+
'label' => 'Label'
|
163
|
+
}
|
164
|
+
LABELS_DATES = %w(Update Access)
|
165
|
+
|
166
|
+
# This isn't supported but kept here for reference. We *might* support this.
|
167
|
+
# OPENED_MAP = {
|
168
|
+
# 'change' => 'Change',
|
169
|
+
# 'client' => 'Client',
|
170
|
+
# 'user' => 'User'
|
171
|
+
# }
|
172
|
+
|
173
|
+
STREAMS_MAP = {
|
174
|
+
'desc' => 'Description'
|
175
|
+
}
|
176
|
+
STREAMS_DATES = %w(Update Access)
|
177
|
+
|
178
|
+
USERS_MAP = {
|
179
|
+
'passwordChange' => 'PasswordChange'
|
180
|
+
}
|
181
|
+
USERS_DATES = %w(Update Access PasswordChange)
|
182
|
+
|
183
|
+
# For each 'spec type' returns a function that will ensure that:
|
184
|
+
# - case is consistent between different return calls, prefer 'spec' types
|
185
|
+
# - dates are returned
|
186
|
+
#
|
187
|
+
# Parameters:
|
188
|
+
# - spec_type is the 'plural' form of spec class, e.g., 'users', 'clients'
|
189
|
+
# - offset is the current server offset, retrieve this via 'p4 info' command
|
190
|
+
#
|
191
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
192
|
+
def self.normalizer(spec_type, offset)
|
193
|
+
case spec_type
|
194
|
+
when 'branches'
|
195
|
+
make_normalizer(BRANCHES_MAP, offset, BRANCHES_DATES)
|
196
|
+
when 'changes'
|
197
|
+
make_normalizer(CHANGES_MAP, offset, CHANGES_DATES)
|
198
|
+
when 'clients'
|
199
|
+
make_normalizer(CLIENTS_MAP, offset, CLIENTS_DATES)
|
200
|
+
when 'depots'
|
201
|
+
make_normalizer(DEPOTS_MAP, offset, DEPOTS_DATES)
|
202
|
+
when 'dirs'
|
203
|
+
make_normalizer(DIRS_MAP, offset)
|
204
|
+
when 'files'
|
205
|
+
make_normalizer(FILES_MAP, offset, FILES_DATES)
|
206
|
+
when 'groups'
|
207
|
+
date_and_case = make_normalizer(GROUPS_MAP, offset)
|
208
|
+
lambda do |results|
|
209
|
+
results = date_and_case.call(results)
|
210
|
+
P4Util.replace_unset_with_0(results)
|
211
|
+
results
|
212
|
+
end
|
213
|
+
when 'jobs'
|
214
|
+
make_normalizer(JOBS_MAP, offset, JOBS_DATES)
|
215
|
+
when 'labels'
|
216
|
+
make_normalizer(LABELS_MAP, offset, LABELS_DATES)
|
217
|
+
when 'streams'
|
218
|
+
make_normalizer(STREAMS_MAP, offset, STREAMS_DATES)
|
219
|
+
when 'users'
|
220
|
+
make_normalizer(USERS_MAP, offset, USERS_DATES)
|
221
|
+
else
|
222
|
+
# By default, do no translation
|
223
|
+
return ->(x) { x }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def self.make_normalizer(field_map, offset, date_fields = nil)
|
228
|
+
lambda do |results|
|
229
|
+
return unless results
|
230
|
+
|
231
|
+
# We need to ignore any instances of P4::Spec since that will 'validate'
|
232
|
+
# fields on it's accessor methods
|
233
|
+
results.map! do |result|
|
234
|
+
if result.class <= P4::Spec
|
235
|
+
spec = result
|
236
|
+
result = Hash.new
|
237
|
+
result.merge!(spec)
|
238
|
+
else
|
239
|
+
result
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
results.each do |result|
|
244
|
+
update_fields(field_map, result)
|
245
|
+
update_dates(date_fields, result, offset)
|
246
|
+
end
|
247
|
+
results
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# These are 'single values' of properties of Group objects we use while
|
252
|
+
# collating them.
|
253
|
+
GROUP_PROPERTIES = %w(
|
254
|
+
Group MaxResults MaxScanRows MaxLockTime Timeout PasswordTimeout
|
255
|
+
)
|
256
|
+
|
257
|
+
# Will apply the normalizer to set up a consistent field naming and date
|
258
|
+
# format, but then 'collates' the output into a single list of 'groups'.
|
259
|
+
#
|
260
|
+
# The groups output is kind of funny, because it basically lists users,
|
261
|
+
# with group access fields. It's very non-obvious, and in the end, most
|
262
|
+
# clients will need to run logic like this anyway.
|
263
|
+
def self.collate_group_results(results)
|
264
|
+
collated = results.group_by { |x| x['Group'] }
|
265
|
+
updated = collated.map do |_key, items|
|
266
|
+
# The first item sets most of the values, we then figure out array
|
267
|
+
# subvalues
|
268
|
+
|
269
|
+
group = {}
|
270
|
+
GROUP_PROPERTIES.each do |p|
|
271
|
+
group[p] = items.first[p] if items.first.key?(p)
|
272
|
+
end
|
273
|
+
|
274
|
+
group['Users'] =
|
275
|
+
items.find_all { |x| x['IsUser'] == '1' }.map { |x| x['User'] }
|
276
|
+
|
277
|
+
group['Subgroups'] =
|
278
|
+
items.find_all { |x| x['IsSubGroup'] == '1' }.map { |x| x['User'] }
|
279
|
+
|
280
|
+
group['Owners'] =
|
281
|
+
items.find_all { |x| x['IsOwner'] == '1' }.map { |x| x['User'] }
|
282
|
+
|
283
|
+
group
|
284
|
+
end
|
285
|
+
|
286
|
+
results.replace(updated)
|
287
|
+
end
|
288
|
+
|
289
|
+
def self.replace_unset_with_0(results)
|
290
|
+
results.each do |r|
|
291
|
+
r.each_key { |k| r[k] = '0' if r[k] == 'unset' }
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Returns true if .to_i will actually convert this string to an integer
|
296
|
+
def self.i?(str)
|
297
|
+
(str =~ /\A[-+]?\d+\z/)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns true if we can parse the string as a date
|
301
|
+
def self.date_str?(str)
|
302
|
+
Date.parse(str) rescue false
|
303
|
+
end
|
304
|
+
|
305
|
+
# In general we get dates without any offset information, which has to be
|
306
|
+
# retrieved via the serverDate field from 'p4 info'
|
307
|
+
def self.p4_date_to_i(offset, p4date)
|
308
|
+
DateTime.parse("#{p4date} #{offset}").to_time.to_i
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.p4_date_offset(str)
|
312
|
+
DateTime.parse(str).zone
|
313
|
+
end
|
314
|
+
|
315
|
+
def self.resolve_host(env, settings)
|
316
|
+
if env.key?('P4_HOST') && settings.allow_env_p4_config
|
317
|
+
env['P4_HOST']
|
318
|
+
else
|
319
|
+
settings.p4['host']
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def self.resolve_port(env, settings)
|
324
|
+
if env.key?('P4_PORT') && settings.allow_env_p4_config
|
325
|
+
env['P4_PORT']
|
326
|
+
else
|
327
|
+
settings.p4['port']
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.resolve_charset(env, settings)
|
332
|
+
if env.key?('P4_CHARSET') && settings.allow_env_p4_config
|
333
|
+
env['P4_CHARSET']
|
334
|
+
else
|
335
|
+
settings.p4['charset'] if settings.p4.key?('charset')
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def self.resolve_password(env, settings)
|
340
|
+
password = env['AUTH_CREDENTIALS'].last
|
341
|
+
if !P4Util.uuid?(password)
|
342
|
+
password
|
343
|
+
else
|
344
|
+
P4WebAPI::Auth.read_token(password, settings)['ticket']
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
private
|
349
|
+
|
350
|
+
def self.create_p4(options)
|
351
|
+
p4 = P4.new
|
352
|
+
|
353
|
+
init_p4(p4)
|
354
|
+
|
355
|
+
PROPERTIES.each do|key|
|
356
|
+
p4.method(key.to_s + '=').call(options[key]) if options[key]
|
357
|
+
end
|
358
|
+
|
359
|
+
# From the WTF department, if you think you don't want a charset set,
|
360
|
+
# make damn sure P4Ruby isn't going to get confused.
|
361
|
+
p4.charset = nil if p4.charset == 'none'
|
362
|
+
|
363
|
+
# Make P4Ruby only raise exceptions if there are errors. Warnings
|
364
|
+
# (such as 'no such file(s)' don't get the same treatment.
|
365
|
+
p4.exception_level = P4::RAISE_ERRORS
|
366
|
+
|
367
|
+
p4
|
368
|
+
end
|
369
|
+
|
370
|
+
# Before we do anything, clear out any environment variables that may have
|
371
|
+
# leaked in from your environment
|
372
|
+
#
|
373
|
+
# Some of our HTTP APIs (e.g, Git Fusion) *do* utilize environment setups
|
374
|
+
# for a system user, that may be installed alongside this API. We want this
|
375
|
+
# system to always explicitly state which configuration to use. (So, it's
|
376
|
+
# not just a concern of our development environments.)
|
377
|
+
def self.init_p4(p4)
|
378
|
+
p4.client = 'invalid'
|
379
|
+
p4.port = ''
|
380
|
+
p4.host = ''
|
381
|
+
p4.password = ''
|
382
|
+
end
|
383
|
+
|
384
|
+
def self.make_p4_error(p4)
|
385
|
+
if p4.messages && p4.messages.first
|
386
|
+
m = p4.messages.first
|
387
|
+
P4WebAPI::P4Error.new(m.msgid, m.severity, m.to_s)
|
388
|
+
else
|
389
|
+
P4WebAPI::P4Error.default_error($ERROR_INFO.to_s)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def self.update_fields(field_map, result)
|
394
|
+
field_map.each_key do |key|
|
395
|
+
next unless result.key?(key)
|
396
|
+
val = result[key]
|
397
|
+
result.delete(key)
|
398
|
+
result[field_map[key]] = val
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Ensures that all dates looks like epoch second (integer) times
|
403
|
+
def self.update_dates(date_fields, result, offset)
|
404
|
+
if date_fields
|
405
|
+
date_fields.each do |date_key|
|
406
|
+
next unless result.key?(date_key)
|
407
|
+
val = result[date_key]
|
408
|
+
if i?(val)
|
409
|
+
result[date_key] = val.to_i
|
410
|
+
elsif date_str?(val)
|
411
|
+
result[date_key] = p4_date_to_i(offset, val)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|