mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-926: Merge support for importing connections via CSV/JSON/YAML.
This commit is contained in:
22
doc/licenses/base64-js-1.5.1/LICENSE
Normal file
22
doc/licenses/base64-js-1.5.1/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jameson Little
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Footer
|
8
doc/licenses/base64-js-1.5.1/README
Normal file
8
doc/licenses/base64-js-1.5.1/README
Normal file
@@ -0,0 +1,8 @@
|
||||
base64-js (https://github.com/beatgammit/base64-js)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 1.5.1
|
||||
From: 'Jameson Little' (https://github.com/beatgammit/)
|
||||
License(s):
|
||||
MIT (bundled/base640-js-1.5.1/LICENSE)
|
||||
|
1
doc/licenses/base64-js-1.5.1/dep-coordinates.txt
Normal file
1
doc/licenses/base64-js-1.5.1/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
base64-js:1.5.1
|
21
doc/licenses/buffer-4.9.2/LICENSE
Normal file
21
doc/licenses/buffer-4.9.2/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Feross Aboukhadijeh, and other contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
7
doc/licenses/buffer-4.9.2/README
Normal file
7
doc/licenses/buffer-4.9.2/README
Normal file
@@ -0,0 +1,7 @@
|
||||
buffer (https://github.com/feross/buffer)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 4.9.2
|
||||
From: 'Feross Aboukhadijeh' (https://github.com/feross)
|
||||
License(s):
|
||||
MIT (bundled/buffer-4.9.2/LICENSE)
|
1
doc/licenses/buffer-4.9.2/dep-coordinates.txt
Normal file
1
doc/licenses/buffer-4.9.2/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
buffer:4.9.2
|
19
doc/licenses/core-util-is-1.0.3/LICENSE
Normal file
19
doc/licenses/core-util-is-1.0.3/LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright Node.js contributors. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
7
doc/licenses/core-util-is-1.0.3/README
Normal file
7
doc/licenses/core-util-is-1.0.3/README
Normal file
@@ -0,0 +1,7 @@
|
||||
core-util-is (https://github.com/isaacs/core-util-is)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 1.0.3
|
||||
From: 'Node.js contributors'
|
||||
License(s):
|
||||
MIT (bundled/core-util-is-1.0.3/LICENSE)
|
1
doc/licenses/core-util-is-1.0.3/dep-coordinates.txt
Normal file
1
doc/licenses/core-util-is-1.0.3/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
core-util-is:1.0.3
|
21
doc/licenses/csv-6.2.5/LICENSE
Normal file
21
doc/licenses/csv-6.2.5/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010 Adaltas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
7
doc/licenses/csv-6.2.5/README
Normal file
7
doc/licenses/csv-6.2.5/README
Normal file
@@ -0,0 +1,7 @@
|
||||
node-csv (https://github.com/adaltas/node-csv)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 6.2.5
|
||||
From: 'Adaltas' (https://github.com/adaltas)
|
||||
License(s):
|
||||
MIT (bundled/csv-6.2.5/LICENSE)
|
2
doc/licenses/csv-6.2.5/dep-coordinates.txt
Normal file
2
doc/licenses/csv-6.2.5/dep-coordinates.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
csv:6.2.5
|
||||
csv-parse:5.3.3
|
22
doc/licenses/events-3.3.0/LICENSE
Normal file
22
doc/licenses/events-3.3.0/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT
|
||||
|
||||
Copyright Joyent, Inc. and other Node contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
7
doc/licenses/events-3.3.0/README
Normal file
7
doc/licenses/events-3.3.0/README
Normal file
@@ -0,0 +1,7 @@
|
||||
events (https://github.com/browserify/events)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 3.3.0
|
||||
From: 'Node.js contributors, Joyent, Inc., and other Node contributors'
|
||||
License(s):
|
||||
MIT (bundled/events-3.3.0/LICENSE)
|
1
doc/licenses/events-3.3.0/dep-coordinates.txt
Normal file
1
doc/licenses/events-3.3.0/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
events:3.3.0
|
11
doc/licenses/ieee754-1.2.1/LICENSE
Normal file
11
doc/licenses/ieee754-1.2.1/LICENSE
Normal file
@@ -0,0 +1,11 @@
|
||||
Copyright 2008 Fair Oaks Labs, Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
7
doc/licenses/ieee754-1.2.1/README
Normal file
7
doc/licenses/ieee754-1.2.1/README
Normal file
@@ -0,0 +1,7 @@
|
||||
ieee754 (https://github.com/feross/ieee754)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 1.2.1
|
||||
From: 'Fair Oaks Labs, Inc'
|
||||
License(s):
|
||||
MIT (bundled/ieee754-1.2.1/LICENSE)
|
1
doc/licenses/ieee754-1.2.1/dep-coordinates.txt
Normal file
1
doc/licenses/ieee754-1.2.1/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
ieee754:1.2.1
|
15
doc/licenses/inherits-2.0.4/LICENSE
Normal file
15
doc/licenses/inherits-2.0.4/LICENSE
Normal file
@@ -0,0 +1,15 @@
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
8
doc/licenses/inherits-2.0.4/README
Normal file
8
doc/licenses/inherits-2.0.4/README
Normal file
@@ -0,0 +1,8 @@
|
||||
inherits (https://github.com/isaacs/inherits)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 2.0.4
|
||||
From: 'Isaac Z. Schlueter' (https://github.com/isaacs)
|
||||
License(s):
|
||||
ISC (bundled/inherits-2.0.4/LICENSE)
|
||||
|
1
doc/licenses/inherits-2.0.4/dep-coordinates.txt
Normal file
1
doc/licenses/inherits-2.0.4/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
inherits:2.0.4
|
21
doc/licenses/isarray-1.0.0/LICENSE
Normal file
21
doc/licenses/isarray-1.0.0/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
8
doc/licenses/isarray-1.0.0/README
Normal file
8
doc/licenses/isarray-1.0.0/README
Normal file
@@ -0,0 +1,8 @@
|
||||
isarray (https://github.com/juliangruber/isarray)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 1.0.0
|
||||
From: 'Julian Gruber' (https://github.com/juliangruber)
|
||||
License(s):
|
||||
MIT (bundled/isarray-1.0.0/LICENSE)
|
||||
|
1
doc/licenses/isarray-1.0.0/dep-coordinates.txt
Normal file
1
doc/licenses/isarray-1.0.0/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
isarray:1.0.0
|
8
doc/licenses/process-nextick-args-2.0.1/README
Normal file
8
doc/licenses/process-nextick-args-2.0.1/README
Normal file
@@ -0,0 +1,8 @@
|
||||
process-nextick-args (https://github.com/calvinmetcalf/process-nextick-args)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 2.0.1
|
||||
From: 'Calvin Metcalf' (https://github.com/calvinmetcalf)
|
||||
License(s):
|
||||
MIT (bundled/process-nextick-args-2.0.1/license.md)
|
||||
|
@@ -0,0 +1 @@
|
||||
process-nextick-args:2.0.1
|
19
doc/licenses/process-nextick-args-2.0.1/license.md
Normal file
19
doc/licenses/process-nextick-args-2.0.1/license.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2015 Calvin Metcalf
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.**
|
47
doc/licenses/readable-stream-2.3.7/LICENSE
Normal file
47
doc/licenses/readable-stream-2.3.7/LICENSE
Normal file
@@ -0,0 +1,47 @@
|
||||
Node.js is licensed for use as follows:
|
||||
|
||||
"""
|
||||
Copyright Node.js contributors. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
This license applies to parts of Node.js originating from the
|
||||
https://github.com/joyent/node repository:
|
||||
|
||||
"""
|
||||
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
8
doc/licenses/readable-stream-2.3.7/README
Normal file
8
doc/licenses/readable-stream-2.3.7/README
Normal file
@@ -0,0 +1,8 @@
|
||||
readable-stream (https://github.com/nodejs/readable-stream)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 2.3.7
|
||||
From: 'Node.js contributors, Joyent, Inc., and other Node contributors'
|
||||
License(s):
|
||||
MIT (bundled/readable-stream-2.3.7/LICENSE)
|
||||
|
1
doc/licenses/readable-stream-2.3.7/dep-coordinates.txt
Normal file
1
doc/licenses/readable-stream-2.3.7/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
readable-stream:2.3.7
|
21
doc/licenses/safe-buffer-5.1.2/LICENSE
Normal file
21
doc/licenses/safe-buffer-5.1.2/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Feross Aboukhadijeh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
8
doc/licenses/safe-buffer-5.1.2/README
Normal file
8
doc/licenses/safe-buffer-5.1.2/README
Normal file
@@ -0,0 +1,8 @@
|
||||
safe-buffer (https://github.com/feross/safe-buffer)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 5.1.2
|
||||
From: 'Feross Aboukhadijeh' (https://github.com/feross)
|
||||
License(s):
|
||||
MIT (bundled/safe-buffer-5.1.2/LICENSE)
|
||||
|
1
doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt
Normal file
1
doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
safe-buffer:5.1.2
|
20
doc/licenses/setimmediate-1.0.5/LICENSE.txt
Normal file
20
doc/licenses/setimmediate-1.0.5/LICENSE.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
doc/licenses/setimmediate-1.0.5/README
Normal file
8
doc/licenses/setimmediate-1.0.5/README
Normal file
@@ -0,0 +1,8 @@
|
||||
setImmediate.js (https://github.com/YuzuJS/setImmediate)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 1.0.5
|
||||
From: 'Yuzu (by Barnes & Noble Education)' (https://github.com/YuzuJS)
|
||||
License(s):
|
||||
MIT (bundled/setimmediate-1.0.5/LICENSE.txt)
|
||||
|
1
doc/licenses/setimmediate-1.0.5/dep-coordinates.txt
Normal file
1
doc/licenses/setimmediate-1.0.5/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
setimmediate:1.0.5
|
20
doc/licenses/stream-browserify-2.0.2/LICENSE
Normal file
20
doc/licenses/stream-browserify-2.0.2/LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) James Halliday
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
doc/licenses/stream-browserify-2.0.2/README
Normal file
8
doc/licenses/stream-browserify-2.0.2/README
Normal file
@@ -0,0 +1,8 @@
|
||||
stream-browserify (https://github.com/browserify/stream-browserify)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 2.0.2
|
||||
From: 'James Halliday'
|
||||
License(s):
|
||||
MIT (bundled/stream-browserify-2.0.2/LICENSE)
|
||||
|
1
doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt
Normal file
1
doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
stream-browserify:2.0.2
|
47
doc/licenses/string_decoder-1.1.1/LICENSE
Normal file
47
doc/licenses/string_decoder-1.1.1/LICENSE
Normal file
@@ -0,0 +1,47 @@
|
||||
Node.js is licensed for use as follows:
|
||||
|
||||
"""
|
||||
Copyright Node.js contributors. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
This license applies to parts of Node.js originating from the
|
||||
https://github.com/joyent/node repository:
|
||||
|
||||
"""
|
||||
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
8
doc/licenses/string_decoder-1.1.1/README
Normal file
8
doc/licenses/string_decoder-1.1.1/README
Normal file
@@ -0,0 +1,8 @@
|
||||
string_decoder (https://github.com/nodejs/string_decoder)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 1.1.1
|
||||
From: 'Node.js contributors, Joyent, Inc., and other Node contributors'
|
||||
License(s):
|
||||
MIT (bundled/string_decoder-1.1.1/LICENSE)
|
||||
|
1
doc/licenses/string_decoder-1.1.1/dep-coordinates.txt
Normal file
1
doc/licenses/string_decoder-1.1.1/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
string_decoder:1.1.1
|
23
doc/licenses/timers-browserify-2.0.12/LICENSE.md
Normal file
23
doc/licenses/timers-browserify-2.0.12/LICENSE.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# timers-browserify
|
||||
|
||||
This project uses the [MIT](http://jryans.mit-license.org/) license:
|
||||
|
||||
Copyright © 2012 J. Ryan Stinnett <jryans@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the “Software”),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
8
doc/licenses/timers-browserify-2.0.12/README
Normal file
8
doc/licenses/timers-browserify-2.0.12/README
Normal file
@@ -0,0 +1,8 @@
|
||||
timers-browserify (https://github.com/browserify/timers-browserify)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 2.0.12
|
||||
From: 'J. Ryan Stinnett' (https://github.com/jryans)
|
||||
License(s):
|
||||
MIT (bundled/timers-browserify-2.0.12/LICENSE.md)
|
||||
|
@@ -0,0 +1 @@
|
||||
timers-browserify:2.0.12
|
24
doc/licenses/util-deprecate-1.0.2/LICENSE
Normal file
24
doc/licenses/util-deprecate-1.0.2/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 Nathan Rajlich <nathan@tootallnate.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
8
doc/licenses/util-deprecate-1.0.2/README
Normal file
8
doc/licenses/util-deprecate-1.0.2/README
Normal file
@@ -0,0 +1,8 @@
|
||||
util-deprecate (https://github.com/TooTallNate/util-deprecate)
|
||||
---------------------------------------------
|
||||
|
||||
Version: 1.0.2
|
||||
From: 'Nathan Rajlich' (https://github.com/TooTallNate)
|
||||
License(s):
|
||||
MIT (bundled/util-deprecate-1.0.2/LICENSE)
|
||||
|
1
doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt
Normal file
1
doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
util-deprecate:1.0.2
|
@@ -45,6 +45,7 @@ import org.apache.guacamole.auth.jdbc.security.SaltService;
|
||||
import org.apache.guacamole.auth.jdbc.security.SecureRandomSaltService;
|
||||
import org.apache.guacamole.auth.jdbc.permission.SystemPermissionService;
|
||||
import org.apache.guacamole.auth.jdbc.user.UserService;
|
||||
import org.apache.ibatis.session.ExecutorType;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.apache.guacamole.auth.jdbc.permission.ConnectionGroupPermissionMapper;
|
||||
import org.apache.guacamole.auth.jdbc.permission.ConnectionGroupPermissionService;
|
||||
@@ -126,6 +127,11 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule {
|
||||
// Transaction factory
|
||||
bindTransactionFactoryType(JdbcTransactionFactory.class);
|
||||
|
||||
// Set the JDBC Auth provider to use batch execution when possible
|
||||
bindConfigurationSetting(configuration -> {
|
||||
configuration.setDefaultExecutorType(ExecutorType.BATCH);
|
||||
});
|
||||
|
||||
// Add MyBatis mappers
|
||||
addMapperClass(ConnectionMapper.class);
|
||||
addMapperClass(ConnectionGroupMapper.class);
|
||||
|
@@ -25,16 +25,14 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
|
||||
import org.apache.guacamole.auth.jdbc.base.JDBCDirectory;
|
||||
import org.apache.guacamole.net.auth.ActiveConnection;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
|
||||
/**
|
||||
* Implementation of a Directory which contains all currently-active
|
||||
* connections.
|
||||
*/
|
||||
public class ActiveConnectionDirectory extends RestrictedObject
|
||||
implements Directory<ActiveConnection> {
|
||||
public class ActiveConnectionDirectory extends JDBCDirectory<ActiveConnection> {
|
||||
|
||||
/**
|
||||
* Service for retrieving and manipulating active connections.
|
||||
|
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.auth.jdbc.base;
|
||||
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.net.auth.AtomicDirectoryOperation;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
import org.apache.guacamole.net.auth.Identifiable;
|
||||
import org.mybatis.guice.transactional.Transactional;
|
||||
|
||||
/**
|
||||
* An implementation of Directory that uses database transactions to guarantee
|
||||
* atomicity for any operations supplied to tryAtomically().
|
||||
*/
|
||||
public abstract class JDBCDirectory<ObjectType extends Identifiable>
|
||||
extends RestrictedObject implements Directory<ObjectType> {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void tryAtomically(AtomicDirectoryOperation<ObjectType> operation)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Execute the operation atomically - the @Transactional annotation
|
||||
// specifies that the entire operation will be performed in a transaction
|
||||
operation.executeOperation(true, this);
|
||||
|
||||
}
|
||||
}
|
@@ -25,17 +25,15 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
|
||||
import org.apache.guacamole.auth.jdbc.base.JDBCDirectory;
|
||||
import org.apache.guacamole.net.auth.Connection;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
import org.mybatis.guice.transactional.Transactional;
|
||||
|
||||
/**
|
||||
* Implementation of the Connection Directory which is driven by an underlying,
|
||||
* arbitrary database.
|
||||
*/
|
||||
public class ConnectionDirectory extends RestrictedObject
|
||||
implements Directory<Connection> {
|
||||
public class ConnectionDirectory extends JDBCDirectory<Connection> {
|
||||
|
||||
/**
|
||||
* Service for managing connection objects.
|
||||
|
@@ -25,17 +25,15 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
|
||||
import org.apache.guacamole.auth.jdbc.base.JDBCDirectory;
|
||||
import org.apache.guacamole.net.auth.ConnectionGroup;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
import org.mybatis.guice.transactional.Transactional;
|
||||
|
||||
/**
|
||||
* Implementation of the ConnectionGroup Directory which is driven by an
|
||||
* underlying, arbitrary database.
|
||||
*/
|
||||
public class ConnectionGroupDirectory extends RestrictedObject
|
||||
implements Directory<ConnectionGroup> {
|
||||
public class ConnectionGroupDirectory extends JDBCDirectory<ConnectionGroup> {
|
||||
|
||||
/**
|
||||
* Service for managing connection group objects.
|
||||
|
@@ -24,8 +24,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
import org.apache.guacamole.auth.jdbc.base.JDBCDirectory;
|
||||
import org.apache.guacamole.net.auth.SharingProfile;
|
||||
import org.mybatis.guice.transactional.Transactional;
|
||||
|
||||
@@ -33,8 +32,7 @@ import org.mybatis.guice.transactional.Transactional;
|
||||
* Implementation of the SharingProfile Directory which is driven by an
|
||||
* underlying, arbitrary database.
|
||||
*/
|
||||
public class SharingProfileDirectory extends RestrictedObject
|
||||
implements Directory<SharingProfile> {
|
||||
public class SharingProfileDirectory extends JDBCDirectory<SharingProfile> {
|
||||
|
||||
/**
|
||||
* Service for managing sharing profile objects.
|
||||
|
@@ -25,8 +25,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
import org.apache.guacamole.auth.jdbc.base.JDBCDirectory;
|
||||
import org.apache.guacamole.net.auth.User;
|
||||
import org.mybatis.guice.transactional.Transactional;
|
||||
|
||||
@@ -34,8 +33,7 @@ import org.mybatis.guice.transactional.Transactional;
|
||||
* Implementation of the User Directory which is driven by an underlying,
|
||||
* arbitrary database.
|
||||
*/
|
||||
public class UserDirectory extends RestrictedObject
|
||||
implements Directory<User> {
|
||||
public class UserDirectory extends JDBCDirectory<User> {
|
||||
|
||||
/**
|
||||
* Service for managing user objects.
|
||||
|
@@ -24,8 +24,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
import org.apache.guacamole.auth.jdbc.base.JDBCDirectory;
|
||||
import org.apache.guacamole.net.auth.UserGroup;
|
||||
import org.mybatis.guice.transactional.Transactional;
|
||||
|
||||
@@ -33,8 +32,7 @@ import org.mybatis.guice.transactional.Transactional;
|
||||
* Implementation of the UserGroup Directory which is driven by an underlying,
|
||||
* arbitrary database.
|
||||
*/
|
||||
public class UserGroupDirectory extends RestrictedObject
|
||||
implements Directory<UserGroup> {
|
||||
public class UserGroupDirectory extends JDBCDirectory<UserGroup> {
|
||||
|
||||
/**
|
||||
* Service for managing user group objects.
|
||||
|
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.net.auth;
|
||||
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
|
||||
/**
|
||||
* An operation that should be attempted atomically when passed to
|
||||
* {@link Directory#tryAtomically}, if atomic operations are supported by
|
||||
* the Directory.
|
||||
*/
|
||||
public interface AtomicDirectoryOperation<ObjectType extends Identifiable> {
|
||||
|
||||
/**
|
||||
* Attempt the operation atomically. If the Directory does not support
|
||||
* atomic operations, the atomic flag will be set to false. If the atomic
|
||||
* flag is set to true, the provided directory is guaranteed to perform
|
||||
* the operations within this function atomically. Atomicity of the
|
||||
* provided directory outside this function, or of the directory invoking
|
||||
* this function are not guaranteed.
|
||||
*
|
||||
* <p>NOTE: If atomicity is required for this operation, a
|
||||
* GuacamoleException may be thrown by this function before any changes are
|
||||
* made, ensuring the operation will only ever be performed atomically.
|
||||
*
|
||||
* @param atomic
|
||||
* True if the provided directory is guaranteed to perform the operation
|
||||
* atomically within the context of this function.
|
||||
*
|
||||
* @param directory
|
||||
* A directory that will perform the operation atomically if the atomic
|
||||
* flag is set to true. If the flag is false, the directory may still
|
||||
* be used, though atomicity is not guaranteed.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an issue occurs during the operation.
|
||||
*/
|
||||
void executeOperation(boolean atomic, Directory<ObjectType> directory)
|
||||
throws GuacamoleException;
|
||||
}
|
@@ -90,4 +90,10 @@ public class DelegatingDirectory<ObjectType extends Identifiable>
|
||||
directory.remove(identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tryAtomically(AtomicDirectoryOperation<ObjectType> operation)
|
||||
throws GuacamoleException {
|
||||
directory.tryAtomically(operation);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -215,8 +215,29 @@ public interface Directory<ObjectType extends Identifiable> {
|
||||
* @param identifier The identifier of the object to remove.
|
||||
*
|
||||
* @throws GuacamoleException If an error occurs while removing the object,
|
||||
* or if removing object is not allowed.
|
||||
* or if removing the object is not allowed.
|
||||
*/
|
||||
void remove(String identifier) throws GuacamoleException;
|
||||
|
||||
/**
|
||||
* Attempt to perform the provided operation atomically if possible. If the
|
||||
* operation can be performed atomically, the atomic flag will be set to
|
||||
* true, and the directory passed to the provided operation callback will
|
||||
* peform directory operations atomically within the operation callback.
|
||||
*
|
||||
* @param operation
|
||||
* The directory operation that should be performed atomically.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs during execution of the provided operation.
|
||||
*/
|
||||
default void tryAtomically(AtomicDirectoryOperation<ObjectType> operation)
|
||||
throws GuacamoleException {
|
||||
|
||||
// By default, perform the operation non-atomically. If atomic operation
|
||||
// is supported by an implementation, it must be implemented there.
|
||||
operation.executeOperation(false, this);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
3907
guacamole/src/main/frontend/package-lock.json
generated
3907
guacamole/src/main/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,18 @@
|
||||
"angular-translate-interpolation-messageformat": "^2.19.0",
|
||||
"angular-translate-loader-static-files": "^2.19.0",
|
||||
"blob-polyfill": ">=7.0.20220408",
|
||||
"csv": "^6.2.5",
|
||||
"datalist-polyfill": "^1.25.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"jquery": "^3.6.4",
|
||||
"jstz": "^2.1.1",
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.17.21",
|
||||
"yaml": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"babel-loader": "^8.3.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"closure-webpack-plugin": "^2.6.1",
|
||||
"copy-webpack-plugin": "^5.1.2",
|
||||
|
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which allows multiple files to be uploaded. Dragging files onto
|
||||
* the associated element will call the provided callback function with any
|
||||
* dragged files.
|
||||
*/
|
||||
angular.module('element').directive('guacDrop', ['$injector', function guacDrop($injector) {
|
||||
|
||||
// Required services
|
||||
const guacNotification = $injector.get('guacNotification');
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacDrop($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* The function to call whenever files are dragged. The callback is
|
||||
* provided a single parameter: the FileList containing all dragged
|
||||
* files.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const guacDrop = $scope.$eval($attrs.guacDrop);
|
||||
|
||||
/**
|
||||
* Any number of space-seperated classes to be applied to the
|
||||
* element a drop is pending: when the user has dragged something
|
||||
* over the element, but not yet dropped. These classes will be
|
||||
* removed when a drop is not pending.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
const guacDraggedClass = $scope.$eval($attrs.guacDraggedClass);
|
||||
|
||||
/**
|
||||
* Whether upload of multiple files should be allowed. If false, an
|
||||
* error will be displayed explaining the restriction, otherwise
|
||||
* any number of files may be dragged. Defaults to true if not set.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
const guacMultiple = 'guacMultiple' in $attrs
|
||||
? $scope.$eval($attrs.guacMultiple) : true;
|
||||
|
||||
/**
|
||||
* The element which will register drag event.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Applies any classes provided in the guacDraggedClass attribute.
|
||||
* Further propagation and default behavior of the given event is
|
||||
* automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the in-progress drag/drop operation.
|
||||
*/
|
||||
const notifyDragStart = function notifyDragStart(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Skip further processing if no classes were provided
|
||||
if (!guacDraggedClass)
|
||||
return;
|
||||
|
||||
// Add each provided class
|
||||
guacDraggedClass.split(' ').forEach(classToApply =>
|
||||
element.classList.add(classToApply));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes any classes provided in the guacDraggedClass attribute.
|
||||
* Further propagation and default behavior of the given event is
|
||||
* automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the end of the drag/drop operation.
|
||||
*/
|
||||
const notifyDragEnd = function notifyDragEnd(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Skip further processing if no classes were provided
|
||||
if (!guacDraggedClass)
|
||||
return;
|
||||
|
||||
// Remove each provided class
|
||||
guacDraggedClass.split(' ').forEach(classToRemove =>
|
||||
element.classList.remove(classToRemove));
|
||||
|
||||
};
|
||||
|
||||
// Add listeners to the drop target to ensure that the visual state
|
||||
// stays up to date
|
||||
element.addEventListener('dragenter', notifyDragStart);
|
||||
element.addEventListener('dragover', notifyDragStart);
|
||||
element.addEventListener('dragleave', notifyDragEnd);
|
||||
|
||||
/**
|
||||
* Event listener that will be invoked if the user drops anything
|
||||
* onto the event. If a valid file is provided, the onFile callback
|
||||
* provided to this directive will be called; otherwise an error
|
||||
* will be displayed, if appropriate.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The drop event that triggered this handler.
|
||||
*/
|
||||
element.addEventListener('drop', e => {
|
||||
|
||||
notifyDragEnd(e);
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
|
||||
// Ignore any non-files that are dragged into the drop area
|
||||
if (files.length < 1)
|
||||
return;
|
||||
|
||||
// If multi-file upload is disabled, If more than one file was
|
||||
// provided, print an error explaining the problem
|
||||
if (!guacMultiple && files.length >= 2) {
|
||||
|
||||
guacNotification.showStatus({
|
||||
className : 'error',
|
||||
title : 'APP.DIALOG_HEADER_ERROR',
|
||||
text: { key : 'APP.ERROR_SINGLE_FILE_ONLY'},
|
||||
|
||||
// Add a button to hide the error
|
||||
actions : [{
|
||||
name : 'APP.ACTION_ACKNOWLEDGE',
|
||||
callback : () => guacNotification.showStatus(false)
|
||||
}]
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Invoke the callback with the files. Note that if guacMultiple
|
||||
// is set to false, this will always be a single file.
|
||||
guacDrop(files);
|
||||
|
||||
});
|
||||
|
||||
} // end guacDrop link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -18,9 +18,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which allows multiple files to be uploaded. Clicking on the
|
||||
* associated element will result in a file selector dialog, which then calls
|
||||
* the provided callback function with any chosen files.
|
||||
* A directive which allows files to be uploaded. Clicking on the associated
|
||||
* element will result in a file selector dialog, which then calls the provided
|
||||
* callback function with any chosen files.
|
||||
*/
|
||||
angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) {
|
||||
|
||||
@@ -36,32 +36,43 @@ angular.module('element').directive('guacUpload', ['$document', function guacUpl
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
var guacUpload = $scope.$eval($attrs.guacUpload);
|
||||
const guacUpload = $scope.$eval($attrs.guacUpload);
|
||||
|
||||
/**
|
||||
* The element which will register the drag gesture.
|
||||
* Whether upload of multiple files should be allowed. If false, the
|
||||
* file dialog will only allow a single file to be chosen at once,
|
||||
* otherwise any number of files may be chosen. Defaults to true if
|
||||
* not set.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
const guacMultiple = 'guacMultiple' in $attrs
|
||||
? $scope.$eval($attrs.guacMultiple) : true;
|
||||
|
||||
/**
|
||||
* The element which will register the click.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var element = $element[0];
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Internal form, containing a single file input element.
|
||||
*
|
||||
* @type HTMLFormElement
|
||||
*/
|
||||
var form = $document[0].createElement('form');
|
||||
const form = $document[0].createElement('form');
|
||||
|
||||
/**
|
||||
* Internal file input element.
|
||||
*
|
||||
* @type HTMLInputElement
|
||||
*/
|
||||
var input = $document[0].createElement('input');
|
||||
const input = $document[0].createElement('input');
|
||||
|
||||
// Init input element
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.multiple = guacMultiple;
|
||||
|
||||
// Add input element to internal form
|
||||
form.appendChild(input);
|
||||
|
@@ -0,0 +1,665 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* global _ */
|
||||
|
||||
/**
|
||||
* The allowed MIME type for CSV files.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
const CSV_MIME_TYPE = 'text/csv';
|
||||
|
||||
/**
|
||||
* The allowed MIME type for JSON files.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
const JSON_MIME_TYPE = 'application/json';
|
||||
|
||||
/**
|
||||
* The allowed MIME types for YAML files.
|
||||
* NOTE: There is no registered MIME type for YAML files. This may result in a
|
||||
* wide variety of possible browser-supplied MIME types.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
const YAML_MIME_TYPES = [
|
||||
'text/x-yaml',
|
||||
'text/yaml',
|
||||
'text/yml',
|
||||
'application/x-yaml',
|
||||
'application/x-yml',
|
||||
'application/yaml',
|
||||
'application/yml'
|
||||
];
|
||||
|
||||
/*
|
||||
* All file types supported for connection import.
|
||||
*
|
||||
* @type {String[]}
|
||||
*/
|
||||
const LEGAL_MIME_TYPES = [CSV_MIME_TYPE, JSON_MIME_TYPE, ...YAML_MIME_TYPES];
|
||||
|
||||
/**
|
||||
* The controller for the connection import page.
|
||||
*/
|
||||
angular.module('import').controller('importConnectionsController', ['$scope', '$injector',
|
||||
function importConnectionsController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
const $location = $injector.get('$location');
|
||||
const $q = $injector.get('$q');
|
||||
const $routeParams = $injector.get('$routeParams');
|
||||
const connectionParseService = $injector.get('connectionParseService');
|
||||
const connectionService = $injector.get('connectionService');
|
||||
const guacNotification = $injector.get('guacNotification');
|
||||
const permissionService = $injector.get('permissionService');
|
||||
const userService = $injector.get('userService');
|
||||
const userGroupService = $injector.get('userGroupService');
|
||||
|
||||
// Required types
|
||||
const DirectoryPatch = $injector.get('DirectoryPatch');
|
||||
const Error = $injector.get('Error');
|
||||
const ParseError = $injector.get('ParseError');
|
||||
const PermissionSet = $injector.get('PermissionSet');
|
||||
const User = $injector.get('User');
|
||||
const UserGroup = $injector.get('UserGroup');
|
||||
|
||||
/**
|
||||
* The result of parsing the current upload, if successful.
|
||||
*
|
||||
* @type {ParseResult}
|
||||
*/
|
||||
$scope.parseResult = null;
|
||||
|
||||
/**
|
||||
* The failure associated with the current attempt to create connections
|
||||
* through the API, if any.
|
||||
*
|
||||
* @type {Error}
|
||||
*/
|
||||
$scope.patchFailure = null;;
|
||||
|
||||
/**
|
||||
* True if the file is fully uploaded and ready to be processed, or false
|
||||
* otherwise.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
$scope.dataReady = false;
|
||||
|
||||
/**
|
||||
* True if the file upload has been aborted mid-upload, or false otherwise.
|
||||
*/
|
||||
$scope.aborted = false;
|
||||
|
||||
/**
|
||||
* True if fully-uploaded data is being processed, or false otherwise.
|
||||
*/
|
||||
$scope.processing = false;
|
||||
|
||||
/**
|
||||
* The MIME type of the uploaded file, if any.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
$scope.mimeType = null;
|
||||
|
||||
/**
|
||||
* The raw string contents of the uploaded file, if any.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
$scope.fileData = null;
|
||||
|
||||
/**
|
||||
* The file reader currently being used to upload the file, if any. If
|
||||
* null, no file upload is currently in progress.
|
||||
*
|
||||
* @type {FileReader}
|
||||
*/
|
||||
$scope.fileReader = null;
|
||||
|
||||
/**
|
||||
* Clear all file upload state.
|
||||
*/
|
||||
function resetUploadState() {
|
||||
|
||||
$scope.aborted = false;
|
||||
$scope.dataReady = false;
|
||||
$scope.processing = false;
|
||||
$scope.fileData = null;
|
||||
$scope.mimeType = null;
|
||||
$scope.fileReader = null;
|
||||
$scope.parseResult = null;
|
||||
$scope.patchFailure = null;
|
||||
$scope.fileName = null;
|
||||
|
||||
}
|
||||
|
||||
// Indicate that data is currently being loaded / processed if the the file
|
||||
// has been provided but not yet fully uploaded, or if the the file is
|
||||
// fully loaded and is currently being processed.
|
||||
$scope.isLoading = () => (
|
||||
($scope.fileName && !$scope.dataReady && !$scope.patchFailure)
|
||||
|| $scope.processing);
|
||||
|
||||
/**
|
||||
* Create all users and user groups mentioned in the import file that don't
|
||||
* already exist in the current data source. If either creation fails, any
|
||||
* already-created entities will be cleaned up, and the returned promise
|
||||
* will be rejected.
|
||||
*
|
||||
* @param {ParseResult} parseResult
|
||||
* The result of parsing the user-supplied import file.
|
||||
*
|
||||
* @return {Promise.<Object>}
|
||||
* A promise resolving to an object containing the results of the calls
|
||||
* to create the users and groups.
|
||||
*/
|
||||
function createUsersAndGroups(parseResult) {
|
||||
|
||||
const dataSource = $routeParams.dataSource;
|
||||
|
||||
return $q.all({
|
||||
existingUsers : userService.getUsers(dataSource),
|
||||
existingGroups : userGroupService.getUserGroups(dataSource)
|
||||
}).then(({existingUsers, existingGroups}) => {
|
||||
|
||||
const userPatches = Object.keys(parseResult.users)
|
||||
|
||||
// Filter out any existing users
|
||||
.filter(identifier => !existingUsers[identifier])
|
||||
|
||||
// A patch to create each new user
|
||||
.map(username => new DirectoryPatch({
|
||||
op: 'add',
|
||||
path: '/',
|
||||
value: new User({ username })
|
||||
}));
|
||||
|
||||
const groupPatches = Object.keys(parseResult.groups)
|
||||
|
||||
// Filter out any existing groups
|
||||
.filter(identifier => !existingGroups[identifier])
|
||||
|
||||
// A patch to create each new user group
|
||||
.map(identifier => new DirectoryPatch({
|
||||
op: 'add',
|
||||
path: '/',
|
||||
value: new UserGroup({ identifier })
|
||||
}));
|
||||
|
||||
// First, create any required users and groups, automatically cleaning
|
||||
// up any created already-created entities if a call fails.
|
||||
// NOTE: Generally we'd want to do these calls in parallel, using
|
||||
// `$q.all()`. However, `$q.all()` rejects immediately if any of the
|
||||
// wrapped promises reject, so the users may not be ready for cleanup
|
||||
// at the time that the group promise rejects, or vice versa. While
|
||||
// it would be possible to juggle promises and still do these calls
|
||||
// in parallel, the code gets pretty complex, so for readability and
|
||||
// simplicity, they are executed serially. The performance cost of
|
||||
// doing so should be low.
|
||||
return userService.patchUsers(dataSource, userPatches).then(userResponse => {
|
||||
|
||||
// Then, if that succeeds, create any required groups
|
||||
return userGroupService.patchUserGroups(dataSource, groupPatches).then(
|
||||
|
||||
// If user group creation succeeds, resolve the returned promise
|
||||
userGroupResponse => ({ userResponse, userGroupResponse}))
|
||||
|
||||
// If the group creation request fails, clean up any created users
|
||||
.catch(groupFailure => {
|
||||
cleanUpUsers(userResponse);
|
||||
return groupFailure;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant read permissions for each user and group in the supplied parse
|
||||
* result to each connection in their connection list. Note that there will
|
||||
* be a seperate request for each user and group.
|
||||
*
|
||||
* @param {ParseResult} parseResult
|
||||
* The result of successfully parsing a user-supplied import file.
|
||||
*
|
||||
* @param {Object} response
|
||||
* The response from the PATCH API request.
|
||||
*
|
||||
* @returns {Promise.<Object>}
|
||||
* A promise that will resolve with the result of every permission
|
||||
* granting request.
|
||||
*/
|
||||
function grantConnectionPermissions(parseResult, response) {
|
||||
|
||||
const dataSource = $routeParams.dataSource;
|
||||
|
||||
// All connection grant requests, one per user/group
|
||||
const userRequests = {};
|
||||
const groupRequests = {};
|
||||
|
||||
// Create a PermissionSet granting access to all connections at
|
||||
// the provided indices within the provided parse result
|
||||
const createPermissionSet = indices =>
|
||||
new PermissionSet({ connectionPermissions: indices.reduce(
|
||||
(permissions, index) => {
|
||||
const connectionId = response.patches[index].identifier;
|
||||
permissions[connectionId] = [
|
||||
PermissionSet.ObjectPermissionType.READ];
|
||||
return permissions;
|
||||
}, {}) });
|
||||
|
||||
// Now that we've created all the users, grant access to each
|
||||
_.forEach(parseResult.users, (connectionIndices, identifier) =>
|
||||
|
||||
// Grant the permissions - note the group flag is `false`
|
||||
userRequests[identifier] = permissionService.patchPermissions(
|
||||
dataSource, identifier,
|
||||
|
||||
// Create the permissions to these connections for this user
|
||||
createPermissionSet(connectionIndices),
|
||||
|
||||
// Do not remove any permissions
|
||||
new PermissionSet(),
|
||||
|
||||
// This call is not for a group
|
||||
false));
|
||||
|
||||
// Now that we've created all the groups, grant access to each
|
||||
_.forEach(parseResult.groups, (connectionIndices, identifier) =>
|
||||
|
||||
// Grant the permissions - note the group flag is `true`
|
||||
groupRequests[identifier] = permissionService.patchPermissions(
|
||||
dataSource, identifier,
|
||||
|
||||
// Create the permissions to these connections for this user
|
||||
createPermissionSet(connectionIndices),
|
||||
|
||||
// Do not remove any permissions
|
||||
new PermissionSet(),
|
||||
|
||||
// This call is for a group
|
||||
true));
|
||||
|
||||
// Return the result from all the permission granting calls
|
||||
return $q.all({ ...userRequests, ...groupRequests });
|
||||
}
|
||||
|
||||
// Given a PATCH API response, create an array of patches to delete every
|
||||
// entity created in the original request that generated this response
|
||||
const createDeletionPatches = creationResponse =>
|
||||
creationResponse.patches.map(patch =>
|
||||
|
||||
// Add one deletion patch per original creation patch
|
||||
new DirectoryPatch({
|
||||
op: 'remove',
|
||||
path: '/' + patch.identifier
|
||||
}));
|
||||
|
||||
/**
|
||||
* Given a successful response to a connection PATCH request, make another
|
||||
* request to delete every created connection in the provided request, i.e.
|
||||
* clean up every connection that was created.
|
||||
*
|
||||
* @param {DirectoryPatchResponse} creationResponse
|
||||
* The response to the connection PATCH request.
|
||||
*
|
||||
* @returns {DirectoryPatchResponse}
|
||||
* The response to the PATCH deletion request.
|
||||
*/
|
||||
function cleanUpConnections(creationResponse) {
|
||||
|
||||
return connectionService.patchConnections(
|
||||
$routeParams.dataSource, createDeletionPatches(creationResponse));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a successful response to a user PATCH request, make another
|
||||
* request to delete every created user in the provided request.
|
||||
*
|
||||
* @param {DirectoryPatchResponse} creationResponse
|
||||
* The response to the user PATCH request.
|
||||
*
|
||||
* @returns {DirectoryPatchResponse}
|
||||
* The response to the PATCH deletion request.
|
||||
*/
|
||||
function cleanUpUsers(creationResponse) {
|
||||
|
||||
return userService.patchUsers(
|
||||
$routeParams.dataSource, createDeletionPatches(creationResponse));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a successfully parsed import file, creating any specified
|
||||
* connections, creating and granting permissions to any specified users
|
||||
* and user groups. If successful, the user will be shown a success message.
|
||||
* If not, any errors will be displayed and any already-created entities
|
||||
* will be rolled back.
|
||||
*
|
||||
* @param {ParseResult} parseResult
|
||||
* The result of parsing the user-supplied import file.
|
||||
*/
|
||||
function handleParseSuccess(parseResult) {
|
||||
|
||||
$scope.parseResult = parseResult;
|
||||
|
||||
// If errors were encounted during file parsing, abort further
|
||||
// processing - the user will have a chance to fix the errors and try
|
||||
// again
|
||||
if (parseResult.hasErrors)
|
||||
return;
|
||||
|
||||
const dataSource = $routeParams.dataSource;
|
||||
|
||||
// First, attempt to create the connections
|
||||
connectionService.patchConnections(dataSource, parseResult.patches)
|
||||
.then(connectionResponse =>
|
||||
|
||||
// If connection creation is successful, create users and groups
|
||||
createUsersAndGroups(parseResult).then(() =>
|
||||
|
||||
grantConnectionPermissions(parseResult, connectionResponse)
|
||||
.then(() => {
|
||||
|
||||
$scope.processing = false;
|
||||
|
||||
// Display a success message if everything worked
|
||||
guacNotification.showStatus({
|
||||
className : 'success',
|
||||
title : 'IMPORT.DIALOG_HEADER_SUCCESS',
|
||||
text : {
|
||||
key: 'IMPORT.INFO_CONNECTIONS_IMPORTED_SUCCESS',
|
||||
variables: { NUMBER: parseResult.patches.length }
|
||||
},
|
||||
|
||||
// Add a button to acknowledge and redirect to
|
||||
// the connection listing page
|
||||
actions : [{
|
||||
name : 'IMPORT.ACTION_ACKNOWLEDGE',
|
||||
callback : () => {
|
||||
|
||||
// Close the notification
|
||||
guacNotification.showStatus(false);
|
||||
|
||||
// Redirect to connection list page
|
||||
$location.url('/settings/' + dataSource + '/connections');
|
||||
}
|
||||
}]
|
||||
});
|
||||
}))
|
||||
|
||||
// If an error occurs while trying to users or groups, or while trying
|
||||
// to assign permissions to users or groups, clean up the already-created
|
||||
// connections, displaying an error to the user along with a blank slate
|
||||
// so they can fix their problems and try again.
|
||||
.catch(error => {
|
||||
cleanUpConnections(connectionResponse);
|
||||
handleError(error);
|
||||
}))
|
||||
|
||||
// If an error occurred when the call to create the connections was made,
|
||||
// skip any further processing - the user will have a chance to fix the
|
||||
// problems and try again
|
||||
.catch(patchFailure => {
|
||||
$scope.processing = false;
|
||||
$scope.patchFailure = patchFailure;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the provided error to the user in a dismissable dialog.
|
||||
*
|
||||
* @argument {ParseError|Error} error
|
||||
* The error to display.
|
||||
*/
|
||||
const handleError = error => {
|
||||
|
||||
// Any error indicates that processing of the file has failed, so clear
|
||||
// all upload state to allow for a fresh retry
|
||||
resetUploadState();
|
||||
|
||||
let text;
|
||||
|
||||
// If it's a import file parsing error
|
||||
if (error instanceof ParseError)
|
||||
text = {
|
||||
|
||||
// Use the translation key if available
|
||||
key: error.key || error.message,
|
||||
variables: error.variables
|
||||
};
|
||||
|
||||
// If it's a generic REST error
|
||||
else if (error instanceof Error)
|
||||
text = error.translatableMessage;
|
||||
|
||||
// If it's an unknown type, just use the message directly
|
||||
else
|
||||
text = { key: error };
|
||||
|
||||
guacNotification.showStatus({
|
||||
className : 'error',
|
||||
title : 'IMPORT.DIALOG_HEADER_ERROR',
|
||||
text,
|
||||
|
||||
// Add a button to hide the error
|
||||
actions : [{
|
||||
name : 'IMPORT.ACTION_ACKNOWLEDGE',
|
||||
callback : () => guacNotification.showStatus(false)
|
||||
}]
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Process the uploaded import file, importing the connections, granting
|
||||
* connection permissions, or displaying errors to the user if there are
|
||||
* problems with the provided file.
|
||||
*
|
||||
* @param {String} mimeType
|
||||
* The MIME type of the uploaded data file.
|
||||
*
|
||||
* @param {String} data
|
||||
* The raw string contents of the import file.
|
||||
*/
|
||||
function processData(mimeType, data) {
|
||||
|
||||
// Data processing has begun
|
||||
$scope.processing = true;
|
||||
|
||||
// The function that will process all the raw data and return a list of
|
||||
// patches to be submitted to the API
|
||||
let processDataCallback;
|
||||
|
||||
// Choose the appropriate parse function based on the mimetype
|
||||
if (mimeType === JSON_MIME_TYPE)
|
||||
processDataCallback = connectionParseService.parseJSON;
|
||||
|
||||
else if (mimeType === CSV_MIME_TYPE)
|
||||
processDataCallback = connectionParseService.parseCSV;
|
||||
|
||||
else if (YAML_MIME_TYPES.indexOf(mimeType) >= 0)
|
||||
processDataCallback = connectionParseService.parseYAML;
|
||||
|
||||
// The file type was validated before being uploaded - this should
|
||||
// never happen
|
||||
else
|
||||
processDataCallback = () => {
|
||||
throw new ParseError({
|
||||
message: "Unexpected invalid file type: " + mimeType
|
||||
});
|
||||
};
|
||||
|
||||
// Make the call to process the data into a series of patches
|
||||
processDataCallback(data)
|
||||
|
||||
// Send the data off to be imported if parsing is successful
|
||||
.then(handleParseSuccess)
|
||||
|
||||
// Display any error found while parsing the file
|
||||
.catch(handleError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the uploaded import data. Only usuable if the upload is fully
|
||||
* complete.
|
||||
*/
|
||||
$scope.import = () => processData($scope.mimeType, $scope.fileData);
|
||||
|
||||
/**
|
||||
* Returns true if import should be disabled, or false if import should be
|
||||
* allowed.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* True if import should be disabled, otherwise false.
|
||||
*/
|
||||
$scope.importDisabled = () =>
|
||||
|
||||
// Disable import if no data is ready
|
||||
!$scope.dataReady ||
|
||||
|
||||
// Disable import if the file is currently being processed
|
||||
$scope.processing;
|
||||
|
||||
/**
|
||||
* Cancel any in-progress upload, or clear any uploaded-but-errored-out
|
||||
* batch.
|
||||
*/
|
||||
$scope.cancel = function() {
|
||||
|
||||
// If the upload is in progress, stop it now; the FileReader will
|
||||
// reset the upload state when it stops
|
||||
if ($scope.fileReader) {
|
||||
$scope.aborted = true;
|
||||
$scope.fileReader.abort();
|
||||
}
|
||||
|
||||
// Clear any upload state - there's no FileReader handler to do it
|
||||
else
|
||||
resetUploadState();
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if cancellation should be disabled, or false if
|
||||
* cancellation should be allowed.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* True if cancellation should be disabled, or false if cancellation
|
||||
* should be allowed.
|
||||
*/
|
||||
$scope.cancelDisabled = () =>
|
||||
|
||||
// Disable cancellation if the import has already been cancelled
|
||||
$scope.aborted ||
|
||||
|
||||
// Disable cancellation if the file is currently being processed
|
||||
$scope.processing ||
|
||||
|
||||
// Disable cancellation if no data is ready or being uploaded
|
||||
!($scope.fileReader || $scope.dataReady);
|
||||
|
||||
/**
|
||||
* Handle a provided File upload, reading all data onto the scope for
|
||||
* import processing, should the user request an import. Note that this
|
||||
* function is used as a callback for directives that invoke it with a file
|
||||
* list, but directive-level checking should ensure that there is only ever
|
||||
* one file provided at a time.
|
||||
*
|
||||
* @argument {File[]} files
|
||||
* The files to upload onto the scope for further processing. There
|
||||
* should only ever be a single file in the array.
|
||||
*/
|
||||
$scope.handleFiles = files => {
|
||||
|
||||
// There should only ever be a single file in the array
|
||||
const file = files[0];
|
||||
|
||||
// The MIME type of the provided file
|
||||
const mimeType = file.type;
|
||||
|
||||
// Check if the mimetype is one of the supported types,
|
||||
// e.g. "application/json" or "text/csv"
|
||||
if (LEGAL_MIME_TYPES.indexOf(mimeType) < 0) {
|
||||
|
||||
// If the provided file is not one of the supported types,
|
||||
// display an error and abort processing
|
||||
handleError(new ParseError({
|
||||
message: "Invalid file type: " + mimeType,
|
||||
key: 'IMPORT.ERROR_INVALID_MIME_TYPE',
|
||||
variables: { TYPE: mimeType }
|
||||
}));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
$scope.fileName = file.name;
|
||||
|
||||
// Initialize upload state
|
||||
$scope.aborted = false;
|
||||
$scope.dataReady = false;
|
||||
$scope.processing = false;
|
||||
$scope.uploadStarted = true;
|
||||
|
||||
// Save the MIME type to the scope
|
||||
$scope.mimeType = file.type;
|
||||
|
||||
// Save the file to the scope when ready
|
||||
$scope.fileReader = new FileReader();
|
||||
$scope.fileReader.onloadend = (e => {
|
||||
|
||||
// If the upload was explicitly aborted, clear any upload state and
|
||||
// do not process the data
|
||||
if ($scope.aborted)
|
||||
resetUploadState();
|
||||
|
||||
else {
|
||||
|
||||
// Save the uploaded data
|
||||
$scope.fileData = e.target.result;
|
||||
|
||||
// Mark the data as ready
|
||||
$scope.dataReady = true;
|
||||
|
||||
// Clear the file reader from the scope now that this file is
|
||||
// fully uploaded
|
||||
$scope.fileReader = null;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Read all the data into memory
|
||||
$scope.fileReader.readAsBinaryString(file);
|
||||
};
|
||||
|
||||
/**
|
||||
* The name of the file that's currently being uploaded, or has yet to
|
||||
* be imported, if any.
|
||||
*/
|
||||
$scope.fileName = null;
|
||||
|
||||
}]);
|
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* global _ */
|
||||
|
||||
/**
|
||||
* A directive that displays errors that occurred during parsing of a connection
|
||||
* import file, or errors that were returned from the API during the connection
|
||||
* batch creation attempt.
|
||||
*/
|
||||
angular.module('import').directive('connectionImportErrors', [
|
||||
function connectionImportErrors() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/import/templates/connectionErrors.html',
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The result of parsing the import file. Any errors in this file
|
||||
* will be displayed to the user.
|
||||
*
|
||||
* @type ParseResult
|
||||
*/
|
||||
parseResult : '=',
|
||||
|
||||
/**
|
||||
* The error associated with an attempt to batch create the
|
||||
* connections represented by the ParseResult, if the ParseResult
|
||||
* had no errors. If the provided ParseResult has errors, no request
|
||||
* should have been made, and any provided patch error will be
|
||||
* ignored.
|
||||
*
|
||||
* @type Error
|
||||
*/
|
||||
patchFailure : '=',
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector',
|
||||
function connectionImportErrorsController($scope, $injector) {
|
||||
|
||||
// Required types
|
||||
const DisplayErrorList = $injector.get('DisplayErrorList');
|
||||
const ImportConnectionError = $injector.get('ImportConnectionError');
|
||||
const ParseError = $injector.get('ParseError');
|
||||
const SortOrder = $injector.get('SortOrder');
|
||||
|
||||
// Required services
|
||||
const $q = $injector.get('$q');
|
||||
const $translate = $injector.get('$translate');
|
||||
|
||||
// There are errors to display if the parse result generated errors, or
|
||||
// if the patch request failed
|
||||
$scope.hasErrors = () =>
|
||||
!!_.get($scope, 'parseResult.hasErrors') || !!$scope.patchFailure;
|
||||
|
||||
/**
|
||||
* All connections with their associated errors for display. These may
|
||||
* be either parsing failures, or errors returned from the API. Both
|
||||
* error types will be adapted to a common display format, though the
|
||||
* error types will never be mixed, because no REST request should ever
|
||||
* be made if there are client-side parse errors.
|
||||
*
|
||||
* @type {ImportConnectionError[]}
|
||||
*/
|
||||
$scope.connectionErrors = [];
|
||||
|
||||
/**
|
||||
* SortOrder instance which maintains the sort order of the visible
|
||||
* connection errors.
|
||||
*
|
||||
* @type SortOrder
|
||||
*/
|
||||
$scope.errorOrder = new SortOrder([
|
||||
'rowNumber',
|
||||
'name',
|
||||
'protocol',
|
||||
'errors',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Array of all connection error properties that are filterable.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
$scope.filteredErrorProperties = [
|
||||
'rowNumber',
|
||||
'name',
|
||||
'protocol',
|
||||
'errors',
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate a ImportConnectionError representing any errors associated
|
||||
* with the row at the given index within the given parse result.
|
||||
*
|
||||
* @param {ParseResult} parseResult
|
||||
* The result of parsing the connection import file.
|
||||
*
|
||||
* @param {Integer} index
|
||||
* The current row within the import file, 0-indexed.
|
||||
*
|
||||
* @returns {ImportConnectionError}
|
||||
* The connection error object associated with the given row in the
|
||||
* given parse result.
|
||||
*/
|
||||
const generateConnectionError = (parseResult, index) => {
|
||||
|
||||
// Get the patch associated with the current row
|
||||
const patch = parseResult.patches[index];
|
||||
|
||||
// The value of a patch is just the Connection object
|
||||
const connection = patch.value;
|
||||
|
||||
return new ImportConnectionError({
|
||||
|
||||
// Add 1 to the index to get the position in the file
|
||||
rowNumber: index + 1,
|
||||
|
||||
// Basic connection information - name and protocol.
|
||||
name: connection.name,
|
||||
protocol: connection.protocol,
|
||||
|
||||
// The human-readable error messages
|
||||
errors: new DisplayErrorList(
|
||||
[ ...(parseResult.errors[index] || []) ])
|
||||
});
|
||||
};
|
||||
|
||||
// If a new connection patch failure is seen, update the display list
|
||||
$scope.$watch('patchFailure', function patchFailureChanged(patchFailure) {
|
||||
|
||||
const { parseResult } = $scope;
|
||||
|
||||
// Do not attempt to process anything before the data has loaded
|
||||
if (!patchFailure || !parseResult)
|
||||
return;
|
||||
|
||||
// All promises from all translation requests. The scope will not be
|
||||
// updated until all translations are ready.
|
||||
const translationPromises = [];
|
||||
|
||||
// Set up the list of connection errors based on the existing parse
|
||||
// result, with error messages fetched from the patch failure
|
||||
const connectionErrors = parseResult.patches.map(
|
||||
(patch, index) => {
|
||||
|
||||
// Generate a connection error for display
|
||||
const connectionError = generateConnectionError(parseResult, index);
|
||||
|
||||
// Set the error from the PATCH request, if there is one
|
||||
const error = _.get(patchFailure, ['patches', index, 'error']);
|
||||
if (error)
|
||||
|
||||
// Fetch the translation and update it when it's ready
|
||||
translationPromises.push($translate(
|
||||
error.key, error.variables)
|
||||
.then(translatedError =>
|
||||
connectionError.errors.getArray().push(translatedError)
|
||||
));
|
||||
|
||||
return connectionError;
|
||||
|
||||
});
|
||||
|
||||
// Once all the translations have been completed, update the
|
||||
// connectionErrors all in one go, to ensure no excessive reloading
|
||||
$q.all(translationPromises).then(() => {
|
||||
$scope.connectionErrors = connectionErrors;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// If a new parse result with errors is seen, update the display list
|
||||
$scope.$watch('parseResult', function parseResultChanged(parseResult) {
|
||||
|
||||
// Do not process if there are no errors in the provided result
|
||||
if (!parseResult || !parseResult.hasErrors)
|
||||
return;
|
||||
|
||||
// All promises from all translation requests. The scope will not be
|
||||
// updated until all translations are ready.
|
||||
const translationPromises = [];
|
||||
|
||||
// The parse result should only be updated on a fresh file import;
|
||||
// therefore it should be safe to skip checking the patch errors
|
||||
// entirely - if set, they will be from the previous file and no
|
||||
// longer relevant.
|
||||
|
||||
// Set up the list of connection errors based on the updated parse
|
||||
// result
|
||||
const connectionErrors = parseResult.patches.map(
|
||||
(patch, index) => {
|
||||
|
||||
// Generate a connection error for display
|
||||
const connectionError = generateConnectionError(parseResult, index);
|
||||
|
||||
// Go through the errors and check if any are translateable
|
||||
connectionError.errors.getArray().forEach(
|
||||
(error, errorIndex) => {
|
||||
|
||||
// If this error is a ParseError, it can be translated.
|
||||
// NOTE: Generally one would translate error messages in the
|
||||
// template, but in this case, the connection errors need to
|
||||
// be raw strings in order to enable sorting and filtering.
|
||||
if (error instanceof ParseError)
|
||||
|
||||
// Fetch the translation and update it when it's ready
|
||||
translationPromises.push($translate(
|
||||
error.key, error.variables)
|
||||
.then(translatedError => {
|
||||
connectionError.errors.getArray()[errorIndex] = translatedError;
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
return connectionError;
|
||||
|
||||
});
|
||||
|
||||
// Once all the translations have been completed, update the
|
||||
// connectionErrors all in one go, to ensure no excessive reloading
|
||||
$q.all(translationPromises).then(() => {
|
||||
$scope.connectionErrors = connectionErrors;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
24
guacamole/src/main/frontend/src/app/import/importModule.js
Normal file
24
guacamole/src/main/frontend/src/app/import/importModule.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The module for code supporting importing user-supplied files. Currently, only
|
||||
* connection import is supported.
|
||||
*/
|
||||
angular.module('import', ['element', 'list', 'notification', 'rest']);
|
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* 'License'); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* global _ */
|
||||
|
||||
// A suffix that indicates that a particular header refers to a parameter
|
||||
const PARAMETER_SUFFIX = ' (parameter)';
|
||||
|
||||
// A suffix that indicates that a particular header refers to an attribute
|
||||
const ATTRIBUTE_SUFFIX = ' (attribute)';
|
||||
|
||||
/**
|
||||
* A service for parsing user-provided CSV connection data for bulk import.
|
||||
*/
|
||||
angular.module('import').factory('connectionCSVService',
|
||||
['$injector', function connectionCSVService($injector) {
|
||||
|
||||
// Required types
|
||||
const ParseError = $injector.get('ParseError');
|
||||
const ImportConnection = $injector.get('ImportConnection');
|
||||
const TranslatableMessage = $injector.get('TranslatableMessage');
|
||||
|
||||
// Required services
|
||||
const $q = $injector.get('$q');
|
||||
const $routeParams = $injector.get('$routeParams');
|
||||
const schemaService = $injector.get('schemaService');
|
||||
|
||||
const service = {};
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to a object detailing the connection
|
||||
* attributes for the current data source, as well as the connection
|
||||
* paremeters for every protocol, for the current data source.
|
||||
*
|
||||
* The object that the promise will contain an "attributes" key that maps to
|
||||
* a set of attribute names, and a "protocolParameters" key that maps to an
|
||||
* object mapping protocol names to sets of parameter names for that protocol.
|
||||
*
|
||||
* The intended use case for this object is to determine if there is a
|
||||
* connection parameter or attribute with a given name, by e.g. checking the
|
||||
* path `.protocolParameters[protocolName]` to see if a protocol exists,
|
||||
* checking the path `.protocolParameters[protocolName][fieldName]` to see
|
||||
* if a parameter exists for a given protocol, or checking the path
|
||||
* `.attributes[fieldName]` to check if a connection attribute exists.
|
||||
*
|
||||
* @returns {Promise.<Object>}
|
||||
* A promise that resolves to a object detailing the connection
|
||||
* attributes and parameters for every protocol, for the current data
|
||||
* source.
|
||||
*/
|
||||
function getFieldLookups() {
|
||||
|
||||
// The current data source - the one that the connections will be
|
||||
// imported into
|
||||
const dataSource = $routeParams.dataSource;
|
||||
|
||||
// Fetch connection attributes and protocols for the current data source
|
||||
return $q.all({
|
||||
attributes : schemaService.getConnectionAttributes(dataSource),
|
||||
protocols : schemaService.getProtocols(dataSource)
|
||||
})
|
||||
.then(function connectionStructureRetrieved({attributes, protocols}) {
|
||||
|
||||
return {
|
||||
|
||||
// Translate the forms and fields into a flat map of attribute
|
||||
// name to `true` boolean value
|
||||
attributes: attributes.reduce(
|
||||
(attributeMap, form) => {
|
||||
form.fields.forEach(
|
||||
field => attributeMap[field.name] = true);
|
||||
return attributeMap
|
||||
}, {}),
|
||||
|
||||
// Translate the protocol definitions into a map of protocol
|
||||
// name to map of field name to `true` boolean value
|
||||
protocolParameters: _.mapValues(
|
||||
protocols, protocol => protocol.connectionForms.reduce(
|
||||
(protocolFieldMap, form) => {
|
||||
form.fields.forEach(
|
||||
field => protocolFieldMap[field.name] = true);
|
||||
return protocolFieldMap;
|
||||
}, {}))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a raw user-provided, semicolon-seperated list of identifiers into
|
||||
* an array of identifiers. If identifiers contain semicolons, they can be
|
||||
* escaped with backslashes, and backslashes can also be escaped using other
|
||||
* backslashes.
|
||||
*
|
||||
* @param {String} rawIdentifiers
|
||||
* The raw string value as fetched from the CSV.
|
||||
*
|
||||
* @returns {Array.<String>}
|
||||
* An array of identifier values.
|
||||
*/
|
||||
function splitIdentifiers(rawIdentifiers) {
|
||||
|
||||
// Keep track of whether a backslash was seen
|
||||
let escaped = false;
|
||||
|
||||
return _.reduce(rawIdentifiers, (identifiers, ch) => {
|
||||
|
||||
// The current identifier will be the last one in the final list
|
||||
let identifier = identifiers[identifiers.length - 1];
|
||||
|
||||
// If a semicolon is seen, set the "escaped" flag and continue
|
||||
// to the next character
|
||||
if (!escaped && ch == '\\') {
|
||||
escaped = true;
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
// End the current identifier and start a new one if there's an
|
||||
// unescaped semicolon
|
||||
else if (!escaped && ch == ';') {
|
||||
identifiers.push('');
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
// In all other cases, just append to the identifier
|
||||
else {
|
||||
identifier += ch;
|
||||
escaped = false;
|
||||
}
|
||||
|
||||
// Save the updated identifier to the list
|
||||
identifiers[identifiers.length - 1] = identifier;
|
||||
|
||||
return identifiers;
|
||||
|
||||
}, [''])
|
||||
|
||||
// Filter out any 0-length (empty) identifiers
|
||||
.filter(identifier => identifier.length);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a CSV header row, create and return a promise that will resolve to
|
||||
* a function that can take a CSV data row and return a ImportConnection
|
||||
* object. If an error occurs while parsing a particular row, the resolved
|
||||
* function will throw a ParseError describing the failure.
|
||||
*
|
||||
* The provided CSV must contain columns for name and protocol. Optionally,
|
||||
* the parentIdentifier of the target parent connection group, or a connection
|
||||
* name path e.g. "ROOT/parent/child" may be included. Additionallty,
|
||||
* connection parameters or attributes can be included.
|
||||
*
|
||||
* The names of connection attributes and parameters are not guaranteed to
|
||||
* be mutually exclusive, so the CSV import format supports a distinguishing
|
||||
* suffix. A column may be explicitly declared to be a parameter using a
|
||||
* " (parameter)" suffix, or an attribute using an " (attribute)" suffix.
|
||||
* No suffix is required if the name is unique across connections and
|
||||
* attributes.
|
||||
*
|
||||
* If a parameter or attribute name conflicts with the standard
|
||||
* "name", "protocol", "group", or "parentIdentifier" fields, the suffix is
|
||||
* required.
|
||||
*
|
||||
* If a failure occurs while attempting to create the transformer function,
|
||||
* the promise will be rejected with a ParseError describing the failure.
|
||||
*
|
||||
* @returns {Promise.<Function.<String[], ImportConnection>>}
|
||||
* A promise that will resolve to a function that translates a CSV data
|
||||
* row (array of strings) to a ImportConnection object.
|
||||
*/
|
||||
service.getCSVTransformer = function getCSVTransformer(headerRow) {
|
||||
|
||||
// A promise that will be resolved with the transformer or rejected if
|
||||
// an error occurs
|
||||
const deferred = $q.defer();
|
||||
|
||||
getFieldLookups().then(({attributes, protocolParameters}) => {
|
||||
|
||||
// All configuration required to generate a function that can
|
||||
// transform a row of CSV into a connection object.
|
||||
// NOTE: This is a single object instead of a collection of variables
|
||||
// to ensure that no stale references are used - e.g. when one getter
|
||||
// invokes another getter
|
||||
const transformConfig = {
|
||||
|
||||
// Callbacks for required fields
|
||||
nameGetter: undefined,
|
||||
protocolGetter: undefined,
|
||||
|
||||
// Callbacks for a parent group ID or group path
|
||||
groupGetter: undefined,
|
||||
parentIdentifierGetter: undefined,
|
||||
|
||||
// Callbacks for user and user group identifiers
|
||||
usersGetter: () => [],
|
||||
userGroupsGetter: () => [],
|
||||
|
||||
// Callbacks that will generate either connection attributes or
|
||||
// parameters. These callbacks will return a {type, name, value}
|
||||
// object containing the type ("parameter" or "attribute"),
|
||||
// the name of the attribute or parameter, and the corresponding
|
||||
// value.
|
||||
parameterOrAttributeGetters: []
|
||||
|
||||
};
|
||||
|
||||
// A set of all headers that have been seen so far. If any of these
|
||||
// are duplicated, the CSV is invalid.
|
||||
const headerSet = {};
|
||||
|
||||
// Iterate through the headers one by one
|
||||
headerRow.forEach((rawHeader, index) => {
|
||||
|
||||
// Trim to normalize all headers
|
||||
const header = rawHeader.trim();
|
||||
|
||||
// Check if the header is duplicated
|
||||
if (headerSet[header]) {
|
||||
deferred.reject(new ParseError({
|
||||
message: 'Duplicate CSV Header: ' + header,
|
||||
translatableMessage: new TranslatableMessage({
|
||||
key: 'IMPORT.ERROR_DUPLICATE_CSV_HEADER',
|
||||
variables: { HEADER: header }
|
||||
})
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark that this particular header has already been seen
|
||||
headerSet[header] = true;
|
||||
|
||||
// A callback that returns the field at the current index
|
||||
const fetchFieldAtIndex = row => row[index];
|
||||
|
||||
// A callback that splits raw string identifier lists by
|
||||
// semicolon characters into an array of identifiers
|
||||
const identifierListCallback = row =>
|
||||
splitIdentifiers(fetchFieldAtIndex(row));
|
||||
|
||||
// Set up the name callback
|
||||
if (header == 'name')
|
||||
transformConfig.nameGetter = fetchFieldAtIndex;
|
||||
|
||||
// Set up the protocol callback
|
||||
else if (header == 'protocol')
|
||||
transformConfig.protocolGetter = fetchFieldAtIndex;
|
||||
|
||||
// Set up the group callback
|
||||
else if (header == 'group')
|
||||
transformConfig.groupGetter = fetchFieldAtIndex;
|
||||
|
||||
// Set up the group parent ID callback
|
||||
else if (header == 'parentIdentifier')
|
||||
transformConfig.parentIdentifierGetter = fetchFieldAtIndex;
|
||||
|
||||
// Set the user identifiers callback
|
||||
else if (header == 'users')
|
||||
transformConfig.usersGetter = (
|
||||
identifierListCallback);
|
||||
|
||||
// Set the user group identifiers callback
|
||||
else if (header == 'groups')
|
||||
transformConfig.userGroupsGetter = (
|
||||
identifierListCallback);
|
||||
|
||||
// At this point, any other header might refer to a connection
|
||||
// parameter or to an attribute
|
||||
|
||||
// A field may be explicitly specified as a parameter
|
||||
else if (header.endsWith(PARAMETER_SUFFIX)) {
|
||||
|
||||
// Push as an explicit parameter getter
|
||||
const parameterName = header.replace(PARAMETER_SUFFIX);
|
||||
transformConfig.parameterOrAttributeGetters.push(
|
||||
row => ({
|
||||
type: 'parameters',
|
||||
name: parameterName,
|
||||
value: fetchFieldAtIndex(row)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// A field may be explicitly specified as a parameter
|
||||
else if (header.endsWith(ATTRIBUTE_SUFFIX)) {
|
||||
|
||||
// Push as an explicit attribute getter
|
||||
const attributeName = header.replace(ATTRIBUTE_SUFFIX);
|
||||
transformConfig.parameterOrAttributeGetters.push(
|
||||
row => ({
|
||||
type: 'attributes',
|
||||
name: parameterName,
|
||||
value: fetchFieldAtIndex(row)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// The field is ambiguous, either an attribute or parameter,
|
||||
// so the getter will have to determine this for every row
|
||||
else
|
||||
transformConfig.parameterOrAttributeGetters.push(row => {
|
||||
|
||||
// The name is just the value of the current header
|
||||
const name = header;
|
||||
|
||||
// The value is at the index that matches the position
|
||||
// of the header
|
||||
const value = fetchFieldAtIndex(row);
|
||||
|
||||
// The protocol may determine whether a field is
|
||||
// a parameter or an attribute (or both)
|
||||
const protocol = transformConfig.protocolGetter(row);
|
||||
|
||||
// Determine if the field refers to an attribute or a
|
||||
// parameter (or both, which is an error)
|
||||
const isAttribute = !!attributes[name];
|
||||
const isParameter = !!_.get(
|
||||
protocolParameters, [protocol, name]);
|
||||
|
||||
// If there is both an attribute and a protocol-specific
|
||||
// parameter with the provided name, it's impossible to
|
||||
// figure out which this should be
|
||||
if (isAttribute && isParameter)
|
||||
throw new ParseError({
|
||||
message: 'Ambiguous CSV Header: ' + header,
|
||||
key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER',
|
||||
variables: { HEADER: header }
|
||||
});
|
||||
|
||||
// It's neither an attribute or a parameter
|
||||
else if (!isAttribute && !isParameter)
|
||||
throw new ParseError({
|
||||
message: 'Invalid CSV Header: ' + header,
|
||||
key: 'IMPORT.ERROR_INVALID_CSV_HEADER',
|
||||
variables: { HEADER: header }
|
||||
});
|
||||
|
||||
// Choose the appropriate type
|
||||
const type = isAttribute ? 'attributes' : 'parameters';
|
||||
|
||||
return { type, name, value };
|
||||
});
|
||||
});
|
||||
|
||||
const {
|
||||
nameGetter, protocolGetter,
|
||||
parentIdentifierGetter, groupGetter,
|
||||
usersGetter, userGroupsGetter,
|
||||
parameterOrAttributeGetters
|
||||
} = transformConfig;
|
||||
|
||||
// Fail if the name wasn't provided
|
||||
if (!nameGetter)
|
||||
return deferred.reject(new ParseError({
|
||||
message: 'The connection name must be provided',
|
||||
key: 'IMPORT.ERROR_REQUIRED_NAME'
|
||||
}));
|
||||
|
||||
// Fail if the protocol wasn't provided
|
||||
if (!protocolGetter)
|
||||
return deferred.reject(new ParseError({
|
||||
message: 'The connection protocol must be provided',
|
||||
key: 'IMPORT.ERROR_REQUIRED_PROTOCOL'
|
||||
}));
|
||||
|
||||
// The function to transform a CSV row into a connection object
|
||||
deferred.resolve(function transformCSVRow(row) {
|
||||
|
||||
// Get name and protocol
|
||||
const name = nameGetter(row);
|
||||
const protocol = protocolGetter(row);
|
||||
|
||||
// Get any users or user groups who should be granted access
|
||||
const users = usersGetter(row);
|
||||
const groups = userGroupsGetter(row);
|
||||
|
||||
// Get the parent group ID and/or group path
|
||||
const group = groupGetter && groupGetter(row);
|
||||
const parentIdentifier = (
|
||||
parentIdentifierGetter && parentIdentifierGetter(row));
|
||||
|
||||
return new ImportConnection({
|
||||
|
||||
// Fields that are not protocol-specific
|
||||
name,
|
||||
protocol,
|
||||
parentIdentifier,
|
||||
group,
|
||||
users,
|
||||
groups,
|
||||
|
||||
// Fields that might potentially be either attributes or
|
||||
// parameters, depending on the protocol
|
||||
...parameterOrAttributeGetters.reduce((values, getter) => {
|
||||
|
||||
// Determine the type, name, and value
|
||||
const { type, name, value } = getter(row);
|
||||
|
||||
// Set the value and continue on to the next attribute
|
||||
// or parameter
|
||||
values[type][name] = value;
|
||||
return values;
|
||||
|
||||
}, {parameters: {}, attributes: {}})
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* 'License'); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* global _ */
|
||||
|
||||
import { parse as parseCSVData } from 'csv-parse/lib/sync'
|
||||
import { parse as parseYAMLData } from 'yaml'
|
||||
|
||||
/**
|
||||
* A service for parsing user-provided JSON, YAML, or JSON connection data into
|
||||
* an appropriate format for bulk uploading using the PATCH REST endpoint.
|
||||
*/
|
||||
angular.module('import').factory('connectionParseService',
|
||||
['$injector', function connectionParseService($injector) {
|
||||
|
||||
// Required types
|
||||
const Connection = $injector.get('Connection');
|
||||
const DirectoryPatch = $injector.get('DirectoryPatch');
|
||||
const ParseError = $injector.get('ParseError');
|
||||
const ParseResult = $injector.get('ParseResult');
|
||||
const TranslatableMessage = $injector.get('TranslatableMessage');
|
||||
|
||||
// Required services
|
||||
const $q = $injector.get('$q');
|
||||
const $routeParams = $injector.get('$routeParams');
|
||||
const schemaService = $injector.get('schemaService');
|
||||
const connectionCSVService = $injector.get('connectionCSVService');
|
||||
const connectionGroupService = $injector.get('connectionGroupService');
|
||||
|
||||
const service = {};
|
||||
|
||||
/**
|
||||
* Perform basic checks, common to all file types - namely that the parsed
|
||||
* data is an array, and contains at least one connection entry. Returns an
|
||||
* error if any of these basic checks fails.
|
||||
*
|
||||
* @returns {ParseError}
|
||||
* An error describing the parsing failure, if one of the basic checks
|
||||
* fails.
|
||||
*/
|
||||
function performBasicChecks(parsedData) {
|
||||
|
||||
// Make sure that the file data parses to an array (connection list)
|
||||
if (!(parsedData instanceof Array))
|
||||
return new ParseError({
|
||||
message: 'Import data must be a list of connections',
|
||||
key: 'IMPORT.ERROR_ARRAY_REQUIRED'
|
||||
});
|
||||
|
||||
// Make sure that the connection list is not empty - contains at least
|
||||
// one connection
|
||||
if (!parsedData.length)
|
||||
return new ParseError({
|
||||
message: 'The provided file is empty',
|
||||
key: 'IMPORT.ERROR_EMPTY_FILE'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to an object mapping potential groups
|
||||
* that might be encountered in an imported connection to group identifiers.
|
||||
*
|
||||
* The idea is that a user-provided import file might directly specify a
|
||||
* parentIdentifier, or it might specify a named group path like "ROOT",
|
||||
* "ROOT/parent", or "ROOT/parent/child". This object resolved by the
|
||||
* promise returned from this function will map all of the above to the
|
||||
* identifier of the appropriate group, if defined.
|
||||
*
|
||||
* @returns {Promise.<Object>}
|
||||
* A promise that resolves to an object mapping groups to group
|
||||
* identifiers.
|
||||
*/
|
||||
function getGroupLookups() {
|
||||
|
||||
// The current data source - defines all the groups that the connections
|
||||
// might be imported into
|
||||
const dataSource = $routeParams.dataSource;
|
||||
|
||||
const deferredGroupLookups = $q.defer();
|
||||
|
||||
connectionGroupService.getConnectionGroupTree(dataSource).then(
|
||||
rootGroup => {
|
||||
|
||||
const groupLookup = {};
|
||||
|
||||
// Add the specified group to the lookup, appending all specified
|
||||
// prefixes, and then recursively call saveLookups for all children
|
||||
// of the group, appending to the prefix for each level
|
||||
const saveLookups = (prefix, group) => {
|
||||
|
||||
// To get the path for the current group, add the name
|
||||
const currentPath = prefix + group.name;
|
||||
|
||||
// Add the current path to the lookup
|
||||
groupLookup[currentPath] = group.identifier;
|
||||
|
||||
// Add each child group to the lookup
|
||||
const nextPrefix = currentPath + "/";
|
||||
_.forEach(group.childConnectionGroups,
|
||||
childGroup => saveLookups(nextPrefix, childGroup));
|
||||
|
||||
}
|
||||
|
||||
// Start at the root group
|
||||
saveLookups("", rootGroup);
|
||||
|
||||
// Resolve with the now fully-populated lookups
|
||||
deferredGroupLookups.resolve(groupLookup);
|
||||
|
||||
});
|
||||
|
||||
return deferredGroupLookups.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve to a transformer function that will
|
||||
* take an object that may contain a "group" field, replacing it if present
|
||||
* with a "parentIdentifier". If both a "group" and "parentIdentifier" field
|
||||
* are present on the provided object, or if no group exists at the specified
|
||||
* path, the function will throw a ParseError describing the failure.
|
||||
*
|
||||
* @returns {Promise.<Function<Object, Object>>}
|
||||
* A promise that will resolve to a function that will transform a
|
||||
* "group" field into a "parentIdentifier" field if possible.
|
||||
*/
|
||||
function getGroupTransformer() {
|
||||
return getGroupLookups().then(lookups => connection => {
|
||||
|
||||
// If there's no group to translate, do nothing
|
||||
if (!connection.group)
|
||||
return connection;
|
||||
|
||||
// If both are specified, the parent group is ambigious
|
||||
if (connection.parentIdentifier)
|
||||
throw new ParseError({
|
||||
message: 'Only one of group or parentIdentifier can be set',
|
||||
key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP'
|
||||
});
|
||||
|
||||
// Look up the parent identifier for the specified group path
|
||||
const identifier = lookups[connection.group];
|
||||
|
||||
// If the group doesn't match anything in the tree
|
||||
if (!identifier)
|
||||
throw new ParseError({
|
||||
message: 'No group found named: ' + connection.group,
|
||||
key: 'IMPORT.ERROR_INVALID_GROUP',
|
||||
variables: { GROUP: connection.group }
|
||||
});
|
||||
|
||||
// Set the parent identifier now that it's known
|
||||
return {
|
||||
...connection,
|
||||
parentIdentifier: identifier
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a provided ImportConnection array into a ParseResult. Any provided
|
||||
* transform functions will be run on each entry in `connectionData` before
|
||||
* any other processing is done.
|
||||
*
|
||||
* @param {*[]} connectionData
|
||||
* An arbitrary array of data. This must evaluate to a ImportConnection
|
||||
* object after being run through all functions in `transformFunctions`.
|
||||
*
|
||||
* @param {Function[]} transformFunctions
|
||||
* An array of transformation functions to run on each entry in
|
||||
* `connection` data.
|
||||
*
|
||||
* @return {Promise.<Object>}
|
||||
* A promise resolving to ParseResult object representing the result of
|
||||
* parsing all provided connection data.
|
||||
*/
|
||||
function parseConnectionData(connectionData, transformFunctions) {
|
||||
|
||||
// Check that the provided connection data array is not empty
|
||||
const checkError = performBasicChecks(connectionData);
|
||||
if (checkError) {
|
||||
const deferred = $q.defer();
|
||||
deferred.reject(checkError);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Get the group transformer to apply to each connection
|
||||
return getGroupTransformer().then(groupTransformer =>
|
||||
connectionData.reduce((parseResult, data, index) => {
|
||||
|
||||
const { patches, users, groups } = parseResult;
|
||||
|
||||
// Run the array data through each provided transform
|
||||
let connectionObject = data;
|
||||
_.forEach(transformFunctions, transform => {
|
||||
connectionObject = transform(connectionObject);
|
||||
});
|
||||
|
||||
// All errors found while parsing this connection
|
||||
const connectionErrors = [];
|
||||
parseResult.errors.push(connectionErrors);
|
||||
|
||||
// Translate the group on the object to a parentIdentifier
|
||||
try {
|
||||
connectionObject = groupTransformer(connectionObject);
|
||||
}
|
||||
|
||||
// If there was a problem with the group or parentIdentifier
|
||||
catch (error) {
|
||||
connectionErrors.push(error);
|
||||
}
|
||||
|
||||
// The users and user groups that should be granted access
|
||||
const connectionUsers = connectionObject.users || [];
|
||||
const connectionGroups = connectionObject.groups || [];
|
||||
|
||||
// Add this connection index to the list for each user
|
||||
connectionUsers.forEach(identifier => {
|
||||
|
||||
// If there's an existing list, add the index to that
|
||||
if (users[identifier])
|
||||
users[identifier].push(index);
|
||||
|
||||
// Otherwise, create a new list with just this index
|
||||
else
|
||||
users[identifier] = [index];
|
||||
});
|
||||
|
||||
// Add this connection index to the list for each group
|
||||
connectionGroups.forEach(identifier => {
|
||||
|
||||
// If there's an existing list, add the index to that
|
||||
if (groups[identifier])
|
||||
groups[identifier].push(index);
|
||||
|
||||
// Otherwise, create a new list with just this index
|
||||
else
|
||||
groups[identifier] = [index];
|
||||
});
|
||||
|
||||
// Translate to a full-fledged Connection
|
||||
const connection = new Connection(connectionObject);
|
||||
|
||||
// Finally, add a patch for creating the connection
|
||||
patches.push(new DirectoryPatch({
|
||||
op: 'add',
|
||||
path: '/',
|
||||
value: connection
|
||||
}));
|
||||
|
||||
// If there are any errors for this connection, fail the whole batch
|
||||
if (connectionErrors.length)
|
||||
parseResult.hasErrors = true;
|
||||
|
||||
return parseResult;
|
||||
|
||||
}, new ParseResult()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a provided CSV representation of a connection list into a JSON
|
||||
* object to be submitted to the PATCH REST endpoint, as well as a list of
|
||||
* objects containing lists of user and user group identifiers to be granted
|
||||
* to each connection.
|
||||
*
|
||||
* @param {String} csvData
|
||||
* The CSV-encoded connection list to process.
|
||||
*
|
||||
* @return {Promise.<Object>}
|
||||
* A promise resolving to ParseResult object representing the result of
|
||||
* parsing all provided connection data.
|
||||
*/
|
||||
service.parseCSV = function parseCSV(csvData) {
|
||||
|
||||
// Convert to an array of arrays, one per CSV row (including the header)
|
||||
// NOTE: skip_empty_lines is required, or a trailing newline will error
|
||||
let parsedData;
|
||||
try {
|
||||
parsedData = parseCSVData(csvData, {skip_empty_lines: true});
|
||||
}
|
||||
|
||||
// If the CSV parser throws an error, reject with that error. No
|
||||
// translation key will be available here.
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
const deferred = $q.defer();
|
||||
deferred.reject(new ParseError({ message: error.message }));
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The header row - an array of string header values
|
||||
const header = parsedData.length ? parsedData[0] : [];
|
||||
|
||||
// Slice off the header row to get the data rows
|
||||
const connectionData = parsedData.slice(1);
|
||||
|
||||
// Generate the CSV transform function, and apply it to every row
|
||||
// before applying all the rest of the standard transforms
|
||||
return connectionCSVService.getCSVTransformer(header).then(
|
||||
csvTransformer =>
|
||||
|
||||
// Apply the CSV transform to every row
|
||||
parseConnectionData(connectionData, [csvTransformer]));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a provided YAML representation of a connection list into a JSON
|
||||
* object to be submitted to the PATCH REST endpoint, as well as a list of
|
||||
* objects containing lists of user and user group identifiers to be granted
|
||||
* to each connection.
|
||||
*
|
||||
* @param {String} yamlData
|
||||
* The YAML-encoded connection list to process.
|
||||
*
|
||||
* @return {Promise.<Object>}
|
||||
* A promise resolving to ParseResult object representing the result of
|
||||
* parsing all provided connection data.
|
||||
*/
|
||||
service.parseYAML = function parseYAML(yamlData) {
|
||||
|
||||
// Parse from YAML into a javascript array
|
||||
let connectionData;
|
||||
try {
|
||||
connectionData = parseYAMLData(yamlData);
|
||||
}
|
||||
|
||||
// If the YAML parser throws an error, reject with that error. No
|
||||
// translation key will be available here.
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
const deferred = $q.defer();
|
||||
deferred.reject(new ParseError({ message: error.message }));
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Produce a ParseResult
|
||||
return parseConnectionData(connectionData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a provided JSON-encoded representation of a connection list into
|
||||
* an array of patches to be submitted to the PATCH REST endpoint, as well
|
||||
* as a list of objects containing lists of user and user group identifiers
|
||||
* to be granted to each connection.
|
||||
*
|
||||
* @param {String} jsonData
|
||||
* The JSON-encoded connection list to process.
|
||||
*
|
||||
* @return {Promise.<Object>}
|
||||
* A promise resolving to ParseResult object representing the result of
|
||||
* parsing all provided connection data.
|
||||
*/
|
||||
service.parseJSON = function parseJSON(jsonData) {
|
||||
|
||||
// Parse from JSON into a javascript array
|
||||
let connectionData;
|
||||
try {
|
||||
connectionData = JSON.parse(jsonData);
|
||||
}
|
||||
|
||||
// If the JSON parse attempt throws an error, reject with that error.
|
||||
// No translation key will be available here.
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
const deferred = $q.defer();
|
||||
deferred.reject(new ParseError({ message: error.message }));
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Produce a ParseResult
|
||||
return parseConnectionData(connectionData);
|
||||
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
59
guacamole/src/main/frontend/src/app/import/styles/help.css
Normal file
59
guacamole/src/main/frontend/src/app/import/styles/help.css
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.import.help {
|
||||
|
||||
text-transform: none;
|
||||
|
||||
}
|
||||
|
||||
.import.help p {
|
||||
|
||||
max-width: 70em;
|
||||
|
||||
}
|
||||
|
||||
.import.help h2 {
|
||||
|
||||
padding-bottom: 0px;
|
||||
|
||||
}
|
||||
|
||||
.import.help p, .import.help pre {
|
||||
|
||||
margin-left: 1em;
|
||||
|
||||
}
|
||||
|
||||
.import.help pre {
|
||||
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
padding: 10px;
|
||||
width: fit-content;
|
||||
|
||||
}
|
||||
|
||||
.import.help .footnotes {
|
||||
|
||||
border-top: 1px solid gray;
|
||||
padding-top: 1em;
|
||||
width: fit-content;
|
||||
margin-left: 1em;
|
||||
|
||||
}
|
160
guacamole/src/main/frontend/src/app/import/styles/import.css
vendored
Normal file
160
guacamole/src/main/frontend/src/app/import/styles/import.css
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.import .import-buttons {
|
||||
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
.import .errors table {
|
||||
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.import .errors .error-message {
|
||||
|
||||
color: red;
|
||||
|
||||
}
|
||||
|
||||
.import .errors .error-message ul {
|
||||
|
||||
margin: 0px;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 24px 24px;
|
||||
|
||||
width: fit-content;
|
||||
|
||||
border: 1px solid rgba(0,0,0,.25);
|
||||
box-shadow: 1px 1px 2px rgb(0 0 0 / 25%);
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container.file-selected {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 100px;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .clear {
|
||||
|
||||
margin: 0;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .upload-header {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 500px;
|
||||
margin-bottom: 5px;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .file-error {
|
||||
|
||||
color: red;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .file-options {
|
||||
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .file-upload-input {
|
||||
|
||||
display: none;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .drop-target {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
|
||||
width: 500px;
|
||||
height: 200px;
|
||||
|
||||
background: rgba(0,0,0,.04);
|
||||
border: 1px solid black;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .drop-target.file-present {
|
||||
|
||||
background: rgba(0,0,0,.15);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.file-upload-container .drop-target .file-name {
|
||||
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .drop-target.drop-pending {
|
||||
|
||||
background: #3161a9;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .drop-target.drop-pending > * {
|
||||
|
||||
opacity: 0.5;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .drop-target .title {
|
||||
|
||||
font-weight: bold;
|
||||
font-size: 1.25em;
|
||||
|
||||
}
|
||||
|
||||
.file-upload-container .drop-target .browse-link {
|
||||
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<div ng-show="hasErrors()" class="errors">
|
||||
|
||||
<!-- Connection / Error filter -->
|
||||
<guac-filter filtered-items="filteredErrors" items="connectionErrors"
|
||||
placeholder="'IMPORT.FIELD_PLACEHOLDER_FILTER' | translate"
|
||||
properties="filteredErrorProperties"></guac-filter>
|
||||
|
||||
<!-- List of connection import errors -->
|
||||
<table class="sorted">
|
||||
<thead>
|
||||
<tr>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'rowNumber'">
|
||||
{{'IMPORT.TABLE_HEADER_ROW_NUMBER' | translate}}
|
||||
</th>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'name'">
|
||||
{{'IMPORT.TABLE_HEADER_NAME' | translate}}
|
||||
</th>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'protocol'">
|
||||
{{'IMPORT.TABLE_HEADER_PROTOCOL' | translate}}
|
||||
</th>
|
||||
<th guac-sort-order="errorOrder" guac-sort-property="'errors'">
|
||||
{{'IMPORT.TABLE_HEADER_ERRORS' | translate}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="error in errorPage">
|
||||
<td>{{error.rowNumber}}</td>
|
||||
<td>{{error.name}}</td>
|
||||
<td>{{error.protocol}}</td>
|
||||
<td class="error-message" ng-class="{ 'has-errors' : error.errors.getArray().length }">
|
||||
<ul>
|
||||
<li ng-repeat="message in error.errors.getArray()">
|
||||
{{ message }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pager for connection error list -->
|
||||
<guac-pager page="errorPage" page-size="25"
|
||||
items="filteredErrors | orderBy : errorOrder.predicate"></guac-pager>
|
||||
</div>
|
@@ -0,0 +1,60 @@
|
||||
<div class="settings-view view import">
|
||||
|
||||
<div class="header">
|
||||
<h2>{{'IMPORT.SECTION_HEADER_CONNECTION_IMPORT' | translate}}</h2>
|
||||
<guac-user-menu></guac-user-menu>
|
||||
</div>
|
||||
|
||||
<div ng-show="fileName" class="file-upload-container file-selected">
|
||||
<div class="file-name"> {{fileName}} </div>
|
||||
<button class="danger clear" ng-click="cancel()">
|
||||
{{'IMPORT.ACTION_CLEAR' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ng-show="!fileName" class="file-upload-container">
|
||||
|
||||
<div class="upload-header">
|
||||
<span class="file-options">{{'IMPORT.HELP_UPLOAD_FILE_TYPES' | translate}}</span>
|
||||
<a
|
||||
href="#/import/connection/file-format-help" target="_blank"
|
||||
class="file-help-link">{{'IMPORT.ACTION_VIEW_FORMAT_HELP' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="drop-target" guac-upload="handleFiles"
|
||||
guac-drop="handleFiles" guac-multiple="false"
|
||||
guac-dragged-class="'drop-pending'"
|
||||
ng-class="{'file-present': fileName}">
|
||||
|
||||
<div class="title">{{'IMPORT.HELP_UPLOAD_DROP_TITLE' | translate}}</div>
|
||||
|
||||
<input type="file" class="file-upload-input"/>
|
||||
<a ng-click="openFileBrowser()" class="browse-link">
|
||||
{{'IMPORT.ACTION_BROWSE' | translate}}
|
||||
</a>
|
||||
|
||||
<div class="file-name"> {{fileName}} </div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="import-buttons">
|
||||
<button
|
||||
ng-click="import()" ng-disabled="importDisabled()" class="save import">
|
||||
{{'IMPORT.ACTION_IMPORT' | translate}}
|
||||
</button>
|
||||
<button
|
||||
ng-click="cancel()" ng-disabled="cancelDisabled()" class="cancel">
|
||||
{{'IMPORT.ACTION_CANCEL' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ng-show="isLoading()" class="loading"></div>
|
||||
|
||||
<!-- Connection specific errors, if there are any -->
|
||||
<connection-import-errors parse-result="parseResult" patch-failure="patchFailure" />
|
||||
|
||||
|
||||
</div>
|
@@ -0,0 +1,29 @@
|
||||
<div class="import view help">
|
||||
|
||||
<div class="header">
|
||||
<h2>{{'IMPORT.SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE' | translate}}</h2>
|
||||
<guac-user-menu></guac-user-menu>
|
||||
</div>
|
||||
|
||||
<h2>{{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}</p>
|
||||
|
||||
<h2>{{'IMPORT.SECTION_HEADER_CSV' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}</p>
|
||||
<p>{{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}</p>
|
||||
<pre>{{'IMPORT.HELP_CSV_EXAMPLE' | translate }}</pre>
|
||||
|
||||
<h2>{{'IMPORT.SECTION_HEADER_JSON' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}</p>
|
||||
<p>{{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}</p>
|
||||
<pre>{{'IMPORT.HELP_JSON_EXAMPLE' | translate }}</pre>
|
||||
|
||||
<h2>{{'IMPORT.SECTION_HEADER_YAML' | translate}}</h2>
|
||||
<p>{{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}</p>
|
||||
<pre>{{'IMPORT.HELP_YAML_EXAMPLE' | translate}}</pre>
|
||||
|
||||
<ol class="footnotes">
|
||||
<li>{{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the DisplayErrorList class.
|
||||
*/
|
||||
angular.module('import').factory('DisplayErrorList', [
|
||||
function defineDisplayErrorList() {
|
||||
|
||||
/**
|
||||
* A list of human-readable error messages, intended to be usable in a
|
||||
* sortable / filterable table.
|
||||
*
|
||||
* @constructor
|
||||
* @param {String[]} messages
|
||||
* The error messages that should be prepared for display.
|
||||
*/
|
||||
const DisplayErrorList = function DisplayErrorList(messages) {
|
||||
|
||||
/**
|
||||
* The error messages that should be prepared for display.
|
||||
*
|
||||
* @type {String[]}
|
||||
*/
|
||||
this.messages = messages || [];
|
||||
|
||||
/**
|
||||
* The single String message composed of all messages concatenated
|
||||
* together. This will be used for filtering / sorting, and should only
|
||||
* be calculated once, when toString() is called.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.concatenatedMessage = null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a sortable / filterable representation of all the error messages
|
||||
* wrapped by this DisplayErrorList.
|
||||
*
|
||||
* NOTE: Once this method is called, any changes to the underlying array
|
||||
* will have no effect. This is to ensure that repeated calls to toString()
|
||||
* by sorting / filtering UI code will not regenerate the concatenated
|
||||
* message every time.
|
||||
*
|
||||
* @returns {String}
|
||||
* A sortable / filterable representation of the error messages wrapped
|
||||
* by this DisplayErrorList
|
||||
*/
|
||||
DisplayErrorList.prototype.toString = function messageListToString() {
|
||||
|
||||
// Generate the concatenated message if not already generated
|
||||
if (!this.concatenatedMessage)
|
||||
this.concatenatedMessage = this.messages.join(' ');
|
||||
|
||||
return this.concatenatedMessage;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying array containing the raw error messages, wrapped
|
||||
* by this DisplayErrorList.
|
||||
*
|
||||
* @returns {String[]}
|
||||
* The underlying array containing the raw error messages, wrapped by
|
||||
* this DisplayErrorList
|
||||
*/
|
||||
DisplayErrorList.prototype.getArray = function getUnderlyingArray() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
return DisplayErrorList;
|
||||
|
||||
}]);
|
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the ImportConnection class.
|
||||
*/
|
||||
angular.module('import').factory('ImportConnection', [
|
||||
function defineImportConnection() {
|
||||
|
||||
/**
|
||||
* A representation of a connection to be imported, as parsed from an
|
||||
* user-supplied import file.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ImportConnection|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* Connection.
|
||||
*/
|
||||
const ImportConnection = function ImportConnection(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The unique identifier of the connection group that contains this
|
||||
* connection.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.parentIdentifier = template.parentIdentifier;
|
||||
|
||||
/**
|
||||
* The path to the connection group that contains this connection,
|
||||
* written as e.g. "ROOT/parent/child/group".
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.group = template.group;
|
||||
|
||||
/**
|
||||
* The human-readable name of this connection, which is not necessarily
|
||||
* unique.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.name = template.name;
|
||||
|
||||
/**
|
||||
* The name of the protocol associated with this connection, such as
|
||||
* "vnc" or "rdp".
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.protocol = template.protocol;
|
||||
|
||||
/**
|
||||
* Connection configuration parameters, as dictated by the protocol in
|
||||
* use, arranged as name/value pairs.
|
||||
*
|
||||
* @type Object.<String, String>
|
||||
*/
|
||||
this.parameters = template.parameters || {};
|
||||
|
||||
/**
|
||||
* Arbitrary name/value pairs which further describe this connection.
|
||||
* The semantics and validity of these attributes are dictated by the
|
||||
* extension which defines them.
|
||||
*
|
||||
* @type Object.<String, String>
|
||||
*/
|
||||
this.attributes = template.attributes || {};
|
||||
|
||||
/**
|
||||
* The identifiers of all users who should be granted read access to
|
||||
* this connection.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
this.users = template.users || [];
|
||||
|
||||
/**
|
||||
* The identifiers of all user groups who should be granted read access
|
||||
* to this connection.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
this.groups = template.groups || [];
|
||||
|
||||
};
|
||||
|
||||
return ImportConnection;
|
||||
|
||||
}]);
|
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the ImportConnectionError class.
|
||||
*/
|
||||
angular.module('import').factory('ImportConnectionError', ['$injector',
|
||||
function defineImportConnectionError($injector) {
|
||||
|
||||
// Required types
|
||||
const DisplayErrorList = $injector.get('DisplayErrorList');
|
||||
|
||||
/**
|
||||
* A representation of the errors associated with a connection to be
|
||||
* imported, along with some basic information connection information to
|
||||
* identify the connection having the error, as returned from a parsed
|
||||
* user-supplied import file.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ImportConnectionError|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ImportConnectionError.
|
||||
*/
|
||||
const ImportConnectionError = function ImportConnectionError(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The row number within the original connection import file for this
|
||||
* connection. This should be 1-indexed.
|
||||
*/
|
||||
this.rowNumber = template.rowNumber;
|
||||
|
||||
/**
|
||||
* The human-readable name of this connection, which is not necessarily
|
||||
* unique.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.name = template.name;
|
||||
|
||||
/**
|
||||
* The name of the protocol associated with this connection, such as
|
||||
* "vnc" or "rdp".
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.protocol = template.protocol;
|
||||
|
||||
/**
|
||||
* The error messages associated with this particular connection, if any.
|
||||
*
|
||||
* @type ImportConnectionError
|
||||
*/
|
||||
this.errors = template.errors || new DisplayErrorList();
|
||||
|
||||
};
|
||||
|
||||
return ImportConnectionError;
|
||||
|
||||
}]);
|
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the ParseError class.
|
||||
*/
|
||||
angular.module('import').factory('ParseError', [function defineParseError() {
|
||||
|
||||
/**
|
||||
* An error representing a parsing failure when attempting to convert
|
||||
* user-provided data into a list of Connection objects.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ParseError|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ParseError.
|
||||
*/
|
||||
const ParseError = function ParseError(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* A human-readable message describing the error that occurred.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.message = template.message;
|
||||
|
||||
/**
|
||||
* The key associated with the translation string that used when
|
||||
* displaying this message.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.key = template.key;
|
||||
|
||||
/**
|
||||
* The object which should be passed through to the translation service
|
||||
* for the sake of variable substitution. Each property of the provided
|
||||
* object will be substituted for the variable of the same name within
|
||||
* the translation string.
|
||||
*
|
||||
* @type Object
|
||||
*/
|
||||
this.variables = template.variables;
|
||||
|
||||
// If no translation key is available, fall back to the untranslated
|
||||
// key, passing the raw message directly through the translation system
|
||||
if (!this.key) {
|
||||
this.key = 'APP.TEXT_UNTRANSLATED';
|
||||
this.variables = { MESSAGE: this.message };
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return ParseError;
|
||||
|
||||
}]);
|
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the ParseResult class.
|
||||
*/
|
||||
angular.module('import').factory('ParseResult', [function defineParseResult() {
|
||||
|
||||
/**
|
||||
* The result of parsing a connection import file - containing a list of
|
||||
* API patches ready to be submitted to the PATCH REST API for batch
|
||||
* connection creation, a set of users and user groups to grant access to
|
||||
* each connection, and any errors that may have occurred while parsing
|
||||
* each connection.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ParseResult|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ParseResult.
|
||||
*/
|
||||
const ParseResult = function ParseResult(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* An array of patches, ready to be submitted to the PATCH REST API for
|
||||
* batch connection creation.
|
||||
*
|
||||
* @type {DirectoryPatch[]}
|
||||
*/
|
||||
this.patches = template.patches || [];
|
||||
|
||||
/**
|
||||
* An object whose keys are the user identifiers of users specified
|
||||
* in the batch import, and whose values are an array of indices of
|
||||
* connections to which those users should be granted access.
|
||||
*
|
||||
* @type {Object.<String, Integer[]>}
|
||||
*/
|
||||
this.users = template.users || {};
|
||||
|
||||
/**
|
||||
* An object whose keys are the user group identifiers of every user
|
||||
* group specified in the batch import. i.e. a set of all user group
|
||||
* identifiers.
|
||||
*
|
||||
* @type {Object.<String, Boolean>}
|
||||
*/
|
||||
this.groups = template.users || {};
|
||||
|
||||
/**
|
||||
* An array of errors encountered while parsing the corresponding
|
||||
* connection (at the same array index). Each connection should have a
|
||||
* an array of errors. If empty, no errors occurred for this connection.
|
||||
*
|
||||
* @type {ParseError[][]}
|
||||
*/
|
||||
this.errors = template.errors || [];
|
||||
|
||||
/**
|
||||
* True if any errors were encountered while parsing the connections
|
||||
* represented by this ParseResult. This should always be true if there
|
||||
* are a non-zero number of elements in the errors list for any
|
||||
* connection, or false otherwise.
|
||||
*/
|
||||
this.hasErrors = template.hasErrors || false;
|
||||
|
||||
};
|
||||
|
||||
return ParseResult;
|
||||
|
||||
}]);
|
@@ -126,6 +126,23 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
|
||||
resolve : { routeToUserHomePage: routeToUserHomePage }
|
||||
})
|
||||
|
||||
// Connection import page
|
||||
.when('/import/:dataSource/connection', {
|
||||
title : 'APP.NAME',
|
||||
bodyClassName : 'settings',
|
||||
templateUrl : 'app/import/templates/connectionImport.html',
|
||||
controller : 'importConnectionsController',
|
||||
resolve : { updateCurrentToken: updateCurrentToken }
|
||||
})
|
||||
|
||||
// Connection import file format help page
|
||||
.when('/import/connection/file-format-help', {
|
||||
title : 'APP.NAME',
|
||||
bodyClassName : 'settings',
|
||||
templateUrl : 'app/import/templates/connectionImportFileHelp.html',
|
||||
resolve : { updateCurrentToken: updateCurrentToken }
|
||||
})
|
||||
|
||||
// Management screen
|
||||
.when('/settings/:dataSource?/:tab', {
|
||||
title : 'APP.NAME',
|
||||
|
@@ -35,6 +35,7 @@ angular.module('index', [
|
||||
'client',
|
||||
'clipboard',
|
||||
'home',
|
||||
'import',
|
||||
'login',
|
||||
'manage',
|
||||
'navigation',
|
||||
|
@@ -24,7 +24,6 @@ angular.module('rest').factory('connectionService', ['$injector',
|
||||
function connectionService($injector) {
|
||||
|
||||
// Required services
|
||||
var requestService = $injector.get('requestService');
|
||||
var authenticationService = $injector.get('authenticationService');
|
||||
var cacheService = $injector.get('cacheService');
|
||||
|
||||
@@ -155,6 +154,49 @@ angular.module('rest').factory('connectionService', ['$injector',
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to the REST API to apply a supplied list of connection
|
||||
* patches, returning a promise that can be used for processing the results
|
||||
* of the call.
|
||||
*
|
||||
* This operation is atomic - if any errors are encountered during the
|
||||
* connection patching process, the entire request will fail, and no
|
||||
* changes will be persisted.
|
||||
*
|
||||
* @param {String} dataSource
|
||||
* The identifier of the data source associated with the connections to
|
||||
* be patched.
|
||||
*
|
||||
* @param {DirectoryPatch.<Connection>[]} patches
|
||||
* An array of patches to apply.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise for the HTTP call which will succeed if and only if the
|
||||
* patch operation is successful.
|
||||
*/
|
||||
service.patchConnections = function patchConnections(dataSource, patches) {
|
||||
|
||||
// Make the PATCH request
|
||||
return authenticationService.request({
|
||||
method : 'PATCH',
|
||||
url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections',
|
||||
data : patches
|
||||
})
|
||||
|
||||
// Clear the cache
|
||||
.then(function connectionsPatched(patchResponse){
|
||||
cacheService.connections.removeAll();
|
||||
|
||||
// Clear users cache to force reload of permissions for any
|
||||
// newly created or replaced connections
|
||||
cacheService.users.removeAll();
|
||||
|
||||
return patchResponse;
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to the REST API to delete a connection,
|
||||
* returning a promise that can be used for processing the results of the call.
|
||||
|
@@ -190,6 +190,43 @@ angular.module('rest').factory('userGroupService', ['$injector',
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to the REST API to apply a supplied list of user group
|
||||
* patches, returning a promise that can be used for processing the results
|
||||
* of the call.
|
||||
*
|
||||
* This operation is atomic - if any errors are encountered during the
|
||||
* connection patching process, the entire request will fail, and no
|
||||
* changes will be persisted.
|
||||
*
|
||||
* @param {String} dataSource
|
||||
* The identifier of the data source associated with the user groups to
|
||||
* be patched.
|
||||
*
|
||||
* @param {DirectoryPatch.<UserGroup>[]} patches
|
||||
* An array of patches to apply.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise for the HTTP call which will succeed if and only if the
|
||||
* patch operation is successful.
|
||||
*/
|
||||
service.patchUserGroups = function patchUserGroups(dataSource, patches) {
|
||||
|
||||
// Make the PATCH request
|
||||
return authenticationService.request({
|
||||
method : 'PATCH',
|
||||
url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups',
|
||||
data : patches
|
||||
})
|
||||
|
||||
// Clear the cache
|
||||
.then(function userGroupsPatched(patchResponse){
|
||||
cacheService.users.removeAll();
|
||||
return patchResponse;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
||||
|
@@ -236,6 +236,43 @@ angular.module('rest').factory('userService', ['$injector',
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to the REST API to apply a supplied list of user patches,
|
||||
* returning a promise that can be used for processing the results of the
|
||||
* call.
|
||||
*
|
||||
* This operation is atomic - if any errors are encountered during the
|
||||
* connection patching process, the entire request will fail, and no
|
||||
* changes will be persisted.
|
||||
*
|
||||
* @param {String} dataSource
|
||||
* The identifier of the data source associated with the users to be
|
||||
* patched.
|
||||
*
|
||||
* @param {DirectoryPatch.<User>[]} patches
|
||||
* An array of patches to apply.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise for the HTTP call which will succeed if and only if the
|
||||
* patch operation is successful.
|
||||
*/
|
||||
service.patchUsers = function patchUsers(dataSource, patches) {
|
||||
|
||||
// Make the PATCH request
|
||||
return authenticationService.request({
|
||||
method : 'PATCH',
|
||||
url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users',
|
||||
data : patches
|
||||
})
|
||||
|
||||
// Clear the cache
|
||||
.then(function usersPatched(patchResponse){
|
||||
cacheService.users.removeAll();
|
||||
return patchResponse;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
||||
|
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the DirectoryPatch class.
|
||||
*/
|
||||
angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch() {
|
||||
|
||||
/**
|
||||
* The object consumed by REST API calls when representing changes to an
|
||||
* arbitrary set of directory-based objects.
|
||||
* @constructor
|
||||
*
|
||||
* @template DirectoryObject
|
||||
* The directory-based object type that this DirectoryPatch will
|
||||
* operate on.
|
||||
*
|
||||
* @param {DirectoryObject|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* DirectoryPatch.
|
||||
*/
|
||||
var DirectoryPatch = function DirectoryPatch(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The operation to apply to the objects indicated by the path. Valid
|
||||
* operation values are defined within DirectoryPatch.Operation.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.op = template.op;
|
||||
|
||||
/**
|
||||
* The path of the objects to modify. For creation of new objects, this
|
||||
* should be "/". Otherwise, it should be "/{identifier}", specifying
|
||||
* the identifier of the existing object being modified.
|
||||
*
|
||||
* @type {String}
|
||||
* @default '/'
|
||||
*/
|
||||
this.path = template.path || '/';
|
||||
|
||||
/**
|
||||
* The object being added, or undefined if deleting.
|
||||
*
|
||||
* @type {DirectoryObject}
|
||||
*/
|
||||
this.value = template.value;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* All valid patch operations for directory-based objects.
|
||||
*/
|
||||
DirectoryPatch.Operation = {
|
||||
|
||||
/**
|
||||
* Adds the specified object to the relation.
|
||||
*/
|
||||
ADD : 'add',
|
||||
|
||||
/**
|
||||
* Removes the specified object from the relation.
|
||||
*/
|
||||
REMOVE : 'remove'
|
||||
|
||||
};
|
||||
|
||||
return DirectoryPatch;
|
||||
|
||||
}]);
|
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the DirectoryPatchOutcome class.
|
||||
*/
|
||||
angular.module('rest').factory('DirectoryPatchOutcome', [
|
||||
function defineDirectoryPatchOutcome() {
|
||||
|
||||
/**
|
||||
* An object returned by a PATCH request to a directory REST API,
|
||||
* representing the outcome associated with a particular patch in the
|
||||
* request. This object can indicate either a successful or unsuccessful
|
||||
* response. The error field is only meaningful for unsuccessful patches.
|
||||
* @constructor
|
||||
*
|
||||
* @param {DirectoryPatchOutcome|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* DirectoryPatchOutcome.
|
||||
*/
|
||||
const DirectoryPatchOutcome = function DirectoryPatchOutcome(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The operation to apply to the objects indicated by the path. Valid
|
||||
* operation values are defined within DirectoryPatch.Operation.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.op = template.op;
|
||||
|
||||
/**
|
||||
* The path of the object operated on by the corresponding patch in the
|
||||
* request.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.path = template.path;
|
||||
|
||||
/**
|
||||
* The identifier of the object operated on by the corresponding patch
|
||||
* in the request. If the object was newly created and the PATCH request
|
||||
* did not fail, this will be the identifier of the newly created object.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.identifier = template.identifier;
|
||||
|
||||
/**
|
||||
* The error message associated with the failure, if the patch failed to
|
||||
* apply.
|
||||
*
|
||||
* @type {TranslatableMessage}
|
||||
*/
|
||||
this.error = template.error;
|
||||
|
||||
};
|
||||
|
||||
return DirectoryPatchOutcome;
|
||||
|
||||
}]);
|
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the DirectoryPatchResponse class.
|
||||
*/
|
||||
angular.module('rest').factory('DirectoryPatchResponse', [
|
||||
function defineDirectoryPatchResponse() {
|
||||
|
||||
/**
|
||||
* An object returned by a PATCH request to a directory REST API,
|
||||
* representing the successful response to a patch request.
|
||||
*
|
||||
* @param {DirectoryPatchResponse|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* DirectoryPatchResponse.
|
||||
*/
|
||||
const DirectoryPatchResponse = function DirectoryPatchResponse(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* An outcome for each patch in the corresponding patch request.
|
||||
*
|
||||
* @type {DirectoryPatchOutcome[]}
|
||||
*/
|
||||
this.patches = template.patches;
|
||||
|
||||
};
|
||||
|
||||
return DirectoryPatchResponse;
|
||||
|
||||
}]);
|
@@ -78,6 +78,15 @@ angular.module('rest').factory('Error', [function defineError() {
|
||||
*/
|
||||
this.expected = template.expected;
|
||||
|
||||
/**
|
||||
* The outcome for each patch that was submitted as part of the request
|
||||
* that generated this error, if the request was a directory PATCH
|
||||
* request. In all other cases, this will be null.
|
||||
*
|
||||
* @type DirectoryPatchOutcome[]
|
||||
*/
|
||||
this.patches = template.patches || null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -20,7 +20,7 @@
|
||||
/**
|
||||
* The controller for the session recording player page.
|
||||
*/
|
||||
angular.module('manage').controller('connectionHistoryPlayerController', ['$scope', '$injector',
|
||||
angular.module('settings').controller('connectionHistoryPlayerController', ['$scope', '$injector',
|
||||
function connectionHistoryPlayerController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
|
@@ -20,7 +20,7 @@
|
||||
/**
|
||||
* The controller for the general settings page.
|
||||
*/
|
||||
angular.module('manage').controller('settingsController', ['$scope', '$injector',
|
||||
angular.module('settings').controller('settingsController', ['$scope', '$injector',
|
||||
function settingsController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
|
@@ -20,7 +20,8 @@
|
||||
a.button.add-user,
|
||||
a.button.add-user-group,
|
||||
a.button.add-connection,
|
||||
a.button.add-connection-group {
|
||||
a.button.add-connection-group,
|
||||
a.button.import-connections {
|
||||
font-size: 0.8em;
|
||||
padding-left: 1.8em;
|
||||
position: relative;
|
||||
@@ -29,7 +30,8 @@ a.button.add-connection-group {
|
||||
a.button.add-user::before,
|
||||
a.button.add-user-group::before,
|
||||
a.button.add-connection::before,
|
||||
a.button.add-connection-group::before {
|
||||
a.button.add-connection-group::before,
|
||||
a.button.import-connections::before {
|
||||
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
@@ -59,3 +61,7 @@ a.button.add-connection::before {
|
||||
a.button.add-connection-group::before {
|
||||
background-image: url('images/action-icons/guac-group-add.svg');
|
||||
}
|
||||
|
||||
a.button.import-connections::before {
|
||||
background-image: url('images/action-icons/guac-file-import.svg');
|
||||
}
|
||||
|
@@ -9,6 +9,10 @@
|
||||
<!-- Form action buttons -->
|
||||
<div class="action-buttons">
|
||||
|
||||
<a class="import-connections button"
|
||||
ng-show="canCreateConnections()"
|
||||
href="#/import/{{dataSource | escape}}/connection/">{{'SETTINGS_CONNECTIONS.ACTION_IMPORT' | translate}}</a>
|
||||
|
||||
<a class="add-connection button"
|
||||
ng-show="canCreateConnections()"
|
||||
href="#/manage/{{dataSource | escape}}/connections/">{{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}}</a>
|
||||
|
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M8.813 0A1.813 1.813 0 0 0 7 1.814v60.373C7 63.188 7.812 64 8.813 64h48.374C58.188 64 59 63.188 59 62.187V13.89c0-.482-.19-.943-.531-1.284L46.375.531c-.34-.34-.8-.53-1.281-.531H8.812zm37.49 3.02 10.885 10.867H46.303V3.02zM32.422 12a1.642 1.642 0 0 1 1.258.586L50.459 32.6a1.642 1.642 0 0 1-1.258 2.697h-8.84v17.98a1.642 1.642 0 0 1-1.64 1.643H26.623a1.642 1.642 0 0 1-1.643-1.643v-17.98h-9.337a1.642 1.642 0 0 1-1.258-2.697l16.78-20.014A1.642 1.642 0 0 1 32.421 12z" style="fill:#fff;stroke-width:1.35948"/></svg>
|
After Width: | Height: | Size: 585 B |
@@ -9,11 +9,13 @@
|
||||
|
||||
"ACTION_ACKNOWLEDGE" : "OK",
|
||||
"ACTION_CANCEL" : "Cancel",
|
||||
"ACTION_CLEAR" : "Clear",
|
||||
"ACTION_CLONE" : "Clone",
|
||||
"ACTION_CONTINUE" : "Continue",
|
||||
"ACTION_DELETE" : "Delete",
|
||||
"ACTION_DELETE_SESSIONS" : "Kill Sessions",
|
||||
"ACTION_DOWNLOAD" : "Download",
|
||||
"ACTION_IMPORT" : "Import",
|
||||
"ACTION_LOGIN" : "Login",
|
||||
"ACTION_LOGIN_AGAIN" : "Re-login",
|
||||
"ACTION_LOGOUT" : "Logout",
|
||||
@@ -39,6 +41,7 @@
|
||||
"ERROR_PAGE_UNAVAILABLE" : "An error has occurred and this action cannot be completed. If the problem persists, please notify your system administrator or check your system logs.",
|
||||
"ERROR_PASSWORD_BLANK" : "Your password cannot be blank.",
|
||||
"ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.",
|
||||
"ERROR_SINGLE_FILE_ONLY" : "Please upload only a single file at a time",
|
||||
|
||||
"FIELD_HEADER_PASSWORD" : "Password:",
|
||||
"FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter Password:",
|
||||
@@ -60,8 +63,8 @@
|
||||
|
||||
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
|
||||
"ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
|
||||
"ACTION_CLEAR_CLIENT_MESSAGES" : "Clear",
|
||||
"ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear",
|
||||
"ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR",
|
||||
"ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR",
|
||||
"ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE",
|
||||
"ACTION_DISCONNECT" : "Disconnect",
|
||||
"ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT",
|
||||
@@ -184,6 +187,62 @@
|
||||
|
||||
},
|
||||
|
||||
"IMPORT": {
|
||||
|
||||
"ACTION_ACKNOWLEDGE": "@:APP.ACTION_ACKNOWLEDGE",
|
||||
"ACTION_BROWSE": "Browse for File",
|
||||
"ACTION_CANCEL": "@:APP.ACTION_CANCEL",
|
||||
"ACTION_CLEAR": "@:APP.ACTION_CLEAR",
|
||||
"ACTION_VIEW_FORMAT_HELP": "View Format Tips",
|
||||
"ACTION_IMPORT": "@:APP.ACTION_IMPORT",
|
||||
"ACTION_IMPORT_CONNECTIONS": "Import Connections",
|
||||
|
||||
"DIALOG_HEADER_ERROR": "@:APP.DIALOG_HEADER_ERROR",
|
||||
"DIALOG_HEADER_SUCCESS": "Success",
|
||||
|
||||
"ERROR_AMBIGUOUS_CSV_HEADER": "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter",
|
||||
"ERROR_AMBIGUOUS_PARENT_GROUP": "Both group and parentIdentifier may be not specified at the same time",
|
||||
"ERROR_ARRAY_REQUIRED": "The provided file must contain a list of connections",
|
||||
"ERROR_DUPLICATE_CSV_HEADER": "Duplicate CSV Header: {HEADER}",
|
||||
"ERROR_EMPTY_FILE": "The provided file is empty",
|
||||
"ERROR_INVALID_CSV_HEADER": "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter",
|
||||
"ERROR_INVALID_MIME_TYPE": "Unsupported file type: \"{TYPE}\"",
|
||||
"ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found",
|
||||
"ERROR_INVALID_USER_GROUP_IDENTIFIERS": "User Groups not found: {IDENTIFIER_LIST}",
|
||||
"ERROR_INVALID_USER_IDENTIFIERS": "Users not found: {IDENTIFIER_LIST}",
|
||||
"ERROR_NO_FILE_SUPPLIED": "Please select a file to import",
|
||||
"ERROR_REQUIRED_NAME": "No connection name found in the provided file",
|
||||
"ERROR_REQUIRED_PROTOCOL": "No connection protocol found in the provided file",
|
||||
|
||||
"FIELD_PLACEHOLDER_FILTER": "@:APP.FIELD_PLACEHOLDER_FILTER",
|
||||
|
||||
"HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.",
|
||||
"HELP_CSV_EXAMPLE": "name,protocol,hostname,group,users,groups,guacd-encryption (attribute)\nconn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none\nconn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl\nconn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,\nconn4,kubernetes,,,,,",
|
||||
"HELP_CSV_MORE_DETAILS": "The CSV header for each row specifies the connection field. The connection group ID that the connection should be imported into may be directly specified with \"parentIdentifier\", or the path to the parent group may be specified using \"group\" as shown below. In most cases, there should be no conflict between fields, but if needed, an \" (attribute)\" or \" (parameter)\" suffix may be added to disambiguate. Lists of user or user group identifiers must be semicolon-seperated.¹",
|
||||
"HELP_FILE_TYPE_DESCRIPTION": "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created.",
|
||||
"HELP_FILE_TYPE_HEADER": "File Types",
|
||||
"HELP_JSON_DESCRIPTION": "A connection import JSON file is a list of connection objects. At minimum the connection name and protocol must be specified in each connection object.",
|
||||
"HELP_JSON_EXAMPLE": "[\n \\{\n \"name\": \"conn1\",\n \"protocol\": \"vnc\",\n \"parameters\": \\{ \"hostname\": \"conn1.web.com\" \\},\n \"parentIdentifier\": \"ROOT\",\n \"users\": [ \"guac user 1\", \"guac user 2\" ],\n \"groups\": [ \"Connection 1 Users\" ],\n \"attributes\": \\{ \"guacd-encryption\": \"none\" \\}\n \\},\n \\{\n \"name\": \"conn2\",\n \"protocol\": \"rdp\",\n \"parameters\": \\{ \"hostname\": \"conn2.web.com\" \\},\n \"group\": \"ROOT/Parent Group\",\n \"users\": [ \"guac user 1\" ],\n \"attributes\": \\{ \"guacd-encryption\": \"none\" \\}\n \\},\n \\{\n \"name\": \"conn3\",\n \"protocol\": \"ssh\",\n \"parameters\": \\{ \"hostname\": \"conn3.web.com\" \\},\n \"group\": \"ROOT/Parent Group/Child Group\",\n \"users\": [ \"guac user 2\", \"guac user 3\" ]\n \\},\n \\{\n \"name\": \"conn4\",\n \"protocol\": \"kubernetes\"\n \\}\n]",
|
||||
"HELP_JSON_MORE_DETAILS": "The connection group ID that the connection should be imported into may be directly specified with a \"parentIdentifier\" field, or the path to the parent group may be specified using a \"group\" field as shown below. An array of user and user group identifiers to grant access to may be specified per connection.",
|
||||
"HELP_SEMICOLON_FOOTNOTE": "If present, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"",
|
||||
"HELP_UPLOAD_DROP_TITLE": "Drop a File Here",
|
||||
"HELP_UPLOAD_FILE_TYPES": "CSV, JSON, or YAML",
|
||||
"HELP_YAML_DESCRIPTION": "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.",
|
||||
"HELP_YAML_EXAMPLE": "---\n - name: conn1\n protocol: vnc\n parameters:\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes",
|
||||
"INFO_CONNECTIONS_IMPORTED_SUCCESS": "{NUMBER} {NUMBER, plural, one{connection} other{connections}} imported successfully.",
|
||||
|
||||
"SECTION_HEADER_CONNECTION_IMPORT": "Connection Import",
|
||||
"SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE": "Connection Import File Format",
|
||||
"SECTION_HEADER_CSV": "CSV Format",
|
||||
"SECTION_HEADER_JSON": "JSON Format",
|
||||
"SECTION_HEADER_YAML": "YAML Format",
|
||||
|
||||
"TABLE_HEADER_ERRORS": "Errors",
|
||||
"TABLE_HEADER_NAME": "Name",
|
||||
"TABLE_HEADER_PROTOCOL": "Protocol",
|
||||
"TABLE_HEADER_ROW_NUMBER": "Row #"
|
||||
},
|
||||
|
||||
"DATA_SOURCE_DEFAULT" : {
|
||||
"NAME" : "Default (XML)"
|
||||
},
|
||||
@@ -906,6 +965,7 @@
|
||||
"SETTINGS_CONNECTIONS" : {
|
||||
|
||||
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
|
||||
"ACTION_IMPORT" : "@:APP.ACTION_IMPORT",
|
||||
"ACTION_NEW_CONNECTION" : "New Connection",
|
||||
"ACTION_NEW_CONNECTION_GROUP" : "New Group",
|
||||
"ACTION_NEW_SHARING_PROFILE" : "New Sharing Profile",
|
||||
|
@@ -47,6 +47,22 @@ module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
// NOTE: This is required in order to parse ES2020 language features,
|
||||
// like the optional chaining and nullish coalescing operators. It
|
||||
// specifically needs to operate on the node-modules directory since
|
||||
// Webpack 4 cannot handle such language features.
|
||||
{
|
||||
test: /\.js$/i,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
['@babel/preset-env']
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Automatically extract imported CSS for later reference within separate CSS file
|
||||
{
|
||||
test: /\.css$/i,
|
||||
|
@@ -21,6 +21,7 @@ package org.apache.guacamole.rest;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.apache.guacamole.GuacamoleClientException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleResourceNotFoundException;
|
||||
@@ -31,6 +32,8 @@ import org.apache.guacamole.language.TranslatableMessage;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
|
||||
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome;
|
||||
import org.apache.guacamole.tunnel.GuacamoleStreamException;
|
||||
|
||||
/**
|
||||
@@ -71,6 +74,12 @@ public class APIError {
|
||||
*/
|
||||
private final Collection<Field> expected;
|
||||
|
||||
/**
|
||||
* The outcome of each patch in the associated request, if this was a
|
||||
* JSON Patch request. Otherwise null.
|
||||
*/
|
||||
private List<APIPatchOutcome> patches = null;
|
||||
|
||||
/**
|
||||
* The type of error that occurred.
|
||||
*/
|
||||
@@ -207,6 +216,9 @@ public class APIError {
|
||||
this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY,
|
||||
Collections.singletonMap(UNTRANSLATED_MESSAGE_VARIABLE_NAME, this.message));
|
||||
|
||||
if (exception instanceof APIPatchFailureException)
|
||||
this.patches = ((APIPatchFailureException) exception).getPatches();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,6 +255,18 @@ public class APIError {
|
||||
return expected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the outcome for every patch in the request, if the request was
|
||||
* a JSON patch request. Otherwise, null.
|
||||
*
|
||||
* @return
|
||||
* The outcome for every patch if responding to a JSON Patch request,
|
||||
* otherwise null.
|
||||
*/
|
||||
public List<APIPatchOutcome> getPatches() {
|
||||
return patches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable error message describing the error that
|
||||
* occurred.
|
||||
|
@@ -19,10 +19,15 @@
|
||||
|
||||
package org.apache.guacamole.rest.directory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -37,6 +42,9 @@ import org.apache.guacamole.GuacamoleClientException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleResourceNotFoundException;
|
||||
import org.apache.guacamole.GuacamoleUnsupportedException;
|
||||
import org.apache.guacamole.language.Translatable;
|
||||
import org.apache.guacamole.language.TranslatableMessage;
|
||||
import org.apache.guacamole.net.auth.AtomicDirectoryOperation;
|
||||
import org.apache.guacamole.net.auth.AuthenticatedUser;
|
||||
import org.apache.guacamole.net.auth.AuthenticationProvider;
|
||||
import org.apache.guacamole.net.auth.Directory;
|
||||
@@ -50,8 +58,13 @@ import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
|
||||
import org.apache.guacamole.net.event.DirectoryEvent;
|
||||
import org.apache.guacamole.net.event.DirectoryFailureEvent;
|
||||
import org.apache.guacamole.net.event.DirectorySuccessEvent;
|
||||
import org.apache.guacamole.rest.APIPatch;
|
||||
import org.apache.guacamole.rest.APIError;
|
||||
import org.apache.guacamole.rest.event.ListenerService;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatch;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatchError;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatchResponse;
|
||||
|
||||
/**
|
||||
* A REST resource which abstracts the operations available on all Guacamole
|
||||
@@ -341,6 +354,30 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
|
||||
return resourceFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter and sanitize the provided external object, translate to the
|
||||
* internal type, and return the translated internal object.
|
||||
*
|
||||
* @param object
|
||||
* The external object to filter and translate.
|
||||
*
|
||||
* @return
|
||||
* The filtered and translated internal object.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs while filtering or translating the external
|
||||
* object.
|
||||
*/
|
||||
private InternalType filterAndTranslate(ExternalType object)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Filter and sanitize the external object
|
||||
translator.filterExternalObject(userContext, object);
|
||||
|
||||
// Translate to the internal type
|
||||
return translator.toInternalObject(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of all objects available within this DirectoryResource,
|
||||
* filtering the returned map by the given permission, if specified.
|
||||
@@ -384,49 +421,269 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If the provided throwable is a known Guacamole-specific type, create and
|
||||
* return a APIPatchError with an error message extracted from the error.
|
||||
* If the provided throwable is not a known type, null will be returned.
|
||||
*
|
||||
* @param op
|
||||
* The operation being attempted when the error occurred.
|
||||
*
|
||||
* @param identifier
|
||||
* The identifier of the object in question, if any.
|
||||
*
|
||||
* @param path
|
||||
* The path for the patch that was being applied when the error occurred.
|
||||
*
|
||||
* @param t
|
||||
* The error that occurred while attempting to apply the patch.
|
||||
*
|
||||
* @return
|
||||
* A APIPatchError with an error message extracted from the provided
|
||||
* throwable - if it's a known type, otherwise null.
|
||||
*/
|
||||
@Nullable
|
||||
private APIPatchError createPatchFailure(
|
||||
@Nonnull APIPatch.Operation op, @Nullable String identifier,
|
||||
@Nonnull String path, @Nonnull Throwable t) {
|
||||
|
||||
/*
|
||||
* If the failure is a translatable type, use the translation directly
|
||||
* in the patch error.
|
||||
*/
|
||||
if (t instanceof Translatable)
|
||||
return new APIPatchError(
|
||||
op, identifier, path,
|
||||
((Translatable) t).getTranslatableMessage());
|
||||
|
||||
/*
|
||||
* If the failure represents a known Guacamole exception but is not
|
||||
* translateable, create a patch error containing the raw untranslated
|
||||
* exception message.
|
||||
*/
|
||||
if (t instanceof GuacamoleException) {
|
||||
|
||||
// Create a translated message that will fall
|
||||
// through to the untranslated message
|
||||
TranslatableMessage message = new TranslatableMessage(
|
||||
"APP.TEXT_UNTRANSLATED", Collections.singletonMap(
|
||||
"MESSAGE", ((GuacamoleException) t).getMessage()));
|
||||
|
||||
return new APIPatchError(op, identifier, path, message);
|
||||
}
|
||||
|
||||
// The error is not a known type - no patch error can be generated
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given object patches, updating the underlying directory
|
||||
* accordingly. This operation currently only supports deletion of objects
|
||||
* through the "remove" patch operation. The path of each patch operation is
|
||||
* of the form "/ID" where ID is the identifier of the object being
|
||||
* modified.
|
||||
* accordingly. This operation supports addition and removal of objects
|
||||
* through the "add" and "remove" patch operation. The path of each patch
|
||||
* operation is of the form "/ID" where ID is the identifier of the object
|
||||
* being modified. In the case of object creation, the identifier is
|
||||
* ignored, as the identifier will be automatically provided. This operation
|
||||
* is atomic.
|
||||
*
|
||||
* @param patches
|
||||
* The patches to apply for this request.
|
||||
*
|
||||
* @throws GuacamoleException
|
||||
* If an error occurs while deleting the objects.
|
||||
* If an error occurs while adding, updating, or removing objects.
|
||||
*
|
||||
* @return
|
||||
* A response describing the outcome of each patch. Only the identifier
|
||||
* of each patched object will be included in the response, not the
|
||||
* full object.
|
||||
*/
|
||||
@PATCH
|
||||
public void patchObjects(List<APIPatch<String>> patches)
|
||||
public APIPatchResponse patchObjects(List<APIPatch<ExternalType>> patches)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Apply each operation specified within the patch
|
||||
for (APIPatch<String> patch : patches) {
|
||||
// An outcome for each patch included in the request. This list
|
||||
// may include both success and failure responses, though the
|
||||
// presence of any failure would indicated that the entire
|
||||
// request has failed and no changes have been made.
|
||||
List<APIPatchOutcome> patchOutcomes = new ArrayList<>();
|
||||
|
||||
// Only remove is supported
|
||||
if (patch.getOp() != APIPatch.Operation.remove)
|
||||
throw new GuacamoleUnsupportedException("Only the \"remove\" "
|
||||
+ "operation is supported.");
|
||||
// Perform all requested operations atomically
|
||||
directory.tryAtomically(new AtomicDirectoryOperation<InternalType>() {
|
||||
|
||||
@Override
|
||||
public void executeOperation(boolean atomic, Directory<InternalType> directory)
|
||||
throws GuacamoleException {
|
||||
|
||||
// If the underlying directory implentation does not support
|
||||
// atomic operations, abort the patch operation. This REST
|
||||
// endpoint requires that operations be performed atomically.
|
||||
if (!atomic)
|
||||
throw new GuacamoleUnsupportedException(
|
||||
"The extension providing this directory does not " +
|
||||
"support Atomic Operations. The patch cannot be " +
|
||||
"executed.");
|
||||
|
||||
// Keep a list of all objects that have been successfully
|
||||
// added or removed
|
||||
Collection<InternalType> addedObjects = new ArrayList<>();
|
||||
Collection<String> removedIdentifiers = new ArrayList<>();
|
||||
|
||||
// A list of all responses associated with the successful
|
||||
// creation of new objects
|
||||
List<APIPatchOutcome> creationSuccesses = new ArrayList<>();
|
||||
|
||||
// True if any operation in the patch failed. Any failure will
|
||||
// fail the request, though won't result in immediate stoppage
|
||||
// since more errors may yet be uncovered.
|
||||
boolean failed = false;
|
||||
|
||||
// Apply each operation specified within the patch
|
||||
for (APIPatch<ExternalType> patch : patches) {
|
||||
|
||||
// Retrieve and validate path
|
||||
String path = patch.getPath();
|
||||
if (!path.startsWith("/"))
|
||||
throw new GuacamoleClientException("Patch paths must start with \"/\".");
|
||||
|
||||
// Remove specified object
|
||||
String identifier = path.substring(1);
|
||||
APIPatch.Operation op = patch.getOp();
|
||||
|
||||
if (op == APIPatch.Operation.add) {
|
||||
|
||||
// Filter/sanitize object contents
|
||||
InternalType internal = filterAndTranslate(patch.getValue());
|
||||
|
||||
try {
|
||||
directory.remove(identifier);
|
||||
fireDirectorySuccessEvent(DirectoryEvent.Operation.REMOVE, identifier, null);
|
||||
|
||||
// Attempt to add the new object
|
||||
directory.add(internal);
|
||||
|
||||
// Add the object to the list if addition was successful
|
||||
addedObjects.add(internal);
|
||||
|
||||
// Add a success outcome describing the object creation
|
||||
APIPatchOutcome response = new APIPatchOutcome(
|
||||
op, internal.getIdentifier(), path);
|
||||
patchOutcomes.add(response);
|
||||
creationSuccesses.add(response);
|
||||
|
||||
}
|
||||
|
||||
catch (GuacamoleException | RuntimeException | Error e) {
|
||||
fireDirectoryFailureEvent(DirectoryEvent.Operation.REMOVE, identifier, null, e);
|
||||
failed = true;
|
||||
fireDirectoryFailureEvent(
|
||||
DirectoryEvent.Operation.ADD,
|
||||
internal.getIdentifier(), internal, e);
|
||||
|
||||
// Attempt to generate an API Patch error using the
|
||||
// caught exception
|
||||
APIPatchError patchError = createPatchFailure(
|
||||
op, null, path, e);
|
||||
|
||||
if (patchError != null)
|
||||
patchOutcomes.add(patchError);
|
||||
|
||||
// If an unexpected failure occurs, fall through to
|
||||
// the standard API error handling
|
||||
else
|
||||
throw e;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Append each identifier to the list, to be removed atomically
|
||||
else if (op == APIPatch.Operation.remove) {
|
||||
|
||||
String identifier = path.substring(1);
|
||||
|
||||
try {
|
||||
|
||||
// Attempt to remove the object
|
||||
directory.remove(identifier);
|
||||
|
||||
// Add the object to the list if the removal was successful
|
||||
removedIdentifiers.add(identifier);
|
||||
|
||||
// Add a success outcome describing the object removal
|
||||
APIPatchOutcome response = new APIPatchOutcome(
|
||||
op, identifier, path);
|
||||
patchOutcomes.add(response);
|
||||
creationSuccesses.add(response);
|
||||
}
|
||||
catch (GuacamoleException | RuntimeException | Error e) {
|
||||
failed = true;
|
||||
fireDirectoryFailureEvent(
|
||||
DirectoryEvent.Operation.REMOVE,
|
||||
identifier, null, e);
|
||||
|
||||
// Attempt to generate an API Patch error using the
|
||||
// caught exception
|
||||
APIPatchError patchError = createPatchFailure(
|
||||
op, identifier, path, e);
|
||||
|
||||
if (patchError != null)
|
||||
patchOutcomes.add(patchError);
|
||||
|
||||
// If an unexpected failure occurs, fall through to
|
||||
// the standard API error handling
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
throw new GuacamoleUnsupportedException(
|
||||
"Unsupported patch operation \"" + op + "\". "
|
||||
+ "Only add and remove are supported.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// If any operation failed
|
||||
if (failed) {
|
||||
|
||||
// Any identifiers for objects created during this request
|
||||
// will no longer be valid, since the creation of those
|
||||
// objects will be rolled back.
|
||||
creationSuccesses.forEach(
|
||||
response -> response.clearIdentifier());
|
||||
|
||||
// Return an error response, including any failures that
|
||||
// caused the failure of any patch in the request
|
||||
throw new APIPatchFailureException(
|
||||
"The provided patches failed to apply.", patchOutcomes);
|
||||
|
||||
}
|
||||
|
||||
// Fire directory success events for each created object
|
||||
Iterator<InternalType> addedIterator = addedObjects.iterator();
|
||||
while (addedIterator.hasNext()) {
|
||||
|
||||
InternalType internal = addedIterator.next();
|
||||
fireDirectorySuccessEvent(
|
||||
DirectoryEvent.Operation.ADD,
|
||||
internal.getIdentifier(), internal);
|
||||
|
||||
}
|
||||
|
||||
// Fire directory success events for each removed object
|
||||
Iterator<String> removedIterator = removedIdentifiers.iterator();
|
||||
while (removedIterator.hasNext()) {
|
||||
|
||||
String identifier = removedIterator.next();
|
||||
fireDirectorySuccessEvent(
|
||||
DirectoryEvent.Operation.REMOVE,
|
||||
identifier, null);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Return a list of outcomes, one for each patch in the request
|
||||
return new APIPatchResponse(patchOutcomes);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -453,8 +710,7 @@ public abstract class DirectoryResource<InternalType extends Identifiable, Exter
|
||||
throw new GuacamoleClientException("Data must be submitted when creating objects.");
|
||||
|
||||
// Filter/sanitize object contents
|
||||
translator.filterExternalObject(userContext, object);
|
||||
InternalType internal = translator.toInternalObject(object);
|
||||
InternalType internal = filterAndTranslate(object);
|
||||
|
||||
// Create the new object within the directory
|
||||
try {
|
||||
|
@@ -29,7 +29,7 @@ import javax.ws.rs.core.MediaType;
|
||||
import org.apache.guacamole.GuacamoleClientException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.net.auth.RelatedObjectSet;
|
||||
import org.apache.guacamole.rest.APIPatch;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatch;
|
||||
|
||||
/**
|
||||
* A REST resource which abstracts the operations available on arbitrary sets
|
||||
|
@@ -17,11 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.rest;
|
||||
package org.apache.guacamole.rest.jsonpatch;
|
||||
|
||||
/**
|
||||
* An object for representing the body of a HTTP PATCH method.
|
||||
* See https://tools.ietf.org/html/rfc6902
|
||||
* An object for representing an entry within the body of a
|
||||
* JSON PATCH request. See https://tools.ietf.org/html/rfc6902
|
||||
*
|
||||
* @param <T>
|
||||
* The type of object being patched.
|
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.rest.jsonpatch;
|
||||
|
||||
import org.apache.guacamole.language.TranslatableMessage;
|
||||
import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation;
|
||||
|
||||
/**
|
||||
* A failure outcome associated with a particular patch within a JSON Patch
|
||||
* request. This status indicates that a particular patch failed to apply,
|
||||
* and includes the error describing the failure, along with the operation and
|
||||
* path from the original patch, and the identifier of the object
|
||||
* referenced by the original patch.
|
||||
*/
|
||||
public class APIPatchError extends APIPatchOutcome {
|
||||
|
||||
/**
|
||||
* The error associated with the submitted patch.
|
||||
*/
|
||||
private final TranslatableMessage error;
|
||||
|
||||
/**
|
||||
* Create a failure status associated with a submitted patch from a JSON
|
||||
* patch API request.
|
||||
*
|
||||
* @param op
|
||||
* The operation requested by the failed patch.
|
||||
*
|
||||
* @param identifier
|
||||
* The identifier of the object associated with the failed patch. If
|
||||
* the patch failed to create a new object, this will be null.
|
||||
*
|
||||
* @param path
|
||||
* The patch from the failed patch.
|
||||
*
|
||||
* @param error
|
||||
* The error message associated with the failure that prevented the
|
||||
* patch from applying.
|
||||
*/
|
||||
public APIPatchError(
|
||||
Operation op, String identifier, String path,
|
||||
TranslatableMessage error) {
|
||||
super(op, identifier, path);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error associated with the patch failure.
|
||||
*
|
||||
* @return
|
||||
* The error associated with the patch failure.
|
||||
*/
|
||||
public TranslatableMessage getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.guacamole.rest.jsonpatch;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.guacamole.GuacamoleClientException;
|
||||
|
||||
/**
|
||||
* An exception describing a failure to apply the patches from a JSON Patch
|
||||
* request. A list of outcomes is included, one for each patch in the request.
|
||||
*/
|
||||
public class APIPatchFailureException extends GuacamoleClientException {
|
||||
|
||||
/**
|
||||
* A list of outcomes, each one corresponding to a patch in the request
|
||||
* corresponding to this response. This may include a mix of successes and
|
||||
* failures. Any failure will result in a failure of the entire request
|
||||
* since JSON Patch requests are handled atomically.
|
||||
*/
|
||||
public final List<APIPatchOutcome> patches;
|
||||
|
||||
/**
|
||||
* Create a new patch request failure with the provided list of outcomes
|
||||
* for individual patches.
|
||||
*
|
||||
* @param message
|
||||
* A human-readable message describing the overall request failure.
|
||||
*
|
||||
* @param patches
|
||||
* A list of patch outcomes, one for each patch in the request
|
||||
* associated with this response.
|
||||
*/
|
||||
public APIPatchFailureException(
|
||||
String message, List<APIPatchOutcome> patches) {
|
||||
|
||||
super(message);
|
||||
this.patches = patches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the outcome for each patch in the request corresponding to this
|
||||
* response.
|
||||
*/
|
||||
public List<APIPatchOutcome> getPatches() {
|
||||
return patches;
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user