GUACAMOLE-926: Merge support for importing connections via CSV/JSON/YAML.

This commit is contained in:
Mike Jumper
2023-04-13 09:49:36 -07:00
committed by GitHub
105 changed files with 8429 additions and 79 deletions

View 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

View 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)

View File

@@ -0,0 +1 @@
base64-js:1.5.1

View 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.

View 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)

View File

@@ -0,0 +1 @@
buffer:4.9.2

View 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.

View 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)

View File

@@ -0,0 +1 @@
core-util-is:1.0.3

View 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.

View 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)

View File

@@ -0,0 +1,2 @@
csv:6.2.5
csv-parse:5.3.3

View 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.

View 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)

View File

@@ -0,0 +1 @@
events:3.3.0

View 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.

View 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)

View File

@@ -0,0 +1 @@
ieee754:1.2.1

View 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.

View 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)

View File

@@ -0,0 +1 @@
inherits:2.0.4

View 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.

View 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)

View File

@@ -0,0 +1 @@
isarray:1.0.0

View 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)

View File

@@ -0,0 +1 @@
process-nextick-args:2.0.1

View 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.**

View 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.
"""

View 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)

View File

@@ -0,0 +1 @@
readable-stream:2.3.7

View 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.

View 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)

View File

@@ -0,0 +1 @@
safe-buffer:5.1.2

View 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.

View 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)

View File

@@ -0,0 +1 @@
setimmediate:1.0.5

View 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.

View 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)

View File

@@ -0,0 +1 @@
stream-browserify:2.0.2

View 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.
"""

View 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)

View File

@@ -0,0 +1 @@
string_decoder:1.1.1

View 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.

View 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)

View File

@@ -0,0 +1 @@
timers-browserify:2.0.12

View 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.

View 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)

View File

@@ -0,0 +1 @@
util-deprecate:1.0.2

View File

@@ -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);

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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
};
}]);

View File

@@ -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);

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View 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']);

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View 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;
}

View 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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View File

@@ -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',

View File

@@ -35,6 +35,7 @@ angular.module('index', [
'client',
'clipboard',
'home',
'import',
'login',
'manage',
'navigation',

View File

@@ -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');
@@ -154,6 +153,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,

View File

@@ -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;
}]);

View File

@@ -235,6 +235,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;

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View File

@@ -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;
}]);

View File

@@ -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;
};
/**

View File

@@ -82,4 +82,4 @@ angular.module('rest').factory('RelatedObjectPatch', [function defineRelatedObje
return RelatedObjectPatch;
}]);
}]);

View File

@@ -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

View File

@@ -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

View File

@@ -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');
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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",

View File

@@ -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,

View File

@@ -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.

View File

@@ -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,48 +421,268 @@ 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>() {
// Retrieve and validate path
String path = patch.getPath();
if (!path.startsWith("/"))
throw new GuacamoleClientException("Patch paths must start with \"/\".");
@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 \"/\".");
APIPatch.Operation op = patch.getOp();
if (op == APIPatch.Operation.add) {
// Filter/sanitize object contents
InternalType internal = filterAndTranslate(patch.getValue());
try {
// 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) {
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);
}
// Remove specified object
String identifier = path.substring(1);
try {
directory.remove(identifier);
fireDirectorySuccessEvent(DirectoryEvent.Operation.REMOVE, identifier, null);
}
catch (GuacamoleException | RuntimeException | Error e) {
fireDirectoryFailureEvent(DirectoryEvent.Operation.REMOVE, identifier, null, e);
throw e;
}
}
});
// 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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More