using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Amazon.Auth.AccessControlPolicy;
using Amazon.IdentityManagement;
using Amazon.IdentityManagement.Model;
namespace Amazon.Common.DotNetCli.Tools
{
///
/// Utility class for interacting with console user to select or create an IAM role
///
public static class RoleHelper
{
public const string EC2_ASSUME_ROLE_PRINCIPAL = "ec2.amazonaws.com";
public const string ECS_TASK_ASSUME_ROLE_PRINCIPAL = "ecs-tasks.amazonaws.com";
public const int DEFAULT_ITEM_MAX = 20;
private const int MAX_LINE_LENGTH_FOR_MANAGED_ROLE = 95;
public static readonly TimeSpan SLEEP_TIME_FOR_ROLE_PROPOGATION = TimeSpan.FromSeconds(15);
public static string GenerateUniqueIAMRoleName(IAmazonIdentityManagementService iamClient, string baseName)
{
var existingRoleNames = new HashSet();
var response = new ListRolesResponse();
do
{
var roles = iamClient.ListRolesAsync(new ListRolesRequest { Marker = response.Marker }).Result.Roles;
roles.ForEach(x => existingRoleNames.Add(x.RoleName));
} while (response.IsTruncated);
if (!existingRoleNames.Contains(baseName))
return baseName;
for (int i = 1; true; i++)
{
var name = baseName + "-" + i;
if (!existingRoleNames.Contains(name))
return name;
}
}
public static string ExpandInstanceProfile(IAmazonIdentityManagementService iamClient, string instanceProfile)
{
if (instanceProfile.StartsWith("arn:aws"))
return instanceProfile;
// Wrapping this in a task to avoid dealing with aggregate exception.
var task = Task.Run(async () =>
{
try
{
var request = new GetInstanceProfileRequest { InstanceProfileName = instanceProfile };
var response = await iamClient.GetInstanceProfileAsync(request).ConfigureAwait(false);
return response.InstanceProfile.Arn;
}
catch (NoSuchEntityException)
{
return null;
}
});
if (task.Result == null)
{
throw new ToolsException($"Instance Profile \"{instanceProfile}\" can not be found.", ToolsException.CommonErrorCode.RoleNotFound);
}
return task.Result;
}
public static string ExpandRoleName(IAmazonIdentityManagementService iamClient, string roleName)
{
if (roleName.StartsWith("arn:aws"))
return roleName;
// Wrapping this in a task to avoid dealing with aggregate exception.
var task = Task.Run(async () =>
{
try
{
var request = new GetRoleRequest { RoleName = roleName };
var response = await iamClient.GetRoleAsync(request).ConfigureAwait(false);
return response.Role.Arn;
}
catch (NoSuchEntityException)
{
return null;
}
});
if(task.Result == null)
{
throw new ToolsException($"Role \"{roleName}\" can not be found.", ToolsException.CommonErrorCode.RoleNotFound);
}
return task.Result;
}
public static string ExpandManagedPolicyName(IAmazonIdentityManagementService iamClient, string managedPolicy)
{
if (managedPolicy.StartsWith("arn:aws"))
return managedPolicy;
// Wrapping this in a task to avoid dealing with aggregate exception.
var task = Task.Run(async () =>
{
var listResponse = new ListPoliciesResponse();
do
{
var listRequest = new ListPoliciesRequest { Marker = listResponse.Marker, Scope = PolicyScopeType.All };
listResponse = await iamClient.ListPoliciesAsync(listRequest).ConfigureAwait(false);
var policy = listResponse.Policies.FirstOrDefault(x => string.Equals(managedPolicy, x.PolicyName));
if (policy != null)
return policy.Arn;
} while (listResponse.IsTruncated);
return null;
});
if (task.Result == null)
{
throw new ToolsException($"Policy \"{managedPolicy}\" can not be found.", ToolsException.CommonErrorCode.PolicyNotFound);
}
return task.Result;
}
public static string CreateRole(IAmazonIdentityManagementService iamClient, string roleName, string assumeRolePolicy, params string[] managedPolicies)
{
if (managedPolicies != null && managedPolicies.Length > 0)
{
for(int i = 0; i < managedPolicies.Length; i++)
{
if (managedPolicies[i] != null)
{
managedPolicies[i] = ExpandManagedPolicyName(iamClient, managedPolicies[i]);
}
}
}
string roleArn;
try
{
CreateRoleRequest request = new CreateRoleRequest
{
RoleName = roleName,
AssumeRolePolicyDocument = assumeRolePolicy
};
var response = iamClient.CreateRoleAsync(request).Result;
roleArn = response.Role.Arn;
}
catch (Exception e)
{
throw new ToolsException($"Error creating IAM Role: {e.Message}", ToolsException.CommonErrorCode.IAMCreateRole, e);
}
if (managedPolicies != null && managedPolicies.Length > 0)
{
try
{
foreach (var managedPolicy in managedPolicies)
{
if (managedPolicy != null)
{
var request = new AttachRolePolicyRequest
{
RoleName = roleName,
PolicyArn = managedPolicy
};
iamClient.AttachRolePolicyAsync(request).Wait();
}
}
}
catch (Exception e)
{
throw new ToolsException($"Error assigning managed IAM Policy: {e.Message}", ToolsException.CommonErrorCode.IAMAttachRole, e);
}
}
bool found = false;
do
{
// There is no way check if the role has propagated yet so to
// avoid error during deployment creation do a generous sleep.
Console.WriteLine("Waiting for new IAM Role to propagate to AWS regions");
long start = DateTime.Now.Ticks;
while (TimeSpan.FromTicks(DateTime.Now.Ticks - start).TotalSeconds < SLEEP_TIME_FOR_ROLE_PROPOGATION.TotalSeconds)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.Write(".");
Console.Out.Flush();
}
Console.WriteLine("\t Done");
try
{
var getResponse = iamClient.GetRoleAsync(new GetRoleRequest { RoleName = roleName }).Result;
if (getResponse.Role != null)
found = true;
}
catch (NoSuchEntityException)
{
}
catch (Exception e)
{
throw new ToolsException("Error confirming new role was created: " + e.Message, ToolsException.CommonErrorCode.IAMGetRole, e);
}
} while (!found);
return roleArn;
}
public static async Task> FindManagedPoliciesAsync(IAmazonIdentityManagementService iamClient, PromptRoleInfo promptInfo, int maxPolicies)
{
ListPoliciesRequest request = new ListPoliciesRequest
{
Scope = PolicyScopeType.AWS,
};
ListPoliciesResponse response = null;
IList policies = new List();
do
{
request.Marker = response?.Marker;
response = await iamClient.ListPoliciesAsync(request).ConfigureAwait(false);
foreach (var policy in response.Policies)
{
if (policy.IsAttachable &&
(promptInfo.KnownManagedPolicyDescription.ContainsKey(policy.PolicyName) ||
(promptInfo.AWSManagedPolicyNamePrefix != null && policy.PolicyName.StartsWith(promptInfo.AWSManagedPolicyNamePrefix)))
)
{
policies.Add(policy);
}
if (policies.Count == maxPolicies)
return policies;
}
} while (response.IsTruncated);
response = await iamClient.ListPoliciesAsync(new ListPoliciesRequest
{
Scope = PolicyScopeType.Local
});
foreach (var policy in response.Policies)
{
if (policy.IsAttachable)
policies.Add(policy);
if (policies.Count == maxPolicies)
return policies;
}
return policies;
}
public static async Task> FindExistingRolesAsync(IAmazonIdentityManagementService iamClient, string assumeRolePrincpal, int maxRoles)
{
List roles = new List();
ListRolesRequest request = new ListRolesRequest();
ListRolesResponse response = null;
do
{
if (response != null)
request.Marker = response.Marker;
response = await iamClient.ListRolesAsync(request).ConfigureAwait(false);
foreach (var role in response.Roles)
{
if (AssumeRoleServicePrincipalSelector(role, assumeRolePrincpal))
{
roles.Add(role);
if (roles.Count == maxRoles)
{
break;
}
}
}
} while (response.IsTruncated && roles.Count < maxRoles);
return roles;
}
private static IList FindExistingRoles(IAmazonIdentityManagementService iamClient, string assumeRolePrincpal, int maxRoles)
{
var task = Task.Run>(async () =>
{
return await FindExistingRolesAsync(iamClient, assumeRolePrincpal, maxRoles);
});
return task.Result;
}
private static bool AssumeRoleServicePrincipalSelector(Role r, string servicePrincipal)
{
if (string.IsNullOrEmpty(r.AssumeRolePolicyDocument))
return false;
try
{
var decode = WebUtility.UrlDecode(r.AssumeRolePolicyDocument);
var policy = Policy.FromJson(decode);
foreach (var statement in policy.Statements)
{
if (statement.Actions.Contains(new ActionIdentifier("sts:AssumeRole")) &&
statement.Principals.Contains(new Principal("Service", servicePrincipal)))
{
return true;
}
}
return r.AssumeRolePolicyDocument.Contains(servicePrincipal);
}
catch (Exception)
{
return false;
}
}
public static async Task> FindExistingInstanceProfilesAsync(IAmazonIdentityManagementService iamClient, int maxRoles)
{
var profiles = new List();
ListInstanceProfilesRequest request = new ListInstanceProfilesRequest();
ListInstanceProfilesResponse response = null;
do
{
if (response != null)
request.Marker = response.Marker;
response = await iamClient.ListInstanceProfilesAsync(request).ConfigureAwait(false);
foreach (var profile in response.InstanceProfiles)
{
profiles.Add(profile);
}
} while (response.IsTruncated && profiles.Count < maxRoles);
return profiles;
}
public static string PromptForRole(IAmazonIdentityManagementService iamClient, PromptRoleInfo promptInfo)
{
var existingRoles = FindExistingRoles(iamClient, promptInfo.AssumeRolePrincipal, DEFAULT_ITEM_MAX);
if (existingRoles.Count == 0)
{
return PromptToCreateRole(iamClient, promptInfo);
}
var roleArn = SelectFromExisting(iamClient, promptInfo, existingRoles);
return roleArn;
}
private static string SelectFromExisting(IAmazonIdentityManagementService iamClient, PromptRoleInfo promptInfo, IList existingRoles)
{
Console.Out.WriteLine("Select IAM Role that to provide AWS credentials to your code:");
for (int i = 0; i < existingRoles.Count; i++)
{
Console.Out.WriteLine($" {(i + 1).ToString().PadLeft(2)}) {existingRoles[i].RoleName}");
}
Console.Out.WriteLine($" {(existingRoles.Count + 1).ToString().PadLeft(2)}) *** Create new IAM Role ***");
Console.Out.Flush();
int chosenIndex = Utilities.WaitForPromptResponseByIndex(1, existingRoles.Count + 1);
if (chosenIndex - 1 < existingRoles.Count)
{
return existingRoles[chosenIndex - 1].Arn;
}
else
{
return PromptToCreateRole(iamClient, promptInfo);
}
}
private static string PromptToCreateRole(IAmazonIdentityManagementService iamClient, PromptRoleInfo promptInfo)
{
Console.Out.WriteLine($"Enter name of the new IAM Role:");
var roleName = Console.ReadLine();
if (string.IsNullOrWhiteSpace(roleName))
return null;
roleName = roleName.Trim();
Console.Out.WriteLine("Select IAM Policy to attach to the new role and grant permissions");
var managedPolices = FindManagedPoliciesAsync(iamClient, promptInfo, DEFAULT_ITEM_MAX).Result;
for (int i = 0; i < managedPolices.Count; i++)
{
var line = $" {(i + 1).ToString().PadLeft(2)}) {managedPolices[i].PolicyName}";
var description = AttemptToGetPolicyDescription(managedPolices[i].Arn, promptInfo.KnownManagedPolicyDescription);
if (!string.IsNullOrEmpty(description))
{
if ((line.Length + description.Length) > MAX_LINE_LENGTH_FOR_MANAGED_ROLE)
description = description.Substring(0, MAX_LINE_LENGTH_FOR_MANAGED_ROLE - line.Length) + " ...";
line += $" ({description})";
}
Console.Out.WriteLine(line);
}
Console.Out.WriteLine($" {(managedPolices.Count + 1).ToString().PadLeft(2)}) *** No policy, add permissions later ***");
Console.Out.Flush();
int chosenIndex = Utilities.WaitForPromptResponseByIndex(1, managedPolices.Count + 1);
string managedPolicyArn = null;
if (chosenIndex < managedPolices.Count)
{
var selectedPolicy = managedPolices[chosenIndex - 1];
managedPolicyArn = selectedPolicy.Arn;
}
var roleArn = CreateRole(iamClient, roleName, Utilities.GetAssumeRolePolicy(promptInfo.AssumeRolePrincipal), managedPolicyArn);
return roleArn;
}
///
/// Because description does not come back in the list policy operation cache known lambda policy descriptions to
/// help users understand which role to pick.
///
///
///
///
private static string AttemptToGetPolicyDescription(string policyArn, Dictionary knownManagedPolicyDescription)
{
string content;
if (!knownManagedPolicyDescription.TryGetValue(policyArn, out content))
return null;
return content;
}
public class PromptRoleInfo
{
///
/// The principal searched for in existing roles when displaying available roles to user to select.
///
public string AssumeRolePrincipal { get; set; }
///
/// If prompting to create a role based on a managed policy display any aws provided
/// managed policies that start with this name.
///
public string AWSManagedPolicyNamePrefix { get; set; }
///
/// A list of known AWS managed policies to show along with their description.
///
public Dictionary KnownManagedPolicyDescription { get; set; }
}
}
}