From f76b1d151f8f9b451ad1660c73865b0cc6a3b873 Mon Sep 17 00:00:00 2001 From: Stefan Pandzic <stefan.pandzic> Date: Wed, 18 Dec 2024 14:21:52 +0100 Subject: [PATCH] added contacts separation between users --- backendAWS/bin/backend_aws.ts | 6 +- backendAWS/functions/create-contact/index.ts | 4 ++ backendAWS/functions/delete-contact/index.ts | 25 +++++++- backendAWS/functions/get-contact/index.ts | 27 +++++++- backendAWS/functions/update-contact/index.ts | 33 ++++++++-- backendAWS/lib/authentication_stack.ts | 66 ++++++++++++++++++++ backendAWS/lib/lambda_stack.ts | 2 +- backendAWS/lib/storage_stack.ts | 12 +++- 8 files changed, 157 insertions(+), 18 deletions(-) diff --git a/backendAWS/bin/backend_aws.ts b/backendAWS/bin/backend_aws.ts index bf2d2b0..8fa1412 100644 --- a/backendAWS/bin/backend_aws.ts +++ b/backendAWS/bin/backend_aws.ts @@ -3,7 +3,7 @@ import { AuthenticationStack } from '../lib/authentication_stack'; import { StorageStack } from '../lib/storage_stack'; import { LambdaStack } from '../lib/lambda_stack'; import { ApiGatewayStack } from '../lib/apigateway_stack'; -import { FrontendStack } from '../lib/frontend_stack'; +//import { FrontendStack } from '../lib/frontend_stack'; const app = new cdk.App(); @@ -42,6 +42,6 @@ new ApiGatewayStack(app, 'ContactApp-ApiGatewayStack', { env, }); -new FrontendStack(app, 'ContactApp-FrontendStack', { +/* new FrontendStack(app, 'ContactApp-FrontendStack', { env, -}); +}); */ diff --git a/backendAWS/functions/create-contact/index.ts b/backendAWS/functions/create-contact/index.ts index bbea234..82f0f57 100644 --- a/backendAWS/functions/create-contact/index.ts +++ b/backendAWS/functions/create-contact/index.ts @@ -9,6 +9,9 @@ const bucketName = process.env.BUCKET_NAME; const cloudfrontDomain = process.env.CLOUDFRONT_DOMAIN; export const handler = async (event: any) => { + // Extract Cognito Identity ID (user's ID from Identity Pool) + const userId = event.requestContext.authorizer.claims.sub; // Using API Gateway Cognito authorizer + const body = JSON.parse(event.body); if (!body.image || !body.name || !body.phone || !body.email) { @@ -37,6 +40,7 @@ export const handler = async (event: any) => { // Build DynamoDB contact object const contact = { id: contactId, + userId, // Store the Cognito Identity ID to link the contact to the user name: body.name, phone: body.phone, email: body.email, diff --git a/backendAWS/functions/delete-contact/index.ts b/backendAWS/functions/delete-contact/index.ts index a2d84dd..80a65cf 100644 --- a/backendAWS/functions/delete-contact/index.ts +++ b/backendAWS/functions/delete-contact/index.ts @@ -13,12 +13,21 @@ export const handler = async (event: any) => { return withCors(400, { error: 'Missing required contact ID' }); } + // Extract userId from the claims + const userId = event.requestContext.authorizer.claims.sub; + if (!userId) { + return withCors(403, { error: 'User not authenticated' }); + } + try { - // Fetch the contact to get the image URL + // Fetch the contact to validate ownership const result = await dynamoDb .get({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, + id: contactId, + }, }) .promise(); @@ -28,6 +37,13 @@ export const handler = async (event: any) => { return withCors(404, { error: 'Contact not found' }); } + // Ensure the authenticated user owns the contact + if (contact.userId !== userId) { + return withCors(403, { + error: 'You do not have permission to delete this contact', + }); + } + // Extract the S3 key from the image URL const imageUrl = contact.imageUrl; const url = new URL(imageUrl); @@ -47,7 +63,10 @@ export const handler = async (event: any) => { await dynamoDb .delete({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, + id: contactId, + }, }) .promise(); diff --git a/backendAWS/functions/get-contact/index.ts b/backendAWS/functions/get-contact/index.ts index e404a85..236cc98 100644 --- a/backendAWS/functions/get-contact/index.ts +++ b/backendAWS/functions/get-contact/index.ts @@ -5,6 +5,12 @@ const dynamoDb = new DynamoDB.DocumentClient(); const tableName = process.env.TABLE_NAME; export const handler = async (event: any) => { + // Extract userId from the claims + const userId = event.requestContext.authorizer.claims.sub; + if (!userId) { + return withCors(403, { error: 'User not authenticated' }); + } + const contactId = event.pathParameters?.contactId; try { @@ -13,7 +19,10 @@ export const handler = async (event: any) => { const result = await dynamoDb .get({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, // Add the partition key + id: contactId, // Add the sort key + }, }) .promise(); @@ -21,12 +30,24 @@ export const handler = async (event: any) => { return withCors(404, { error: 'Contact not found' }); } + // Check ownership of the contact + if (result.Item.userId !== userId) { + return withCors(403, { + error: 'You do not have permission to view this contact', + }); + } + return withCors(200, result.Item); } else { - // Fetch all contacts + // Fetch all contacts for the authenticated user const result = await dynamoDb - .scan({ + .query({ TableName: tableName!, + IndexName: 'userId-index', // Ensure you have a GSI on the `userId` attribute + KeyConditionExpression: 'userId = :userId', + ExpressionAttributeValues: { + ':userId': userId, + }, }) .promise(); diff --git a/backendAWS/functions/update-contact/index.ts b/backendAWS/functions/update-contact/index.ts index f117d2a..75653c4 100644 --- a/backendAWS/functions/update-contact/index.ts +++ b/backendAWS/functions/update-contact/index.ts @@ -10,7 +10,11 @@ const cloudfrontDomain = process.env.CLOUDFRONT_DOMAIN; export const handler = async (event: any) => { const body = JSON.parse(event.body); - console.log('body of Update Lambda', body); + // Extract userId from the claims + const userId = event.requestContext.authorizer.claims.sub; + if (!userId) { + return withCors(403, { error: 'User not authenticated' }); + } if (!body.name || !body.phone || !body.email) { return withCors(400, { error: 'Missing required fields' }); @@ -18,20 +22,37 @@ export const handler = async (event: any) => { const contactId = event.pathParameters?.contactId; + if (!contactId) { + return withCors(400, { error: 'Missing required contact ID' }); + } + try { + // Fetch the contact to validate ownership const contactResult = await dynamoDb .get({ TableName: tableName!, - Key: { id: contactId }, + Key: { + userId: userId, + id: contactId, + }, }) .promise(); - if (!contactResult.Item) { + const contact = contactResult.Item; + + if (!contact) { return withCors(404, { error: 'Contact not found' }); } + // Ensure the authenticated user owns the contact + if (contact.userId !== userId) { + return withCors(403, { + error: 'You do not have permission to update this contact', + }); + } + // Update the contact's image if a new image is provided - let imageUrl = contactResult.Item.imageUrl; + let imageUrl = contact.imageUrl; if (body.image) { const base64Image = body.image.split(',')[1]; const imageBuffer = Buffer.from(base64Image, 'base64'); @@ -52,11 +73,11 @@ export const handler = async (event: any) => { // Update the contact in DynamoDB const updatedContact = { - ...contactResult.Item, + ...contact, name: body.name, phone: body.phone, email: body.email, - isFavorite: body.isFavorite || contactResult.Item.isFavorite, + isFavorite: body.isFavorite || contact.isFavorite, imageUrl, }; diff --git a/backendAWS/lib/authentication_stack.ts b/backendAWS/lib/authentication_stack.ts index 82d2fb7..6bd341b 100644 --- a/backendAWS/lib/authentication_stack.ts +++ b/backendAWS/lib/authentication_stack.ts @@ -1,10 +1,13 @@ import * as cdk from 'aws-cdk-lib'; import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as iam from 'aws-cdk-lib/aws-iam'; //CognitoService export class AuthenticationStack extends cdk.Stack { public readonly userPool: cognito.UserPool; public readonly userPoolClient: cognito.UserPoolClient; + public readonly identityPool: cdk.aws_cognito.CfnIdentityPool; + public readonly authenticatedRole: iam.Role; constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); @@ -35,6 +38,69 @@ export class AuthenticationStack extends cdk.Stack { value: this.userPoolClient.userPoolClientId, }); + // Identity Pool + this.identityPool = new cognito.CfnIdentityPool( + this, + 'ContactsAppIdentityPool', + { + allowUnauthenticatedIdentities: false, + cognitoIdentityProviders: [ + { + clientId: this.userPoolClient.userPoolClientId, + providerName: this.userPool.userPoolProviderName, + }, + ], + } + ); + + new cdk.CfnOutput(this, 'ContactsAppIdentityPoolId', { + value: this.identityPool.ref, + }); + + // Authenticated Role with restricted DynamoDB access + this.authenticatedRole = new iam.Role( + this, + 'ContactsAppAuthenticatedRole', + { + assumedBy: new iam.FederatedPrincipal( + 'cognito-identity.amazonaws.com', + { + StringEquals: { + 'cognito-identity.amazonaws.com:aud': this.identityPool.ref, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + 'sts:AssumeRoleWithWebIdentity' + ), + } + ); + + this.authenticatedRole.addToPolicy( + new iam.PolicyStatement({ + actions: ['dynamodb:Query', 'dynamodb:GetItem', 'dynamodb:PutItem'], + resources: ['arn:aws:dynamodb:*:*:table/Contacts'], + conditions: { + 'ForAllValues:StringEquals': { + 'dynamodb:LeadingKeys': ['${cognito-identity.amazonaws.com:sub}'], + }, + }, + }) + ); + + // Attach Authenticated Role to Identity Pool + new cognito.CfnIdentityPoolRoleAttachment( + this, + 'IdentityPoolRoleAttachment', + { + identityPoolId: this.identityPool.ref, + roles: { + authenticated: this.authenticatedRole.roleArn, + }, + } + ); + cdk.Tags.of(this.userPool).add('Service', 'Auth'); } } diff --git a/backendAWS/lib/lambda_stack.ts b/backendAWS/lib/lambda_stack.ts index 6bdb031..55fcd11 100644 --- a/backendAWS/lib/lambda_stack.ts +++ b/backendAWS/lib/lambda_stack.ts @@ -81,7 +81,7 @@ export class LambdaStack extends cdk.Stack { new PolicyStatement({ effect: Effect.ALLOW, resources: [contactsTable.tableArn], - actions: ['dynamodb:Scan', 'dynamodb:GetItem'], + actions: ['dynamodb:Scan', 'dynamodb:GetItem', 'dynamodb:Query'], }) ); diff --git a/backendAWS/lib/storage_stack.ts b/backendAWS/lib/storage_stack.ts index 8dbc003..7b0e8bb 100644 --- a/backendAWS/lib/storage_stack.ts +++ b/backendAWS/lib/storage_stack.ts @@ -13,12 +13,20 @@ export class StorageStack extends cdk.Stack { super(scope, id, props); //DynamoDB Contacts - this.contactsTable = new dynamodb.Table(this, 'Contacts-app_Table', { - partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + this.contactsTable = new dynamodb.Table(this, 'ContactsAppTable', { + partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'id', type: dynamodb.AttributeType.STRING }, // Optional for storing specific records per user tableName: 'Contacts', removalPolicy: cdk.RemovalPolicy.DESTROY, }); + // Add a GSI for querying by `userId` + this.contactsTable.addGlobalSecondaryIndex({ + indexName: 'userId-index', // Name of the GSI + partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }, // GSI partition key + projectionType: dynamodb.ProjectionType.ALL, // Include all attributes in the GSI (can be optimized) + }); + cdk.Tags.of(this.contactsTable).add('Service', 'Database'); //Contacts Images S3 -- GitLab