1
0
mirror of https://github.com/octoleo/plantuml.git synced 2024-06-07 02:40:52 +00:00

Merge pull request #796 from arittner/master

Authentication for SURL with BasicAuth and OAuth2
This commit is contained in:
PlantUML 2021-11-22 16:14:42 +01:00 committed by GitHub
commit 0668801c0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 3176 additions and 85 deletions

View File

@ -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()) {

View File

@ -108,12 +108,12 @@ public class ImportedFiles {
public List<SFile> getPath() {
final List<SFile> 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<SFile> includePath() {
return SecurityUtils.getPath("plantuml.include.path");
return SecurityUtils.getPath(SecurityUtils.PATHS_INCLUDES);
}
private boolean isAbsolute(String nameOrPath) {

View File

@ -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;
@ -51,13 +52,13 @@ import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.swing.ImageIcon;
/**
* Secure replacement for java.io.File.
* <p>
@ -246,11 +247,19 @@ public class SFile implements Comparable<SFile> {
// 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)
try {
if (isDenied()) {
return false;
}
} catch (IOException e) {
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 +293,35 @@ public class SFile implements Comparable<SFile> {
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
* @throws IOException If an I/O error occurs, which is possible because the check the pathname may require
* filesystem queries
*/
private boolean isDenied() throws IOException {
SFile securityPath = SecurityUtils.getSecurityPath();
if (securityPath == null) {
return false;
}
return getSanitizedPath().startsWith(securityPath.getSanitizedPath());
}
/**
* Returns a sanitized, canonical and normalized Path to a file.
*
* @return the Path
* @throws IOException If an I/O error occurs, which is possible because the construction of the canonical pathname
* may require filesystem queries
* @see #getCleanPathSecure()
* @see File#getCanonicalPath()
* @see Path#normalize()
*/
private Path getSanitizedPath() throws IOException {
return Paths.get(new File(getCleanPathSecure()).getCanonicalPath()).normalize();
}
private String getCleanPathSecure() {
String result = internal.getAbsolutePath();
result = result.replace("\0", "");

View File

@ -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.
* <p>
* This class should be used instead of java.net.URL.
* <p>
* 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.
* <p>
* Example:<br/>
* <pre>
* SURL url = SURL.create ("https://jenkins-access@jenkins.mycompany.com/api/json")
* </pre>
* 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.
* <p>
* TODO: Some methods should be moved to a HttpClient implementation, because SURL is not the valid class to manage it.
* <br/>
* TODO: BAD_HOSTS implementation should be reviewed and moved to HttpClient implementation with a circuit-breaker.
* <br/>
* TODO: Token expiration with refresh should be implemented in future.
* <br/>
*/
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<String, Long> BAD_HOSTS = new ConcurrentHashMap<String, Long>();
/**
* 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.
* <p>
* In some test cases (and maybe also needed for other functionality) the bad hosts cache must be cleared.<br/>
* 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<String> 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<String, Long> badHosts = new ConcurrentHashMap<String, Long>();
// Added by Alain Corbiere
/**
* Reads from an endpoint (with configured credentials and proxy) the response as blob.
* <p>
* 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.
* <p>
* 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<byte[]> result = exe.submit(new Callable<byte[]>() {
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<byte[]> 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.
* <p>
* 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).
* <p>
* This method don't use the "bad-host" functionality, because the access to infrastructure services should not be
* obfuscated by some internal management.
* <p>
* <strong>Please don't use this method directly from DSL scripts.</strong>
*
* @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<String, Object> headers) {
if (!isUrlOk()) {
return null;
}
final Future<byte[]> 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.
* <p>
* 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).
* <p>
* This method don't use the "bad-host" functionality, because the access to infrastructure services should not be
* obfuscated by some internal management.
* <p>
* <strong>Please don't use this method directly from DSL scripts.</strong>
*
* @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<String, Object> headers) {
if (!isUrlOk()) {
return null;
}
final Future<byte[]> 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<byte[]> requestWithGetAndResponse(
final URL url, final Proxy proxy, final SecurityAuthentication authentication,
final Map<String, Object> headers) {
return new Callable<byte[]>() {
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<byte[]> requestWithPostAndResponse(
final URL url, final Proxy proxy, final SecurityAuthentication authentication,
final String data, final Map<String, Object> headers) {
return new Callable<byte[]>() {
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<String, Object> headers) {
if ( headers == null || headers.isEmpty() ) {
return;
}
for (Map.Entry<String, Object> 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;
}
}

View File

@ -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 = "<none>";
/**
* 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<SFile> getPath(String prop) {
final List<SFile> result = new ArrayList<>();
String paths = getenv(prop);
@ -155,4 +277,144 @@ 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.
* <p>
* 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.
* <p>
* 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<SFile> 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();
}
}

View File

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

View File

@ -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.<p>
* This kind of information is typically not needed. Useful for debugging purpose.
*/
private final String shape;
/**
* Origin authorization process (e.g. client_credentials.<p>
* 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<String, Object> tokens;
public SecurityAuthentication(String type, Map<String, Object> tokens) {
this(type, null, null, tokens);
}
public SecurityAuthentication(String type, String shape, String grantType, Map<String, Object> 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<String, Object> 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();
}
}
}

View File

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

View File

@ -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<String, Object> EMPTY_MAP = Collections.emptyMap();
/**
* No credentials given.
*/
public static final SecurityCredentials NONE = new SecurityCredentials("<NONE>", "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<String, Object> properties = new HashMap<String, Object>();
/**
* Proxy configuration.<p>
* <p>
* {@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<String, Object> 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.
* <p>
* Example:
* <pre>
* {
* "name": "jenkins",
* "identifier": "alice",
* "secret": "secret",
* "proxy": {
* "type": "socket",
* "address": "192.168.1.250",
* "port": 8080
* }
* }
* </pre>
*
* @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<String, Object> map = new HashMap<String, Object>();
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.
* <p>
* Example:
* <pre>
* {
* "type": "socket",
* "address": "192.168.1.250",
* "port": 8080
* }
* </pre>
*
* @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.
* <p>
* Example:<br/>
* <pre>
* {
* "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
* </pre>
*
* @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<String, Object> 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<String, Object> 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());
}
}

View File

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

View File

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

View File

@ -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<String, Object> EMPTY_MAP = Collections.emptyMap();
@Override
public SecurityAuthentication create(SecurityCredentials credentials) {
return new SecurityAuthentication("public", EMPTY_MAP);
}
}

View File

@ -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.
* <p>
* 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();
}
}

View File

@ -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<String, Object> tokens = new HashMap<String, Object>();
tokens.put("identifier", identifier);
if (secret != null) {
tokens.put("secret", secret.clone());
}
return new SecurityAuthentication(type, tokens);
}
}

View File

@ -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.<p>
* Initialize with:
* <pre>
* "Content-Type"="application/x-www-form-urlencoded; charset=UTF-8"
* "Accept"="application/json"
* </pre>
*
* @return headers
*/
protected Map<String, Object> headers() {
Map<String, Object> 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<String, Object> buildAccessDataFromResponse(JsonObject tokenResponse, String tokenType) {
Map<String, Object> 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<String, Object> 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;
}
}

View File

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

View File

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

View File

@ -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.
* <p>
* 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, '*');
}
}
}
}

View File

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

View File

@ -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.
* <p>
* 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<String, Object> 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);
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,56 @@
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();
}
}

View File

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

View File

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

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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();
}
}

View File

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

View File

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

View File

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

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> buildAccessDataFromResponse(JsonObject tokenResponse, String overrideTokenType) {
return super.buildAccessDataFromResponse(tokenResponse, overrideTokenType);
}
@Override
public String urlEncode(String data) {
return super.urlEncode(data);
}
}
}

View File

@ -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<String, Object> 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");
}
}

View File

@ -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<TValue> 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<TValue> values = Collections.singletonList(TValue.fromString(name));
TValue tValue = cut.executeReturnFunction(null, null, null, values, null);
assertThat (tValue.toString()).isNotEmpty();
}
}