Writing authorizers for serverless framework in Ruby - ruby

I'm attempting to write an authorizer to protect calls to a lambda using the serverless framework. I'm using Ruby.
Configuration:
provider:
name: aws
runtime: ruby2.5
iamRoleStatements:
- Effect: Allow
Action:
- KMS:Decrypt
Resource: ${self:custom.kmsSecrets.keyArn}
functions:
authorize:
handler: handler.authorize
hello:
handler: handler.protected
events:
- http:
path: protected
method: get
authorizer: authorize
The authorizer:
def authorize(event:, context:)
if is_authorized?
{
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Resource": [ context.invoked_function_arn ],
"Effect": "Allow"
}
]
},
"principalId": "seeker"
}.to_json
end
end
The authentication I'd like to implement is token based: the is_authorized? method would receive the token and then return a policy that would allow access to the protected lambda function.
I'm not entirely sure what goes in the PrincipalId argument - I have no user.id.
Right now it complains that: seeker-dev-authorize is not authorized to perform: iam:CreatePolicy on resource: policy seeker-allowed which leaves me quite confused: I can't create a policy... on the policy? And where should I set this permission? On IAM or serverless.yml? Because I've set the permissions to encode/decode keys in serverless, maybe I should do the same with this?

I haven't used custom authorizers before, but I put together a small hello world project to try this out and this is what I found.
The protected function and the authorizer function:
def authorize(event:, context:)
{
"principalId": "test",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": event["methodArn"]
}
]
}
}
end
def hello(event:, context:)
{ statusCode: 200, body: JSON.generate('This is a protected endpoint!') }
end
Note that I'm returning the hash and not a string with to_json, I got an error back from the authorizer when using to_json.
Also note that I'm using event["methodArn"] to get the protected lambda ARN, using context.invoked_function_arn also caused me an error.
Besides that, not including an Authorization header in the request, will return an "Unauthorized error":
curl -X GET https://endpoint/dev/hello -H 'Authorization: test'
Lastly, about the principalId:
The principalId is a required property on your authorizer response. It represents the principal identifier for the caller. This may vary from application-to-application, but it could be a username, an email address, or a unique ID.
Source: https://www.alexdebrie.com/posts/lambda-custom-authorizers/

Related

How to solve "The IAM role configured on the integration or API Gateway doesn't have permissions to call the integration

I have a lambda function and an apigatewayv2. I am creating everything via terraform as below.
resource "aws_lambda_function" "prod_options" {
description = "Production Lambda"
environment {
variables = var.prod_env
}
function_name = "prod-func"
handler = "index.handler"
layers = [
aws_lambda_layer_version.node_modules_prod.arn
]
memory_size = 1024
package_type = "Zip"
reserved_concurrent_executions = -1
role = aws_iam_role.lambda_exec.arn
runtime = "nodejs12.x"
s3_bucket = aws_s3_bucket.lambda_bucket_prod.id
s3_key = aws_s3_bucket_object.lambda_node_modules_prod.key
source_code_hash = data.archive_file.lambda_node_modules_prod.output_base64sha256
timeout = 900
tracing_config {
mode = "PassThrough"
}
}
and role
resource "aws_iam_role_policy_attachment" "lambda_policy" {
role = aws_iam_role.lambda_exec.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role" "lambda_exec" {
name = "api_gateway_role"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": [
"apigateway.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
})
}
and then permissions
resource "aws_lambda_permission" "prod_api_gtw" {
statement_id = "AllowExecutionFromApiGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.prod_options.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.gateway_prod.execution_arn}/*/*"
}
After I deploy and try to invoke the url , I gget the following error
"integrationErrorMessage": "The IAM role configured on the integration or API Gateway doesn't have permissions to call the integration. Check the permissions and try again.",
I've been stuck with this for a while now. How can I solve this error?
You may have to create a Lambda permission to allow execution from an API Gateway resource:
resource "aws_lambda_permission" "apigw_lambda" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.layout_editor_prod_options.function_name
principal = "apigateway.amazonaws.com"
# The /*/*/* part allows invocation from any stage, method and resource path
# within API Gateway REST API.
source_arn = "${aws_api_gateway_rest_api.rest_api.execution_arn}/*/*/*"
}
Also, for the Lambda lambda_exec, you don't need apigateway.amazonaws.com principal. The reason why we don't need this is that the execution role applies to the function and allows it to interact with other AWS services. In the other hand, this wont allow anything for the API Gateway, for that we need a Lambda permission.
resource "aws_iam_role" "lambda_exec" {
name = "lambda_exec_role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
In the other hand, I would add a policy to the Lambda execution role to be able to log to CloudWatch. This might be useful for further debugging:
resource "aws_iam_policy" "lambda_logging" {
name = "lambda_logging"
path = "/"
description = "IAM policy for logging from a lambda"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_exec.name
policy_arn = aws_iam_policy.lambda_logging.arn
}

Authenticating AppSync mutation using AWS Cognito User pool on Graphql playground

A very basic scenario where I want to test an AppSync mutation on Graphql playground which was working fine with API key authentication.
I have attached an additional authorization provider besides the API key authentication.
Mutation:
type Mutation {
createPitch(gameID: ID!, pitchID: Int!, pitchEvent: PitchInput!): Pitch
#aws_api_key
predictPitch(userID: String!, gamePitchID: String!, prediction: PredictionInput): Prediction
#aws_cognito_user_pools
}
Invoking predictPitch mutation on graphql playground:
mutation PredictPitch($prediction:PredictionInput) {
predictPitch(userID: "12345", gamePitchID: "29fb2xx-xxxxx-xxxxx-1",
prediction: $prediction ) {
gameID
gamePitchID
}
}
query variables:
{
"prediction": {
"gameID": "29",
"hitterGuess": "Miss",
"pitcherGuess": "Fastball"
}
}
Headers:
{
"X-API-KEY": "da2-o6fs2lq47vbehexxxxxxxx",
"Authorization": "Bearer xxxx-the-pretty-long-jwt-token-from-cognito login"
}
I have tried Authorization header alone and in conjunction with x-api-key.
Nothing worked so far. I am pretty sure I am missing a very tiny bit.
{
"error": {
"errors": [
{
"errorType": "UnauthorizedException",
"message": "Valid authorization header not provided."
}
]
}
}
NOTE: The JWT token AccessToken is generated via aws-cli
aws cognito-idp admin-initiate-auth.
I had to add #aws_cognito_user_pools on type Prediction along with my mutation.
type Prediction #aws_cognito_user_pools {
gameID
gamePitchID
}
Also, from Cognito I had to use idToken like so:
{
"Authorization": "xxxxxxxxxx"
}
Do notice the Bearer is missing.

AWS - Configuring Lambda Destinations with SNS

I'm trying to configure an AWS Lambda function to pipe its output into an SNS notification, but it doesn't seem to work. The function executes successfully in the Lambda console and I can see the output is correct, but SNS never seems to be getting notified or publishing anything. I'm working with Terraform to stand up my infra, here is the Terraform code I'm using, maybe someone can help me out:
resource "aws_lambda_function" "lambda_apigateway_to_sns_function" {
filename = "../node/lambda.zip"
function_name = "LambdaPublishToSns"
handler = "index.snsHandler"
role = aws_iam_role.lambda_apigateway_to_sns_execution_role.arn
runtime = "nodejs12.x"
}
resource "aws_iam_role" "lambda_apigateway_to_sns_execution_role" {
assume_role_policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow"
}
]
}
POLICY
}
resource "aws_iam_role_policy_attachment" "apigateway_to_sns_sns_full_access" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSNSFullAccess"
role = aws_iam_role.lambda_apigateway_to_sns_execution_role.name
}
resource "aws_lambda_function_event_invoke_config" "example" {
function_name = aws_lambda_function.lambda_apigateway_to_sns_function.arn
destination_config {
on_success {
destination = aws_sns_topic.sns_topic.arn
}
on_failure {
destination = aws_sns_topic.sns_topic.arn
}
}
}
And here's my Lambda function code (in NodeJS):
exports.snsHandler = (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
callback(null, {
statusCode: 200,
body: event.body + " apigateway"
);
}
(the function is supposed to take input from API Gateway, and whatever is in the body of the API Gateway request, just append "apigateway" to the end of it and pass the message on; I've tested the integration with API Gateway and that integration works perfectly)
Thanks!

How do I supply different conditions based on a parameter in an IAM Role CloudFormation Template

I'm writing a CloudFormation template for an IAM role that I will assume through STS. I need to add a condition where a key equals a value, where both the key and value depends on a "Stage" parameter. The value I've been able to programmatically change depending on the parameter, but all my attempts to change the key based on Stage have failed.
I've tried both using a map and !FindInMap to get the correct key, as well as tried to construct a condition with both cases using !If.
In the first case...
Mappings:
Constants:
beta:
Id: "beta.example.com"
Endpoint: "beta-link.example.com/api/oauth2/v2:aud"
prod:
Id: "example.com"
Endpoint: "link.example.com/api/oauth2/v2:aud"
AssumeRolePolicyDocument:
Statement:
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
StringEquals:
!FindInMap [Constants, !Ref Stage, Endpoint]:
- !FindInMap [Constants, !Ref Stage, Id]
... I got an error: map keys must be strings; received a map instead
In the second case...
AssumeRolePolicyDocument:
Statement:
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
!If
- !Equals [!Ref Stage, prod]
- StringEquals:
"link.example.com/api/oauth2/v2:aud": "example.com"
- StringEquals:
"beta-link.example.com/api/oauth2/v2:aud": "beta.example.com"
...I got another error: Template format error: Conditions can only be boolean operations on parameters and other conditions
In short, how can I specify a condition where both key and value depend on a parameter?
I have struggled around 8 hours continuously and finally found an answer to this problem.
Just for the viewers I would like to summarise the problem:
Problem Statement : As a infrastructure engineer, I want to write a cloudformation resource for AWS::IAM::Role which defines a AssumeRolePolicyDocument with a Condition clause where the key needs to be parameterised. Also another question is can we use intrinsic functions with Condition keys?
Answer: Yes, it is possible using an out of the box style. AssumeRolePolicyDocument is a String Type as per the AWS Cloudformation documentation for 'AWS::IAM::Role'. So instead of using YAML style data to the AssumeRolePolicyDocument property, just pass a raw JSON formatted Assumerole policy using Fn::Sub and use variables to replace the key without any issues or warnings. Below is an example that you can use. Its a complex use case that i was trying to solve but it shows how you can replace keys under a IAM Condition clause using !SUB,!Select etc.
ExampleAppRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Sub ${Environment}-ExampleAppRole
AssumeRolePolicyDocument:
Fn::Sub:
- |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${AWS::AccountId}:oidc-provider/oidc.eks.${AWS::Region}.amazonaws.com/id/${id}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${clusterid}": "${serviceaccount}"
}
}
}
]
}
-
clusterid: !Join ["",[!Sub "oidc.eks.${AWS::Region}.amazonaws.com/id/",!Select [1, !Split ["//", !Select [0, !Split [".", !GetAtt Cluster.Endpoint]]]],":sub"]]
id: !Select [1, !Split ["//", !Select [0, !Split [".", !GetAtt Cluster.Endpoint]]]]
Region: !Sub ${AWS::Region}
serviceaccount: system:serviceaccount:default:awsiamroleexample
Path: /
ManagedPolicyArns:
- !Ref SnsPublishAccessPolicy
Let me know your thoughts in the comments section.
I don't think using intrinsic functions within an IAM policy condition element is allowed, I haven't seen any example of that. Condition element only takes a set of predefined keys. I think you can try the following template, it's a bit verbose but it should work. Template powered by cloudkast
{
"AWSTemplateFormatVersion":"2010-09-09",
"Description":"Template created by CloudKast",
"Parameters":{
},
"Mappings":{
},
"Conditions":{
"Prod":{
"Fn::Equals":[
{
"Ref":"Stage"
},
"prod"
]
},
"Dev":{
"Fn::Equals":[
{
"Ref":"Stage"
},
"dev"
]
}
},
"Resources":{
"ProdRole":{
"Properties":{
"AssumeRolePolicyDocument":{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"Stmt1567153169873",
"Action":[
"sts:AssumeRoleWithWebIdentity"
],
"Effect":"Allow",
"Resource":"arn:aws:iam::namespace::relativeid"
}
]
},
"RoleName":"ProdRole"
},
"Type":"AWS::IAM::Role",
"Condition":"Prod"
},
"DevRole":{
"Properties":{
"AssumeRolePolicyDocument":{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"Stmt1567153169873",
"Action":[
"sts:AssumeRoleWithWebIdentity"
],
"Effect":"Allow",
"Resource":"arn:aws:iam::namespace::relativeid"
}
]
},
"RoleName":"DevRole"
},
"Type":"AWS::IAM::Role"
}
},
"Outputs":{
}
}

get row from AWS dynamoDB table with cognito authentication

I have AWS dynamo DB table where I store information for AWS Cognito users. I created the table to be private so that only the owner of a row in the table can read/write the data (based on cognito authentication). I need to get the data for the user through a lambda function. I created the IAM role for the function in this way:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:UpdateItem"
],
"Resource": [
"arn:aws:dynamodb:XX-XXXX-X:XXXXXXX:table/tablename"
],
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": [
"${cognito-identity.amazonaws.com:sub}"
]
}
}
}
]
}
In the lamba function (node.js) I need to get the information stored from the user so I call:
let ddb = new AWS.DynamoDB({ apiVersion: 'latest' });
var params = {
TableName: tablename,
Key: { 'id': {S: event.queryStringParameters.user_id}
}
};
ddb.getItem(params, function(err, data) {
if (err) {
console.log("Error", err);
}
else {
console.log("Success", data);
}
});
I get the error:
Error { AccessDeniedException: User: arn:aws:sts::xxxxxx:assumed-role/lambda_dynamo/getPmtDetails is not authorized to perform: dynamodb:GetItem on resource
How can I call getItem with the cognito id in order to retrive the row that belong to the user?

Resources