From e195987b78e8519a7454573c2725359b51472d52 Mon Sep 17 00:00:00 2001 From: Aljoscha Rittner Date: Thu, 18 Nov 2021 07:59:05 +0100 Subject: [PATCH] Authentication for SURL with BasicAuth and OAuth2 (password and client_credentials) --- src/net/sourceforge/plantuml/FileSystem.java | 4 +- .../plantuml/preproc/ImportedFiles.java | 4 +- .../sourceforge/plantuml/security/SFile.java | 24 +- .../sourceforge/plantuml/security/SURL.java | 492 +++++++++++++++--- .../plantuml/security/SecurityUtils.java | 281 +++++++++- .../SecurityAccessInterceptor.java | 54 ++ .../SecurityAuthentication.java | 117 +++++ .../SecurityAuthorizeManager.java | 54 ++ .../authentication/SecurityCredentials.java | 410 +++++++++++++++ .../SecurityCredentialsContainer.java | 49 ++ .../SecurityDefaultNoopAccessInterceptor.java | 50 ++ .../SecurityDefaultNoopAuthorizeManager.java | 54 ++ .../basicauth/BasicAuthAccessInterceptor.java | 80 +++ .../basicauth/BasicAuthAuthorizeManager.java | 64 +++ .../AbstractOAuth2AccessAuthorizeManager.java | 165 ++++++ .../oauth/OAuth2AccessInterceptor.java | 61 +++ .../OAuth2ClientAccessAuthorizeManager.java | 103 ++++ ...h2ResourceOwnerAccessAuthorizeManager.java | 99 ++++ .../authentication/oauth/OAuth2Tokens.java | 62 +++ .../token/TokenAuthAccessInterceptor.java | 72 +++ .../token/TokenAuthAuthorizeManager.java | 56 ++ .../plantuml/tim/stdlib/Getenv.java | 9 +- .../plantuml/version/PSystemVersion.java | 2 +- .../plantuml/security/SFileTest.java | 58 +++ .../plantuml/security/SURLTest.java | 128 +++++ .../plantuml/security/SecurityUtilsTest.java | 163 ++++++ .../SecurityAuthenticationTest.java | 59 +++ .../SecurityCredentialsTest.java | 222 ++++++++ ...curityDefaultNoopAuthorizeManagerTest.java | 22 + .../BasicAuthAuthorizeManagerTest.java | 29 ++ ...tractOAuth2AccessAuthorizeManagerTest.java | 99 ++++ .../token/TokenAuthAuthorizeManagerTest.java | 37 ++ .../plantuml/tim/stdlib/GetenvTest.java | 59 +++ 33 files changed, 3157 insertions(+), 85 deletions(-) create mode 100644 src/net/sourceforge/plantuml/security/authentication/SecurityAccessInterceptor.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/SecurityAuthentication.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/SecurityAuthorizeManager.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/SecurityCredentials.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/SecurityCredentialsContainer.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAccessInterceptor.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManager.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAccessInterceptor.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManager.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManager.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2AccessInterceptor.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ClientAccessAuthorizeManager.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ResourceOwnerAccessAuthorizeManager.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2Tokens.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAccessInterceptor.java create mode 100644 src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManager.java create mode 100644 test/net/sourceforge/plantuml/security/SFileTest.java create mode 100644 test/net/sourceforge/plantuml/security/SURLTest.java create mode 100644 test/net/sourceforge/plantuml/security/SecurityUtilsTest.java create mode 100644 test/net/sourceforge/plantuml/security/authentication/SecurityAuthenticationTest.java create mode 100644 test/net/sourceforge/plantuml/security/authentication/SecurityCredentialsTest.java create mode 100644 test/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManagerTest.java create mode 100644 test/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManagerTest.java create mode 100644 test/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManagerTest.java create mode 100644 test/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManagerTest.java create mode 100644 test/net/sourceforge/plantuml/tim/stdlib/GetenvTest.java diff --git a/src/net/sourceforge/plantuml/FileSystem.java b/src/net/sourceforge/plantuml/FileSystem.java index 857b71213..14257a680 100644 --- a/src/net/sourceforge/plantuml/FileSystem.java +++ b/src/net/sourceforge/plantuml/FileSystem.java @@ -84,7 +84,7 @@ public class FileSystem { } } - for (SFile d : SecurityUtils.getPath("plantuml.include.path")) { + for (SFile d : SecurityUtils.getPath(SecurityUtils.PATHS_INCLUDES)) { assert d.isDirectory(); final SFile file = d.file(nameOrPath); if (file.exists()) { @@ -92,7 +92,7 @@ public class FileSystem { } } - for (SFile d : SecurityUtils.getPath("java.class.path")) { + for (SFile d : SecurityUtils.getPath(SecurityUtils.PATHS_CLASSES)) { assert d.isDirectory(); final SFile file = d.file(nameOrPath); if (file.exists()) { diff --git a/src/net/sourceforge/plantuml/preproc/ImportedFiles.java b/src/net/sourceforge/plantuml/preproc/ImportedFiles.java index be9bc4d7e..9258a6a80 100644 --- a/src/net/sourceforge/plantuml/preproc/ImportedFiles.java +++ b/src/net/sourceforge/plantuml/preproc/ImportedFiles.java @@ -108,12 +108,12 @@ public class ImportedFiles { public List getPath() { final List result = new ArrayList<>(imported); result.addAll(includePath()); - result.addAll(SecurityUtils.getPath("java.class.path")); + result.addAll(SecurityUtils.getPath(SecurityUtils.PATHS_CLASSES)); return result; } private List includePath() { - return SecurityUtils.getPath("plantuml.include.path"); + return SecurityUtils.getPath(SecurityUtils.PATHS_INCLUDES); } private boolean isAbsolute(String nameOrPath) { diff --git a/src/net/sourceforge/plantuml/security/SFile.java b/src/net/sourceforge/plantuml/security/SFile.java index 523de6e1c..ed1193e90 100644 --- a/src/net/sourceforge/plantuml/security/SFile.java +++ b/src/net/sourceforge/plantuml/security/SFile.java @@ -35,6 +35,7 @@ */ package net.sourceforge.plantuml.security; +import javax.swing.ImageIcon; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -56,8 +57,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import javax.swing.ImageIcon; - /** * Secure replacement for java.io.File. *

@@ -246,11 +245,15 @@ public class SFile implements Comparable { // In SANDBOX, we cannot read any files return false; } + // In any case SFile should not access the security folders (the files must be handled internally) + if (isDenied()) { + return false; + } // Files in "plantuml.include.path" and "plantuml.allowlist.path" are ok. - if (isInAllowList(SecurityUtils.getPath("plantuml.include.path"))) { + if (isInAllowList(SecurityUtils.getPath(SecurityUtils.PATHS_INCLUDES))) { return true; } - if (isInAllowList(SecurityUtils.getPath("plantuml.allowlist.path"))) { + if (isInAllowList(SecurityUtils.getPath(SecurityUtils.PATHS_ALLOWED))) { return true; } if (SecurityUtils.getSecurityProfile() == SecurityProfile.INTERNET) { @@ -284,6 +287,19 @@ public class SFile implements Comparable { return false; } + /** + * Checks, if the SFile is inside the folder (-structure) of the security area. + * + * @return true, if the file is not allowed to read/write + */ + private boolean isDenied() { + SFile securityPath = SecurityUtils.getSecurityPath(); + if (securityPath == null) { + return false; + } + return getCleanPathSecure().startsWith(securityPath.getCleanPathSecure()); + } + private String getCleanPathSecure() { String result = internal.getAbsolutePath(); result = result.replace("\0", ""); diff --git a/src/net/sourceforge/plantuml/security/SURL.java b/src/net/sourceforge/plantuml/security/SURL.java index 074ca00bc..e3e690fbc 100644 --- a/src/net/sourceforge/plantuml/security/SURL.java +++ b/src/net/sourceforge/plantuml/security/SURL.java @@ -40,9 +40,14 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.Proxy; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -54,29 +59,71 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.net.ssl.HttpsURLConnection; import javax.swing.ImageIcon; import net.sourceforge.plantuml.StringUtils; +import net.sourceforge.plantuml.security.authentication.SecurityAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; /** * Secure replacement for java.net.URL. *

* This class should be used instead of java.net.URL. *

- * This class does some control access. - * + * This class does some control access and manages access-tokens via URL. If a URL contains a access-token, similar to + * a user prefix, SURL loads the authorization config for this user-token and passes the credentials to the host. + *

+ * Example:
+ *

+ *     SURL url = SURL.create ("https://jenkins-access@jenkins.mycompany.com/api/json")
+ * 
+ * The {@code jenkins-access} will checked against the Security context access token configuration. If a configuration + * exists for this token name, the token will be removed from the URL and the credentials will be added to the headers. + * If the token is not found, the URL remains as it is and no separate authentication will be performed. + *

+ * TODO: Some methods should be moved to a HttpClient implementation, because SURL is not the valid class to manage it. + *
+ * TODO: BAD_HOSTS implementation should be reviewed and moved to HttpClient implementation with a circuit-breaker. + *
+ * TODO: Token expiration with refresh should be implemented in future. + *
*/ public class SURL { + /** + * Indicates, that we have no authentication to access the URL. + */ + public static final String WITHOUT_AUTHENTICATION = SecurityUtils.NO_CREDENTIALS; + + /** + * Regex to remove the UserInfo part from a URL. + */ + private static final Pattern PATTERN_USERINFO = Pattern.compile("(^https?://)(.*@)(.*)"); + + private static final ExecutorService EXE = Executors.newCachedThreadPool(); + + private static final Map BAD_HOSTS = new ConcurrentHashMap(); + + /** + * Internal URL, maybe cleaned from user-token. + */ private final URL internal; - private SURL(String src) throws MalformedURLException { - this(new URL(src)); - } + /** + * Assigned credentials to this URL. + */ + private final String securityIdentifier; - private SURL(URL url) { + private SURL(URL url, String securityIdentifier) { + assert url != null; + assert securityIdentifier != null; this.internal = url; + this.securityIdentifier = securityIdentifier; } public static SURL create(String url) { @@ -85,7 +132,7 @@ public class SURL { } if (url.startsWith("http://") || url.startsWith("https://")) try { - return new SURL(url); + return create(new URL(url)); } catch (MalformedURLException e) { e.printStackTrace(); } @@ -96,7 +143,47 @@ public class SURL { if (url == null) { return null; } - return new SURL(url); + + URL internalUrl; + + String credentialId = url.getUserInfo(); + + if (credentialId == null || credentialId.indexOf(':') > 0) { + // No user info at all, or a user with password (This is a legacy BasicAuth access, and we bypass it): + internalUrl = url; + credentialId = WITHOUT_AUTHENTICATION; + } else { + // Given userInfo, but without a password. We try to find SecurityCredentials + if (SecurityUtils.existsSecurityCredentials(credentialId) ) { + internalUrl = removeUserInfo (url); + } else { + internalUrl = url; + credentialId = WITHOUT_AUTHENTICATION; + } + } + + return new SURL(internalUrl, credentialId); + } + + /** + * Creates a URL without UserInfo part and without SecurityCredentials. + * + * @param url plain URL + * @return SURL without any user credential information. + */ + public static SURL createWithoutUser(URL url) { + return new SURL(removeUserInfo(url), WITHOUT_AUTHENTICATION); + } + + /** + * Clears the bad hosts cache. + *

+ * In some test cases (and maybe also needed for other functionality) the bad hosts cache must be cleared.
+ * E.g., in a test we check the failure on missing credentials and then a test with existing credentials. With a + * bad host cache the second test will fail, or we have unpredicted results. + */ + public static void resetBadHosts() { + BAD_HOSTS.clear(); } @Override @@ -105,7 +192,7 @@ public class SURL { } /** - * Check SecurityProfile to see if this URL can be open. + * Check SecurityProfile to see if this URL can be opened. */ private boolean isUrlOk() { if (SecurityUtils.getSecurityProfile() == SecurityProfile.SANDBOX) { @@ -128,18 +215,13 @@ public class SURL { } final int port = internal.getPort(); // Using INTERNET profile, port 80 and 443 are ok - if (port == 80 || port == 443 || port == -1) { - return true; - } + return port == 80 || port == 443 || port == -1; } return false; } private boolean pureIP(String full) { - if (full.matches("^https?://\\d+\\.\\d+\\.\\d+\\.\\d+.*")) { - return true; - } - return false; + return full.matches("^https?://\\d+\\.\\d+\\.\\d+\\.\\d+.*"); } private boolean isInAllowList() { @@ -153,6 +235,8 @@ public class SURL { } private String cleanPath(String path) { + // Remove user information, because we don't like to store user/password or userTokens in allow-list + path = removeUserInfoFromUrlPath (path); path = path.trim().toLowerCase(Locale.US); // We simplify/normalize the url, removing default ports path = path.replace(":80/", ""); @@ -161,72 +245,268 @@ public class SURL { } private List getAllowList() { - final String env = SecurityUtils.getenv("plantuml.allowlist.url"); + final String env = SecurityUtils.getenv(SecurityUtils.PATHS_ALLOWED); if (env == null) { return Collections.emptyList(); } return Arrays.asList(StringUtils.eventuallyRemoveStartingAndEndingDoubleQuote(env).split(";")); } - private final static ExecutorService exe = Executors.newCachedThreadPool(); - private final static Map badHosts = new ConcurrentHashMap(); - - // Added by Alain Corbiere + /** + * Reads from an endpoint (with configured credentials and proxy) the response as blob. + *

+ * This method allows access to an endpoint, with a configured SecurityCredentials object. The credentials will + * load on the fly and authentication fetched from an authentication-manager. Caching of tokens is not supported. + *

+ * authors: Alain Corbiere, Aljoscha Rittner + * + * @return data loaded data from endpoint + */ public byte[] getBytes() { - if (isUrlOk() == false) { + if (!isUrlOk()) { return null; } - final String host = internal.getHost(); - final Long bad = badHosts.get(host); - if (bad != null) { - final long duration = System.currentTimeMillis() - bad; - if (duration < 1000L * 60) { - // System.err.println("BAD HOST!" + host); - return null; - } - // System.err.println("cleaning " + host); - badHosts.remove(host); - } - final Future result = exe.submit(new Callable() { - public byte[] call() throws IOException { - InputStream input = null; - try { - final URLConnection connection = internal.openConnection(); - if (connection == null) { - return null; - } - input = connection.getInputStream(); - final ByteArrayOutputStream image = new ByteArrayOutputStream(); - final byte[] buffer = new byte[1024]; - int read; - while ((read = input.read(buffer)) > 0) { - image.write(buffer, 0, read); - } - image.close(); - return image.toByteArray(); - } finally { - if (input != null) { - input.close(); - } - } - } - }); - + SecurityCredentials credentials = SecurityUtils.loadSecurityCredentials(securityIdentifier); + SecurityAuthentication authentication = SecurityUtils.getAuthenticationManager(credentials).create(credentials); try { - byte data[] = result.get(SecurityUtils.getSecurityProfile().getTimeout(), TimeUnit.MILLISECONDS); - if (data != null) { - return data; + String host = internal.getHost(); + Long bad = BAD_HOSTS.get(host); + if (bad != null) { + if ((System.currentTimeMillis() - bad) < 1000L * 60) { + return null; + } + BAD_HOSTS.remove(host); + } + + try { + Future result = EXE.submit( + requestWithGetAndResponse(internal, credentials.getProxy(), authentication, null)); + byte[] data = result.get(SecurityUtils.getSecurityProfile().getTimeout(), TimeUnit.MILLISECONDS); + if (data != null) { + return data; + } + } catch (Exception e) { + System.err.println("issue " + host + " " + e); + } + + BAD_HOSTS.put(host, System.currentTimeMillis()); + return null; + } finally { + // clean up. We don't cache tokens, no expire handling. All time a re-request. + credentials.eraseCredentials(); + authentication.eraseCredentials(); + } + } + + /** + * Reads from an endpoint with a given authentication and proxy the response as blob. + *

+ * This method allows a parametrized access to an endpoint, without a configured SecurityCredentials object. This is + * useful to access internally identity providers (IDP), or authorization servers (to request access tokens). + *

+ * This method don't use the "bad-host" functionality, because the access to infrastructure services should not be + * obfuscated by some internal management. + *

+ * Please don't use this method directly from DSL scripts. + * + * @param authentication authentication object data. Caller is responsible to erase credentials + * @param proxy proxy configuration + * @param headers additional headers, if needed + * @return loaded data from endpoint + */ + public byte[] getBytes (Proxy proxy, SecurityAuthentication authentication, Map headers) { + if (!isUrlOk()) { + return null; + } + final Future result = EXE.submit(requestWithGetAndResponse(internal, proxy, authentication, headers)); + byte[] data = null; + try { + data = result.get(SecurityUtils.getSecurityProfile().getTimeout(), TimeUnit.MILLISECONDS); + } catch (Exception e) { + System.err.println("SURL response issue to " + internal.getHost() + " " + e); + } + return data; + } + + + /** + * Post to an endpoint with a given authentication and proxy the response as blob. + *

+ * This method allows a parametrized access to an endpoint, without a configured SecurityCredentials object. This is + * useful to access internally identity providers (IDP), or authorization servers (to request access tokens). + *

+ * This method don't use the "bad-host" functionality, because the access to infrastructure services should not be + * obfuscated by some internal management. + *

+ * Please don't use this method directly from DSL scripts. + * + * @param authentication authentication object data. Caller is responsible to erase credentials + * @param proxy proxy configuration + * @param data content to post + * @param headers headers, if needed + * @return loaded data from endpoint + */ + public byte[] getBytesOnPost(Proxy proxy, SecurityAuthentication authentication, String data, + Map headers) { + if (!isUrlOk()) { + return null; + } + final Future result = EXE.submit( + requestWithPostAndResponse(internal, proxy, authentication, data, headers)); + byte[] response = null; + try { + response = result.get(SecurityUtils.getSecurityProfile().getTimeout(), TimeUnit.MILLISECONDS); + } catch (Exception e) { + System.err.println("SURL response issue to " + internal.getHost() + " " + e); + } + return response; + } + + /** + * Creates a GET request and response handler + * @param url URL to request + * @param proxy proxy to apply + * @param authentication the authentication to use + * @param headers additional headers, if needed + * @return the callable handler. + */ + private static Callable requestWithGetAndResponse( + final URL url, final Proxy proxy, final SecurityAuthentication authentication, + final Map headers) { + return new Callable() { + public byte[] call() throws IOException { + // Add proxy, if passed throw parameters + final URLConnection connection = proxy == null + ? url.openConnection() + : url.openConnection(proxy); + if (connection == null) { + return null; + } + + HttpURLConnection http = (HttpURLConnection) connection; + + applyEndpointAccessAuthentication(http, authentication); + applyAdditionalHeaders (http, headers); + + return retrieveResponseAsBytes(http); + } + }; + } + + /** + * Creates a POST request and response handler with a simple String content. The content will be identified as form + * or JSON data. The charset encoding can be set by header parameters or will be set to UTF-8. The method to some + * fancy logic to simplify it for the user. + * @param url URL to request via POST method + * @param proxy proxy to apply + * @param authentication the authentication to use + * @param headers additional headers, if needed + * @return the callable handler. + */ + private static Callable requestWithPostAndResponse( + final URL url, final Proxy proxy, final SecurityAuthentication authentication, + final String data, final Map headers) { + return new Callable() { + public byte[] call() throws IOException { + // Add proxy, if passed throw parameters + final URLConnection connection = proxy == null + ? url.openConnection() + : url.openConnection(proxy); + if (connection == null) { + return null; + } + + boolean withContent = StringUtils.isNotEmpty(data); + + HttpURLConnection http = (HttpURLConnection) connection; + http.setRequestMethod("POST"); + if (withContent) { + http.setDoOutput(true); + } + + applyEndpointAccessAuthentication(http, authentication); + applyAdditionalHeaders (http, headers); + + Charset charSet = extractCharset(http.getRequestProperty("Content-Type")); + + if ( withContent ) { + sendRequestAsBytes(http, data.getBytes(charSet != null ? charSet : StandardCharsets.UTF_8)); + } + return retrieveResponseAsBytes(http); + } + }; + } + + private static Charset extractCharset(String contentType) { + if ( StringUtils.isEmpty(contentType) ) { + return null; + } + Matcher matcher = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)").matcher(contentType); + if (matcher.find()) { + try { + return Charset.forName(matcher.group(1)); + } catch (Exception e) { + e.printStackTrace(); } - } catch (Exception e) { - System.err.println("issue " + host + " " + e); } - badHosts.put(host, System.currentTimeMillis()); return null; } + /** + * Loads a response from an endpoint as a byte[] array. + * + * @param connection the URL connection + * @return the loaded byte arrays + * @throws IOException an exception, if the connection cannot establish or the download was broken + */ + private static byte[] retrieveResponseAsBytes(HttpURLConnection connection) throws IOException { + int responseCode = connection.getResponseCode(); + if ( responseCode < HttpURLConnection.HTTP_BAD_REQUEST) { + try (InputStream input = connection.getInputStream()) { + return retrieveData(input); + } + } else { + try (InputStream error = connection.getErrorStream()) { + byte[] bytes = retrieveData(error); + throw new IOException("HTTP error " + + responseCode + " with " + new String(bytes, StandardCharsets.UTF_8)); + } + } + } + + /** + * Reads data in a byte[] array. + * @param input input stream + * @return byte data + * @throws IOException if something went wrong + */ + private static byte[] retrieveData (InputStream input) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final byte[] buffer = new byte[1024]; + int read; + while ((read = input.read(buffer)) > 0) { + out.write(buffer, 0, read); + } + out.close(); + return out.toByteArray(); + } + + /** + * Sends a request content payload to an endpoint. + * @param connection HTTP connection + * @param data data as byte array + * @throws IOException if something went wrong + */ + private static void sendRequestAsBytes (HttpURLConnection connection, byte[] data) throws IOException { + connection.setFixedLengthStreamingMode(data.length); + try (OutputStream os = connection.getOutputStream()) { + os.write(data); + } + } + public InputStream openStream() { if (isUrlOk()) { - final byte data[] = getBytes(); + final byte[] data = getBytes(); if (data != null) { return new ByteArrayInputStream(data); } @@ -245,4 +525,86 @@ public class SURL { return null; } + /** + * Informs, if SecurityCredentials are configured for this connection. + * + * @return true, if credentials will be used for a connection + */ + public boolean isAuthorizationConfigured() { + return !WITHOUT_AUTHENTICATION.equals(securityIdentifier); + } + + /** + * Applies the given authentication data to the http connection. + * + * @param http HTTP URL connection (must be an encrypted https-TLS/SSL connection, or http must be + * activated with a property) + * @param authentication the data to request the access + * @see SecurityUtils#getAccessInterceptor(SecurityAuthentication) + * @see SecurityUtils#isNonSSLAuthenticationAllowed() + */ + private static void applyEndpointAccessAuthentication (URLConnection http, SecurityAuthentication authentication) { + if (authentication.isPublic()) { + // Shortcut: No need to apply authentication. + return; + } + if (http instanceof HttpsURLConnection || SecurityUtils.isNonSSLAuthenticationAllowed()) { + SecurityAccessInterceptor accessInterceptor = SecurityUtils.getAccessInterceptor(authentication); + accessInterceptor.apply(authentication, http); + } else { + // We cannot allow applying secret tokens on plain connections. Everyone can read the data. + throw new IllegalStateException( + "The transport of authentication data over an unencrypted http connection is not allowed"); + } + } + + /** + * Set the headers for a URL connection + * @param headers map Keys with values (can be String or list of String) + */ + private static void applyAdditionalHeaders(URLConnection http, Map headers) { + if ( headers == null || headers.isEmpty() ) { + return; + } + for (Map.Entry header : headers.entrySet()) { + Object value = header.getValue(); + if ( value instanceof String ) { + http.setRequestProperty(header.getKey(), (String) value); + } else if (value instanceof List) { + for ( Object item: (List) value) { + if ( item != null ) { + http.addRequestProperty(header.getKey(), item.toString()); + } + } + } + } + } + + /** + * Removes the userInfo part from the URL, because we want to use the SecurityCredentials instead. + * @param url URL with UserInfo part + * @return url without UserInfo part + */ + private static URL removeUserInfo(URL url) { + try { + return new URL(removeUserInfoFromUrlPath (url.toExternalForm())); + } catch (MalformedURLException e) { + e.printStackTrace(); + return url; + } + } + + /** + * Removes the userInfo part from the URL, because we want to use the SecurityCredentials instead. + * @param url URL with UserInfo part + * @return url without UserInfo part + */ + private static String removeUserInfoFromUrlPath(String url) { + // Simple solution: + Matcher matcher = PATTERN_USERINFO.matcher(url); + if (matcher.find()) { + return matcher.replaceFirst("$1$3"); + } + return url; + } } diff --git a/src/net/sourceforge/plantuml/security/SecurityUtils.java b/src/net/sourceforge/plantuml/security/SecurityUtils.java index 38df34d81..1c4962ca0 100644 --- a/src/net/sourceforge/plantuml/security/SecurityUtils.java +++ b/src/net/sourceforge/plantuml/security/SecurityUtils.java @@ -5,12 +5,12 @@ * (C) Copyright 2009-2020, Arnaud Roques * * Project Info: http://plantuml.com - * + * * If you like this project or if you find it useful, you can support us at: - * + * * http://plantuml.com/patreon (only 1$ per month!) * http://plantuml.com/paypal - * + * * This file is part of PlantUML. * * PlantUML is free software; you can redistribute it and/or modify it @@ -30,13 +30,34 @@ * * * Original Author: Arnaud Roques - * + * * */ package net.sourceforge.plantuml.security; +import net.sourceforge.plantuml.OptionFlags; +import net.sourceforge.plantuml.StringUtils; +import net.sourceforge.plantuml.json.Json; +import net.sourceforge.plantuml.json.JsonValue; +import net.sourceforge.plantuml.security.authentication.SecurityAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; +import net.sourceforge.plantuml.security.authentication.SecurityDefaultNoopAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.SecurityDefaultNoopAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.basicauth.BasicAuthAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.basicauth.BasicAuthAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.oauth.OAuth2AccessInterceptor; +import net.sourceforge.plantuml.security.authentication.oauth.OAuth2ClientAccessAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.oauth.OAuth2ResourceOwnerAccessAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.token.TokenAuthAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.token.TokenAuthAuthorizeManager; + +import javax.swing.ImageIcon; import java.awt.Image; import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; @@ -44,20 +65,100 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; +import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.StringTokenizer; - -import javax.swing.ImageIcon; - -import net.sourceforge.plantuml.OptionFlags; -import net.sourceforge.plantuml.StringUtils; +import java.util.regex.Pattern; public class SecurityUtils { + /** + * Indicates, that we have no authentication and credentials to access the URL. + */ + public static final String NO_CREDENTIALS = ""; + + /** + * Java class paths to import files from. + */ + public static final String PATHS_CLASSES = "java.class.path"; + + /** + * Paths to include files. + */ + public static final String PATHS_INCLUDES = "plantuml.include.path"; + + /** + * Whitelist of paths from where scripts can load data. + */ + public static final String PATHS_ALLOWED = "plantuml.allowlist.path"; + + /** + * Paths to folders with security specific content (not allowed to read via SFile). + */ + public static final String PATHS_SECURITY = "plantuml.security.credentials.path"; + + public static final String SECURITY_ALLOW_NONSSL_AUTH = "plantuml.security.allowNonSSLAuth"; + + /** + * Standard BasicAuth authentication interceptor, to generate a SecurityAuthentication from credentials. + */ + private static final SecurityAuthorizeManager PUBLIC_AUTH_MANAGER + = new SecurityDefaultNoopAuthorizeManager(); + + /** + * Standard interceptor for public endpoint access. + */ + private static final SecurityAccessInterceptor PUBLIC_ACCESS_INTERCEPTOR + = new SecurityDefaultNoopAccessInterceptor(); + + /** + * Standard TokenAuth authorize manager, to generate a SecurityAuthentication from credentials. + */ + private static final SecurityAuthorizeManager TOKEN_AUTH_MANAGER + = new TokenAuthAuthorizeManager(); + + /** + * Standard token access interceptor. + */ + private static final SecurityAccessInterceptor TOKEN_ACCESS_INTERCEPTOR = new TokenAuthAccessInterceptor(); + + /** + * Standard BasicAuth authorize manager, to generate a SecurityAuthentication from credentials. + */ + private static final SecurityAuthorizeManager BASICAUTH_AUTH_MANAGER + = new BasicAuthAuthorizeManager(); + + /** + * Standard BasicAuth access interceptor. + */ + private static final SecurityAccessInterceptor BASICAUTH_ACCESS_INTERCEPTOR = new BasicAuthAccessInterceptor(); + + /** + * OAuth2 client credentials authorization manager. + */ + private static final SecurityAuthorizeManager OAUTH2_CLIENT_AUTH_MANAGER + = new OAuth2ClientAccessAuthorizeManager(); + + /** + * OAuth2 resource owner authorization manager. + */ + private static final SecurityAuthorizeManager OAUTH2_RESOURCEOWNER_AUTH_MANAGER + = new OAuth2ResourceOwnerAccessAuthorizeManager(); + + /** + * Standard 'bearer' OAuth2 access interceptor. + */ + private static final SecurityAccessInterceptor OAUTH2_ACCESS_INTERCEPTOR = new OAuth2AccessInterceptor(); + + /** + * Filesystem-save characters. + */ + private static final Pattern SECURE_CHARS = Pattern.compile("^[a-zA-Z0-9\\-]+$"); + static private SecurityProfile current = null; public static synchronized SecurityProfile getSecurityProfile() { @@ -83,6 +184,27 @@ public class SecurityUtils { return System.getenv(name); } + /** + * Checks the environment variable and returns true if the variable is used in security context. In this case, the + * value should not be displayed in scripts. + * + * @param name Environment variable to check + * @return true, if this is a secret variable + */ + public static boolean isSecurityEnv(String name) { + return name != null && name.toLowerCase().startsWith("plantuml.security."); + } + + /** + * Configuration for Non-SSL authentication methods. + * + * @return true, if plantUML should allow authentication in plain connections (without encryption). + * @see #SECURITY_ALLOW_NONSSL_AUTH + */ + public static boolean isNonSSLAuthenticationAllowed() { + return Boolean.parseBoolean(getenv(SECURITY_ALLOW_NONSSL_AUTH)); + } + public static List getPath(String prop) { final List result = new ArrayList<>(); String paths = getenv(prop); @@ -155,4 +277,145 @@ public class SecurityUtils { return new FileOutputStream(path); } + /** + * Returns the authorize-manager for a security credentials configuration. + * + * @param credentialConfiguration the credentials + * @return the manager. + */ + public static SecurityAuthorizeManager getAuthenticationManager(SecurityCredentials credentialConfiguration) { + if (credentialConfiguration == SecurityCredentials.NONE) { + return PUBLIC_AUTH_MANAGER; + } else if ("tokenauth".equalsIgnoreCase(credentialConfiguration.getType())) { + return TOKEN_AUTH_MANAGER; + } else if ("basicauth".equalsIgnoreCase(credentialConfiguration.getType())) { + return BASICAUTH_AUTH_MANAGER; + } else if ("oauth2".equalsIgnoreCase(credentialConfiguration.getType())) { + String grantType = credentialConfiguration.getPropertyStr("grantType"); + if ("client_credentials".equalsIgnoreCase(grantType)) { + return OAUTH2_CLIENT_AUTH_MANAGER; + } else if ("password".equalsIgnoreCase(grantType)) { + return OAUTH2_RESOURCEOWNER_AUTH_MANAGER; + } + } + return PUBLIC_AUTH_MANAGER; + } + + /** + * Returns the authentication interceptor for a {@link SecurityAuthentication}. + * + * @param authentication the authentication data + * @return the interceptor. + */ + public static SecurityAccessInterceptor getAccessInterceptor(SecurityAuthentication authentication) { + if (authentication != null) { + String type = authentication.getType(); + if ("public".equals(type)) { + return PUBLIC_ACCESS_INTERCEPTOR; + } else if ("tokenauth".equalsIgnoreCase(type)) { + return TOKEN_ACCESS_INTERCEPTOR; + } else if ("basicauth".equalsIgnoreCase(type)) { + return BASICAUTH_ACCESS_INTERCEPTOR; + } else if ("oauth2".equalsIgnoreCase(type)) { + return OAUTH2_ACCESS_INTERCEPTOR; + } + } + // Unknown? Fall back to public: + return PUBLIC_ACCESS_INTERCEPTOR; + } + + /** + * Checks if user credentials existing. + * + * @param userToken name of the credential file + * @return boolean, if exists + */ + public static boolean existsSecurityCredentials(String userToken) { + SFile securityPath = getSecurityPath(); + if (securityPath != null) { + // SFile does not allow access to the security path (to hide the credentials in DSL scripts) + File securityFilePath = securityPath.conv(); + File userCredentials = new File(securityFilePath, userToken + ".credential"); + return userCredentials.exists() && userCredentials.canRead() && !userCredentials.isDirectory() + && userCredentials.length() > 2; + + } + return false; + } + + /** + * Loads the user credentials from the file system. + * + * @param userToken name of the credential file + * @return the credentials or NONE + */ + public static SecurityCredentials loadSecurityCredentials(String userToken) { + if (userToken != null && checkFileSystemSaveCharactersStrict(userToken) && !NO_CREDENTIALS.equals(userToken)) { + SFile securityPath = getSecurityPath(); + if (securityPath != null) { + // SFile does not allow access to the security path (to hide the credentials in DSL scripts) + File securityFilePath = securityPath.conv(); + File userCredentials = new File(securityFilePath, userToken + ".credential"); + JsonValue jsonValue = loadJson(userCredentials); + return SecurityCredentials.fromJson(jsonValue); + + } + } + return SecurityCredentials.NONE; + } + + /** + * Checks, if the token of a pathname (filename, ext, directory-name) uses only a very strict set of characters and + * not longer than 64 characters. + *

+ * Only characters from a to Z, Numbers and - are allowed. + * + * @param pathNameToken filename, ext, directory-name + * @return true, if the string fits to the strict allow-list of characters + * @see #SECURE_CHARS + */ + private static boolean checkFileSystemSaveCharactersStrict(String pathNameToken) { + return StringUtils.isNotEmpty(pathNameToken) + && SECURE_CHARS.matcher(pathNameToken).matches() && pathNameToken.length() <= 64; + } + + /** + * Loads the path to the configured security folder, if existing. + *

+ * Please note: A SFile referenced to a security folder cannot access the files. The content of the files in the + * security path should never have passed to DSL scripts. + * + * @return SFile folder or null + */ + public static SFile getSecurityPath() { + List paths = getPath(PATHS_SECURITY); + if (!paths.isEmpty()) { + SFile secureSFile = paths.get(0); + File securityFolder = secureSFile.conv(); + if (securityFolder.exists() && securityFolder.isDirectory()) { + return secureSFile; + } + } + return null; + } + + + /** + * Loads a file as JSON object. If no file exists or the file is not parsable, the method returns an empty JSON. + * + * @param jsonFile file path to the JSON file + * @return a Json vale (maybe empty) + */ + private static JsonValue loadJson(File jsonFile) { + if (jsonFile.exists() && jsonFile.canRead() && jsonFile.length() > 2) { + // we have a file with at least two bytes and readable, hopefully it's a JSON + try (Reader r = new BufferedReader(new FileReader(jsonFile))) { + return Json.parse(r); + } catch (IOException e) { + e.printStackTrace(); + } + } + return Json.object(); + } + } diff --git a/src/net/sourceforge/plantuml/security/authentication/SecurityAccessInterceptor.java b/src/net/sourceforge/plantuml/security/authentication/SecurityAccessInterceptor.java new file mode 100644 index 000000000..a554a3dc3 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/SecurityAccessInterceptor.java @@ -0,0 +1,54 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication; + +import java.net.URLConnection; + +/** + * The security access interceptor applies the authentication information to a HTTP connection. This can be a + * user/password combination for BasicAuth or a bearer token for OAuth2. + * + * @author Aljoscha Rittner + */ +public interface SecurityAccessInterceptor { + /** + * Applies to a connection the authentication information. + * + * @param authentication the determined authentication data to authorize for the endpoint access + * @param connection the connection to the endpoint + */ + void apply(SecurityAuthentication authentication, URLConnection connection); +} diff --git a/src/net/sourceforge/plantuml/security/authentication/SecurityAuthentication.java b/src/net/sourceforge/plantuml/security/authentication/SecurityAuthentication.java new file mode 100644 index 000000000..c3c585f45 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/SecurityAuthentication.java @@ -0,0 +1,117 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication; + +import java.util.Arrays; +import java.util.Map; + +/** + * The authentication to access an endpoint. This information will be generated by a SecurityAuthenticationInterceptor. + * + * @author Aljoscha Rittner + */ +public class SecurityAuthentication implements SecurityCredentialsContainer { + + /** + * Type of authentication (e.g. basicauth, oauth2) + */ + private final String type; + + /** + * Characteristic of an authentication (e.g. openId). Can be null.

+ * This kind of information is typically not needed. Useful for debugging purpose. + */ + private final String shape; + + /** + * Origin authorization process (e.g. client_credentials.

+ * This kind of information is typically not needed. Useful for debugging purpose. + */ + private final String grantType; + + /** + * A map of needed data tokens to authenticate access to an endpoint. + */ + private final Map tokens; + + public SecurityAuthentication(String type, Map tokens) { + this(type, null, null, tokens); + } + + public SecurityAuthentication(String type, String shape, String grantType, Map tokens) { + this.type = type; + this.shape = shape; + this.grantType = grantType; + this.tokens = tokens; + } + + public String getType() { + return type; + } + + public String getShape() { + return shape; + } + + public String getGrantType() { + return grantType; + } + + /** + * Requests the state of this authentication. + * + * @return true, if we have no authentication. + */ + public boolean isPublic() { + return "public".equalsIgnoreCase(type) && (tokens == null || tokens.isEmpty()); + } + + public Map getTokens() { + return tokens; + } + + @Override + public void eraseCredentials() { + if (tokens != null && !tokens.isEmpty()) { + for (Object tokenVal : tokens.values()) { + if (tokenVal instanceof char[]) { + Arrays.fill((char[]) tokenVal, '*'); + } + } + tokens.clear(); + } + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/SecurityAuthorizeManager.java b/src/net/sourceforge/plantuml/security/authentication/SecurityAuthorizeManager.java new file mode 100644 index 000000000..fee4cbb48 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/SecurityAuthorizeManager.java @@ -0,0 +1,54 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication; + +/** + * Creates from credentials a {@link SecurityAuthentication} object or authorize as principal to retrieve an + * authentication object. + * + * @author Aljoscha Rittner + */ +public interface SecurityAuthorizeManager { + /** + * Creates from the credentials the authentication object to access an endpoint. If the credentials defines a + * principal (e.g. in OAuth2), the create method should authorize the principal and get the final authentication + * data to access an endpoint. + * + * @param credentials the configured credentials + * @return the authentication object. + */ + SecurityAuthentication create(SecurityCredentials credentials); +} diff --git a/src/net/sourceforge/plantuml/security/authentication/SecurityCredentials.java b/src/net/sourceforge/plantuml/security/authentication/SecurityCredentials.java new file mode 100644 index 000000000..81d56913a --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/SecurityCredentials.java @@ -0,0 +1,410 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication; + +import net.sourceforge.plantuml.StringUtils; +import net.sourceforge.plantuml.json.JsonObject; +import net.sourceforge.plantuml.json.JsonValue; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Defines a configuration for credentials. + * + * @author Aljoscha Rittner + */ +public class SecurityCredentials implements SecurityCredentialsContainer { + + private static final Map EMPTY_MAP = Collections.emptyMap(); + + /** + * No credentials given. + */ + public static final SecurityCredentials NONE = new SecurityCredentials("", "public", null, null); + + /** + * Name of the configuration. + */ + private final String name; + /** + * The type of authorization and access process (e.g. "basicauth" or "oauth2"). + */ + private final String type; + /** + * Username or client identifier. + */ + private final String identifier; + /** + * User/Client secret information. + */ + private final char[] secret; + /** + * Properties defined for a specific authorization and access process. + */ + private final Map properties = new HashMap(); + /** + * Proxy configuration.

+ *

+ * {@link Proxy#NO_PROXY} means, we want direct access. null means, we use the system proxy configuration. + */ + private final Proxy proxy; + + /** + * Creates BasicAuth credentials without a proxy. + * + * @param name Name of the credentials + * @param type The type of authentication and access process (e.g. "basicauth" or "oauth2") + * @param identifier username, clientId, ... + * @param secret the secret information to authenticate the client or user + */ + public SecurityCredentials(String name, String type, String identifier, char[] secret) { + this(name, type, identifier, secret, EMPTY_MAP, null); + } + + /** + * Creates BasicAuth credentials with a proxy. + * + * @param name Name of the credentials + * @param type The type of authentication and access process (e.g. "basicauth" or "oauth2") + * @param identifier username, clientId, ... + * @param secret the secret information to authenticate the client or user + * @param proxy proxy configuration + */ + public SecurityCredentials(String name, String type, String identifier, char[] secret, + Map properties, Proxy proxy) { + if (name == null) { + throw new NullPointerException("Credential name should not be null"); + } + this.name = name; + this.type = type; + this.identifier = identifier; + this.secret = secret; + this.proxy = proxy; + this.properties.putAll(properties); + } + + /** + * Creates BasicAuth credentials. + * + * @param identifier the basic auth user name. + * @param secret password + * @return credential object + */ + public static SecurityCredentials basicAuth(String identifier, char[] secret) { + return new SecurityCredentials(identifier, "basicauth", identifier, secret); + } + + /** + * Creates a SecurityCredentials from a JSON. + *

+ * Example: + *

+	 *     {
+	 *         "name": "jenkins",
+	 *         "identifier": "alice",
+	 *         "secret": "secret",
+	 *         "proxy": {
+	 *             "type": "socket",
+	 *             "address": "192.168.1.250",
+	 *             "port": 8080
+	 *         }
+	 *     }
+	 * 
+ * + * @param jsonValue a JSON structure + * @return the created SecurityCredentials + */ + public static SecurityCredentials fromJson(JsonValue jsonValue) { + try { + JsonObject securityObject = jsonValue.asObject(); + JsonValue name = securityObject.get("name"); + JsonValue type = securityObject.get("type"); + JsonValue identifier = securityObject.get("identifier"); + JsonValue secret = securityObject.get("secret"); + + Map map = new HashMap(); + buildProperties("", securityObject.get("properties"), map); + + if (type != null && !type.isNull() && "tokenauth".equals(type.asString())) { + return new SecurityCredentials(name.asString(), "tokenauth", + null, null, map, proxyFromJson(securityObject.get("proxy"))); + } else if (StringUtils.isNotEmpty(name.asString()) && StringUtils.isNotEmpty(identifier.asString())) { + String authType = type != null && !type.isNull() ? type.asString() : "basicauth"; + return new SecurityCredentials(name.asString(), authType, + identifier.asString(), extractSecret(secret), + map, proxyFromJson(securityObject.get("proxy"))); + } + + } catch (UnsupportedOperationException use) { + // We catch UnsupportedOperationException to stop parsing on unexpected elements + } + return NONE; + } + + /** + * Creates a Proxy object from a JSON value. + *

+ * Example: + *

+	 *     {
+	 *         "type": "socket",
+	 *         "address": "192.168.1.250",
+	 *         "port": 8080
+	 *     }
+	 * 
+ * + * @param proxyValue JSON, that represents a Proxy object + * @return Proxy object or null + */ + private static Proxy proxyFromJson(JsonValue proxyValue) { + if (proxyValue != null && !proxyValue.isNull() && proxyValue.isObject()) { + Proxy.Type type = Proxy.Type.DIRECT; + + JsonObject proxyObject = proxyValue.asObject(); + JsonValue proxyType = proxyObject.get("type"); + if (proxyType != null && !proxyType.isNull()) { + type = Proxy.Type.valueOf(proxyType.asString().toUpperCase()); + } + if (type == Proxy.Type.DIRECT) { + return Proxy.NO_PROXY; + } + JsonValue proxyAddress = proxyObject.get("address"); + JsonValue proxyPort = proxyObject.get("port"); + if (proxyAddress != null && !proxyAddress.isNull() && !proxyPort.isNull() && proxyPort.isNumber()) { + InetSocketAddress address = new InetSocketAddress(proxyAddress.asString(), proxyPort.asInt()); + return new Proxy(type, address); + } + } + return null; + } + + /** + * Extracts a password, if it is not empty or null. + * + * @param pwd password json value + * @return password or null + */ + private static char[] extractSecret(JsonValue pwd) { + if (pwd == null || pwd.isNull()) { + return null; + } + String pwdStr = pwd.asString(); + if (StringUtils.isEmpty(pwdStr)) { + return null; + } + + return pwdStr.toCharArray(); + } + + /** + * Creates a properties map from all given key/values. + *

+ * Example:
+ *

+	 *     {
+	 *         "grantType": "client_credentials",
+	 *         "scope": "read write",
+	 *         "accessTokenUri": "https://login-demo.curity.io/oauth/v2/oauth-token"
+	 *         "credentials": {
+	 *             "identifier": "serviceId",
+	 *             "secret": "ServiceSecret"
+	 *         }
+	 *     }
+	 *
+	 *     will be transformed to:
+	 *
+	 *     grantType -> client_credentials
+	 *     scope -> read write
+	 *     accessTokenUri -> https://login-demo.curity.io/oauth/v2/oauth-token
+	 *     credentials.identifier -> serviceId
+	 *     credentials.secret -> ServiceSecret
+	 * 
+ * + * @param prefix the prefix for the direct children + * @param fromValue parent JSON value to read from + * @param toMap map to populate + */ + private static void buildProperties(String prefix, JsonValue fromValue, Map toMap) { + if (!isJsonObjectWithMembers(fromValue)) { + return; + } + JsonObject members = fromValue.asObject(); + for (String name : members.names()) { + JsonValue child = members.get(name); + if (child.isArray() || child.isNull()) { + // currently, not supported or not needed + continue; + } + String key = StringUtils.isEmpty(prefix) ? name : prefix + '.' + name; + if (child.isObject()) { + buildProperties(key, child, toMap); + } else { + if (child.isString()) { + toMap.put(key, child.asString()); + } else if (child.isBoolean()) { + toMap.put(key, child.asBoolean()); + } else if (child.isNumber()) { + toMap.put(key, child.asDouble()); + } + } + } + } + + /** + * Checks, if we have a JSON object with members. + * + * @param jsonValue the value to check + * @return true, if we have members in the JSON object + */ + private static boolean isJsonObjectWithMembers(JsonValue jsonValue) { + return jsonValue != null && !jsonValue.isNull() && jsonValue.isObject() && !jsonValue.asObject().isEmpty(); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getIdentifier() { + return identifier; + } + + public char[] getSecret() { + return secret; + } + + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + /** + * Returns the property as String. + * + * @param key Name of the property + * @return String representation + */ + public String getPropertyStr(String key) { + Object value = getProperties().get(key); + if (value != null) { + return value.toString(); + } + return null; + } + + /** + * Returns the property as characters. + * + * @param key Name of the property + * @return char[] representation + */ + public char[] getPropertyChars(String key) { + Object value = getProperties().get(key); + if (value != null) { + return value.toString().toCharArray(); + } + return null; + } + + /** + * Returns the property as boolean. + * + * @param key Name of the property + * @return boolean representation + */ + public boolean getPropertyBool(String key) { + Object value = getProperties().get(key); + if (value != null) { + if (value instanceof Boolean) { + return (Boolean) value; + } else if (value instanceof String) { + return Boolean.parseBoolean((String) value); + } + } + return false; + } + + /** + * Returns the property as Number. + * + * @param key Name of the property + * @return boolean representation + */ + public Number getPropertyNum(String key) { + Object value = getProperties().get(key); + if (value != null) { + if (value instanceof Number) { + return (Number) value; + } else if (value instanceof String) { + return Double.parseDouble((String) value); + } + } + return null; + } + + public Proxy getProxy() { + return proxy; + } + + @Override + public void eraseCredentials() { + if (secret != null && secret.length > 0) { + Arrays.fill(secret, '*'); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SecurityCredentials)) return false; + SecurityCredentials that = (SecurityCredentials) o; + return getName().equals(that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } + +} diff --git a/src/net/sourceforge/plantuml/security/authentication/SecurityCredentialsContainer.java b/src/net/sourceforge/plantuml/security/authentication/SecurityCredentialsContainer.java new file mode 100644 index 000000000..e5439583b --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/SecurityCredentialsContainer.java @@ -0,0 +1,49 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication; + +/** + * Indicates that the implementing object contains sensitive data, which can be erased + * using the {@code eraseCredentials} method. + * + * @author Aljoscha Rittner + */ +public interface SecurityCredentialsContainer { + /** + * Get called, if the secret information should be erased. + */ + void eraseCredentials(); +} diff --git a/src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAccessInterceptor.java b/src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAccessInterceptor.java new file mode 100644 index 000000000..ad2fcae43 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAccessInterceptor.java @@ -0,0 +1,50 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication; + +import java.net.URLConnection; + +/** + * This interceptor does nothing. + * + * @author Aljoscha Rittner + */ +public class SecurityDefaultNoopAccessInterceptor implements SecurityAccessInterceptor { + @Override + public void apply(SecurityAuthentication authentication, URLConnection connection) { + // do nothing + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManager.java b/src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManager.java new file mode 100644 index 000000000..6f5980d32 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManager.java @@ -0,0 +1,54 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication; + +import java.util.Collections; +import java.util.Map; + +/** + * Creates a public access authentication data object. + * + * @author Aljoscha Rittner + */ +public class SecurityDefaultNoopAuthorizeManager implements SecurityAuthorizeManager { + + private static final Map EMPTY_MAP = Collections.emptyMap(); + + @Override + public SecurityAuthentication create(SecurityCredentials credentials) { + return new SecurityAuthentication("public", EMPTY_MAP); + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAccessInterceptor.java b/src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAccessInterceptor.java new file mode 100644 index 000000000..26c550bd9 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAccessInterceptor.java @@ -0,0 +1,80 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.basicauth; + +import net.sourceforge.plantuml.StringUtils; +import net.sourceforge.plantuml.code.Base64Coder; +import net.sourceforge.plantuml.security.authentication.SecurityAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; + +import java.net.URLConnection; + +/** + * Applies from {@link SecurityAuthentication} data a BasicAuth authentication access header. + * + * @author Aljoscha Rittner + */ +public class BasicAuthAccessInterceptor implements SecurityAccessInterceptor { + + /** + * Applies from {@link SecurityAuthentication} data a BasicAuth authentication access header. + *

+ * Expects "identifier" and "secret" to build a Authorization header. + * + * @param authentication the determined authentication data to authorize for the endpoint access + * @param connection the connection to the endpoint + */ + @Override + public void apply(SecurityAuthentication authentication, URLConnection connection) { + String auth = getAuth(authentication); + String authorization = Base64Coder.encodeString(auth); + String authHeaderValue = "Basic " + authorization; + connection.setRequestProperty("Authorization", authHeaderValue); + } + + private String getAuth(SecurityAuthentication authentication) { + String id = (String) authentication.getTokens().get("identifier"); + char[] secret = (char[]) authentication.getTokens().get("secret"); + StringBuilder auth = new StringBuilder(); + if (StringUtils.isNotEmpty(id)) { + auth.append(id); + if (secret != null && secret.length > 0) { + auth.append(':').append(secret); + } + } + return auth.toString(); + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManager.java b/src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManager.java new file mode 100644 index 000000000..4aad91848 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManager.java @@ -0,0 +1,64 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.basicauth; + +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; + +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link BasicAuthAuthorizeManager} creates the authentication on the fly from the credentials without + * any access to other services. + * + * @author Aljoscha Rittner + */ +public class BasicAuthAuthorizeManager implements SecurityAuthorizeManager { + @Override + public SecurityAuthentication create(SecurityCredentials credentials) { + String type = credentials.getType(); + String identifier = credentials.getIdentifier(); + char[] secret = credentials.getSecret(); + Map tokens = new HashMap(); + tokens.put("identifier", identifier); + if (secret != null) { + tokens.put("secret", secret.clone()); + } + return new SecurityAuthentication(type, tokens); + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManager.java b/src/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManager.java new file mode 100644 index 000000000..5d76f1348 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManager.java @@ -0,0 +1,165 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.oauth; + +import net.sourceforge.plantuml.json.Json; +import net.sourceforge.plantuml.json.JsonObject; +import net.sourceforge.plantuml.json.JsonValue; +import net.sourceforge.plantuml.security.SURL; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityAuthorizeManager; + +import java.io.UnsupportedEncodingException; +import java.net.Proxy; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * Default abstract OAuth2 AccessAuthorizeManager for OAuth2 managers. + * + * @author Aljoscha Rittner + */ +public abstract class AbstractOAuth2AccessAuthorizeManager implements SecurityAuthorizeManager { + + /** + * Default headers for token service access.

+ * Initialize with: + *

+	 * "Content-Type"="application/x-www-form-urlencoded; charset=UTF-8"
+	 * "Accept"="application/json"
+	 * 
+ * + * @return headers + */ + protected Map headers() { + Map map = new HashMap<>(); + map.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + map.put("Accept", "application/json"); + return map; + } + + /** + * Builds the access parameter map. + * + * @param tokenResponse the JSOn object with the response data + * @param tokenType token type to use instead of token_type from response + * @return data-map + */ + protected Map buildAccessDataFromResponse(JsonObject tokenResponse, String tokenType) { + Map map = new HashMap<>(); + + toMap(map, tokenResponse, OAuth2Tokens.ACCESS_TOKEN); + toMap(map, tokenResponse, OAuth2Tokens.SCOPE); + toMap(map, tokenResponse, OAuth2Tokens.EXPIRES_IN); + + if (tokenType == null) { + toMap(map, tokenResponse, OAuth2Tokens.TOKEN_TYPE); + if (!map.isEmpty() && !map.containsKey(OAuth2Tokens.TOKEN_TYPE.key())) { + // default token type is bearer + map.put(OAuth2Tokens.TOKEN_TYPE.key(), "bearer"); + } + } else { + // Caller don't belief in the token_type response + if (!map.isEmpty()) { + map.put(OAuth2Tokens.TOKEN_TYPE.key(), tokenType); + } + } + + return map; + } + + /** + * Translates the JSON value to a map key/value. + * + * @param map collection to store + * @param response values from response + * @param name name of the value + */ + private void toMap(Map map, JsonObject response, OAuth2Tokens name) { + JsonValue jsonValue = response.get(name.key()); + if (jsonValue != null && !jsonValue.isNull()) { + if (jsonValue.isString()) { + map.put(name.key(), jsonValue.asString()); + } else if (jsonValue.isNumber()) { + map.put(name.key(), jsonValue.asInt()); + } else if (jsonValue.isBoolean()) { + map.put(name.key(), jsonValue.asBoolean()); + } + } + } + + /** + * Encodes the data to UTF-8 into {@code application/x-www-form-urlencoded}. + * + * @param data data to encode + * @return the encoded data + */ + protected String urlEncode(String data) { + try { + return URLEncoder.encode(data, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return data; + } + } + + /** + * Calls the endpoint to load the token response and create a SecurityAuthentication. + * + * @param proxy Proxy for the access + * @param grantType grant type + * @param tokenType token type to use instead of token_type from response + * @param tokenService URL to token service + * @param content body content + * @param basicAuth principal basicAuth + * @return the authentication object to access resources (or null) + */ + protected SecurityAuthentication requestAndCreateAuthFromResponse( + Proxy proxy, String grantType, String tokenType, + SURL tokenService, String content, SecurityAuthentication basicAuth) { + byte[] bytes = tokenService.getBytesOnPost(proxy, basicAuth, content, headers()); + if (bytes != null) { + JsonValue tokenResponse = Json.parse(new String(bytes, StandardCharsets.UTF_8)); + if (tokenResponse != null && !tokenResponse.isNull()) { + return new SecurityAuthentication("oauth2", null, grantType, + buildAccessDataFromResponse(tokenResponse.asObject(), tokenType)); + } + } + return null; + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2AccessInterceptor.java b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2AccessInterceptor.java new file mode 100644 index 000000000..52441bad9 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2AccessInterceptor.java @@ -0,0 +1,61 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.oauth; + +import net.sourceforge.plantuml.StringUtils; +import net.sourceforge.plantuml.security.authentication.SecurityAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; + +import java.net.URLConnection; + +/** + * Applies from {@link SecurityAuthentication} data an OAuth2 Authorization access header. + * + * @author Aljoscha Rittner + */ +public class OAuth2AccessInterceptor implements SecurityAccessInterceptor { + @Override + public void apply(SecurityAuthentication authentication, URLConnection connection) { + connection.setRequestProperty("Authorization", getAuth(authentication)); + } + + private String getAuth(SecurityAuthentication authentication) { + String accessToken = (String) authentication.getTokens().get(OAuth2Tokens.ACCESS_TOKEN.key()); + String type = (String) authentication.getTokens().get(OAuth2Tokens.TOKEN_TYPE.key()); + return StringUtils.capitalize(type) + ' ' + accessToken; + } + +} diff --git a/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ClientAccessAuthorizeManager.java b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ClientAccessAuthorizeManager.java new file mode 100644 index 000000000..715af3831 --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ClientAccessAuthorizeManager.java @@ -0,0 +1,103 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.oauth; + +import net.sourceforge.plantuml.StringUtils; +import net.sourceforge.plantuml.security.SURL; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; +import net.sourceforge.plantuml.security.authentication.basicauth.BasicAuthAuthorizeManager; + +import java.util.Arrays; + +/** + * Authorize the principal (from {@link SecurityCredentials} and creates a {@link SecurityAuthentication} object with a + * bearer token secret. + * + * @author Aljoscha Rittner + */ +public class OAuth2ClientAccessAuthorizeManager extends AbstractOAuth2AccessAuthorizeManager { + + /** + * Basic Auth manager to access the token service with authorization. + */ + private final BasicAuthAuthorizeManager basicAuthManager = new BasicAuthAuthorizeManager(); + + @Override + public SecurityAuthentication create(SecurityCredentials credentials) { + String grantType = credentials.getPropertyStr("grantType"); + String requestScope = credentials.getPropertyStr("scope"); + String accessTokenUri = credentials.getPropertyStr("accessTokenUri"); + String tokenType = credentials.getPropertyStr("tokenType"); + + // Extra BasicAuth data to access the token service endpoint (if needed) + String identifier = credentials.getPropertyStr("credentials.identifier"); + char[] secret = credentials.getPropertyChars("credentials.secret"); + + try { + SURL tokenService = SURL.create(accessTokenUri); + + StringBuilder content = new StringBuilder() + .append("grant_type=") + .append(urlEncode(grantType)); + if (StringUtils.isNotEmpty(requestScope)) { + content.append("&scope=").append(urlEncode(requestScope)); + } + + SecurityAuthentication basicAuth; + if (identifier != null) { + // OAuth2 with extra Endpoint BasicAuth credentials + basicAuth = basicAuthManager.create( + SecurityCredentials.basicAuth(identifier, secret)); + // We need to add the principal to the form + content.append("&client_id").append(urlEncode(credentials.getIdentifier())) + .append("&client_secret").append(urlEncode(new String(credentials.getSecret()))); + } else { + // OAuth2 with BasicAuth via principal (standard) + basicAuth = basicAuthManager.create( + SecurityCredentials.basicAuth(credentials.getIdentifier(), credentials.getSecret())); + } + + return requestAndCreateAuthFromResponse( + credentials.getProxy(), grantType, tokenType, tokenService, content.toString(), basicAuth); + } finally { + if (secret != null && secret.length > 0) { + Arrays.fill(secret, '*'); + } + } + } + +} diff --git a/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ResourceOwnerAccessAuthorizeManager.java b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ResourceOwnerAccessAuthorizeManager.java new file mode 100644 index 000000000..8e45bc37c --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2ResourceOwnerAccessAuthorizeManager.java @@ -0,0 +1,99 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.oauth; + +import net.sourceforge.plantuml.StringUtils; +import net.sourceforge.plantuml.security.SURL; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; +import net.sourceforge.plantuml.security.authentication.basicauth.BasicAuthAuthorizeManager; + +import java.util.Arrays; + +/** + * Authorize via principal a resource owner (from {@link SecurityCredentials} and creates a + * {@link SecurityAuthentication} object with a bearer token secret. + *

+ * Because a pass through of username/password is an anti-pattern in OAuth2, this authorization method should be + * avoided. However, it may be necessary in some environments to gain access with the ROPC flow. + * + * @author Aljoscha Rittner + */ +public class OAuth2ResourceOwnerAccessAuthorizeManager extends AbstractOAuth2AccessAuthorizeManager { + + /** + * Basic Auth manager to access the token service with authorization. + */ + private final BasicAuthAuthorizeManager basicAuthManager = new BasicAuthAuthorizeManager(); + + @Override + public SecurityAuthentication create(SecurityCredentials credentials) { + String grantType = credentials.getPropertyStr("grantType"); + String requestScope = credentials.getPropertyStr("scope"); + String accessTokenUri = credentials.getPropertyStr("accessTokenUri"); + String tokenType = credentials.getPropertyStr("tokenType"); + + // Resource owner + String username = credentials.getPropertyStr("resourceOwner.identifier"); + char[] password = credentials.getPropertyChars("resourceOwner.secret"); + + try { + SURL tokenService = SURL.create(accessTokenUri); + + StringBuilder content = new StringBuilder() + .append("grant_type=") + .append(urlEncode(grantType)); + if (StringUtils.isNotEmpty(requestScope)) { + content.append("&scope=").append(urlEncode(requestScope)); + } + + // OAuth2 with BasicAuth via principal (standard) + SecurityAuthentication basicAuth = basicAuthManager.create( + SecurityCredentials.basicAuth(credentials.getIdentifier(), credentials.getSecret())); + // We need to add the principal to the form + content.append("&username=").append(urlEncode(username)) + .append("&password=").append(urlEncode(new String(password))); + + return requestAndCreateAuthFromResponse( + credentials.getProxy(), grantType, tokenType, tokenService, content.toString(), basicAuth); + } finally { + if (password != null && password.length > 0) { + Arrays.fill(password, '*'); + } + } + } + +} diff --git a/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2Tokens.java b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2Tokens.java new file mode 100644 index 000000000..01535eb4b --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/oauth/OAuth2Tokens.java @@ -0,0 +1,62 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.oauth; + +/** + * Some useful constants. + * + * @author Aljoscha Rittner + */ +public enum OAuth2Tokens { + + ACCESS_TOKEN("access_token"), + + TOKEN_TYPE("token_type"), + + SCOPE("scope"), + + EXPIRES_IN("expires_in"); + + private final String key; + + OAuth2Tokens(String key) { + this.key = key; + } + + public String key() { + return key; + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAccessInterceptor.java b/src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAccessInterceptor.java new file mode 100644 index 000000000..0e3c64fad --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAccessInterceptor.java @@ -0,0 +1,72 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.token; + +import net.sourceforge.plantuml.security.authentication.SecurityAccessInterceptor; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; + +import java.net.URLConnection; +import java.util.Map; + +/** + * Applies from {@link SecurityAuthentication} data plain token authentication access headers. This is a raw header + * injection with static data. + * + * @author Aljoscha Rittner + */ +public class TokenAuthAccessInterceptor implements SecurityAccessInterceptor { + + /** + * Applies from {@link SecurityAuthentication} data plain token authentication access headers. + *

+ * Expects headers.* key value pairs to pass it directly to the connection. + * + * @param authentication the determined authentication data to authorize for the endpoint access + * @param connection the connection to the endpoint + */ + @Override + public void apply(SecurityAuthentication authentication, URLConnection connection) { + + for (Map.Entry header : authentication.getTokens().entrySet() ) { + if (!header.getKey().startsWith("headers.") || header.getValue() == null) { + continue; + } + String key = header.getKey().substring(8); + String value = header.getValue().toString(); + connection.setRequestProperty(key, value); + } + } +} diff --git a/src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManager.java b/src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManager.java new file mode 100644 index 000000000..dfe14f28a --- /dev/null +++ b/src/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManager.java @@ -0,0 +1,56 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2021, Arnaud Roques + * + * Project Info: http://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * http://plantuml.com/patreon (only 1$ per month!) + * http://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * + * + */ +package net.sourceforge.plantuml.security.authentication.token; + +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; + +import java.util.HashMap; +import java.util.Map; + +/** + * The {@link TokenAuthAuthorizeManager} creates the authentication on the fly from the credentials without + * any access to other services. + * + * @author Aljoscha Rittner + */ +public class TokenAuthAuthorizeManager implements SecurityAuthorizeManager { + @Override + public SecurityAuthentication create(SecurityCredentials credentials) { + return new SecurityAuthentication(credentials.getType(), new HashMap<>(credentials.getProperties())); + } +} diff --git a/src/net/sourceforge/plantuml/tim/stdlib/Getenv.java b/src/net/sourceforge/plantuml/tim/stdlib/Getenv.java index 794929101..a83cad713 100644 --- a/src/net/sourceforge/plantuml/tim/stdlib/Getenv.java +++ b/src/net/sourceforge/plantuml/tim/stdlib/Getenv.java @@ -40,6 +40,7 @@ import java.util.Set; import net.sourceforge.plantuml.LineLocation; import net.sourceforge.plantuml.OptionFlags; +import net.sourceforge.plantuml.security.SecurityUtils; import net.sourceforge.plantuml.tim.EaterException; import net.sourceforge.plantuml.tim.EaterExceptionLocated; import net.sourceforge.plantuml.tim.TContext; @@ -71,11 +72,15 @@ public class Getenv extends SimpleReturnFunction { } private String getenv(String name) { + // Check, if the script requests secret information. A plantuml server should have an own SecurityManager to + // avoid access to properties and environment variables, but we should also stop here in other deployments. + if (SecurityUtils.isSecurityEnv(name)) { + return null; + } final String env = System.getProperty(name); if (env != null) { return env; } - final String getenv = System.getenv(name); - return getenv; + return System.getenv(name); } } diff --git a/src/net/sourceforge/plantuml/version/PSystemVersion.java b/src/net/sourceforge/plantuml/version/PSystemVersion.java index dae9fcd9e..aadbc23a1 100644 --- a/src/net/sourceforge/plantuml/version/PSystemVersion.java +++ b/src/net/sourceforge/plantuml/version/PSystemVersion.java @@ -169,7 +169,7 @@ public class PSystemVersion extends PlainStringsDiagram { strings.add("Word Mode"); strings.add("Command Line: " + Run.getCommandLine()); strings.add("Current Dir: " + new SFile(".").getAbsolutePath()); - strings.add("plantuml.include.path: " + PreprocessorUtils.getenv("plantuml.include.path")); + strings.add("plantuml.include.path: " + PreprocessorUtils.getenv(SecurityUtils.PATHS_INCLUDES)); } } strings.add(" "); diff --git a/test/net/sourceforge/plantuml/security/SFileTest.java b/test/net/sourceforge/plantuml/security/SFileTest.java new file mode 100644 index 000000000..1983095d9 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/SFileTest.java @@ -0,0 +1,58 @@ +package net.sourceforge.plantuml.security; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests some features of {@link SFile}. + */ +class SFileTest { + + private static String oldSecurity; + + @TempDir + Path tempDir; + + @BeforeAll + static void storeSecurityProperty() { + oldSecurity = System.getProperty(SecurityUtils.PATHS_SECURITY); + } + + @AfterAll + static void loadSecurityProperty() { + if (oldSecurity != null) { + System.setProperty(SecurityUtils.PATHS_SECURITY, oldSecurity); + } else { + System.getProperties().remove(SecurityUtils.PATHS_SECURITY); + } + } + + /** + * Checks, if we cannot see a secret file in a security folder. + * + * @throws Exception Hopefully not + */ + @Test + void testFileDenied() throws Exception { + File secureFolder = tempDir.toFile(); + System.setProperty(SecurityUtils.PATHS_SECURITY, secureFolder.getCanonicalPath()); + + // A file is needed: + File secretFile = File.createTempFile("user", ".credentials", secureFolder); + + assertThat(secretFile).describedAs("File should be visible with standard java.io.File") + .exists(); + + SFile file = new SFile(secretFile.getAbsolutePath()); + + assertThat(file.exists()).describedAs("File should be invisible for SFile") + .isFalse(); + } +} diff --git a/test/net/sourceforge/plantuml/security/SURLTest.java b/test/net/sourceforge/plantuml/security/SURLTest.java new file mode 100644 index 000000000..e0f136343 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/SURLTest.java @@ -0,0 +1,128 @@ +package net.sourceforge.plantuml.security; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Checks some security features + */ +class SURLTest { + + private static final String EXT = ".credential"; + + private static String oldSecurity; + + @TempDir + Path tempDir; + + @BeforeAll + static void storeSecurityProperty() { + oldSecurity = System.getProperty(SecurityUtils.PATHS_SECURITY); + } + + @AfterAll + static void loadSecurityProperty() { + if (oldSecurity != null) { + System.setProperty(SecurityUtils.PATHS_SECURITY, oldSecurity); + } else { + System.getProperties().remove(SecurityUtils.PATHS_SECURITY); + } + } + + /** + * Checks a SURL without a Security context. + */ + @ParameterizedTest + @ValueSource(strings = { + "http://localhost:8080/api", + "http://alice@localhost:8080/api", + "http://alice:secret@localhost:8080/api", + "https://localhost:8080/api", + "https://alice@localhost:8080/api", + "https://alice:secret@localhost:8080/api"}) + void urlWithoutSecurity(String url) { + SURL surl = SURL.create(url); + + assertThat(surl).isNotNull(); + assertThat(surl.isAuthorizationConfigured()).isFalse(); + + assertThat(surl).describedAs("URL should be untouched") + .hasToString(url); + } + + /** + * Checks a SURL after removing the UserInfo part. + * + * @throws MalformedURLException this should not be happened + */ + @ParameterizedTest + @ValueSource(strings = { + "http://localhost:8080/api", + "http://alice@localhost:8080/api", + "http://alice:secret@localhost:8080/api", + "https://localhost:8080/api", + "https://alice@localhost:8080/api", + "https://alice:secret@localhost:8080/api"}) + void removeUserInfo(String url) throws MalformedURLException { + SURL surl = SURL.createWithoutUser(new URL(url)); + + assertThat(surl).isNotNull(); + assertThat(surl.isAuthorizationConfigured()).isFalse(); + // Check http and https and removed UserInfo part + assertThat(surl.toString()).describedAs("User info should be removed from URL") + .startsWith("http").endsWith("://localhost:8080/api"); + } + + /** + * Checks a SURL without a Security context. + * + * @throws Exception please not + */ + @ParameterizedTest + @ValueSource(strings = { + "http://bob@localhost:8080/api", + "https://bob@localhost:8080/api"}) + void urlWithSecurity(String url) throws Exception { + + File secureFolder = tempDir.toFile(); + System.setProperty(SecurityUtils.PATHS_SECURITY, secureFolder.getCanonicalPath()); + + // A credential file is needed: + File secretFile = new File(secureFolder, "bob" + EXT); + + String jsonProxy = "\"proxy\": {\"type\": \"socks\", \"address\": \"192.168.92.250\", \"port\":8080}"; + String jsonCredentials = "{\"name\": \"bob\", \"identifier\": \"bob\", \"secret\": \"bobssecret\"" + + ", " + jsonProxy + "}"; + + Files.write(secretFile.toPath(), jsonCredentials.getBytes(StandardCharsets.UTF_8)); + + // pre-check, if test can start + assertThat(secretFile).describedAs("File should be existing with content") + .exists().isNotEmpty(); + + assertThat(SecurityUtils.getSecurityPath()).isNotNull(); + + // Our test goes here + SURL surl = SURL.create(url); + + assertThat(surl).isNotNull(); + assertThat(surl.isAuthorizationConfigured()).isTrue(); + + assertThat(surl.toString()).describedAs("User info should be removed from URL") + .startsWith("http").endsWith("://localhost:8080/api"); + + secretFile.delete(); + } +} diff --git a/test/net/sourceforge/plantuml/security/SecurityUtilsTest.java b/test/net/sourceforge/plantuml/security/SecurityUtilsTest.java new file mode 100644 index 000000000..4455b9172 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/SecurityUtilsTest.java @@ -0,0 +1,163 @@ +package net.sourceforge.plantuml.security; + +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Checks some aspects in {@link SecurityUtils}. + */ +class SecurityUtilsTest { + + private static final String EXT = ".credential"; + + private static String oldSecurity; + + @TempDir + Path tempDir; + + @BeforeAll + static void storeSecurityProperty() { + oldSecurity = System.getProperty(SecurityUtils.PATHS_SECURITY); + } + + @AfterAll + static void loadSecurityProperty() { + if (oldSecurity != null) { + System.setProperty(SecurityUtils.PATHS_SECURITY, oldSecurity); + } else { + System.getProperties().remove(SecurityUtils.PATHS_SECURITY); + } + } + + /** + * Checks, if SecurityUtils can loadSecurityCredentials. + * + * @throws Exception nobody wants an exception + */ + @Test + void testLoadOfSecurityCredentials() throws Exception { + File secureFolder = tempDir.toFile(); + System.setProperty(SecurityUtils.PATHS_SECURITY, secureFolder.getCanonicalPath()); + + // A file is needed: + File secretFile = File.createTempFile("user", EXT, secureFolder); + + String jsonProxy = "\"proxy\": {\"type\": \"socks\", \"address\": \"192.168.92.250\", \"port\":8080}"; + String jsonCredentials = "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"secret\": \"secret\"" + + ", " + jsonProxy + "}"; + + Files.write(secretFile.toPath(), jsonCredentials.getBytes(StandardCharsets.UTF_8)); + + assertThat(secretFile).describedAs("File should be existing with content") + .exists().isNotEmpty(); + + assertThat(SecurityUtils.getSecurityPath()).isNotNull(); + + String secretFileName = secretFile.getName(); + + SecurityCredentials credentials = SecurityUtils.loadSecurityCredentials( + secretFileName.substring(0, secretFileName.length() - EXT.length())); + + assertThat(credentials).isNotNull(); + + assertThat(credentials.getName()).isEqualTo("jenkins"); + assertThat(credentials.getIdentifier()).isEqualTo("alice"); + assertThat(credentials.getSecret()).isEqualTo(new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + + assertThat(credentials.getProxy()).isNotNull(); + Proxy proxy = credentials.getProxy(); + assertThat(proxy.type()).isEqualTo(Proxy.Type.SOCKS); + + assertThat(proxy.address()).isNotNull(); + assertThat(proxy.address()).isInstanceOf(InetSocketAddress.class); + + InetSocketAddress address = (InetSocketAddress) proxy.address(); + assertThat(address.getPort()).isEqualTo(8080); + assertThat(address.getHostString()).isEqualTo("192.168.92.250"); + } + + /** + * Tests unsecure names. + * + * @param name name of filepart + */ + @ParameterizedTest + @ValueSource(strings = { + "_unsecure", "unse%cure", " unsecure04343 ", + "tooLong012345678901234567890123456789012345678901234567890123456789unsecure" + }) + void testUnsecureNames(String name) throws IOException { + File secureFolder = tempDir.toFile(); + System.setProperty(SecurityUtils.PATHS_SECURITY, secureFolder.getCanonicalPath()); + + // A file is needed: + File secretFile = File.createTempFile(name, EXT, secureFolder); + + String jsonProxy = "\"proxy\": {\"type\": \"socks\", \"address\": \"192.168.92.250\", \"port\":8080}"; + String jsonCredentials = "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"secret\": \"secret\"" + + ", " + jsonProxy + "}"; + + Files.write(secretFile.toPath(), jsonCredentials.getBytes(StandardCharsets.UTF_8)); + + assertThat(secretFile).describedAs("File should be existing with content") + .exists().isNotEmpty(); + + String secretFileName = secretFile.getName(); + + SecurityCredentials credentials = SecurityUtils.loadSecurityCredentials( + secretFileName.substring(0, secretFileName.length() - EXT.length())); + + assertThat(credentials).isEqualTo(SecurityCredentials.NONE); + } + + + /** + * Tests secure names. + * + * @param name name of filepart + */ + @ParameterizedTest + @ValueSource(strings = { + "secure", "Secure", "123secure", "secure123", "1290565234", "45435-543534-fdgfdg" + }) + void testSecureNames(String name) throws IOException { + File secureFolder = tempDir.toFile(); + System.setProperty(SecurityUtils.PATHS_SECURITY, secureFolder.getCanonicalPath()); + + // A file is needed: + File secretFile = File.createTempFile(name, EXT, secureFolder); + + String jsonProxy = "\"proxy\": {\"type\": \"socks\", \"address\": \"192.168.92.250\", \"port\":8080}"; + String jsonCredentials = "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"secret\": \"secret\"" + + ", " + jsonProxy + "}"; + + Files.write(secretFile.toPath(), jsonCredentials.getBytes(StandardCharsets.UTF_8)); + + assertThat(secretFile).describedAs("File should be existing with content") + .exists().isNotEmpty(); + + String secretFileName = secretFile.getName(); + + SecurityCredentials credentials = SecurityUtils.loadSecurityCredentials( + secretFileName.substring(0, secretFileName.length() - EXT.length())); + + assertThat(credentials).isNotEqualTo(SecurityCredentials.NONE); + } + + +} diff --git a/test/net/sourceforge/plantuml/security/authentication/SecurityAuthenticationTest.java b/test/net/sourceforge/plantuml/security/authentication/SecurityAuthenticationTest.java new file mode 100644 index 000000000..886ce94b8 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/authentication/SecurityAuthenticationTest.java @@ -0,0 +1,59 @@ +package net.sourceforge.plantuml.security.authentication; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Checks SecurityAuthentication. + */ +class SecurityAuthenticationTest { + + private static final Map EMPTY_MAP = Collections.emptyMap(); + + @Test + void isPublicAsPublicTest() { + SecurityAuthentication cut = new SecurityAuthentication("public", null, null, EMPTY_MAP); + + assertThat(cut).isNotNull(); + assertThat(cut.isPublic()).isTrue(); + } + + @Test + void isPublicAsBasicAuthTest() { + SecurityAuthentication cut = new SecurityAuthentication("basicauth", null, null, EMPTY_MAP); + + assertThat(cut.isPublic()).isFalse(); + } + + @Test + void getTokensTest() { + Map tokens = new HashMap<>(); + tokens.put("identifier", "alice"); + tokens.put("secret", new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + SecurityAuthentication cut = new SecurityAuthentication("basicauth", null, null, tokens); + + assertThat(cut.getTokens()) + .containsEntry("identifier", "alice") + .containsEntry("secret", new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + } + + @Test + void eraseCredentialsTest() { + Map tokens = new HashMap<>(); + tokens.put("identifier", "alice"); + tokens.put("secret", new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + SecurityAuthentication cut = new SecurityAuthentication("basicauth", null, null, tokens); + + assertThat(cut.getTokens()) + .containsEntry("identifier", "alice") + .containsEntry("secret", new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + + cut.eraseCredentials(); + assertThat(cut.getTokens()).isEmpty(); + } +} \ No newline at end of file diff --git a/test/net/sourceforge/plantuml/security/authentication/SecurityCredentialsTest.java b/test/net/sourceforge/plantuml/security/authentication/SecurityCredentialsTest.java new file mode 100644 index 000000000..e3b20b84f --- /dev/null +++ b/test/net/sourceforge/plantuml/security/authentication/SecurityCredentialsTest.java @@ -0,0 +1,222 @@ +package net.sourceforge.plantuml.security.authentication; + +import net.sourceforge.plantuml.json.Json; +import net.sourceforge.plantuml.json.JsonValue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SecurityCredentials}. + */ +class SecurityCredentialsTest { + + /** + * Tests, if the {@link SecurityCredentials} can be created from JSON. + * + * @throws Exception hopefully not + */ + @Test + void fromJsonTestComplete() throws Exception { + JsonValue jsonValue = + Json.parse("{\"name\": \"jenkins\", \"type\": \"basicauth\", " + + "\"identifier\": \"alice\", \"secret\": \"secret\"}"); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials).isNotNull().isNotEqualTo(SecurityCredentials.NONE); + + assertThat(credentials.getName()).isEqualTo("jenkins"); + assertThat(credentials.getType()).isEqualTo("basicauth"); + assertThat(credentials.getIdentifier()).isEqualTo("alice"); + assertThat(credentials.getSecret()).isEqualTo(new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + assertThat(credentials.getProperties()).isEmpty(); + + assertThat(credentials.getProxy()).isNull(); + } + + /** + * Tests, if the {@link SecurityCredentials} can be created from JSON with direct access. + * + * @throws Exception hopefully not + */ + @Test + void fromJsonTestCompleteWithProxyDirect() throws Exception { + String jsonProxy = "\"proxy\": {\"type\": \"direct\"}"; + JsonValue jsonValue = + Json.parse("{\"name\": \"jenkins\", \"type\": \"basicauth\", " + + "\"identifier\": \"alice\", \"secret\": \"secret\"" + + ", " + jsonProxy + "}"); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials).isNotNull().isNotEqualTo(SecurityCredentials.NONE); + + assertThat(credentials.getName()).isEqualTo("jenkins"); + assertThat(credentials.getType()).isEqualTo("basicauth"); + assertThat(credentials.getIdentifier()).isEqualTo("alice"); + assertThat(credentials.getSecret()).isEqualTo(new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + assertThat(credentials.getProperties()).isEmpty(); + + assertThat(credentials.getProxy()).isNotNull(); + Proxy proxy = credentials.getProxy(); + assertThat(proxy.type()).isEqualTo(Proxy.Type.DIRECT); + assertThat(proxy.address()).isNull(); + } + + /** + * Tests, if the {@link SecurityCredentials} can be created from JSON with socket proxy. + * + * @throws Exception hopefully not + */ + @Test + void fromJsonTestCompleteWithProxySocksAddress() throws Exception { + String jsonProxy = "\"proxy\": {\"type\": \"socks\", \"address\": \"192.168.92.250\", \"port\":8080}"; + JsonValue jsonValue = + Json.parse("{\"name\": \"jenkins\", \"identifier\": \"alice\", \"secret\": \"secret\"" + + ", " + jsonProxy + "}"); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials).isNotNull().isNotEqualTo(SecurityCredentials.NONE); + + assertThat(credentials.getName()).isEqualTo("jenkins"); + assertThat(credentials.getType()).as("basicauth should be the default").isEqualTo("basicauth"); + assertThat(credentials.getIdentifier()).isEqualTo("alice"); + assertThat(credentials.getSecret()).isEqualTo(new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + assertThat(credentials.getProperties()).isEmpty(); + + assertThat(credentials.getProxy()).isNotNull(); + Proxy proxy = credentials.getProxy(); + assertThat(proxy.type()).isEqualTo(Proxy.Type.SOCKS); + + assertThat(proxy.address()).isNotNull(); + assertThat(proxy.address()).isInstanceOf(InetSocketAddress.class); + + InetSocketAddress address = (InetSocketAddress) proxy.address(); + assertThat(address.getPort()).isEqualTo(8080); + assertThat(address.getHostString()).isEqualTo("192.168.92.250"); + } + + /** + * Tests, if the {@link SecurityCredentials} can be created from JSON with http-high-level proxy. + * + * @throws Exception hopefully not + */ + @Test + void fromJsonTestCompleteWithProxyHttpAddress() throws Exception { + String jsonProxy = "\"proxy\": {\"type\": \"http\", \"address\": \"proxy.example.com\", \"port\":8080}"; + JsonValue jsonValue = + Json.parse("{\"name\": \"jenkins\", \"identifier\": \"alice\", \"secret\": \"secret\"" + + ", " + jsonProxy + "}"); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials).isNotNull().isNotEqualTo(SecurityCredentials.NONE); + + assertThat(credentials.getName()).isEqualTo("jenkins"); + assertThat(credentials.getType()).as("basicauth should be the default").isEqualTo("basicauth"); + assertThat(credentials.getIdentifier()).isEqualTo("alice"); + assertThat(credentials.getSecret()).isEqualTo(new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + assertThat(credentials.getProperties()).isEmpty(); + + assertThat(credentials.getProxy()).isNotNull(); + Proxy proxy = credentials.getProxy(); + assertThat(proxy.type()).isEqualTo(Proxy.Type.HTTP); + + assertThat(proxy.address()).isNotNull(); + assertThat(proxy.address()).isInstanceOf(InetSocketAddress.class); + + InetSocketAddress address = (InetSocketAddress) proxy.address(); + assertThat(address.getPort()).isEqualTo(8080); + assertThat(address.getHostString()).isEqualTo("proxy.example.com"); + } + + /** + * Tests, if the {@link SecurityCredentials} can be created from JSON. + * + * @throws Exception hopefully not + */ + @Test + void fromJsonTokenTest() throws Exception { + + String headers = "{\"Authorization\": \"ApiKey a4db08b7-5729-4ba9-8c08-f2df493465a1\"}"; + String properties = "{\"headers\": " + headers + "}"; + JsonValue jsonValue = + Json.parse("{\"name\": \"github\", \"type\": \"tokenauth\", " + + "\"properties\": " + properties + "}"); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials).isNotNull().isNotEqualTo(SecurityCredentials.NONE); + + assertThat(credentials.getName()).isEqualTo("github"); + assertThat(credentials.getType()).isEqualTo("tokenauth"); + assertThat(credentials.getProperties()) + .isNotEmpty().containsEntry("headers.Authorization", "ApiKey a4db08b7-5729-4ba9-8c08-f2df493465a1"); + + assertThat(credentials.getProxy()).isNull(); + } + + /** + * Tests, if the {@link SecurityCredentials} can be created from JSON with empty password. + * + * @throws Exception hopefully not + */ + @ParameterizedTest + @ValueSource(strings = { + "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"secret\": null}", // null password + "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"secret\": \"\"}", // empty password + "{\"name\": \"jenkins\", \"identifier\": \"alice\"}", // no password + "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"pwd\": \"Xyz\"}" // pwd ignored + }) + void fromJsonTestNoPassword(String json) throws Exception { + JsonValue jsonValue = Json.parse(json); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials).isNotNull().isNotEqualTo(SecurityCredentials.NONE); + + assertThat(credentials.getName()).isEqualTo("jenkins"); + assertThat(credentials.getIdentifier()).isEqualTo("alice"); + assertThat(credentials.getSecret()).isNull(); + assertThat(credentials.getProperties()).isEmpty(); + + assertThat(credentials.getProxy()).isNull(); + } + + /** + * Checks, if the property parser can read simple values. + */ + @Test + void fromJsonWithSimpleProperties() { + String props = "{\"grantType\": \"client_credentials\", \"test\": true, \"number\": 1.0, \"x\": null}"; + String json = "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"properties\": " + props + " }"; + JsonValue jsonValue = Json.parse(json); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials.getProperties()) + .isNotEmpty() + .containsEntry("grantType", "client_credentials") + .containsEntry("test", Boolean.TRUE) + .containsEntry("number", 1.0d) + .doesNotContainKey("x"); + } + + /** + * Checks, if the property parser can read nested values. + */ + @Test + void fromJsonWithNestedProperties() { + String nested = "{\"identifier\": \"serviceId\",\"secret\": \"ServiceSecret\"}"; + String props = "{\"grantType\": \"client_credentials\", \"nested\": " + nested + "}"; + String json = "{\"name\": \"jenkins\", \"identifier\": \"alice\", \"properties\": " + props + " }"; + JsonValue jsonValue = Json.parse(json); + SecurityCredentials credentials = SecurityCredentials.fromJson(jsonValue); + + assertThat(credentials.getProperties()) + .isNotEmpty() + .containsEntry("grantType", "client_credentials") + .containsEntry("nested.identifier", "serviceId") + .containsEntry("nested.secret", "ServiceSecret"); + } +} diff --git a/test/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManagerTest.java b/test/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManagerTest.java new file mode 100644 index 000000000..8e2745970 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/authentication/SecurityDefaultNoopAuthorizeManagerTest.java @@ -0,0 +1,22 @@ +package net.sourceforge.plantuml.security.authentication; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SecurityDefaultNoopAuthorizeManagerTest { + + /** + * Tests the creation of SecurityAuthentication via SecurityDefaultNoopAuthenticationInterceptor. + */ + @Test + void createTest() { + SecurityAuthorizeManager cut = new SecurityDefaultNoopAuthorizeManager(); + + SecurityAuthentication securityAuthentication = cut.create(null); + + assertThat(securityAuthentication).isNotNull(); + + assertThat(securityAuthentication.isPublic()).isTrue(); + } +} \ No newline at end of file diff --git a/test/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManagerTest.java b/test/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManagerTest.java new file mode 100644 index 000000000..ae8778164 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/authentication/basicauth/BasicAuthAuthorizeManagerTest.java @@ -0,0 +1,29 @@ +package net.sourceforge.plantuml.security.authentication.basicauth; + +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class BasicAuthAuthorizeManagerTest { + + /** + * Tests the creation of SecurityAuthentication via BasicAuthAuthorizeManager. + */ + @Test + void createTest() { + SecurityAuthorizeManager cut = new BasicAuthAuthorizeManager(); + + SecurityAuthentication securityAuthentication = cut.create( + SecurityCredentials.basicAuth("alice", new char[]{'s', 'e', 'c', 'r', 'e', 't'})); + + assertThat(securityAuthentication).isNotNull(); + + assertThat(securityAuthentication.isPublic()).isFalse(); + assertThat(securityAuthentication.getTokens()) + .containsEntry("identifier", "alice") + .containsEntry("secret", new char[]{'s', 'e', 'c', 'r', 'e', 't'}); + } +} \ No newline at end of file diff --git a/test/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManagerTest.java b/test/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManagerTest.java new file mode 100644 index 000000000..efa886736 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/authentication/oauth/AbstractOAuth2AccessAuthorizeManagerTest.java @@ -0,0 +1,99 @@ +package net.sourceforge.plantuml.security.authentication.oauth; + +import net.sourceforge.plantuml.json.Json; +import net.sourceforge.plantuml.json.JsonObject; +import net.sourceforge.plantuml.json.JsonValue; +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class AbstractOAuth2AccessAuthorizeManagerTest { + + private final MockedOAuth2AccessAuthorizeManager cut = new MockedOAuth2AccessAuthorizeManager(); + + @ParameterizedTest + @ValueSource(strings = {"{\"access_token\":\"7fea8201-eebb-4101-a76f-ddc1efdd3bbd\",\"scope\":\"read write\"," + + "\"token_type\":\"bearer\",\"expires_in\":300}", + "{\"access_token\":\"7fea8201-eebb-4101-a76f-ddc1efdd3bbd\",\"scope\":\"read write\",\"expires_in\":300}" + }) + void accessDataTest(String jsonResponse) { + JsonValue response = Json.parse(jsonResponse); + Map responseMap = cut.buildAccessDataFromResponse(response.asObject(), null); + + assertThat(responseMap) + .containsEntry(OAuth2Tokens.ACCESS_TOKEN.key(), "7fea8201-eebb-4101-a76f-ddc1efdd3bbd") + .containsEntry(OAuth2Tokens.SCOPE.key(), "read write") + .containsEntry(OAuth2Tokens.TOKEN_TYPE.key(), "bearer") + .containsEntry(OAuth2Tokens.EXPIRES_IN.key(), 300); + } + + @ParameterizedTest + @ValueSource(strings = {"{\"access_token\":\"7fea8201-eebb-4101-a76f-ddc1efdd3bbd\",\"scope\":\"read write\"," + + "\"token_type\":\"bearer\",\"expires_in\":300}", + "{\"access_token\":\"7fea8201-eebb-4101-a76f-ddc1efdd3bbd\",\"scope\":\"read write\",\"expires_in\":300}" + }) + void accessDataOverrideTokenTypeTest(String jsonResponse) { + JsonValue response = Json.parse(jsonResponse); + Map responseMap = cut.buildAccessDataFromResponse(response.asObject(), "apikey"); + + assertThat(responseMap) + .containsEntry(OAuth2Tokens.ACCESS_TOKEN.key(), "7fea8201-eebb-4101-a76f-ddc1efdd3bbd") + .containsEntry(OAuth2Tokens.SCOPE.key(), "read write") + .containsEntry(OAuth2Tokens.TOKEN_TYPE.key(), "apikey") + .containsEntry(OAuth2Tokens.EXPIRES_IN.key(), 300); + } + + @Test + void accessDataEmptyTest() { + String jsonResponse = "{}"; + JsonValue response = Json.parse(jsonResponse); + Map responseMap = cut.buildAccessDataFromResponse(response.asObject(), null); + + assertThat(responseMap).as("Empty map should not contain default token-type 'bearer'").isEmpty(); + } + + @Test + void accessDataEmptyAndTokenOverrideTest() { + String jsonResponse = "{}"; + JsonValue response = Json.parse(jsonResponse); + Map responseMap = cut.buildAccessDataFromResponse(response.asObject(), "apikey"); + + assertThat(responseMap).as("Empty map should not contain override token-type 'apikey'").isEmpty(); + } + + @Test + void urlEncodeTest() { + assertThat(cut.urlEncode("alice")).isEqualTo("alice"); + assertThat(cut.urlEncode("bob")).isEqualTo("bob"); + assertThat(cut.urlEncode("alice and bob")).isEqualTo("alice+and+bob"); + assertThat(cut.urlEncode("Müller")).isEqualTo("M%C3%BCller"); + assertThat(cut.urlEncode("s?ecret=-110%")).isEqualTo("s%3Fecret%3D-110%25"); + } + + /** + * Mock to make methods public for testing. + */ + static class MockedOAuth2AccessAuthorizeManager extends AbstractOAuth2AccessAuthorizeManager { + + @Override + public SecurityAuthentication create(SecurityCredentials credentials) { + return null; + } + + @Override + public Map buildAccessDataFromResponse(JsonObject tokenResponse, String overrideTokenType) { + return super.buildAccessDataFromResponse(tokenResponse, overrideTokenType); + } + + @Override + public String urlEncode(String data) { + return super.urlEncode(data); + } + } +} \ No newline at end of file diff --git a/test/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManagerTest.java b/test/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManagerTest.java new file mode 100644 index 000000000..0c0f17367 --- /dev/null +++ b/test/net/sourceforge/plantuml/security/authentication/token/TokenAuthAuthorizeManagerTest.java @@ -0,0 +1,37 @@ +package net.sourceforge.plantuml.security.authentication.token; + +import net.sourceforge.plantuml.security.authentication.SecurityAuthentication; +import net.sourceforge.plantuml.security.authentication.SecurityAuthorizeManager; +import net.sourceforge.plantuml.security.authentication.SecurityCredentials; +import net.sourceforge.plantuml.security.authentication.basicauth.BasicAuthAuthorizeManager; +import org.junit.jupiter.api.Test; + +import java.net.Proxy; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class TokenAuthAuthorizeManagerTest { + + /** + * Tests the creation of SecurityAuthentication via {@link TokenAuthAuthorizeManager}. + */ + @Test + void createSimpleTest() { + SecurityAuthorizeManager cut = new TokenAuthAuthorizeManager(); + + Map properties = new HashMap<>(); + properties.put("headers.Authorization", "ApiKey a4db08b7-5729-4ba9-8c08-f2df493465a1"); + SecurityCredentials credentials = new SecurityCredentials("test", "token", null, null, + properties, Proxy.NO_PROXY); + + SecurityAuthentication securityAuthentication = cut.create(credentials); + + assertThat(securityAuthentication).isNotNull(); + + assertThat(securityAuthentication.isPublic()).isFalse(); + assertThat(securityAuthentication.getTokens()) + .containsEntry("headers.Authorization", "ApiKey a4db08b7-5729-4ba9-8c08-f2df493465a1"); + } +} \ No newline at end of file diff --git a/test/net/sourceforge/plantuml/tim/stdlib/GetenvTest.java b/test/net/sourceforge/plantuml/tim/stdlib/GetenvTest.java new file mode 100644 index 000000000..9c7206099 --- /dev/null +++ b/test/net/sourceforge/plantuml/tim/stdlib/GetenvTest.java @@ -0,0 +1,59 @@ +package net.sourceforge.plantuml.tim.stdlib; + +import net.sourceforge.plantuml.tim.EaterException; +import net.sourceforge.plantuml.tim.EaterExceptionLocated; +import net.sourceforge.plantuml.tim.expression.TValue; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the internal function %getenv. + */ +class GetenvTest { + + /** + * Tests getenv should not publish plantuml.security.* environment variables. + * + * @throws EaterException should not + * @throws EaterExceptionLocated should not + */ + @ParameterizedTest + @ValueSource(strings = { + "plantuml.security.blabla", + "plantuml.SECURITY.blabla", + "plantuml.security.credentials.path", + }) + void executeReturnFunctionSecurityTest(String name) throws EaterException, EaterExceptionLocated { + System.setProperty("plantuml.security.blabla", "example"); + Getenv cut = new Getenv(); + + List values = Collections.singletonList(TValue.fromString(name)); + TValue tValue = cut.executeReturnFunction(null, null, null, values, null); + assertThat (tValue.toString()).isEmpty(); + } + + /** + * Tests getenv still returns 'good' variables. + * + * @throws EaterException should not + * @throws EaterExceptionLocated should not + */ + @ParameterizedTest + @ValueSource(strings = { + "java.version", + "path.separator", + "line.separator", + }) + void executeReturnFunctionTest(String name) throws EaterException, EaterExceptionLocated { + Getenv cut = new Getenv(); + + List values = Collections.singletonList(TValue.fromString(name)); + TValue tValue = cut.executeReturnFunction(null, null, null, values, null); + assertThat (tValue.toString()).isNotEmpty(); + } +} \ No newline at end of file