oracle.nosql.driver.iam.SignatureProvider Maven / Gradle / Ivy
Show all versions of nosqldriver Show documentation
/*-
* Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Universal Permissive License v 1.0 as shown at
* https://oss.oracle.com/licenses/upl/
*/
package oracle.nosql.driver.iam;
import static oracle.nosql.driver.iam.Utils.HEADER_DELIMITER;
import static oracle.nosql.driver.iam.Utils.RSA;
import static oracle.nosql.driver.iam.Utils.SIGNATURE_HEADER_FORMAT;
import static oracle.nosql.driver.iam.Utils.SINGATURE_VERSION;
import static oracle.nosql.driver.iam.Utils.createFormatter;
import static oracle.nosql.driver.iam.Utils.sign;
import static oracle.nosql.driver.util.HttpConstants.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import oracle.nosql.driver.AuthorizationProvider;
import oracle.nosql.driver.NoSQLHandleConfig;
import oracle.nosql.driver.Region;
import oracle.nosql.driver.Region.RegionProvider;
import oracle.nosql.driver.ops.Request;
import oracle.nosql.driver.util.LruCache;
import io.netty.handler.codec.http.HttpHeaders;
/**
* Cloud service only.
*
* An instance of {@link AuthorizationProvider} that generates and caches
* signature for each request as authorization string. A number of pieces of
* information are required for configuration. See
* SDK Configuration File
* and
* Required Keys and OCIDs
* for additional information as well as instructions on how to create required
* keys and OCIDs for configuration. The required information includes:
*
* - A signing key, used to sign requests.
* - A pass phrase for the key, if it is encrypted
* - The fingerprint of the key pair used for signing
* - The OCID of the tenancy
* - The OCID of a user in the tenancy
*
* All of this information is required to authenticate and authorize access to
* the service.
*
* There are two mechanisms for providing authorization information:
*
* - Using a user's identity and optional profile. This authenticates and
* authorizes the application based on a specific user identity.
* - Using an Instance Principal, which can be done when running on a
* compute instance in the Oracle Cloud Infrastructure (OCI). See
* {@link #createWithInstancePrincipal} and
* Calling Services from Instances.
*
*
*
* The latter can be simpler to use when running on an OCI compute instance,
* but limits the ability to use a compartment name vs OCID when naming
* compartments and tables in {@link Request} classes and when naming tables
* in queries. A specific user identity is best for naming flexibility,
* allowing both compartment names and OCIDs.
*
* When using a specific user's identity there are several options to provide
* the required information:
*
* - Using a configuration file. See
* SDK Configuration File
* for details on the file contents. By default the file is stored in
* ~/.oci/config, but you may supply a path to another location. The
* configuration file may include multiple profiles. The constructors that use
* a configuration include
*
* - {@link #SignatureProvider()}
* - {@link #SignatureProvider(String)}
* - {@link #SignatureProvider(String, String)}
*
*
* - Using information passed programmatically. The constructors that use this
* mechanism include
*
* - {@link #SignatureProvider(String, String, String, String, char[])}
* - {@link #SignatureProvider(String, String, String, File, char[])}
*
*
*
*/
public class SignatureProvider
implements AuthorizationProvider, RegionProvider {
private static final String SIGNING_HEADERS = "(request-target) host date";
private static final String SIGNING_HEADERS_WITH_OBO =
"(request-target) host date opc-obo-token";
private static final String OBO_TOKEN_HEADER = "opc-obo-token";
/* Cache key name */
private static final String CACHE_KEY = "signature";
/* Maximum lifetime of signature 300 seconds */
protected static final int MAX_ENTRY_LIFE_TIME = 300;
/* Default refresh time before signature expiry, 10 seconds*/
protected static final int DEFAULT_REFRESH_AHEAD = 10000;
/* User profile and key providers */
private final AuthenticationProfileProvider provider;
private final PrivateKeyProvider privateKeyProvider;
/* Delegation token specified for signing */
private String delegationToken;
private final LruCache signatureCache;
/* Refresh timer */
private volatile Timer refresher;
/* Refresh time before signature expired from cache */
private long refreshAheadMs = DEFAULT_REFRESH_AHEAD;
/* Refresh interval, if zero, no refresh will be scheduled */
private long refreshIntervalMs = 0;
private String serviceHost;
private Region region;
private Logger logger;
/**
* Creates a SignatureProvider using a default configuration file and
* profile. The configuration file used is ~/.oci/config
. See
* SDK Configuration File
* for details of the file's contents and format.
*
* @throws IOException if error loading profile from OCI configuration file
*/
public SignatureProvider() throws IOException {
this(new OCIConfigFileProvider());
}
/**
* Creates a SignatureProvider using the specified profile. The
* configuration file used is ~/.oci/config
. See
* SDK Configuration File
* for details of the file's contents and format
*
* @param profileName user profile name
*
* @throws IOException if error loading profile from OCI configuration file
*/
public SignatureProvider(String profileName) throws IOException {
this(new OCIConfigFileProvider(profileName));
}
/**
* Creates a SignatureProvider using the specified config file and
* profile. See
* SDK Configuration File
* for details of the file's contents and format
*
* @param configFile path of configuration file
*
* @param profileName user profile name
*
* @throws IOException if error loading profile from OCI configuration file
*/
public SignatureProvider(String configFile, String profileName)
throws IOException {
this(new OCIConfigFileProvider(configFile, profileName));
}
/**
* Creates a SignatureProvider using directly provided user authentication
* information. See
* Required Keys and OCIDs
* for details of the required parameters.
*
* @param tenantId tenant id
*
* @param userId user id
*
* @param fingerprint fingerprint of the key being used
*
* @param privateKey the string of private key used to sign request
*
* @param passphrase optional passphrase for the (encrypted) private key
*/
public SignatureProvider(String tenantId,
String userId,
String fingerprint,
String privateKey,
char[] passphrase) {
this(SimpleProfileProvider.builder()
.tenantId(tenantId)
.userId(userId)
.fingerprint(fingerprint)
.passphrase(passphrase)
.privateKeySupplier(new PrivateKeyStringSupplier(privateKey))
.build());
}
/**
* Creates a SignatureProvider using directly provided user authentication
* information. See
* Required Keys and OCIDs
* for details of the required parameters.
*
* @param tenantId tenant id
*
* @param userId user id
*
* @param fingerprint fingerprint of the key being used
*
* @param privateKeyFile the file of the private key used to sign request
*
* @param passphrase optional passphrase for the (encrypted) private key
*/
public SignatureProvider(String tenantId,
String userId,
String fingerprint,
File privateKeyFile,
char[] passphrase) {
this(SimpleProfileProvider.builder()
.tenantId(tenantId)
.userId(userId)
.fingerprint(fingerprint)
.passphrase(passphrase)
.privateKeySupplier(new PrivateKeyFileSupplier(privateKeyFile))
.build());
}
/**
* Creates a SignatureProvider using directly provided user authentication
* information. See
* Required Keys and OCIDs
* for details of the required parameters.
*
* @param tenantId tenant id
*
* @param userId user id
*
* @param fingerprint fingerprint of the key being used
*
* @param privateKeyFile the file of the private key used to sign request
*
* @param passphrase optional passphrase for the (encrypted) private key
*
* @param region identifies the region will be accessed by the NoSQLHandle.
*/
public SignatureProvider(String tenantId,
String userId,
String fingerprint,
File privateKeyFile,
char[] passphrase,
Region region) {
this(SimpleProfileProvider.builder()
.tenantId(tenantId)
.userId(userId)
.fingerprint(fingerprint)
.passphrase(passphrase)
.privateKeySupplier(new PrivateKeyFileSupplier(privateKeyFile))
.region(region)
.build());
}
/**
* Creates a SignatureProvider using an instance principal. This
* constructor may be used when calling the Oracle NoSQL Database
* Cloud Service from an Oracle Cloud compute instance.
* It authenticates with the instance principal and
* uses a security token issued by IAM to do the actual request signing.
*
* When using an instance principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Calling Services from Instances.
* @return SignatureProvider
*/
public static SignatureProvider createWithInstancePrincipal() {
return new SignatureProvider(
InstancePrincipalsProvider.builder().build());
}
/**
* Creates a SignatureProvider using an instance principal. This
* constructor may be used when calling the Oracle NoSQL Database
* Cloud Service from an Oracle Cloud compute instance.
* It authenticates with the instance principal and
* uses a security token issued by IAM to do the actual request signing.
*
* When using an instance principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Calling Services from Instances.
*
* @param region identifies the region will be accessed by the NoSQLHandle.
*
* @return SignatureProvider
*/
public static SignatureProvider createWithInstancePrincipal(Region region) {
return new SignatureProvider(
InstancePrincipalsProvider.builder().setRegion(region).build());
}
/**
* Creates a SignatureProvider using an instance principal. This
* constructor may be used when calling the Oracle NoSQL Database
* Cloud Service from an Oracle Cloud compute instance.
* It authenticates with the instance principal and
* uses a security token issued by IAM to do the actual request signing.
*
* When using an instance principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Calling Services from Instances.
*
* @param iamAuthUri The URI is usually detected automatically, specify
* the URI if you need to overwrite the default, or encounter the
* Invalid IAM URI
error.
*
* @return SignatureProvider
*/
public static SignatureProvider
createWithInstancePrincipal(String iamAuthUri) {
return new SignatureProvider(
InstancePrincipalsProvider.builder()
.setFederationEndpoint(iamAuthUri)
.build());
}
/**
* Creates a SignatureProvider using an instance principal. This
* constructor may be used when calling the Oracle NoSQL Database
* Cloud Service from an Oracle Cloud compute instance.
* It authenticates with the instance principal and
* uses a security token issued by IAM to do the actual request signing.
*
* When using an instance principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Calling Services from Instances.
*
* @param iamAuthUri The URI is usually detected automatically, specify
* the URI if you need to overwrite the default, or encounter the
* Invalid IAM URI
error.
* @param region the region to use, it may be null
* @param logger the logger used by the SignatureProvider.
*
* @return SignatureProvider
*/
public static SignatureProvider
createWithInstancePrincipal(String iamAuthUri,
Region region,
Logger logger) {
SignatureProvider provider = new SignatureProvider(
InstancePrincipalsProvider.builder()
.setFederationEndpoint(iamAuthUri)
.setLogger(logger)
.setRegion(region)
.build());
provider.setLogger(logger);
return provider;
}
/**
* Creates a SignatureProvider using an instance principal with a
* delegation token. This constructor may be used when calling the
* Oracle NoSQL Database Cloud Service from an Oracle Cloud compute
* instance. It authenticates with the instance principal and uses a
* security token issued by IAM to do the actual request signing.
* The delegation token allows the instance to assume the privileges
* of the user for which the token was created.
*
* When using an instance principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Calling Services from Instances.
*
* @param delegationToken this token allows an instance to assume the
* privileges of a specific user and act on-behalf-of that user
*
* @return SignatureProvider
*/
public static SignatureProvider
createWithInstancePrincipalForDelegation(String delegationToken) {
SignatureProvider provider = new SignatureProvider(
InstancePrincipalsProvider.builder().build());
provider.setDelegationToken(delegationToken);
return provider;
}
/**
* Creates a SignatureProvider using an instance principal with a
* delegation token. This constructor may be used when calling the
* Oracle NoSQL Database Cloud Service from an Oracle Cloud compute
* instance. It authenticates with the instance principal and uses a
* security token issued by IAM to do the actual request signing.
* The delegation token allows the instance to assume the privileges
* of the user for which the token was created.
*
* When using an instance principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Calling Services from Instances.
*
* @param iamAuthUri The URI is usually detected automatically, specify
* the URI if you need to overwrite the default, or encounter the
* Invalid IAM URI
error.
* @param region the region to use, it may be null
* @param delegationToken this token allows an instance to assume the
* privileges of a specific user and act on-behalf-of that user
* @param logger the logger used by the SignatureProvider.
*
* @return SignatureProvider
*/
public static SignatureProvider
createWithInstancePrincipalForDelegation(String iamAuthUri,
Region region,
String delegationToken,
Logger logger) {
SignatureProvider provider = new SignatureProvider(
InstancePrincipalsProvider.builder()
.setFederationEndpoint(iamAuthUri)
.setLogger(logger)
.setRegion(region)
.build());
provider.setLogger(logger);
provider.setDelegationToken(delegationToken);
return provider;
}
/**
* Creates a SignatureProvider using a resource principal. This
* constructor may be used when calling the Oracle NoSQL Database
* Cloud Service from other Oracle Cloud service resource such as
* Functions. It uses a resource provider session token (RPST) that
* enables the resource such as function to authenticate itself.
*
* When using an resource principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Accessing Other Oracle Cloud Infrastructure Resources from Running Functions.
*
* @return SignatureProvider
*/
public static SignatureProvider createWithResourcePrincipal() {
return SignatureProvider.createWithResourcePrincipal(null);
}
/**
* Creates a SignatureProvider using a resource principal. This
* constructor may be used when calling the Oracle NoSQL Database
* Cloud Service from other Oracle Cloud Service resource such as
* Functions. It uses a resource provider session token (RPST) that
* enables the resource such as the function to authenticate itself.
*
*
* When using an resource principal the compartment id (OCID) must be
* specified on each request or defaulted by using
* {@link NoSQLHandleConfig#setDefaultCompartment}. If the compartment id
* is not specified for an operation an exception will be thrown.
*
* See Accessing Other Oracle Cloud Infrastructure Resources from Running Functions.
*
* @param logger the logger used by the SignatureProvider
*
* @return SignatureProvider
*/
public static SignatureProvider createWithResourcePrincipal(Logger logger) {
SignatureProvider provider = new SignatureProvider(
ResourcePrincipalProvider.build(logger));
provider.setLogger(logger);
return provider;
}
/*
* The SignatureProvider that generates and caches request signature using
* key id and private key supplied by {@link AuthenticationProfileProvider}.
*/
protected SignatureProvider(AuthenticationProfileProvider provider) {
this(provider, MAX_ENTRY_LIFE_TIME, DEFAULT_REFRESH_AHEAD);
}
/*
* The constructor that is able to set refresh time before AT expired,
* currently this is hidden for simplicity.
*/
protected SignatureProvider(AuthenticationProfileProvider profileProvider,
int durationSeconds,
int refreshAhead) {
if (profileProvider instanceof RegionProvider) {
this.region = ((RegionProvider) profileProvider).getRegion();
}
this.provider = profileProvider;
this.privateKeyProvider = (provider == null) ?
null : new PrivateKeyProvider(provider);
if (durationSeconds > MAX_ENTRY_LIFE_TIME) {
throw new IllegalArgumentException(
"Access token cannot be cached longer than " +
MAX_ENTRY_LIFE_TIME + " seconds");
}
this.signatureCache =
new LruCache(2, durationSeconds * 1000);
this.refreshAheadMs = refreshAhead;
long durationMS = durationSeconds * 1000;
if (durationMS > refreshAheadMs) {
this.refreshIntervalMs = durationMS - refreshAheadMs;
}
}
@Override
public String getAuthorizationString(Request request) {
if (serviceHost == null) {
throw new IllegalArgumentException(
"Unable to find service host, use setServiceHost " +
"to load from NoSQLHandleConfig");
}
SignatureDetails sigDetails = getSignatureDetails();
if (sigDetails != null) {
return sigDetails.getSignatureHeader();
}
return null;
}
@Override
public void setRequiredHeaders(String authString,
Request request,
HttpHeaders headers) {
SignatureDetails sigDetails = getSignatureDetails();
if (sigDetails == null) {
return;
}
headers.add(AUTHORIZATION, sigDetails.getSignatureHeader());
headers.add(DATE, sigDetails.getDate());
if (delegationToken != null) {
headers.add(OBO_TOKEN_HEADER, delegationToken);
}
String compartment = request.getCompartment();
if (compartment == null) {
/*
* If request doesn't has compartment id, set the tenant id as the
* default compartment, which is the root compartment in IAM if
* using user principal. If using an instance principal this
* value is null.
*/
compartment = getTenantOCID();
}
if (compartment != null) {
headers.add(REQUEST_COMPARTMENT_ID, compartment);
} else {
throw new IllegalArgumentException(
"Compartment is null. When authenticating using an " +
"Instance Principal the compartment for the operation " +
"must be specified.");
}
}
/**
* Get tenant OCID if using user principal.
* @return tenant OCID of user
*/
private String getTenantOCID() {
if (provider instanceof UserAuthenticationProfileProvider) {
return ((UserAuthenticationProfileProvider)this.provider)
.getTenantId();
}
return null;
}
@Override
public void close() {
signatureCache.stop(false);
if (refresher != null) {
refresher.cancel();
refresher = null;
}
}
@Override
public Region getRegion() {
return region;
}
public SignatureProvider setServiceHost(NoSQLHandleConfig config) {
URL serviceURL = config.getServiceURL();
if (serviceURL == null) {
throw new IllegalArgumentException(
"Must set service URL first");
}
this.serviceHost = serviceURL.getHost();
return this;
}
/**
* Sets a Logger instance for this provider. If not set, the logger
* associated with the driver is used.
*
* @param logger the logger
*/
public void setLogger(Logger logger) {
this.logger = logger;
}
/**
* Returns the logger of this provider if set, null if not.
*
* @return logger
*/
public Logger getLogger() {
return logger;
}
/**
* Resource principal session tokens carry JWT claims. Permit the
* retrieval of the value from the token by given key.
* See {@link ResourcePrincipalClaimKeys}
*
* @param key the name of a claim in the session token
*
* @return the claim value.
*/
public String getResourcePrincipalClaim(String key) {
if (!(provider instanceof ResourcePrincipalProvider)) {
throw new IllegalArgumentException(
"Only resource principal support");
}
ResourcePrincipalProvider rpp = (ResourcePrincipalProvider)provider;
return rpp.getClaim(key);
}
/* visible for testing */
void setDelegationToken(String token) {
this.delegationToken = token;
}
private void logMessage(Level level, String msg) {
if (logger != null) {
logger.log(level, msg);
}
}
private SignatureDetails getSignatureDetails() {
SignatureDetails sigDetails = signatureCache.get(CACHE_KEY);
if (sigDetails != null) {
return sigDetails;
}
return getSignatureDetailsInternal();
}
/* visible for testing */
synchronized SignatureDetails getSignatureDetailsInternal() {
String date = createFormatter().format(new Date());
String keyId = provider.getKeyId();
if (provider instanceof InstancePrincipalsProvider) {
privateKeyProvider.reload(provider.getPrivateKey(),
provider.getPassphraseCharacters());
}
String signature;
try {
signature = sign(signingContent(date), privateKeyProvider.getKey());
} catch (Exception e) {
logMessage(Level.SEVERE, "Error signing request " + e.getMessage());
return null;
}
String signingHeader = (delegationToken == null)
? SIGNING_HEADERS : SIGNING_HEADERS_WITH_OBO;
String sigHeader = String.format(SIGNATURE_HEADER_FORMAT,
signingHeader,
keyId,
RSA,
signature,
SINGATURE_VERSION);
SignatureDetails sigDetails = new SignatureDetails(sigHeader, date);
signatureCache.put(CACHE_KEY, sigDetails);
scheduleRefresh();
return sigDetails;
}
String signingContent(String date) {
StringBuilder sb = new StringBuilder();
sb.append(REQUEST_TARGET).append(HEADER_DELIMITER)
.append("post /").append(NOSQL_DATA_PATH).append("\n")
.append(HOST).append(HEADER_DELIMITER)
.append(serviceHost).append("\n")
.append(DATE).append(HEADER_DELIMITER).append(date);
if (delegationToken != null) {
sb.append("\n").append(OBO_TOKEN_HEADER)
.append(HEADER_DELIMITER).append(delegationToken);
}
return sb.toString();
}
private void scheduleRefresh() {
/* If refresh interval is 0, don't schedule a refresh */
if (refreshIntervalMs == 0) {
return;
}
if (refresher != null) {
refresher.cancel();
refresher = null;
}
refresher = new Timer(true /* isDaemon */);
refresher.schedule(new RefreshTask(), refreshIntervalMs);
}
private class RefreshTask extends TimerTask {
@Override
public void run() {
try {
getSignatureDetailsInternal();
} catch (Exception e) {
/*
* Ignore the failure of refresh. The driver would try to
* generate signature in the next request if signature is not
* available, the failure would be reported at that moment.
*/
logMessage(Level.WARNING,
"Unable to refresh cached request signature, " +
e.getMessage());
refresher.cancel();
refresher = null;
}
}
}
static class SignatureDetails {
/* Signature header string */
private String signatureHeader;
/*
* Signing date, keep it and pass along with each request,
* so requests can reuse the signature within the 5-mins
* time window.
*/
private String date;
SignatureDetails(String signatureHeader, String date) {
this.signatureHeader = signatureHeader;
this.date = date;
}
String getDate() {
return date;
}
String getSignatureHeader() {
return signatureHeader;
}
boolean isValid(String header) {
if (header == null) {
return false;
}
return header.equals(signatureHeader);
}
}
/**
* Claim keys in the resource principal session token(RPST).
*
* They can be used to retrieve resource principal metadata such as its
* compartment and tenancy OCID.
*/
public static class ResourcePrincipalClaimKeys {
/**
* The claim name that the RPST holds for the resource compartment.
* This can be passed to
* {@link SignatureProvider#getResourcePrincipalClaim(String)}
* to retrieve the resource's compartment OCID.
*/
public final static String COMPARTMENT_ID_CLAIM_KEY = "res_compartment";
/**
* The claim name that the RPST holds for the resource tenancy.
* This can be passed to
* {@link SignatureProvider#getResourcePrincipalClaim(String)}
* to retrieve the resource's tenancy OCID.
*/
public final static String TENANT_ID_CLAIM_KEY = "res_tenant";
}
}