From 5e6bed337c818784d0f3577b49af263fd088276d Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 15 Feb 2021 18:23:49 +0100 Subject: [PATCH] bug4811: working towards MFA enablement --- .../createLocalAwsApiP2Repository.sh | 6 +- .../META-INF/MANIFEST.MF | 3 +- .../LandscapeManagementWriteService.java | 15 +++ .../LandscapeManagementWriteServiceAsync.java | 16 +++ .../sailing/landscape/ui/impl/Activator.java | 7 ++ .../LandscapeManagementWriteServiceImpl.java | 112 +++++++++++++++--- ...sSessionCredentialsFromUserPreference.java | 40 +++++++ .../AwsSessionCredentialsWithExpiry.java | 10 ++ .../AwsSessionCredentialsWithExpiryImpl.java | 39 ++++++ .../sap/sailing/server/impl/Activator.java | 1 + .../sap/sse/landscape/aws/AwsLandscape.java | 17 ++- .../landscape/aws/impl/AwsLandscapeImpl.java | 32 +++-- ...referenceConverterRegistrationManager.java | 1 - .../sse/security/interfaces/UserStore.java | 4 +- .../com/sap/sse/security/SecurityService.java | 4 +- .../util}/GenericJSONPreferenceConverter.java | 3 +- 16 files changed, 271 insertions(+), 39 deletions(-) create mode 100755 java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsFromUserPreference.java create mode 100755 java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiry.java create mode 100755 java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiryImpl.java rename java/{com.sap.sailing.server/src/com/sap/sailing/server/impl => com.sap.sse.security/src/com/sap/sse/security/util}/GenericJSONPreferenceConverter.java (96%) diff --git a/java/com.amazon.aws.aws-java-api/createLocalAwsApiP2Repository.sh b/java/com.amazon.aws.aws-java-api/createLocalAwsApiP2Repository.sh index 64776c60257..3ed491601a0 100755 --- a/java/com.amazon.aws.aws-java-api/createLocalAwsApiP2Repository.sh +++ b/java/com.amazon.aws.aws-java-api/createLocalAwsApiP2Repository.sh @@ -125,4 +125,8 @@ echo "Patching SDK version in target platform definition ${TARGET_DEFINITION}... sed -i -e 's///' ${TARGET_DEFINITION} echo "You may test your target platform locally by creating race-analysis-p2-local.target by running the script createLocalTargetDef.sh." echo "You can also try a Hudson build with the -v option, generating and using the local target platform during the build." -echo "When all this works, update the P2 repository at p2.sapsailing.com using the script uploadAwsApiRepositoryToServer.sh." +echo "In this case, start with an unpatched remote target platform (race-analysis-p2-remote.target) and an unpatched" +echo "java/com.sap.sse.feature.runtime/feature.xml so that running this script during the Hudson build can resolve" +echo "the target platform." +echo "When all this works, commit and push the patched to race-analysis-p2-remote.target and feature.xml" +echo "and update the P2 repository at p2.sapsailing.com using the script uploadAwsApiRepositoryToServer.sh." diff --git a/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF index c2847992034..c44e6e6c805 100755 --- a/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF @@ -22,7 +22,8 @@ Require-Bundle: com.sap.sse.gwt, com.sap.sse.landscape.common, com.sap.sse.landscape.aws.common, com.jcraft.jsch;bundle-version="0.1.54", - elemental2;bundle-version="1.1.0" + elemental2;bundle-version="1.1.0", + org.json.simple Export-Package: com.google.gwt.user.client.rpc.core.com.sap.sse.landscape.aws.common.shared, com.google.gwt.user.client.rpc.core.com.sap.sse.landscape.common.shared, com.sap.sailing.landscape.ui.client, diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java index 618f7805917..a89e054c287 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java @@ -37,4 +37,19 @@ public interface LandscapeManagementWriteService extends RemoteService { AmazonMachineImageDTO upgradeAmazonMachineImage(String awsAccessKey, String awsSecret, String region, String machineImageId) throws Exception; void scaleMongo(String awsAccessKey, String awsSecret, String region, MongoScalingInstructionsDTO mongoScalingInstructions) throws Exception; + + /** + * For a combination of an AWS access key ID, the corresponding secret plus an MFA token code produces new session + * credentials and stores them in the user's preference store from where they can be obtained again using + * {@link #getSessionCredentials()}. Any session credentials previously stored in the current user's preference store + * will be overwritten by this. The current user must have the {@code LANDSCAPE:MANAGE:AWS} permission. + */ + void createMfaSessionCredentials(String awsAccessKey, String awsSecret, String mfaTokenCode); + + /** + * For the current user who has to have the {@code LANDSCAPE:MANAGE:AWS} permission, clears the preference in the + * user's preference store which holds any session credentials created previously using + * {@link #createMfaSessionCredentials(String, String, String)}. + */ + void clearSessionCredentials(); } diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java index 0f8e1dc9ea9..87e3645bd7e 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java @@ -57,4 +57,20 @@ public interface LandscapeManagementWriteServiceAsync { void scaleMongo(String awsAccessKey, String awsSecret, String region, MongoScalingInstructionsDTO mongoScalingInstructions, AsyncCallback asyncCallback); + + /** + * For a combination of an AWS access key ID, the corresponding secret plus an MFA token code produces new session + * credentials and stores them in the user's preference store from where they can be obtained again using + * {@link #getSessionCredentials()}. Any session credentials previously stored in the current user's preference store + * will be overwritten by this. The current user must have the {@code LANDSCAPE:MANAGE:AWS} permission. + */ + void createMfaSessionCredentials(String awsAccessKey, String awsSecret, String mfaTokenCode, + AsyncCallback callback); + + /** + * For the current user who has to have the {@code LANDSCAPE:MANAGE:AWS} permission, clears the preference in the + * user's preference store which holds any session credentials created previously using + * {@link #createMfaSessionCredentials(String, String, String)}. + */ + void clearSessionCredentials(AsyncCallback callback); } diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/impl/Activator.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/impl/Activator.java index 7957f4f52f3..cd9ce38658e 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/impl/Activator.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/impl/Activator.java @@ -3,12 +3,19 @@ package com.sap.sailing.landscape.ui.impl; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import com.sap.sailing.landscape.ui.shared.AwsSessionCredentialsFromUserPreference; +import com.sap.sse.security.interfaces.PreferenceConverter; +import com.sap.sse.security.util.GenericJSONPreferenceConverter; + public class Activator implements BundleActivator { private static BundleContext context; @Override public void start(BundleContext context) throws Exception { Activator.context = context; + context.registerService(PreferenceConverter.class, + new GenericJSONPreferenceConverter<>(() -> new AwsSessionCredentialsFromUserPreference()), + /* properties */ null); } @Override diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java index 4c3d5ae77e1..29a510673df 100755 --- a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java @@ -15,6 +15,9 @@ import com.sap.sailing.landscape.procedures.UpgradeAmi; import com.sap.sailing.landscape.ui.client.LandscapeManagementWriteService; import com.sap.sailing.landscape.ui.impl.Activator; import com.sap.sailing.landscape.ui.shared.AmazonMachineImageDTO; +import com.sap.sailing.landscape.ui.shared.AwsSessionCredentialsFromUserPreference; +import com.sap.sailing.landscape.ui.shared.AwsSessionCredentialsWithExpiry; +import com.sap.sailing.landscape.ui.shared.AwsSessionCredentialsWithExpiryImpl; import com.sap.sailing.landscape.ui.shared.MongoEndpointDTO; import com.sap.sailing.landscape.ui.shared.MongoScalingInstructionsDTO; import com.sap.sailing.landscape.ui.shared.SSHKeyPairDTO; @@ -43,6 +46,7 @@ import com.sap.sse.security.ui.server.SecurityDTOUtil; import software.amazon.awssdk.services.ec2.model.InstanceType; import software.amazon.awssdk.services.ec2.model.KeyPairInfo; +import software.amazon.awssdk.services.sts.model.Credentials; public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRemoteServiceServlet implements LandscapeManagementWriteService { @@ -51,6 +55,8 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem private static final Optional IMAGE_UPGRADE_TIMEOUT = Optional.of(Duration.ONE_MINUTE.times(10)); private final FullyInitializedReplicableTracker securityServiceTracker; + + private static final String USER_PREFERENCE_FOR_SESSION_TOKEN = "___aws.session.token___"; public > LandscapeManagementWriteServiceImpl() { @@ -66,10 +72,67 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem } } + /** + * For the logged-in user checks the LANDSCAPE:MANAGE:AWS permission, and if present, tries to obtain the user preference + * named like {@link #USER_PREFERENCE_FOR_SESSION_TOKEN}. If found and not yet expired, they are returned. Otherwise, + * {@code null} is returned, indicating to the caller that new session credentials shall be obtained which shall then be + * stored to the user preference again for future reference. + */ + private AwsSessionCredentialsWithExpiry getSessionCredentials() { + final AwsSessionCredentialsWithExpiry result; + checkLandscapeManageAwsPermission(); + final AwsSessionCredentialsFromUserPreference credentialsPreferences = getSecurityService().getPreferenceObject( + getSecurityService().getCurrentUser().getName(), USER_PREFERENCE_FOR_SESSION_TOKEN); + if (credentialsPreferences != null) { + final AwsSessionCredentialsWithExpiry credentials = credentialsPreferences.getAwsSessionCredentialsWithExpiry(); + if (credentials.getExpiration().after(TimePoint.now())) { + result = null; + } else { + result = credentials; + } + } else { + result = null; + } + return result; + } + + /** + * For a combination of an AWS access key ID, the corresponding secret plus an MFA token code produces new session + * credentials and stores them in the user's preference store from where they can be obtained again using + * {@link #getSessionCredentials()}. Any session credentials previously stored in the current user's preference store + * will be overwritten by this. The current user must have the {@code LANDSCAPE:MANAGE:AWS} permission. + */ @Override - public ArrayList getRegions() { + public void createMfaSessionCredentials(String awsAccessKey, String awsSecret, String mfaTokenCode) { + checkLandscapeManageAwsPermission(); + final Credentials credentials = getLandscape(awsAccessKey, awsSecret).getMfaSessionCredentials(mfaTokenCode); + final AwsSessionCredentialsWithExpiryImpl result = new AwsSessionCredentialsWithExpiryImpl( + credentials.accessKeyId(), credentials.secretAccessKey(), credentials.sessionToken(), + TimePoint.of(credentials.expiration().toEpochMilli())); + final AwsSessionCredentialsFromUserPreference credentialsPreferences = new AwsSessionCredentialsFromUserPreference(result); + getSecurityService().setPreferenceObject( + getSecurityService().getCurrentUser().getName(), USER_PREFERENCE_FOR_SESSION_TOKEN, credentialsPreferences); + } + + /** + * For the current user who has to have the {@code LANDSCAPE:MANAGE:AWS} permission, clears the preference in the + * user's preference store which holds any session credentials created previously using + * {@link #createMfaSessionCredentials(String, String, String)}. + */ + @Override + public void clearSessionCredentials() { + checkLandscapeManageAwsPermission(); + getSecurityService().unsetPreference(getSecurityService().getCurrentUser().getName(), USER_PREFERENCE_FOR_SESSION_TOKEN); + } + + private void checkLandscapeManageAwsPermission() { SecurityUtils.getSubject().checkPermission(SecuredLandscapeTypes.LANDSCAPE.getStringPermissionForTypeRelativeIdentifier(SecuredLandscapeTypes.LandscapeActions.MANAGE, new TypeRelativeObjectIdentifier("AWS"))); + } + + @Override + public ArrayList getRegions() { + checkLandscapeManageAwsPermission(); final ArrayList result = new ArrayList<>(); Util.addAll(Util.map(AwsLandscape.obtain().getRegions(), r->r.getId()), result); return result; @@ -84,10 +147,9 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem @Override public ArrayList getMongoEndpoints(String awsAccessKey, String awsSecret, String region) { - SecurityUtils.getSubject().checkPermission(SecuredLandscapeTypes.LANDSCAPE.getStringPermissionForTypeRelativeIdentifier(SecuredLandscapeTypes.LandscapeActions.MANAGE, - new TypeRelativeObjectIdentifier("AWS"))); + checkLandscapeManageAwsPermission(); final ArrayList result = new ArrayList<>(); - for (final MongoEndpoint mongoEndpoint : AwsLandscape.obtain(awsAccessKey, awsSecret).getMongoEndpoints(new AwsRegion(region))) { + for (final MongoEndpoint mongoEndpoint : getLandscape(awsAccessKey, awsSecret).getMongoEndpoints(new AwsRegion(region))) { final MongoEndpointDTO dto; if (mongoEndpoint.isReplicaSet()) { final MongoReplicaSet replicaSet = mongoEndpoint.asMongoReplicaSet(); @@ -105,6 +167,23 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem return result; } + private AwsLandscape getLandscape(String awsAccessKey, String awsSecret) { + final String keyId; + final String secret; + final Optional sessionToken; + final AwsSessionCredentialsWithExpiry sessionCredentials = getSessionCredentials(); + if (sessionCredentials != null) { + keyId = sessionCredentials.getAccessKeyId(); + secret = sessionCredentials.getSecretAccessKey(); + sessionToken = Optional.of(sessionCredentials.getSessionToken()); + } else { + keyId = awsAccessKey; + secret = awsSecret; + sessionToken = Optional.empty(); + } + return AwsLandscape.obtain(keyId, secret, sessionToken); + } + @Override public MongoEndpointDTO getMongoEndpoint(String awsAccessKey, String awsSecret, String region, String replicaSetName) { return getMongoEndpoints(awsAccessKey, awsSecret, region).stream().filter(mep->Util.equalsWithNull(mep.getReplicaSetName(), replicaSetName)).findAny().orElse(null); @@ -118,7 +197,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem final SSHKeyPair keyPair = getSecurityService().setOwnershipCheckPermissionForObjectCreationAndRevertOnError(dummyKeyPairForSecurityCheck.getPermissionType(), dummyKeyPairForSecurityCheck.getIdentifier().getTypeRelativeObjectIdentifier(), keyName, ()->{ - return AwsLandscape.obtain(awsAccessKey, awsSecret) + return getLandscape(awsAccessKey, awsSecret) .createKeyPair(new AwsRegion(regionId), keyName, privateKeyEncryptionPassphrase.getBytes()); }); return convertToSSHKeyPairDTO(keyPair); @@ -133,7 +212,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem final SSHKeyPair keyPair = getSecurityService().setOwnershipCheckPermissionForObjectCreationAndRevertOnError(dummyKeyPairForSecurityCheck.getPermissionType(), dummyKeyPairForSecurityCheck.getIdentifier().getTypeRelativeObjectIdentifier(), keyName, ()->{ - return AwsLandscape.obtain(awsAccessKey, awsSecret) + return getLandscape(awsAccessKey, awsSecret) .importKeyPair(new AwsRegion(regionId), publicKey.getBytes(), encryptedPrivateKey.getBytes(), keyName); }); return convertToSSHKeyPairDTO(keyPair); @@ -148,7 +227,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem @Override public ArrayList getSshKeys(String awsAccessKey, String awsSecret, String regionId) { final ArrayList result = new ArrayList<>(); - final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret); + final AwsLandscape landscape = getLandscape(awsAccessKey, awsSecret); final AwsRegion region = new AwsRegion(regionId); for (final KeyPairInfo keyPairInfo : landscape.getAllKeyPairInfos(region)) { final SSHKeyPair key = landscape.getSSHKeyPair(region, keyPairInfo.keyName()); @@ -164,7 +243,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem @Override public void removeSshKey(String awsAccessKey, String awsSecret, SSHKeyPairDTO keyPair) { getSecurityService().checkPermissionAndDeleteOwnershipForObjectRemoval(keyPair, - ()->AwsLandscape.obtain(awsAccessKey, awsSecret).deleteKeyPair(new AwsRegion(keyPair.getRegionId()), keyPair.getName())); + ()->getLandscape(awsAccessKey, awsSecret).deleteKeyPair(new AwsRegion(keyPair.getRegionId()), keyPair.getName())); } @Override @@ -185,11 +264,10 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem @Override public ArrayList getAmazonMachineImages(String awsAccessKey, String awsSecret, String region) { - SecurityUtils.getSubject().checkPermission(SecuredLandscapeTypes.LANDSCAPE.getStringPermissionForTypeRelativeIdentifier(SecuredLandscapeTypes.LandscapeActions.MANAGE, - new TypeRelativeObjectIdentifier("AWS"))); + checkLandscapeManageAwsPermission(); final ArrayList result = new ArrayList<>(); final AwsRegion awsRegion = new AwsRegion(region); - final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret); + final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret, /* sessionToken */ Optional.empty()); for (final String imageType : landscape.getMachineImageTypes(awsRegion)) { for (final AmazonMachineImage machineImage : landscape.getAllImagesWithType(awsRegion, imageType)) { final AmazonMachineImageDTO dto = new AmazonMachineImageDTO(machineImage.getId(), @@ -203,18 +281,16 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem @Override public void removeAmazonMachineImage(String awsAccessKey, String awsSecret, String region, String machineImageId) { - SecurityUtils.getSubject().checkPermission(SecuredLandscapeTypes.LANDSCAPE.getStringPermissionForTypeRelativeIdentifier(SecuredLandscapeTypes.LandscapeActions.MANAGE, - new TypeRelativeObjectIdentifier("AWS"))); - final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret); + checkLandscapeManageAwsPermission(); + final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret, /* sessionToken */ Optional.empty()); final AmazonMachineImage ami = landscape.getImage(new AwsRegion(region), machineImageId); ami.delete(); } @Override public AmazonMachineImageDTO upgradeAmazonMachineImage(String awsAccessKey, String awsSecret, String region, String machineImageId) throws Exception { - SecurityUtils.getSubject().checkPermission(SecuredLandscapeTypes.LANDSCAPE.getStringPermissionForTypeRelativeIdentifier(SecuredLandscapeTypes.LandscapeActions.MANAGE, - new TypeRelativeObjectIdentifier("AWS"))); - final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret); + checkLandscapeManageAwsPermission(); + final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret, /* sessionToken */ Optional.empty()); final AwsRegion awsRegion = new AwsRegion(region); final AmazonMachineImage ami = landscape.getImage(awsRegion, machineImageId); final UpgradeAmi.Builder> upgradeAmiBuilder = UpgradeAmi.builder(); @@ -234,7 +310,7 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem if (mongoScalingInstructions.getReplicaSetName() == null) { throw new IllegalArgumentException("Can only scale MongoDB Replica Sets, not standalone instances"); } - final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret); + final AwsLandscape landscape = AwsLandscape.obtain(awsAccessKey, awsSecret, /* sessionToken */ Optional.empty()); final AwsRegion region = new AwsRegion(regionId); for (int i=0; i startMongoProcessBuilder = StartMongoDBServer.builder(); diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsFromUserPreference.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsFromUserPreference.java new file mode 100755 index 00000000000..e052ed8edd3 --- /dev/null +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsFromUserPreference.java @@ -0,0 +1,40 @@ +package com.sap.sailing.landscape.ui.shared; + +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.settings.generic.AbstractGenericSerializableSettings; +import com.sap.sse.common.settings.generic.LongSetting; +import com.sap.sse.common.settings.generic.StringSetting; + +public class AwsSessionCredentialsFromUserPreference extends AbstractGenericSerializableSettings { + private static final long serialVersionUID = -3250243915670349222L; + + private StringSetting accessKeyId; + private StringSetting secretAccessKey; + private StringSetting sessionToken; + private LongSetting expiry; + + @Override + protected void addChildSettings() { + accessKeyId = new StringSetting("accessKeyId", this); + secretAccessKey = new StringSetting("secretAccessKey", this); + sessionToken = new StringSetting("sessionToken", this); + expiry = new LongSetting("expiry", this); + } + + /** + * The default settings + */ + public AwsSessionCredentialsFromUserPreference() { + } + + public AwsSessionCredentialsFromUserPreference(AwsSessionCredentialsWithExpiry awsSessionCredentialsWithExpiry) { + this.accessKeyId.setValue(awsSessionCredentialsWithExpiry.getAccessKeyId()); + this.secretAccessKey.setValue(awsSessionCredentialsWithExpiry.getSecretAccessKey()); + this.sessionToken.setValue(awsSessionCredentialsWithExpiry.getSessionToken()); + this.expiry.setValue(awsSessionCredentialsWithExpiry.getExpiration().asMillis()); + } + + public AwsSessionCredentialsWithExpiry getAwsSessionCredentialsWithExpiry() { + return new AwsSessionCredentialsWithExpiryImpl(accessKeyId.getValue(), secretAccessKey.getValue(), sessionToken.getValue(), TimePoint.of(expiry.getValue())); + } +} diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiry.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiry.java new file mode 100755 index 00000000000..b68f0313b19 --- /dev/null +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiry.java @@ -0,0 +1,10 @@ +package com.sap.sailing.landscape.ui.shared; + +import com.sap.sse.common.TimePoint; + +public interface AwsSessionCredentialsWithExpiry { + String getAccessKeyId(); + String getSecretAccessKey(); + String getSessionToken(); + TimePoint getExpiration(); +} diff --git a/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiryImpl.java b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiryImpl.java new file mode 100755 index 00000000000..f7a903cf007 --- /dev/null +++ b/java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsSessionCredentialsWithExpiryImpl.java @@ -0,0 +1,39 @@ +package com.sap.sailing.landscape.ui.shared; + +import com.sap.sse.common.TimePoint; + +public class AwsSessionCredentialsWithExpiryImpl implements AwsSessionCredentialsWithExpiry { + private final String accessKeyId; + private final String secretAccessKey; + private final String sessionToken; + private final TimePoint expiration; + + public AwsSessionCredentialsWithExpiryImpl(String accessKeyId, String secretAccessKey, String sessionToken, + TimePoint expiration) { + super(); + this.accessKeyId = accessKeyId; + this.secretAccessKey = secretAccessKey; + this.sessionToken = sessionToken; + this.expiration = expiration; + } + + @Override + public String getAccessKeyId() { + return accessKeyId; + } + + @Override + public String getSecretAccessKey() { + return secretAccessKey; + } + + @Override + public String getSessionToken() { + return sessionToken; + } + + @Override + public TimePoint getExpiration() { + return expiration; + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java index eb6f9cf9ed6..aafa19b9e4f 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java @@ -71,6 +71,7 @@ import com.sap.sse.security.SecurityService; import com.sap.sse.security.SecurityUrlPathProvider; import com.sap.sse.security.interfaces.PreferenceConverter; import com.sap.sse.security.shared.HasPermissions.DefaultActions; +import com.sap.sse.security.util.GenericJSONPreferenceConverter; import com.sap.sse.security.shared.HasPermissionsProvider; import com.sap.sse.security.shared.RoleDefinition; import com.sap.sse.util.ClearStateTestSupport; diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java index 3472fd42bc0..ef9a32c237f 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java @@ -47,6 +47,7 @@ import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetHealth import software.amazon.awssdk.services.route53.Route53Client; import software.amazon.awssdk.services.route53.model.ChangeInfo; import software.amazon.awssdk.services.route53.model.RRType; +import software.amazon.awssdk.services.sts.model.Credentials; /** * A simplified, largely stateless view onto the AWS SDK API that is geared towards specific ways and patterns of @@ -64,7 +65,7 @@ import software.amazon.awssdk.services.route53.model.RRType; *

* * Clients may also create dedicated instances of this service wrapper, using their own credentials. See - * {@link #obtain(String, String)}. + * {@link #obtain(String, String, Optional)}. *

* * This object interacts with an instance of {@link AwsLandscapeState} which keeps persistent and replicable state about @@ -118,7 +119,9 @@ public interface AwsLandscape extends Landscape { * Based on system properties for the AWS access key ID and the secret access key (see * {@link #ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME} and {@link #SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME}), this method * returns a landscape object which internally has access to the clients for the underlying AWS landscape, such as - * an EC2 client, a Route53 client, etc. + * an EC2 client, a Route53 client, etc. Note that this way no multi-factor authentication (MFA) is possible. If + * the system properties described above are not set or not valid, an unauthenticated landscape object will result; + * some rudimentary things may still work, such as querying the set of regions. */ static > @@ -134,8 +137,8 @@ public interface AwsLandscape extends Landscape { */ static > - AwsLandscape obtain(String accessKey, String secret) { - final AwsLandscape result = new AwsLandscapeImpl<>(Activator.getInstance().getLandscapeState(), accessKey, secret); + AwsLandscape obtain(String accessKey, String secret, Optional mfaTokenCode) { + final AwsLandscape result = new AwsLandscapeImpl<>(Activator.getInstance().getLandscapeState(), accessKey, secret, mfaTokenCode); return result; } @@ -556,4 +559,10 @@ public interface AwsLandscape extends Landscape { String tagName, BiFunction processFactoryFromHostAndServerDirectory, Optional optionalTimeout, Optional optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; + /** + * Obtains session credentials using an MFA token code valid for the user for which this landscape object was authenticated + * during its creation with an access key ID and a secret. + */ + Credentials getMfaSessionCredentials(String nonEmptyMfaTokenCode); + } diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java index b8750c9ae2b..4a4f5883782 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java @@ -62,6 +62,7 @@ import com.sap.sse.security.SessionUtils; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.regions.Region; @@ -135,6 +136,8 @@ import software.amazon.awssdk.services.route53.model.GetChangeRequest; import software.amazon.awssdk.services.route53.model.RRType; import software.amazon.awssdk.services.route53.model.ResourceRecord; import software.amazon.awssdk.services.route53.model.ResourceRecordSet; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.Credentials; public class AwsLandscapeImpl implements AwsLandscape { private static final String DEFAULT_TARGET_GROUP_PREFIX = "D"; @@ -150,25 +153,26 @@ public class AwsLandscapeImpl implements AwsLandscape private static final String DEFAULT_NON_DNS_MAPPED_ALB_NAME = "DefDyn"; private final String accessKeyId; private final String secretAccessKey; + private final Optional sessionToken; private final AwsRegion globalRegion; private final AwsLandscapeState landscapeState; public AwsLandscapeImpl(AwsLandscapeState awsLandscapeState) { this(awsLandscapeState, - System.getProperty(ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME), System.getProperty(SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME)); + System.getProperty(ACCESS_KEY_ID_SYSTEM_PROPERTY_NAME), System.getProperty(SECRET_ACCESS_KEY_SYSTEM_PROPERTY_NAME), Optional.empty()); } - public AwsLandscapeImpl(AwsLandscapeState awsLandscapeState, String accessKeyId, String secretAccessKey) { - this(accessKeyId, secretAccessKey, + public AwsLandscapeImpl(AwsLandscapeState awsLandscapeState, String accessKeyId, String secretAccessKey, Optional mfaTokenCode) { + this(accessKeyId, secretAccessKey, mfaTokenCode, // by using MongoDBService.INSTANCE the default test configuration will be used if nothing else is configured - PersistenceFactory.INSTANCE.getDomainObjectFactory(MongoDBService.INSTANCE), - PersistenceFactory.INSTANCE.getMongoObjectFactory(MongoDBService.INSTANCE), awsLandscapeState); + PersistenceFactory.INSTANCE.getDomainObjectFactory(MongoDBService.INSTANCE), PersistenceFactory.INSTANCE.getMongoObjectFactory(MongoDBService.INSTANCE), awsLandscapeState); } public AwsLandscapeImpl(String accessKeyId, String secretAccessKey, - DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory, AwsLandscapeState landscapeState) { + Optional sessionToken, DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory, AwsLandscapeState landscapeState) { this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; + this.sessionToken = sessionToken; this.globalRegion = new AwsRegion(Region.AWS_GLOBAL); this.landscapeState = landscapeState; } @@ -397,7 +401,7 @@ public class AwsLandscapeImpl implements AwsLandscape } private Route53Client getRoute53Client() { - return Route53Client.builder().region(getRegion(globalRegion)).build(); + return getClient(Route53Client.builder(), getRegion(globalRegion)); } @Override @@ -577,8 +581,20 @@ public class AwsLandscapeImpl implements AwsLandscape }; } + /** + * If a {@link #sessionToken} was provided to this landscape, use it to create {@link AwsSessionCredentials}; otherwise + * an {@link AwsBasicCredentials} object will be produced from the {@link #accessKeyId} and the {@link #secretAccessKey}. + * @return + */ private AwsCredentials getCredentials() { - return AwsBasicCredentials.create(accessKeyId, secretAccessKey); + return sessionToken.map(nonEmptySessionToken->(AwsCredentials) AwsSessionCredentials.create(accessKeyId, secretAccessKey, sessionToken.get())) + .orElse(AwsBasicCredentials.create(accessKeyId, secretAccessKey)); + } + + @Override + public Credentials getMfaSessionCredentials(String nonEmptyMfaTokenCode) { + return StsClient.builder().credentialsProvider(()->AwsBasicCredentials.create(accessKeyId, secretAccessKey)).build() + .getSessionToken(b->b.tokenCode(nonEmptyMfaTokenCode)).credentials(); } @Override diff --git a/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/PreferenceConverterRegistrationManager.java b/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/PreferenceConverterRegistrationManager.java index 7bd7a0e647f..8f6a3bc4369 100644 --- a/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/PreferenceConverterRegistrationManager.java +++ b/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/PreferenceConverterRegistrationManager.java @@ -52,7 +52,6 @@ public class PreferenceConverterRegistrationManager implements Stoppable { } private class Cutomizer implements ServiceTrackerCustomizer, PreferenceConverter> { - @Override public PreferenceConverter addingService(ServiceReference> reference) { final String preferenceKey = (String) reference.getProperty(PreferenceConverter.KEY_PARAMETER_NAME); diff --git a/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/UserStore.java b/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/UserStore.java index 3f54ad88b6d..bfbc4779106 100644 --- a/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/UserStore.java +++ b/java/com.sap.sse.security.interface/src/com/sap/sse/security/interfaces/UserStore.java @@ -30,8 +30,8 @@ public interface UserStore extends BasicUserStore { /** *

- * In an OSGi environment, this shouldn't be called manually, but instead automatically managed by setting a - * {@link PreferenceConverterRegistrationManager} up. {@link PreferenceConverter}s should be registered in the OSGi + * In an OSGi environment, this shouldn't be called manually, but instead automatically managed by setting up a + * {@link PreferenceConverterRegistrationManager}. {@link PreferenceConverter}s should be registered in the OSGi * service registry with {@link PreferenceConverter#KEY_PARAMETER_NAME} containing the associated preference key * added as property of the service registration. *

diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java index 1c31a5a4a6e..c8225267d10 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java @@ -307,13 +307,13 @@ public interface SecurityService extends ReplicableWithObjectInputStream see - * {@link #registerPreferenceConverter(String, PreferenceConverter)}. + * {@link UserStore#registerPreferenceConverter(String, PreferenceConverter)}. */ T getPreferenceObject(String username, String key); /** * Gets all preference objects resolving to a certain key. Always returns a valid map. May be empty. - * {@link #registerPreferenceConverter(String, PreferenceConverter)}. + * {@link UserStore#registerPreferenceConverter(String, PreferenceConverter)}. */ Map getPreferenceObjectsByKey(String key); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/GenericJSONPreferenceConverter.java b/java/com.sap.sse.security/src/com/sap/sse/security/util/GenericJSONPreferenceConverter.java similarity index 96% rename from java/com.sap.sailing.server/src/com/sap/sailing/server/impl/GenericJSONPreferenceConverter.java rename to java/com.sap.sse.security/src/com/sap/sse/security/util/GenericJSONPreferenceConverter.java index ba600330f45..58294ad2514 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/GenericJSONPreferenceConverter.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/util/GenericJSONPreferenceConverter.java @@ -1,4 +1,4 @@ -package com.sap.sailing.server.impl; +package com.sap.sse.security.util; import java.util.function.Supplier; @@ -24,5 +24,4 @@ public class GenericJSONPreferenceConverter