AWS IAM CDK: tagging and creating access key for a user - amazon-iam

I'm trying to use AWS CDK to create a user with minimal permissions through a custom policy, but I'm stuck with tagging that user and creating its access keys.
Below there's my code:
public class Sample extends App {
public static void main(final String[] args) {
App app = new App();
new UserStack(app, "user-stack");
app.run();
}
public static class UserStack extends Stack {
// Not able to add Tag and Create Access Key
public UserStack(final App parent, final String name) {
super(parent, name);
PolicyStatement statement = new PolicyStatement(PolicyStatementEffect.Allow);
statement.addResource("*");
statement.addAction("lambda:UpdateFunctionCode");
User user = new User(this, "LambdaDeployer", UserProps.builder().withUserName("lambda-deployer").withPath("/").build());
user.addToPolicy(statement);
Tag tag = new Tag("Project", "devops");
// how to tag the user ??
new CfnOutput(this, "LambdaDeployerOutputAccessKey", CfnOutputProps.builder().withValue("AWS::IAM::AccessKey").build());
new CfnOutput(this, "LambdaDeployerOutputSecretAccessKey", CfnOutputProps.builder().withValue("AWS::IAM::SecretAccessKey").build());
}
}
}

You'll probably have to use a Custom Resource in order to call the TagUser API, since adding tags to a User is not available natively in CloudFormation.
You can use a new feature in the CDK to help you author your Custom Resource: https://github.com/awslabs/aws-cdk/pull/1850

Related

Role.addToPolicy() only on new Role

Why is the addtoPolicy method not available on the Role object if I get an existing role by name (or by arn)? The fromRoleName method should return a Role object. For example:
let testRole=Role.fromRoleName(this,"test-role","test-role");
testRole.addToPolicy()//Method not found
On the other hand, this works:
testRole = new Role(this,"test-role", {
assumedBy: new ServicePrincipal('lambda.amazonaws.com')
})
testRole.addToPolicy() //OK
This is because fromRoleName returns an IRole interface. This interface does not have a addToPolicy method.

GCP API - Determining what role an resource instance has been created with

For the project I'm on, I am tasked with creating a testing app that uses Terraform to create a resource instance and then test that it was created properly. The purpose is testing the Terraform Script result by validating certain characteristics of the resource created. That's the broad outline.
For several of these scripts a resource is assigned a role. It could be a PubSub subscription, DataCatalog, etc.
Example Terraform code for a Spanner Database assigning roles/spanner.databaseAdmin:
resource "google_spanner_database_iam_member" "user_database_admin" {
for_each = toset(var.iam_user_database_admins)
project = var.project
instance = var.instance_id
database = google_spanner_database.spanner_database.name
role = "roles/spanner.databaseAdmin"
member = "user:${each.key}"
}
So my question is this: Is there a way using a .NET GCP API to make a call to determine that the role was assigned? I can test for permissions via a TestIamPermissions method off of the client object and that's what I'm currently doing. But that gives me a sometimes long list of possible permissions. Is there a way to say "does this spanner database have the roles/spanner.databaseAdmin assigned?"
Here's an example of code testing for permissions on a PubSub Subscription:
TestIamPermissionsRequest subscriptionRequest = new TestIamPermissionsRequest
{
ResourceAsResourceName = SubscriptionName.FromProjectSubscription(projectId, subscriptionId),
Permissions = {
"pubsub.subscriptions.get",
"pubsub.subscriptions.delete",
"pubsub.subscriptions.update"
}
};
TestIamPermissionsResponse subscriptionResponse = publisher.TestIamPermissions(subscriptionRequest);
Seems like there ought to be a cleaner way to do this, but being somewhat new to GCP, I haven't found a way yet. Suggestions will be welcome.
Thought I should close this question off with what I eventually discovered. The proper question isn't what role is assigned an instance of a resource, but what users have been allowed to use the resource and with what role.
The proper call is GetIamPolicy which is available in the APIs for all of the resources that I've been working with. The problem was that I wasn't seeing anything due to no user accounts being assigned to the resource. I updated the Terraform script to assign a user to the resource with the required roles. When calling GetIamPolicy, it returns an array in the Bindings that lists roles and users that are assigned. This was the information I needed. Going down the path of using TestIamPermissions was unneeded.
Here's an example my use of this:
bool roleFound = false;
bool userFound = false;
bool exception = false;
try
{
Policy policyResponse = Client.GetIamPolicy(Resource);
var bindings = policyResponse.Bindings;
foreach (var item in bindings)
{
if (AcceptedRoles.Contains(item.Role))
roleFound = true;
foreach (var user in item.Members)
{
string testUser = user;
if (user.Substring(0, 5) == "user:")
{
testUser = user.Substring(5);
}
else if (user.Substring(0, 6) == "group:")
{
testUser = user.Substring(6);
}
if (Settings.UserTestList.Contains(testUser))
userFound = true;
}
}
}
catch (Grpc.Core.RpcException)
{
exception = true;
}
Assert.True(roleFound);
Assert.True(userFound);
Assert.False(exception);

Rejected to connect from lambda to cognito due to not authorized

If CognitoIdentityProvider::Client doesn't provide access_key_id and secret_access_key, it will retrieve from the ENV by default.
The strange thing is it works at a totally new lambda but not in another.
client = Aws::CognitoIdentityProvider::Client.new(region: ENV['AWS_REGION'])
client.admin_get_user({
user_pool_id: Jets.application.config.cognito[:user_pool_id],
username: 'username'
})
I would get this error message but had no idea where can I set the policy for cognito.
"errorMessage": "User: arn:aws:sts::123123123123:assumed-role/firstage-api-stag-IamRole-1DYOOEVSCURMY/xxxxxx-api-stag-mes_controller-show is not authorized to perform: cognito-idp:AdminGetUser on resource: arn:aws:cognito-idp:ap-northeast-2:319924209672:userpool/ap-northeast-2_0cSCFMK4r",
"errorType": "Function<Aws::CognitoIdentityProvider::Errors::AccessDeniedException>",
I would like to use the default key_id and access_key in the lambda ENV rather than IAM user.
What should I do???
Best practice for Lambda functions is to ensure your IAM role associated with the Lambda function has the policy to let it invoke the specific AWS Service. Do not hard code creds in Lambda functions.
For some services, you need to write a custom policy. For example, for Pinpoint voice, you need to write a custom policy using JSON so the Lambda function can invoke that service.
For CognitoIdentityProvider, try using this policy for your IAM role:
When in doubt which services your IAM role can invoke - check the Access Advisor tab:
I just tested this use case and built a Lambda function that creates a CognitoIdentityProviderClient object and gets users in a specific user pool. I implemented this using the Lambda Java runtime API, but it does not matter what supported Lambda programming language you use. You still need to configure your IAM role in the same way.
This code works perfectly. Here is the Handler class:
public class Handler implements RequestHandler<Map<String,String>, String> {
#Override
public String handleRequest(Map<String,String> event, Context context) {
LambdaLogger logger = context.getLogger();
String pool = event.get("Pool");
logger.log("pool: " + pool);
CognitoInfo cog = new CognitoInfo();
String xml = cog.getUsers(pool);
logger.log("XML: " + xml);
return xml;
}
}
Here is a method named getUsers located in the CognitoInfo class that invokes the AWS Service:
public String getUsers( String userPoolId) {
CognitoIdentityProviderClient cognitoclient = CognitoIdentityProviderClient.builder()
.region(Region.US_EAST_1)
.build();
try {
ArrayList<String> userList = new ArrayList();
// List all users
ListUsersRequest usersRequest = ListUsersRequest.builder()
.userPoolId(userPoolId)
.build();
ListUsersResponse response = cognitoclient.listUsers(usersRequest);
for(UserType user : response.users()) {
userList.add(user.username());
}
return convertToString(toXml(userList));
} catch (CognitoIdentityProviderException e){
System.err.println(e.awsErrorDetails().errorMessage());
System.exit(1);
}
return "";
}
The Lambda function is configured to use an IAM role named lambda-support2 that has the AmazonCognitoPowerUser policy.
This Lambda function successfully retrieves the users in a specific user pool:
Make sure that all your Lambda functions have the proper IAM role configured.

How to deploy AWS CDK app multiple times?

Is it possible to deploy a CDK app to the same account multiple times? I want to run synth once and the run cdk deploy against that synthesised template multiple times.
I can see that the recent 1.28.0 release of the CDK allows for passing CloudFormation parameters into the deploy command (via #1237). This means I can parameterize the contents of a stack, but I don't know how to change the name/id of the app itself.
For example, here is a simple app:
public class ExampleApp {
public static void main(final String[] args) {
App app = new App();
new ExampleStack(app, "ExampleStack");
app.synth();
}
}
and here is a simple do-nothing stack:
public class ExampleStack extends Stack {
public ExampleStack(final Construct scope, final String id) {
this(scope, id, null);
}
public ExampleStack(final Construct scope, final String id, final StackProps props) {
super(scope, id, props);
CfnParameter someVar = CfnParameter.Builder.create(this, "SomeVar")
.description("Some variable that can be passed in at deploy-time.")
.type("String")
.build();
// rest of stack here
}
}
I can run cdk synth and output the template somewhere, then run
cdk --app path/to/cdk.out deploy ExampleStack --parameters "ExampleStack:SomeVar=SomeValue"
and the parameter will be passed into the stack at deploy-time.
However, I don't see how to deploy the app multiple times with different names (or ids). Is this possible?
The background to why I want to do this, NOT run synth multiple times, is because for compliance reasons, I need a single artifact - the cdk.out directory - and then deploy that multiple times. To that end, I can't use answers based around multiple runs of synth.
Try this:
You need to make your stack name a parameter passed in from the command line
remove the app paramter from your cdk.json and specify the app parameter on the command line passing in the "prefix" parameter
Any resources in your stack with with service level names or IDs need to also be modified, for example two stacks can't create a secret with the same name
In my solution I derived from the StackProps object to add a "PrefixName" property to the stack and the prefix can be used within my stack to influence the naming or resources.
My program.cs looks as follows:
using Amazon.CDK;
namespace DevconnectListener
{
sealed class Program
{
public static void Main(string[] args)
{
var app = new App();
var props = new DevConnectStackProps()
{
PrefixName = args[0],
StackName = args[0] + "-DevconnectListenerStack"
};
var s = new DevconnectListenerStack(app, "DevconnectListenerStack", props);
app.Synth();
}
}
}
Here is my custom DevConnectStackProps class:
using System.Collections.Generic;
using Amazon.CDK;
using Amazon.CDK.AWS.DynamoDB;
using Amazon.CDK.AWS.Lambda;
namespace DevconnectListener
{
public class DevConnectStackProps : StackProps, IStackProps
{
public DevConnectStackProps()
{
}
public string PrefixName { get; set; }
}
}
My cdk.json look like this, removed the app property:
{
"context": {
"#aws-cdk/core:enableStackNameDuplicates": "true",
"aws-cdk:enableDiffNoFail": "true",
"#aws-cdk/core:stackRelativeExports": "true"
}
}
Then there is a CMD file named deploy-dev.cmd (which contains the aws profile i want to use and the dev prefix parameter:
cdk deploy --app "dotnet run -p src/DevconnectListener/DevconnectListener.csproj dev" --profile dev_aws_profile
If you run this again with a different prefix, you'll see a new stack in the CloudFormation console.

Is it possible to add to IConfiguration after the WebHost has started?

I am using AWS Systems Manager Parameter Store to hold database connection strings which are used to dynamically build a DbContext in my .NET Core Application
I am using the .NET Core AWS configuration provider (from https://aws.amazon.com/blogs/developer/net-core-configuration-provider-for-aws-systems-manager/) which injects my parameters into the IConfiguration at runtime.
At the moment I am having to keep my AWS access key/secret in code so it can be accessed by the ConfigurationBuilder but would like to move this out of the code base and stored it in appsettings or similar.
Here is my method to create the webhost builder called at startup
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
var webHost = WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
AWSCredentials credentials = new BasicAWSCredentials("xxxx", "xxxx");
AWSOptions options = new AWSOptions()
{
Credentials = credentials,
Region = Amazon.RegionEndpoint.USEast2
};
webHost.ConfigureAppConfiguration(config =>
{
config.AddJsonFile("appsettings.json");
config.AddSystemsManager("/ParameterPath", options, reloadAfter: new System.TimeSpan(0, 1, 0)); // Reload every minute
});
return webHost;
}
I need to be able to inject the BasicAWSCredentials parameter from somewhere.
You need to access an already built configuration to be able to retrieve the information you seek.
Consider building one to retrieve the needed credentials
public static IWebHostBuilder CreateWebHostBuilder(string[] args) {
var webHost = WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var access_key = configuration.GetValue<string>("access_key:path_here");
var secret_key = configuration.GetValue<string>("secret_key:path_here");
AWSCredentials credentials = new BasicAWSCredentials(access_key, secret_key);
AWSOptions options = new AWSOptions() {
Credentials = credentials,
Region = Amazon.RegionEndpoint.USEast2
};
webHost.ConfigureAppConfiguration(config => {
config.AddJsonFile("appsettings.json");
config.AddSystemsManager("/ParameterPath", options, reloadAfter: new System.TimeSpan(0, 1, 0)); // Reload every minute
});
return webHost;
}
I would also suggest reviewing Configuring AWS Credentials from the docs to use the SDK to find a possible alternative way to storing and retrieving the credentials.