/* * Copyright 2015 Amazon Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: * * http://aws.amazon.com/apache2.0 * * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and * limitations under the License. */ package com.amazonaws.eclipse.elasticbeanstalk.server.ui; import java.util.ArrayList; import java.util.List; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.beans.PojoObservables; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.jface.databinding.swt.SWTObservables; import org.eclipse.jface.databinding.viewers.ViewersObservables; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Link; import org.eclipse.wst.server.ui.wizard.IWizardHandle; import com.amazonaws.eclipse.core.AwsToolkitCore; import com.amazonaws.eclipse.elasticbeanstalk.deploy.DeployWizardDataModel; import com.amazonaws.eclipse.elasticbeanstalk.jobs.LoadIamRolesJob; import com.amazonaws.eclipse.elasticbeanstalk.jobs.LoadResourcesCallback; import com.amazonaws.eclipse.elasticbeanstalk.util.BeanstalkConstants; import com.amazonaws.eclipse.elasticbeanstalk.util.OnUiThreadProxyFactory; import com.amazonaws.services.identitymanagement.model.Role; import com.amazonaws.util.StringUtils; public class DeployWizardRoleSelectionPage extends AbstractDeployWizardPage { public final RoleWidgetBuilder instanceRoleWidgetBuilder = new RoleWidgetBuilder() .withDefaultRole(BeanstalkConstants.DEFAULT_INSTANCE_ROLE_NAME) .withDataBindingFieldName(DeployWizardDataModel.INSTANCE_ROLE_NAME).withTrustEntity("ec2.amazonaws.com"); public final RoleWidgetBuilder serviceRoleWidgetBuilder = new RoleWidgetBuilder() .withDefaultRole(BeanstalkConstants.DEFAULT_SERVICE_ROLE_NAME) .withDataBindingFieldName(DeployWizardDataModel.SERVICE_ROLE_NAME) .withTrustEntity("beanstalk.amazonaws.com"); private static final String SERVICE_ROLE_LABEL_TEXT = "Service Role"; private static final String INSTANCE_PROFILE_ROLE_LABEL_TEXT = "Instance Profile Role"; private static final String SERVICE_ROLE_PERMISSIONS_DOC_URL = "https://docs.aws.amazon.com/console/elasticbeanstalk/roles"; private static final String IAM_ROLE_PERMISSIONS_DOC_URL = "http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/AWSHowTo.iam.roles.logs.html#iampolicy"; private Composite wizardPageRoot; private final LoadResourcesCallback<Role> loadIamRoleCallback; /** Only accessed through the UI thread. No need for synchronization **/ private boolean hasInsufficientIamPermissionDialogBeenShown; protected DeployWizardRoleSelectionPage(DeployWizardDataModel wizardDataModel) { super(wizardDataModel); // At this point the user can finish the wizard. All remaining pages are optional setComplete(true); this.loadIamRoleCallback = OnUiThreadProxyFactory.getProxy(LoadResourcesCallback.class, new LoadIamRolesCallback()); } @Override public String getPageTitle() { return "Permissions"; } @Override public String getPageDescription() { return "Select an instance profile and service role for your AWS Elastic Beanstalk environment"; } @Override public Composite createComposite(Composite parent, IWizardHandle handle) { this.hasInsufficientIamPermissionDialogBeenShown = false; wizardHandle = handle; setDefaultsInDataModel(); handle.setImageDescriptor(AwsToolkitCore.getDefault().getImageRegistry() .getDescriptor(AwsToolkitCore.IMAGE_AWS_LOGO)); this.wizardPageRoot = new Composite(parent, SWT.NONE); wizardPageRoot.setLayout(new GridLayout(1, false)); initializeValidators(); new LoadIamRolesJob(loadIamRoleCallback).schedule(); return wizardPageRoot; } private class LoadIamRolesCallback implements LoadResourcesCallback<Role> { public void onSuccess(List<Role> roles) { createRoleComboBoxControls(roles); } public void onInsufficientPermissions() { if (!hasInsufficientIamPermissionDialogBeenShown) { hasInsufficientIamPermissionDialogBeenShown = true; IAMOperationNotAllowedErrorDialog dialog = new IAMOperationNotAllowedErrorDialog( Display.getDefault().getActiveShell()); int code = dialog.open(); if (code == IAMOperationNotAllowedErrorDialog.OK_BUTTON_CODE || code == IAMOperationNotAllowedErrorDialog.CLOSE) { new LoadIamRolesJob(loadIamRoleCallback).schedule(); } } else { createManualRoleControls(); } } // TODO When we fail to load IAM roles for reasons other then permissions issue then we should // probably throw an error dialog up. It may be that our logic for determining a service error // is a permissions failure has gotten stale and needs to be updated. Not entirely sure what the // experience for this should look like, hence the todo public void onFailure() { onInsufficientPermissions(); } /** * If we do have IAM permissions we display the users roles in a dropdown to choose from */ private void createRoleComboBoxControls(List<Role> roles) { wizardDataModel.setSkipIamRoleAndInstanceProfileCreation(false); createInstanceProfileRoleLabel(wizardPageRoot); instanceRoleWidgetBuilder.setupComboViewer(wizardPageRoot, roles); newInstanceRoleDescLink(wizardPageRoot); createServiceRoleLabel(wizardPageRoot); serviceRoleWidgetBuilder.setupComboViewer(wizardPageRoot, roles); newServiceRoleDescLink(wizardPageRoot); // Redraw wizardPageRoot.layout(true); } /** * If we don't have IAM permissions to list roles then we display manual Text views that allow * users to manually specify the role. */ private void createManualRoleControls() { wizardDataModel.setSkipIamRoleAndInstanceProfileCreation(true); createInstanceProfileRoleLabel(wizardPageRoot); instanceRoleWidgetBuilder.setupManualControls(wizardPageRoot); newInstanceRoleDescLink(wizardPageRoot); createServiceRoleLabel(wizardPageRoot); serviceRoleWidgetBuilder.setupManualControls(wizardPageRoot); newServiceRoleDescLink(wizardPageRoot); // Redraw wizardPageRoot.layout(true); } private void createInstanceProfileRoleLabel(Composite composite) { newFillingLabel(composite, INSTANCE_PROFILE_ROLE_LABEL_TEXT); } private void createServiceRoleLabel(Composite composite) { newFillingLabel(composite, SERVICE_ROLE_LABEL_TEXT); } /** * Description and hyperlink for what the Instance Profile role is needed for */ private void newInstanceRoleDescLink(Composite composite) { adjustLinkLayout(newLink(composite, "If you choose not to use the default role, you must grant the relevant permissions to Elastic Beanstalk. " + "See the <a href=\"" + IAM_ROLE_PERMISSIONS_DOC_URL + "\">AWS Elastic Beanstalk Developer Guide</a> for more details.")); } /** * Description and hyperlink for what the Service role is needed for */ private void newServiceRoleDescLink(Composite composite) { adjustLinkLayout(newLink(composite, "A service role allows the Elastic Beanstalk service to monitor environment resources on your behalf. " + "See <a href=\"" + SERVICE_ROLE_PERMISSIONS_DOC_URL + "\">Service Roles, Instance Profiles, and User Policies</a> in the Elastic Beanstalk developer guide for details.")); } } /** * Set the default values for the roles and vpc in the data model to be reflected in the UI when the * model is bound to a control */ private void setDefaultsInDataModel() { if (StringUtils.isNullOrEmpty(wizardDataModel.getInstanceRoleName())) { wizardDataModel.setInstanceRoleName(BeanstalkConstants.DEFAULT_INSTANCE_ROLE_NAME); } if (StringUtils.isNullOrEmpty(wizardDataModel.getServiceRoleName())) { wizardDataModel.setServiceRoleName(BeanstalkConstants.DEFAULT_SERVICE_ROLE_NAME); } } /** * Class to hold data that differs between different role types (i.e. service role vs instance * role) and build appropriate widgets based on those differences */ private class RoleWidgetBuilder { private String defaultRole; private String dataBindingFieldName; private String trustEntity; public RoleWidgetBuilder withDefaultRole(String defaultRoleName) { this.defaultRole = defaultRoleName; return this; } public RoleWidgetBuilder withDataBindingFieldName(String dataBindingFieldName) { this.dataBindingFieldName = dataBindingFieldName; return this; } public RoleWidgetBuilder withTrustEntity(String trustEntity) { this.trustEntity = trustEntity; return this; } /** * Create the ComboViewer, setup databinding for it, and select the default role * * @param roles * List of IAM roles in the user's account */ public void setupComboViewer(Composite composite, List<Role> roles) { bindRoleComboView(newRoleComboView(composite, transformRoleList(roles)), dataBindingFieldName); } /** * Setup manual text controls when we don't have sufficient IAM permisisons to list roles so * user can still explicitly specify a role */ public void setupManualControls(Composite composite) { IObservableValue instanceRoleObservable = SWTObservables.observeText(newText(composite), SWT.Modify); getBindingContext().bindValue(instanceRoleObservable, PojoObservables.observeValue(wizardDataModel, dataBindingFieldName)); } /** * Setup data-binding for the ComboViewer * * @param comboViewer * @param fieldName * Field name in Data Model to bind to */ private void bindRoleComboView(ComboViewer comboViewer, String fieldName) { IObservableValue roleObservable = ViewersObservables.observeSingleSelection(comboViewer); IObservableValue observable = PojoObservables.observeValue(wizardDataModel, fieldName); getBindingContext().bindValue(roleObservable, observable); } private ComboViewer newRoleComboView(Composite composite, List<String> roles) { ComboViewer roleComboViewer = new ComboViewer(composite, SWT.DROP_DOWN | SWT.READ_ONLY); roleComboViewer.setContentProvider(ArrayContentProvider.getInstance()); roleComboViewer.getCombo().setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); roleComboViewer.setInput(roles); // Custom Label provider to clearly indicate the default role in the ComboViewer roleComboViewer.setLabelProvider(new LabelProvider() { @Override public String getText(Object element) { if (isDefaultRoleName(element)) { return "(Default) " + element; } return super.getText(element); } private boolean isDefaultRoleName(Object element) { return element instanceof String && defaultRole.equals(element); } }); return roleComboViewer; } /** * Transform the list of Role objects to a list of role names and filter out any roles that * don't have the required trust entity. Default role is always appended to the beginning of * the list (injected if it doesn't exist yet) * * @param roles * List of {@link Role} objects to transform * @return List of strings containing all role names that are appropriate for this role type */ private List<String> transformRoleList(List<Role> roles) { List<String> stringRoles = new ArrayList<String>(roles.size() + 1); stringRoles.add(defaultRole); for (Role role : roles) { if (!isDefaultRole(role) && hasRequiredTrustEntity(role)) { stringRoles.add(role.getRoleName()); } } return stringRoles; } /** * We only display those roles that can be assumed by the appropriate entity. For instance * profile roles this is EC2, for the service role this is Beanstalk itself */ private boolean hasRequiredTrustEntity(Role role) { return role.getAssumeRolePolicyDocument().contains(trustEntity); } private boolean isDefaultRole(Role role) { return defaultRole.equals(role.getRoleName()); } /** * DataBindingContext is setup in {@link AbstractDeployWizardPage} * * @return The current data binding context */ private DataBindingContext getBindingContext() { return DeployWizardRoleSelectionPage.this.bindingContext; } } private void adjustLinkLayout(Link link) { adjustLinkLayout(link, 1); } }