window.SimpleInput = {
view: function(vnode) {
- return m("input.form-control", {
+ return m('input.form-control', {
style: {
width: '100%',
},
window.SelectOrAutocomplete = {
view: function(vnode) {
- return m("input.form-control", {
+ return m('input.form-control', {
style: {
width: '100%'
},
valueOpts = vnode.attrs.vocabulary().tags[vnode.attrs.name()].values
}
}
- return m("tr", [
+ return m('tr', [
// Erase tag
- m("td", [
+ m('td', [
vnode.attrs.editMode &&
m('div.text-center', m('a.btn.btn-default.btn-sm', {
style: {
}, m('i.fa.fa-fw.fa-trash-o')))
]),
// Tag key
- m("td", [
+ m('td', [
vnode.attrs.editMode ?
- m("div", {key: 'key'}, [
+ m('div', {key: 'key'}, [
m(inputComponent, {
options: nameOpts,
value: vnode.attrs.name,
- // Allow any tag name unless "strict" is set to true.
+ // Allow any tag name unless 'strict' is set to true.
create: !vnode.attrs.vocabulary().strict,
placeholder: 'key',
})
: vnode.attrs.name
]),
// Tag value
- m("td", [
+ m('td', [
vnode.attrs.editMode ?
- m("div", {key: 'value'}, [
+ m('div', {key: 'value'}, [
m(inputComponent, {
options: valueOpts,
value: vnode.attrs.value,
window.TagEditorTable = {
view: function(vnode) {
- return m("table.table.table-condensed.table-justforlayout", [
- m("colgroup", [
- m("col", {width:"5%"}),
- m("col", {width:"25%"}),
- m("col", {width:"70%"}),
+ return m('table.table.table-condensed.table-justforlayout', [
+ m('colgroup', [
+ m('col', {width:'5%'}),
+ m('col', {width:'25%'}),
+ m('col', {width:'70%'}),
]),
- m("thead", [
- m("tr", [
- m("th"),
- m("th", "Key"),
- m("th", "Value"),
+ m('thead', [
+ m('tr', [
+ m('th'),
+ m('th', 'Key'),
+ m('th', 'Value'),
])
]),
- m("tbody", [
+ m('tbody', [
vnode.attrs.tags.length > 0
? vnode.attrs.tags.map(function(tag, idx) {
return m(TagEditorRow, {
vocabulary: vnode.attrs.vocabulary
})
})
- : m("tr", m("td[colspan=3]", m("center", "Loading tags...")))
+ : m('tr', m('td[colspan=3]', m('center', 'Loading tags...')))
]),
])
}
oninit: function(vnode) {
vnode.state.sessionDB = new SessionDB()
// Get vocabulary
- vnode.state.vocabulary = m.stream({"strict":false, "tags":{}})
+ vnode.state.vocabulary = m.stream({'strict':false, 'tags':{}})
var vocabularyTimestamp = parseInt(Date.now() / 300000) // Bust cache every 5 minutes
m.request('/vocabulary.json?v=' + vocabularyTimestamp).then(vnode.state.vocabulary)
vnode.state.editMode = vnode.attrs.targetEditable
vnode.state.tags = []
vnode.state.dirty = m.stream(false)
vnode.state.dirty.map(m.redraw)
- vnode.state.objPath = '/arvados/v1/'+vnode.attrs.targetController+'/'+vnode.attrs.targetUuid
+ vnode.state.objPath = 'arvados/v1/' + vnode.attrs.targetController + '/' + vnode.attrs.targetUuid
// Get tags
vnode.state.sessionDB.request(
vnode.state.sessionDB.loadLocal(),
- '/arvados/v1/'+vnode.attrs.targetController,
+ 'arvados/v1/' + vnode.attrs.targetController,
{
data: {
filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]),
view: function(vnode) {
return [
vnode.state.editMode &&
- m("div.pull-left", [
- m("a.btn.btn-primary.btn-sm"+(vnode.state.dirty() ? '' : '.disabled'), {
+ m('div.pull-left', [
+ m('a.btn.btn-primary.btn-sm' + (vnode.state.dirty() ? '' : '.disabled'), {
style: {
margin: '10px 0px'
},
vnode.state.sessionDB.request(
vnode.state.sessionDB.loadLocal(),
vnode.state.objPath, {
- method: "PUT",
+ method: 'PUT',
data: {properties: JSON.stringify(tags)}
}
).then(function(v) {
"--local",
"--api=containers",
"--project-uuid=#{params['work_unit']['owner_uuid']}",
- "--collection-keep-cache=#{keep_cache}",
+ "--collection-cache-size=#{keep_cache}",
"/var/lib/cwl/workflow.json#main",
"/var/lib/cwl/cwl.input.json"]
.sort.flat_map do |parts|
[parts + [nil]] + dir_to_tree.call(File.join(parts))
end
- # Then extend that list with files in this directory.
- subnodes + tree[File.split(dirname)]
+ # Then extend that list with files in this directory, except the empty dir placeholders (0:0:. files).
+ subnodes + tree[File.split(dirname)].reject { |_, basename, size| (basename == '.') and (size == 0) }
end
dir_to_tree.call('.')
end
(If none given, leave config files alone in source tree.)
services/api_test="TEST=test/functional/arvados/v1/collections_controller_test.rb"
Restrict apiserver tests to the given file
-sdk/python_test="--test-suite test.test_keep_locator"
+sdk/python_test="--test-suite tests.test_keep_locator"
Restrict Python SDK tests to the given class
apps/workbench_test="TEST=test/integration/pipeline_instances_test.rb"
Restrict Workbench tests to the given file
resp := httptest.NewRecorder()
s.handler.ServeHTTP(resp, req)
c.Check(resp.Code, check.Equals, http.StatusFound)
- c.Check(resp.Header().Get("Location"), check.Matches, `https://0.0.0.0:1/auth/joshid\?return_to=foo&?`)
+ c.Check(resp.Header().Get("Location"), check.Matches, `https://0.0.0.0:1/auth/joshid\?return_to=%2Cfoo&?`)
}
func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
from __future__ import absolute_import
from . import config
+import re
+
+def escape(path):
+ path = re.sub('\\\\', lambda m: '\\134', path)
+ path = re.sub('[:\000-\040]', lambda m: "\\%03o" % ord(m.group(0)), path)
+ return path
+
def normalize_stream(stream_name, stream):
"""Take manifest stream and return a list of tokens in normalized format.
"""
- stream_name = stream_name.replace(' ', '\\040')
+ stream_name = escape(stream_name)
stream_tokens = [stream_name]
sortedfiles = list(stream.keys())
sortedfiles.sort()
for streamfile in sortedfiles:
# Add in file segments
current_span = None
- fout = streamfile.replace(' ', '\\040')
+ fout = escape(streamfile)
for segment in stream[streamfile]:
# Collapse adjacent segments
streamoffset = blocks[segment.locator] + segment.segment_offset
from .arvfile import split, _FileLikeObjectBase, ArvadosFile, ArvadosFileWriter, ArvadosFileReader, WrappableFile, _BlockManager, synchronized, must_be_writable, NoopLock
from .keep import KeepLocator, KeepClient
from .stream import StreamReader
-from ._normalize_stream import normalize_stream
+from ._normalize_stream import normalize_stream, escape
from ._ranges import Range, LocatorAndRange
from .safeapi import ThreadSafeApiCache
import arvados.config as config
def stream_name(self):
raise NotImplementedError()
+
@synchronized
def has_remote_blocks(self):
"""Recursively check for a +R segment locator signature."""
if stream:
buf.append(" ".join(normalize_stream(stream_name, stream)) + "\n")
for dirname in [s for s in sorted_keys if isinstance(self[s], RichCollectionBase)]:
- buf.append(self[dirname].manifest_text(stream_name=os.path.join(stream_name, dirname), strip=strip, normalize=True, only_committed=only_committed))
+ buf.append(self[dirname].manifest_text(
+ stream_name=os.path.join(stream_name, dirname),
+ strip=strip, normalize=True, only_committed=only_committed))
return "".join(buf)
else:
if strip:
self.name = newname
self.lock = self.parent.root_collection().lock
+ @synchronized
+ def _get_manifest_text(self, stream_name, strip, normalize, only_committed=False):
+ """Encode empty directories by using an \056-named (".") empty file"""
+ if len(self._items) == 0:
+ return "%s %s 0:0:\\056\n" % (
+ escape(stream_name), config.EMPTY_BLOCK_LOCATOR)
+ return super(Subcollection, self)._get_manifest_text(stream_name,
+ strip, normalize,
+ only_committed)
+
class CollectionReader(Collection):
"""A read-only collection object.
self.assertIs(c.find("./nonexistant.txt"), None)
self.assertIs(c.find("./nonexistantsubdir/nonexistant.txt"), None)
+ def test_escaped_paths_dont_get_unescaped_on_manifest(self):
+ # Dir & file names are literally '\056' (escaped form: \134056)
+ manifest = './\\134056\\040Test d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\134056\n'
+ c = Collection(manifest)
+ self.assertEqual(c.portable_manifest_text(), manifest)
+
+ def test_other_special_chars_on_file_token(self):
+ cases = [
+ ('\\000', '\0'),
+ ('\\011', '\t'),
+ ('\\012', '\n'),
+ ('\\072', ':'),
+ ('\\134400', '\\400'),
+ ]
+ for encoded, decoded in cases:
+ manifest = '. d41d8cd98f00b204e9800998ecf8427e+0 0:0:some%sfile.txt\n' % encoded
+ c = Collection(manifest)
+ self.assertEqual(c.portable_manifest_text(), manifest)
+ self.assertIn('some%sfile.txt' % decoded, c.keys())
+
+ def test_escaped_paths_do_get_unescaped_on_listing(self):
+ # Dir & file names are literally '\056' (escaped form: \134056)
+ manifest = './\\134056\\040Test d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\134056\n'
+ c = Collection(manifest)
+ self.assertIn('\\056 Test', c.keys())
+ self.assertIn('\\056', c['\\056 Test'].keys())
+
+ def test_make_empty_dir_with_escaped_chars(self):
+ c = Collection()
+ c.mkdirs('./Empty\\056Dir')
+ self.assertEqual(c.portable_manifest_text(),
+ './Empty\\134056Dir d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\n')
+
+ def test_make_empty_dir_with_spaces(self):
+ c = Collection()
+ c.mkdirs('./foo bar/baz waz')
+ self.assertEqual(c.portable_manifest_text(),
+ './foo\\040bar/baz\\040waz d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\n')
+
def test_remove_in_subdir(self):
c = Collection('. 781e5e245d69b566979b86e28d23f2c7+10 0:10:count1.txt\n./foo 781e5e245d69b566979b86e28d23f2c7+10 0:10:count2.txt\n')
c.remove("foo/count2.txt")
- self.assertEqual(". 781e5e245d69b566979b86e28d23f2c7+10 0:10:count1.txt\n", c.portable_manifest_text())
+ self.assertEqual(". 781e5e245d69b566979b86e28d23f2c7+10 0:10:count1.txt\n./foo d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\n", c.portable_manifest_text())
def test_remove_empty_subdir(self):
c = Collection('. 781e5e245d69b566979b86e28d23f2c7+10 0:10:count1.txt\n./foo 781e5e245d69b566979b86e28d23f2c7+10 0:10:count2.txt\n')
end
class Manifest
- STRICT_STREAM_TOKEN_REGEXP = /^(\.)(\/[^\/\s]+)*$/
- STRICT_FILE_TOKEN_REGEXP = /^[[:digit:]]+:[[:digit:]]+:([^\s\/]+(\/[^\s\/]+)*)$/
+ STREAM_TOKEN_REGEXP = /^([^\000-\040\\]|\\[0-3][0-7][0-7])+$/
+ STREAM_NAME_REGEXP = /^(\.)(\/[^\/]+)*$/
+
+ EMPTY_DIR_TOKEN_REGEXP = /^0:0:\.$/ # The exception when a file can have '.' as a name
+ FILE_TOKEN_REGEXP = /^[[:digit:]]+:[[:digit:]]+:([^\000-\040\\]|\\[0-3][0-7][0-7])+$/
+ FILE_NAME_REGEXP = /^[[:digit:]]+:[[:digit:]]+:([^\/]+(\/[^\/]+)*)$/
+
+ NON_8BIT_ENCODED_CHAR = /[^\\]\\[4-7][0-7][0-7]/
# Class to parse a manifest text and provide common views of that data.
def initialize(manifest_text)
end
end
- def unescape(s)
+ def self.unescape(s)
+ return nil if s.nil?
+
# Parse backslash escapes in a Keep manifest stream or file name.
s.gsub(/\\(\\|[0-7]{3})/) do |_|
case $1
end
end
+ def unescape(s)
+ self.class.unescape(s)
+ end
+
def split_file_token token
start_pos, filesize, filename = token.split(':', 3)
if filename.nil?
elsif in_file_tokens or not Locator.valid? token
in_file_tokens = true
- file_tokens = split_file_token(token)
+ start_pos, file_size, file_name = split_file_token(token)
stream_name_adjuster = ''
- if file_tokens[2].include?('/') # '/' in filename
- parts = file_tokens[2].rpartition('/')
- stream_name_adjuster = parts[1] + parts[0] # /dir_parts
- file_tokens[2] = parts[2]
+ if file_name.include?('/') # '/' in filename
+ dirname, sep, basename = file_name.rpartition('/')
+ stream_name_adjuster = sep + dirname # /dir_parts
+ file_name = basename
end
- yield [stream_name + stream_name_adjuster] + file_tokens
+ yield [stream_name + stream_name_adjuster, start_pos, file_size, file_name]
end
end
end
# files. This can help you avoid parsing the entire manifest if you
# just want to check if a small number of files are specified.
if stop_after.nil? or not @files.nil?
- return files.size
+ # Avoid counting empty dir placeholders
+ return files.reject{|_, name, size| name == '.' and size == 0}.size
end
seen_files = {}
- each_file_spec do |streamname, _, _, filename|
+ each_file_spec do |streamname, _, filesize, filename|
+ # Avoid counting empty dir placeholders
+ next if filename == "." and filesize == 0
seen_files[[streamname, filename]] = true
return stop_after if (seen_files.size >= stop_after)
end
count = 0
word = words.shift
- count += 1 if word =~ STRICT_STREAM_TOKEN_REGEXP and word !~ /\/\.\.?(\/|$)/
+ raise ArgumentError.new "Manifest invalid for stream #{line_count}: >8-bit encoded chars not allowed on stream token #{word.inspect}" if word =~ NON_8BIT_ENCODED_CHAR
+ unescaped_word = unescape(word)
+ count += 1 if word =~ STREAM_TOKEN_REGEXP and unescaped_word =~ STREAM_NAME_REGEXP and unescaped_word !~ /\/\.\.?(\/|$)/
raise ArgumentError.new "Manifest invalid for stream #{line_count}: missing or invalid stream name #{word.inspect if word}" if count != 1
count = 0
raise ArgumentError.new "Manifest invalid for stream #{line_count}: missing or invalid locator #{word.inspect if word}" if count == 0
count = 0
- while word =~ STRICT_FILE_TOKEN_REGEXP and ($~[1].split('/') & ['..','.']).empty?
+ raise ArgumentError.new "Manifest invalid for stream #{line_count}: >8-bit encoded chars not allowed on file token #{word.inspect}" if word =~ NON_8BIT_ENCODED_CHAR
+ while unescape(word) =~ EMPTY_DIR_TOKEN_REGEXP or
+ (word =~ FILE_TOKEN_REGEXP and unescape(word) =~ FILE_NAME_REGEXP and ($~[1].split('/') & ['..', '.']).empty?)
word = words.shift
count += 1
end
assert_equal(0, Keep::Manifest.new("").files_count)
end
+ def test_empty_dir_files_count
+ assert_equal(0,
+ Keep::Manifest.new("./empty_dir d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\n").files_count)
+ end
+
def test_empty_files_size
assert_equal(0, Keep::Manifest.new("").files_size)
end
[true, ". 00000000000000000000000000000000+0 0:0:0\n"],
[true, ". 00000000000000000000000000000000+0 0:0:d41d8cd98f00b204e9800998ecf8427e+0+Ad41d8cd98f00b204e9800998ecf8427e00000000@ffffffff\n"],
[true, ". d41d8cd98f00b204e9800998ecf8427e+0+Ad41d8cd98f00b204e9800998ecf8427e00000000@ffffffff 0:0:empty.txt\n"],
+ [true, "./empty_dir d41d8cd98f00b204e9800998ecf8427e+0 0:0:.\n"],
[false, '. d41d8cd98f00b204e9800998ecf8427e 0:0:abc.txt',
"Invalid manifest: does not end with newline"],
[false, "abc d41d8cd98f00b204e9800998ecf8427e 0:0:abc.txt\n",
"invalid stream name \"./abc/..\""],
[false, "./abc/./foo d41d8cd98f00b204e9800998ecf8427e 0:0:abc.txt\n",
"invalid stream name \"./abc/./foo\""],
- [false, ". d41d8cd98f00b204e9800998ecf8427e 0:0:.\n",
- "invalid file token \"0:0:.\""],
+ # non-empty '.'-named file tokens aren't acceptable. Empty ones are used as empty dir placeholders.
+ [false, ". 8cf8463b34caa8ac871a52d5dd7ad1ef+1 0:1:.\n",
+ "invalid file token \"0:1:.\""],
[false, ". d41d8cd98f00b204e9800998ecf8427e 0:0:..\n",
"invalid file token \"0:0:..\""],
[false, ". d41d8cd98f00b204e9800998ecf8427e 0:0:./abc.txt\n",
"Manifest invalid for stream 1: invalid file token \"0:0:foo//bar.txt\""],
[false, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo/\n",
"Manifest invalid for stream 1: invalid file token \"0:0:foo/\""],
+ # escaped chars
+ [true, "./empty_dir d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\n"],
+ [false, "./empty_dir d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\\056\n",
+ "Manifest invalid for stream 1: invalid file token \"0:0:\\\\056\\\\056\""],
+ [false, "./empty_dir d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\\056\\057foo\n",
+ "Manifest invalid for stream 1: invalid file token \"0:0:\\\\056\\\\056\\\\057foo\""],
+ [false, "./empty_dir d41d8cd98f00b204e9800998ecf8427e+0 0\\0720\\072foo\n",
+ "Manifest invalid for stream 1: invalid file token \"0\\\\0720\\\\072foo\""],
+ [false, "./empty_dir d41d8cd98f00b204e9800998ecf8427e+0 \\060:\\060:foo\n",
+ "Manifest invalid for stream 1: invalid file token \"\\\\060:\\\\060:foo\""],
+ [true, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\\057bar\n"],
+ [true, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\072\n"],
+ [true, ".\\057Data d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n"],
+ [true, "\\056\\057Data d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n"],
+ [true, "./\\134444 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n"],
+ [false, "./\\\\444 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \"./\\\\\\\\444\""],
+ [true, "./\\011foo d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n"],
+ [false, "./\\011/.. d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \"./\\\\011/..\""],
+ [false, ".\\056\\057 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \".\\\\056\\\\057\""],
+ [false, ".\\057\\056 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \".\\\\057\\\\056\""],
+ [false, ".\\057Data d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\\444\n",
+ "Manifest invalid for stream 1: >8-bit encoded chars not allowed on file token \"0:0:foo\\\\444\""],
+ [false, "./\\444 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: >8-bit encoded chars not allowed on stream token \"./\\\\444\""],
+ [false, "./\tfoo d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \"./\\tfoo\""],
+ [false, "./foo\\ d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \"./foo\\\\\""],
+ [false, "./foo\\r d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \"./foo\\\\r\""],
+ [false, "./foo\\444 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: >8-bit encoded chars not allowed on stream token \"./foo\\\\444\""],
+ [false, "./foo\\888 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \"./foo\\\\888\""],
+ [false, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\\\n",
+ "Manifest invalid for stream 1: invalid file token \"0:0:foo\\\\\""],
+ [false, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\\r\n",
+ "Manifest invalid for stream 1: invalid file token \"0:0:foo\\\\r\""],
+ [false, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\\444\n",
+ "Manifest invalid for stream 1: >8-bit encoded chars not allowed on file token \"0:0:foo\\\\444\""],
+ [false, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\\888\n",
+ "Manifest invalid for stream 1: invalid file token \"0:0:foo\\\\888\""],
+ [false, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\\057/bar\n",
+ "Manifest invalid for stream 1: invalid file token \"0:0:foo\\\\057/bar\""],
+ [false, ".\\057/Data d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n",
+ "Manifest invalid for stream 1: missing or invalid stream name \".\\\\057/Data\""],
+ [true, "./Data\\040Folder d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo\n"],
+ [false, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\057foo/bar\n",
+ "Manifest invalid for stream 1: invalid file token \"0:0:\\\\057foo/bar\""],
+ [true, ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\134057foo/bar\n"],
+ [false, ". d41d8cd98f00b204e9800998ecf8427e+0 \\040:\\040:foo.txt\n",
+ "Manifest invalid for stream 1: invalid file token \"\\\\040:\\\\040:foo.txt\""],
].each do |ok, manifest, expected_error=nil|
define_method "test_validate manifest #{manifest.inspect}" do
assert_equal ok, Keep::Manifest.valid?(manifest)
@redirect_to = root_path
if params.has_key?(:return_to)
- return send_api_token_to(params[:return_to], user)
+ # return_to param's format is 'remote,return_to_url'. This comes from login()
+ # encoding the remote=zbbbb parameter passed by a client asking for a salted
+ # token.
+ remote, return_to_url = params[:return_to].split(',', 2)
+ if remote !~ /^[0-9a-z]{5}$/ && remote != ""
+ return send_error 'Invalid remote cluster id', status: 400
+ end
+ remote = nil if remote == ''
+ return send_api_token_to(return_to_url, user, remote)
end
redirect_to @redirect_to
end
# to save the return_to parameter (if it exists; see the application
# controller). /auth/joshid bypasses the application controller.
def login
- auth_provider = if params[:auth_provider] then "auth_provider=#{CGI.escape(params[:auth_provider])}" else "" end
-
+ if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
+ return send_error 'Invalid remote cluster id', status: 400
+ end
if current_user and params[:return_to]
# Already logged in; just need to send a token to the requesting
# API client.
# FIXME: if current_user has never authorized this app before,
# ask for confirmation here!
- send_api_token_to(params[:return_to], current_user)
- elsif params[:return_to]
- redirect_to "/auth/joshid?return_to=#{CGI.escape(params[:return_to])}&#{auth_provider}"
- else
- redirect_to "/auth/joshid?#{auth_provider}"
+ return send_api_token_to(params[:return_to], current_user, params[:remote])
end
+ p = []
+ p << "auth_provider=#{CGI.escape(params[:auth_provider])}" if params[:auth_provider]
+ if params[:return_to]
+ # Encode remote param inside callback's return_to, so that we'll get it on
+ # create() after login.
+ remote_param = params[:remote].nil? ? '' : params[:remote]
+ p << "return_to=#{CGI.escape(remote_param + ',' + params[:return_to])}"
+ end
+ redirect_to "/auth/joshid?#{p.join('&')}"
end
- def send_api_token_to(callback_url, user)
+ def send_api_token_to(callback_url, user, remote=nil)
# Give the API client a token for making API calls on behalf of
# the authenticated user
find_or_create_by(url_prefix: api_client_url_prefix)
end
- api_client_auth = ApiClientAuthorization.
+ @api_client_auth = ApiClientAuthorization.
new(user: user,
api_client: @api_client,
created_by_ip_address: remote_ip,
scopes: ["all"])
- api_client_auth.save!
+ @api_client_auth.save!
if callback_url.index('?')
callback_url += '&'
else
callback_url += '?'
end
- callback_url += 'api_token=' + api_client_auth.token
+ if remote.nil?
+ token = @api_client_auth.token
+ else
+ token = @api_client_auth.salted_token(remote: remote)
+ end
+ callback_url += 'api_token=' + token
redirect_to callback_url
end
'v2/' + uuid + '/' + api_token
end
+ def salted_token(remote:)
+ if remote.nil?
+ token
+ end
+ 'v2/' + uuid + '/' + OpenSSL::HMAC.hexdigest('sha1', api_token, remote)
+ end
+
protected
def permission_to_create
assert_not_nil assigns(:api_client)
end
+ test "login with remote param returns a salted token" do
+ authorize_with :inactive
+ api_client_page = 'http://client.example.com/home'
+ remote_prefix = 'zbbbb'
+ get :login, return_to: api_client_page, remote: remote_prefix
+ assert_response :redirect
+ api_client_auth = assigns(:api_client_auth)
+ assert_not_nil api_client_auth
+ assert_includes(@response.redirect_url, 'api_token='+api_client_auth.salted_token(remote: remote_prefix))
+ end
+
+ test "login with malformed remote param returns an error" do
+ authorize_with :inactive
+ api_client_page = 'http://client.example.com/home'
+ remote_prefix = 'invalid_cluster_id'
+ get :login, return_to: api_client_page, remote: remote_prefix
+ assert_response 400
+ end
end
require 'test_helper'
class UserSessionsApiTest < ActionDispatch::IntegrationTest
- def client_url
- 'https://wb.example.com'
+ # remote prefix & return url packed into the return_to param passed around
+ # between API and SSO provider.
+ def client_url(remote: nil)
+ url = ',https://wb.example.com'
+ url = "#{remote}#{url}" unless remote.nil?
+ url
end
- def mock_auth_with(email: nil, username: nil, identity_url: nil)
+ def mock_auth_with(email: nil, username: nil, identity_url: nil, remote: nil, expected_response: :redirect)
mock = {
'provider' => 'josh_id',
'uid' => 'https://edward.example.com',
mock['info']['username'] = username unless username.nil?
mock['info']['identity_url'] = identity_url unless identity_url.nil?
post('/auth/josh_id/callback',
- {return_to: client_url},
+ {return_to: client_url(remote: remote)},
{'omniauth.auth' => mock})
- assert_response :redirect, 'Did not redirect to client with token'
+
+ errors = {
+ :redirect => 'Did not redirect to client with token',
+ 400 => 'Did not return Bad Request error',
+ }
+ assert_response expected_response, errors[expected_response]
end
test 'assign username from sso' do
test 'create new user during omniauth callback' do
mock_auth_with(email: 'edward@example.com')
- assert_equal(0, @response.redirect_url.index(client_url),
+ assert_equal(0, @response.redirect_url.index(client_url.split(',', 2)[1]),
'Redirected to wrong address after succesful login: was ' +
- @response.redirect_url + ', expected ' + client_url + '[...]')
+ @response.redirect_url + ', expected ' + client_url.split(',', 2)[1] + '[...]')
assert_not_nil(@response.redirect_url.index('api_token='),
'Expected api_token in query string of redirect url ' +
@response.redirect_url)
end
+ test 'issue salted token from omniauth callback with remote param' do
+ mock_auth_with(email: 'edward@example.com', remote: 'zbbbb')
+ api_client_auth = assigns(:api_client_auth)
+ assert_not_nil api_client_auth
+ assert_includes(@response.redirect_url, 'api_token=' + api_client_auth.salted_token(remote: 'zbbbb'))
+ end
+
+ test 'error out from omniauth callback with invalid remote param' do
+ mock_auth_with(email: 'edward@example.com', remote: 'invalid_cluster_id', expected_response: 400)
+ end
+
# Test various combinations of auto_setup configuration and email
# address provided during a new user's first session setup.
[{result: :nope, email: nil, cfg: {auto: true, repo: true, vm: true}},
self.assertIn(self.testcollection,
llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
self.assertIn(self.test_project, mount_ls)
- self.assertIn(self.test_project,
+ self.assertIn(self.test_project,
llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
with self.assertRaises(OSError):
r'\./testdir 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
self.pool.apply(fuseRmTestHelperDeleteFile, (self.mounttmp,))
- # Can't have empty directories :-( so manifest will be empty.
+ # Empty directories are represented by an empty file named "."
collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
- self.assertEqual(collection2["manifest_text"], "")
+ self.assertRegexpMatches(collection2["manifest_text"],
+ r'./testdir d41d8cd98f00b204e9800998ecf8427e\+0\+A\S+ 0:0:\\056\n')
self.pool.apply(fuseRmTestHelperRmdir, (self.mounttmp,))
collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
self.assertRegexpMatches(collection2["manifest_text"],
- r'\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt$')
+ r'\. 86fb269d190d2c85f6e0468ceca42a20\+12\+A\S+ 0:12:file1\.txt\n\./testdir d41d8cd98f00b204e9800998ecf8427e\+0\+A\S+ 0:0:\\056\n')
def fuseRenameTestHelper(mounttmp):