diff --git a/.editorconfig b/.editorconfig index 0deb864..c4032ae 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,6 @@ indent_style = tab indent_size = 4 indent_style = space -[*.{json,sh,tf,tfvars,vue,yaml,yml}] +[*.{js,json,sh,tf,tfvars,vue,yaml,yml}] indent_size = 2 indent_style = space diff --git a/.gitignore b/.gitignore index a4d8edd..ff5c144 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .serverless # golang output binary directory +archive bin # golang vendor (dependencies) directory @@ -22,5 +23,3 @@ vendor .env.* .DS_Store - -cmd/scratch* diff --git a/Makefile b/Makefile index 455653d..fb27755 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ build: env GOOS=linux go build -ldflags="-s -w" -o bin/login_user cmd/login_user/main.go env GOOS=linux go build -ldflags="-s -w" -o bin/release cmd/release/main.go env GOOS=linux go build -ldflags="-s -w" -o bin/reset_user_password cmd/reset_user_password/main.go + env GOOS=linux go build -ldflags="-s -w" -o bin/verify_auth cmd/verify_auth/main.go + test: @printf "\n" @@ -24,8 +26,22 @@ test: ./cmd/login_user \ ./cmd/release \ ./cmd/reset_user_password \ + ./cmd/verify_auth \ + +compress: + @printf "\n" + zip archive/CreateRepo.zip bin/create_repo + zip archive/CreateUser.zip bin/create_user + zip archive/DeleteRepo.zip bin/delete_repo + zip archive/DeleteUser.zip bin/delete_user + zip archive/ListRepos.zip bin/list_repos + zip archive/ListUsers.zip bin/list_users + zip archive/LoginUser.zip bin/login_user + zip archive/Release.zip bin/release + zip archive/ResetUserPassword.zip bin/reset_user_password + zip archive/VerifyAuth.zip bin/verify_auth -deploy: build test +deploy: build test compress @printf "\n" serverless deploy --verbose --aws-profile ${AWS_DEFAULT_PROFILE} diff --git a/cmd/create_repo/event.json b/cmd/create_repo/event.json index 0c83515..f62cc4e 100644 --- a/cmd/create_repo/event.json +++ b/cmd/create_repo/event.json @@ -1,6 +1,18 @@ { - "repo_name": "pygithub-go-github-playground", - "repo_owner": "seanturner026", - "branch_head": "new", - "branch_base": "develop" + "resource": "/", + "path": "/create/repo", + "httpMethod": "POST", + "requestContext": { + "resourcePath": "/", + "httpMethod": "POST", + "path": "/create/repo" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{\"repo_name\": \"string\", \"repo_owner\": \"string\", \"branch_head\": \"string\", \"branch_base\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/create_repo/main.go b/cmd/create_repo/main.go index 04e1016..7a61b83 100644 --- a/cmd/create_repo/main.go +++ b/cmd/create_repo/main.go @@ -43,7 +43,7 @@ func generatePutItemInput(e createRepoEvent) (createRepoEvent, map[string]*dynam return e, itemInput, nil } -func (app *application) writeRepoToDB(e createRepoEvent, itemInput map[string]*dynamodb.AttributeValue) error { +func (app application) writeRepoToDB(e createRepoEvent, itemInput map[string]*dynamodb.AttributeValue) error { input := &dynamodb.PutItemInput{ ReturnConsumedCapacity: aws.String("TOTAL"), TableName: aws.String(app.config.TableName), @@ -62,7 +62,7 @@ func (app *application) writeRepoToDB(e createRepoEvent, itemInput map[string]*d return nil } -func (app *application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func (app application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := createRepoEvent{} diff --git a/cmd/create_user/event.json b/cmd/create_user/event.json index dfe156e..21a642c 100644 --- a/cmd/create_user/event.json +++ b/cmd/create_user/event.json @@ -1,3 +1,18 @@ { - "email_address": "string" + "resource": "/", + "path": "/create/user", + "httpMethod": "POST", + "requestContext": { + "resourcePath": "/", + "httpMethod": "POST", + "path": "/create/user" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{\"email_address\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/create_user/main.go b/cmd/create_user/main.go index 176b9da..35712f0 100644 --- a/cmd/create_user/main.go +++ b/cmd/create_user/main.go @@ -29,7 +29,7 @@ type configuration struct { idp cidpif.CognitoIdentityProviderAPI } -func (app *application) createUser(e createUserEvent) error { +func (app application) createUser(e createUserEvent) error { input := &cidp.AdminCreateUserInput{ UserPoolId: aws.String(app.config.UserPoolID), Username: aws.String(e.EmailAddress), @@ -55,7 +55,7 @@ func (app *application) createUser(e createUserEvent) error { return nil } -func (app *application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func (app application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := createUserEvent{} diff --git a/cmd/delete_repo/event.json b/cmd/delete_repo/event.json index 5733a1c..285be4c 100644 --- a/cmd/delete_repo/event.json +++ b/cmd/delete_repo/event.json @@ -1,3 +1,18 @@ { - "repo_name": "pygithub-go-github-playground" + "resource": "/", + "path": "/delete/repo", + "httpMethod": "POST", + "requestContext": { + "resourcePath": "/", + "httpMethod": "POST", + "path": "/delete/repo" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{\"repo_name\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/delete_repo/main.go b/cmd/delete_repo/main.go index e12c93e..00c4149 100644 --- a/cmd/delete_repo/main.go +++ b/cmd/delete_repo/main.go @@ -29,7 +29,7 @@ type configuration struct { db dynamodbiface.DynamoDBAPI } -func (app *application) deleteRepo(e deleteRepoEvent) error { +func (app application) deleteRepo(e deleteRepoEvent) error { input := &dynamodb.DeleteItemInput{ Key: map[string]*dynamodb.AttributeValue{ "pk": { @@ -57,7 +57,7 @@ func (app *application) deleteRepo(e deleteRepoEvent) error { return nil } -func (app application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func (app application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := deleteRepoEvent{} diff --git a/cmd/delete_user/event.json b/cmd/delete_user/event.json index dfe156e..b5a85d7 100644 --- a/cmd/delete_user/event.json +++ b/cmd/delete_user/event.json @@ -1,3 +1,18 @@ { - "email_address": "string" + "resource": "/", + "path": "/delete/user", + "httpMethod": "POST", + "requestContext": { + "resourcePath": "/", + "httpMethod": "POST", + "path": "/delete/user" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{\"email_address\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/delete_user/main.go b/cmd/delete_user/main.go index 5b2b36e..cd72c8f 100644 --- a/cmd/delete_user/main.go +++ b/cmd/delete_user/main.go @@ -29,7 +29,7 @@ type configuration struct { idp cidpif.CognitoIdentityProviderAPI } -func (app *application) deleteUser(e deleteUserEvent) error { +func (app application) deleteUser(e deleteUserEvent) error { input := &cidp.AdminDeleteUserInput{ UserPoolId: aws.String(os.Getenv("USER_POOL_ID")), Username: aws.String(e.EmailAddress), @@ -47,7 +47,7 @@ func (app *application) deleteUser(e deleteUserEvent) error { return nil } -func (app *application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func (app application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := deleteUserEvent{} diff --git a/cmd/list_repos/event.json b/cmd/list_repos/event.json index bd47f70..3db4089 100644 --- a/cmd/list_repos/event.json +++ b/cmd/list_repos/event.json @@ -1,3 +1,18 @@ { - "repo_owner": "seanturner026" + "resource": "/", + "path": "/list/repos", + "httpMethod": "GET", + "requestContext": { + "resourcePath": "/", + "httpMethod": "GET", + "path": "/list/repos" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{\"repo_owner\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/list_repos/main.go b/cmd/list_repos/main.go index 4bff659..4aecd01 100644 --- a/cmd/list_repos/main.go +++ b/cmd/list_repos/main.go @@ -39,7 +39,7 @@ type configuration struct { db dynamodbiface.DynamoDBAPI } -func (app *application) listRepos(e listReposEvent) (dynamodb.QueryOutput, error) { +func (app application) listRepos(e listReposEvent) (dynamodb.QueryOutput, error) { input := &dynamodb.QueryInput{ ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ ":type": { @@ -68,7 +68,7 @@ func (app *application) listRepos(e listReposEvent) (dynamodb.QueryOutput, error return *resp, err } -func (app *application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func (app application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := listReposEvent{} @@ -100,8 +100,7 @@ func (app *application) handler(event events.APIGatewayProxyRequest) (events.API var buf bytes.Buffer json.HTMLEscape(&buf, body) - log.Printf("[DEBUG] body %v", buf.String()) - resp := events.APIGatewayProxyResponse{ + resp := events.APIGatewayV2HTTPResponse{ StatusCode: statusCode, Headers: headers, Body: buf.String(), diff --git a/cmd/list_users/main.go b/cmd/list_users/main.go index b1eb050..f45daac 100644 --- a/cmd/list_users/main.go +++ b/cmd/list_users/main.go @@ -37,7 +37,7 @@ func (userNames *listUsersResponse) appendUserToResponse(user userName) { userNames.Users = append(userNames.Users, user) } -func (app *application) listUsers() (listUsersResponse, error) { +func (app application) listUsers() (listUsersResponse, error) { input := &cidp.ListUsersInput{ AttributesToGet: aws.StringSlice([]string{"email"}), Limit: aws.Int64(60), @@ -63,7 +63,7 @@ func (app *application) listUsers() (listUsersResponse, error) { return *userNames, nil } -func (app *application) handler() (events.APIGatewayProxyResponse, error) { +func (app application) handler() (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} userNames, err := app.listUsers() @@ -81,7 +81,7 @@ func (app *application) handler() (events.APIGatewayProxyResponse, error) { var buf bytes.Buffer json.HTMLEscape(&buf, body) - resp := events.APIGatewayProxyResponse{ + resp := events.APIGatewayV2HTTPResponse{ StatusCode: statusCode, Headers: headers, Body: buf.String(), diff --git a/cmd/login_user/event.json b/cmd/login_user/event.json index 3009210..d1d2526 100644 --- a/cmd/login_user/event.json +++ b/cmd/login_user/event.json @@ -1,4 +1,18 @@ { - "email_address": "string", - "password": "string" + "resource": "/", + "path": "/login/user", + "httpMethod": "POST", + "requestContext": { + "resourcePath": "/", + "httpMethod": "POST", + "path": "/login/user" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{ \"email_address\": \"string\",\"password\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/login_user/main.go b/cmd/login_user/main.go index 1545efb..fed9287 100644 --- a/cmd/login_user/main.go +++ b/cmd/login_user/main.go @@ -38,7 +38,7 @@ type loginUserResponse struct { UserID string `json:"user_id,omitempty"` } -func (app *application) getUserPoolClientSecret() (string, error) { +func (app application) getUserPoolClientSecret() (string, error) { input := &cidp.DescribeUserPoolClientInput{ UserPoolId: aws.String(app.config.UserPoolID), ClientId: aws.String(app.config.ClientPoolID), @@ -57,7 +57,7 @@ func (app *application) getUserPoolClientSecret() (string, error) { return *resp.UserPoolClient.ClientSecret, nil } -func (app *application) loginUser(e loginUserEvent, secretHash string) (loginUserResponse, error) { +func (app application) loginUser(e loginUserEvent, secretHash string) (loginUserResponse, error) { input := &cidp.InitiateAuthInput{ AuthFlow: aws.String("USER_PASSWORD_AUTH"), AuthParameters: map[string]*string{ @@ -80,21 +80,24 @@ func (app *application) loginUser(e loginUserEvent, secretHash string) (loginUse return loginUserResp, err } - if *resp.ChallengeName == "NEW_PASSWORD_REQUIRED" { - log.Printf("[INFO] New password required for %v", e.EmailAddress) - loginUserResp.NewPasswordRequired = true - loginUserResp.SessionID = *resp.Session - loginUserResp.UserID = *resp.ChallengeParameters["USER_ID_FOR_SRP"] - return loginUserResp, nil + if resp.ChallengeName != nil { + if *resp.ChallengeName == "NEW_PASSWORD_REQUIRED" { + log.Printf("[INFO] New password required for %v", e.EmailAddress) + loginUserResp.NewPasswordRequired = true + loginUserResp.SessionID = *resp.Session + loginUserResp.UserID = *resp.ChallengeParameters["USER_ID_FOR_SRP"] + return loginUserResp, nil + } } log.Printf("[INFO] Authenticated user %v successfully", e.EmailAddress) + loginUserResp.AccessToken = *resp.AuthenticationResult.AccessToken loginUserResp.NewPasswordRequired = false return loginUserResp, nil } -func (app *application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func (app application) handler(event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := loginUserEvent{} diff --git a/cmd/login_user/main_test.go b/cmd/login_user/main_test.go index 85ca0fd..b6ba83d 100644 --- a/cmd/login_user/main_test.go +++ b/cmd/login_user/main_test.go @@ -7,50 +7,37 @@ package main // cidpif "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface" // ) -// type mockDescribeUserPoolClient struct { -// cidpif.CognitoIdentityProviderAPI -// Response *cidp.DescribeUserPoolClientOutput -// Error error -// } +// // import ( +// // "testing" -// // type mockInitiateAuth struct { +// // cidp "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" +// // cidpif "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface" +// // ) + +// // type mockDescribeUserPoolClient struct { // // cidpif.CognitoIdentityProviderAPI -// // Response *cidp.InitiateAuthOutput +// // Response *cidp.DescribeUserPoolClientOutput // // Error error // // } -// func (m mockDescribeUserPoolClient) DescribeUserPoolClient(*cidp.DescribeUserPoolClientInput) (*cidp.DescribeUserPoolClientOutput, error) { -// return m.Response, nil +// type mockInitiateAuth struct { +// cidpif.CognitoIdentityProviderAPI +// Response *cidp.InitiateAuthOutput +// Error error // } -// // func (m mockInitiateAuth) InitiateAuth(*cidp.InitiateAuthInput) (*cidp.InitiateAuthOutput, error) { +// // func (m mockDescribeUserPoolClient) DescribeUserPoolClient(*cidp.DescribeUserPoolClientInput) (*cidp.DescribeUserPoolClientOutput, error) { // // return m.Response, nil // // } -// func TestGetUserPoolClientSecret(t *testing.T) { -// t.Run("Successfully obtained client pool secret", func(t *testing.T) { -// idpMock := mockDescribeUserPoolClient{ -// Response: &cidp.DescribeUserPoolClientOutput{}, -// Error: nil, -// } - -// app := application{config: configuration{ -// ClientPoolID: "test", -// UserPoolID: "test", -// idp: idpMock, -// }} - -// _, err := app.getUserPoolClientSecret() -// if err != nil { -// t.Fatal("App secret should have been obtained") -// } -// }) +// func (m mockInitiateAuth) InitiateAuth(*cidp.InitiateAuthInput) (*cidp.InitiateAuthOutput, error) { +// return m.Response, nil // } -// // func TestLoginUser(t *testing.T) { -// // t.Run("Successfully logged in user", func(t *testing.T) { -// // idpMock := mockInitiateAuth{ -// // Response: &cidp.InitiateAuthOutput{}, +// // func TestGetUserPoolClientSecret(t *testing.T) { +// // t.Run("Successfully obtained client pool secret", func(t *testing.T) { +// // idpMock := mockDescribeUserPoolClient{ +// // Response: &cidp.DescribeUserPoolClientOutput{}, // // Error: nil, // // } @@ -60,14 +47,34 @@ package main // // idp: idpMock, // // }} -// // event := loginUserEvent{ -// // EmailAddress: "user@example.com", -// // Password: "example123$%^", -// // } - -// // _, err := app.loginUser(event, "secretHashExample") +// // _, err := app.getUserPoolClientSecret() // // if err != nil { -// // t.Fatal("User should have been logged in") +// // t.Fatal("App secret should have been obtained") // // } // // }) // // } + +// func TestLoginUser(t *testing.T) { +// t.Run("Successfully logged in user", func(t *testing.T) { +// idpMock := mockInitiateAuth{ +// Response: &cidp.InitiateAuthOutput{}, +// Error: nil, +// } + +// app := application{config: configuration{ +// ClientPoolID: "test", +// UserPoolID: "test", +// idp: idpMock, +// }} + +// event := loginUserEvent{ +// EmailAddress: "user@example.com", +// Password: "example123$%^", +// } + +// _, err := app.loginUser(event, "secretHashExample") +// if err != nil { +// t.Fatal("User should have been logged in") +// } +// }) +// } diff --git a/cmd/release/event.json b/cmd/release/event.json index ebb727b..cee09ef 100644 --- a/cmd/release/event.json +++ b/cmd/release/event.json @@ -1,8 +1,18 @@ { - "github_owner": "seanturner026", - "github_repo": "pygithub-go-github-playground", - "branch_head": "new", - "branch_base": "develop", - "release_version": "v0.3", - "release_body": "Automated release created by lambda" + "resource": "/", + "path": "/release", + "httpMethod": "POST", + "requestContext": { + "resourcePath": "/", + "httpMethod": "POST", + "path": "/release" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{\"github_owner\": \"string\", \"github_repo\": \"string\", \"branch_head\": \"string\", \"branch_base\": \"string\", \"release_version\": \"string\", \"release_body\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/release/main.go b/cmd/release/main.go index bb1ca2e..274df84 100644 --- a/cmd/release/main.go +++ b/cmd/release/main.go @@ -107,7 +107,7 @@ func createRelease(githubCtx context.Context, c *github.Client, e releaseEvent) } // handler executes the release and notification workflow -func handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func handler(event events.APIGatewayProxyRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := releaseEvent{} diff --git a/cmd/reset_user_password/event.json b/cmd/reset_user_password/event.json index d94c2e8..29fdaa1 100644 --- a/cmd/reset_user_password/event.json +++ b/cmd/reset_user_password/event.json @@ -1,5 +1,18 @@ { - "email_address": "string", - "new_password": "string", - "session_id": "string" + "resource": "/", + "path": "/reset/user/password", + "httpMethod": "POST", + "requestContext": { + "resourcePath": "/", + "httpMethod": "POST", + "path": "/reset/user/password" + }, + "headers": {}, + "multiValueHeaders": {}, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": null, + "stageVariables": null, + "body": "{\"email_address\": \"string\",\"new_password\": \"string\",\"session_id\": \"string\"}", + "isBase64Encoded": false } diff --git a/cmd/reset_user_password/main.go b/cmd/reset_user_password/main.go index 41481e3..a2d5bed 100644 --- a/cmd/reset_user_password/main.go +++ b/cmd/reset_user_password/main.go @@ -32,7 +32,7 @@ type configuration struct { idp cidpif.CognitoIdentityProviderAPI } -func (app *application) getUserPoolClientSecret() (string, error) { +func (app application) getUserPoolClientSecret() (string, error) { input := &cidp.DescribeUserPoolClientInput{ UserPoolId: aws.String(app.config.UserPoolID), ClientId: aws.String(app.config.ClientPoolID), @@ -51,7 +51,7 @@ func (app *application) getUserPoolClientSecret() (string, error) { return *resp.UserPoolClient.ClientSecret, nil } -func (app *application) resetPassword(e resetPasswordEvent, secretHash string) (string, error) { +func (app application) resetPassword(e resetPasswordEvent, secretHash string) (string, error) { input := &cidp.AdminRespondToAuthChallengeInput{ ChallengeName: aws.String("NEW_PASSWORD_REQUIRED"), ChallengeResponses: map[string]*string{ @@ -77,7 +77,7 @@ func (app *application) resetPassword(e resetPasswordEvent, secretHash string) ( return *resp.AuthenticationResult.AccessToken, nil } -func (app *application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { +func (app application) handler(event events.APIGatewayProxyRequest) (events.APIGatewayV2HTTPResponse, error) { headers := map[string]string{"Content-Type": "application/json"} e := resetPasswordEvent{} diff --git a/cmd/verify_auth/main.go b/cmd/verify_auth/main.go new file mode 100644 index 0000000..9976c76 --- /dev/null +++ b/cmd/verify_auth/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + util "github.com/seanturner026/serverless-release-dashboard/internal/util" +) + +func handler() (events.APIGatewayV2HTTPResponse, error) { + headers := map[string]string{"Content-Type": "application/json"} + resp := util.GenerateResponseBody("Authorized", 200, nil, headers) + return resp, nil +} + +func main() { + lambda.Start(handler) +} diff --git a/cmd/verify_auth/main_test.go b/cmd/verify_auth/main_test.go new file mode 100644 index 0000000..47616bf --- /dev/null +++ b/cmd/verify_auth/main_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" +) + +func TestHandler(t *testing.T) { + t.Run("Sucessfully authorized user", func(t *testing.T) { + expectedBody, err := json.Marshal(map[string]string{"message": "Authorized"}) + if err != nil { + t.Fatal("json marshal error") + } + + var buf bytes.Buffer + json.HTMLEscape(&buf, expectedBody) + + resp, err := handler() + fmt.Println(resp.Body) + if err != nil || resp.Body != buf.String() { + t.Fatal("User should have been authorized") + } + }) +} diff --git a/deployments/cloudformation/acm.yaml b/deployments/cloudformation/acm.yaml new file mode 100644 index 0000000..5262feb --- /dev/null +++ b/deployments/cloudformation/acm.yaml @@ -0,0 +1,10 @@ +Resources: + ACMCertificate: + Type: AWS::CertificateManager::Certificate + Properties: + CertificateTransparencyLoggingPreference: DISABLED + DomainName: ${self:custom.DOMAIN} + ValidationMethod: DNS + Tags: + - Key: Name + Value: ${self:service}-${self:provider.stage} diff --git a/deployments/cloudformation/cloudfront.yaml b/deployments/cloudformation/cloudfront.yaml new file mode 100644 index 0000000..c1b0b5e --- /dev/null +++ b/deployments/cloudformation/cloudfront.yaml @@ -0,0 +1,46 @@ +Resources: + CloudfrontDistribution: + Type: "AWS::CloudFront::Distribution" + Properties: + DistributionConfig: + Aliases: + - ${self:custom.DOMAIN} + Origins: + - Id: !Sub S3-${S3StaticSite.DomainName} + DomainName: !GetAtt S3StaticSite.DomainName + OriginCustomHeaders: + - HeaderName: X-Frame-Options + HeaderValue: SAMEORIGIN + - HeaderName: X-Content-Type-Options + HeaderValue: nosniff + S3OriginConfig: + OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudfrontOriginAccessIdentity} + CustomErrorResponses: + - ErrorCode: 404 + ResponseCode: 200 + ResponsePagePath: /index.html + DefaultCacheBehavior: + ForwardedValues: + Cookies: + Forward: none + QueryString: "false" + TargetOriginId: !Sub S3-${S3StaticSite.DomainName} + ViewerProtocolPolicy: redirect-to-https + DefaultRootObject: index.html + Enabled: "true" + HttpVersion: http2 + PriceClass: PriceClass_100 + ViewerCertificate: + AcmCertificateArn: !Ref ACMCertificate + SslSupportMethod: sni-only + MinimumProtocolVersion: TLSv1.2_2019 + WebACLId: !GetAtt WAFWebACL.Arn + Tags: + - Key: Name + Value: ${self:service}-${self:provider.stage} + + CloudfrontOriginAccessIdentity: + Type: AWS::CloudFront::CloudFrontOriginAccessIdentity + Properties: + CloudFrontOriginAccessIdentityConfig: + Comment: Restrict Access to S3 bucket ${self:service}-${self:provider.stage}-${self:custom.ACCOUNT_ID} diff --git a/deployments/cloudformation/s3.yaml b/deployments/cloudformation/s3.yaml index 599735e..dea694f 100644 --- a/deployments/cloudformation/s3.yaml +++ b/deployments/cloudformation/s3.yaml @@ -1,23 +1,23 @@ -# Resources: -# StaticSite: -# Type: AWS::S3::Bucket -# Properties: -# AccessControl: PublicRead -# BucketName: ${self:service}-${self:provider.stage} -# WebsiteConfiguration: -# IndexDocument: index.html +Resources: + S3StaticSite: + Type: AWS::S3::Bucket + Properties: + AccessControl: PublicRead + BucketName: ${self:service}-${self:provider.stage}-${self:custom.ACCOUNT_ID} + WebsiteConfiguration: + IndexDocument: index.html -# StaticSiteS3BucketPolicy: -# Type: AWS::S3::BucketPolicy -# Properties: -# Bucket: -# Ref: StaticSite -# PolicyDocument: -# Statement: -# - Sid: PublicReadGetObject -# Effect: Allow -# Principal: "*" -# Action: -# - s3:GetObject -# Resource: -# !Join: ["", ["arn:aws:s3:::", { "Ref": "StaticSite" }, "/*"]] + S3StaticSiteBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: + Ref: S3StaticSite + PolicyDocument: + Statement: + - Sid: PublicReadGetObject + Effect: Allow + Principal: + CanonicalUser: !GetAtt CloudfrontOriginAccessIdentity.S3CanonicalUserId + Action: + - s3:GetObject + Resource: !Sub arn:aws:s3:::${S3StaticSite}/* diff --git a/deployments/cloudformation/waf.yaml b/deployments/cloudformation/waf.yaml new file mode 100644 index 0000000..646ca70 --- /dev/null +++ b/deployments/cloudformation/waf.yaml @@ -0,0 +1,35 @@ +Resources: + WAFWebACL: + Type: AWS::WAFv2::WebACL + Properties: + Name: ServerlessReleaseDashboardWAF + Scope: CLOUDFRONT + Description: WAF to prevent all access except for admin IP + DefaultAction: + Block: {} + Rules: + - Name: IPSetWAFRule + Priority: 0 + Action: + Allow: {} + Statement: + IPSetReferenceStatement: + Arn: !GetAtt WAFIPSet.Arn + VisibilityConfig: + SampledRequestsEnabled: false + CloudWatchMetricsEnabled: false + MetricName: IPSetWAFRule + VisibilityConfig: + SampledRequestsEnabled: false + CloudWatchMetricsEnabled: false + MetricName: DefaultAction + + WAFIPSet: + Type: AWS::WAFv2::IPSet + Properties: + Name: ServerlessReleaseDashboardAdminIP + Scope: CLOUDFRONT + Addresses: + - ${self:custom.ADMIN_IP_ADDRESS} + Description: Only allow access from my IP + IPAddressVersion: IPV4 diff --git a/deployments/functions/create_repo.yaml b/deployments/functions/create_repo.yaml index 1d67797..b096270 100644 --- a/deployments/functions/create_repo.yaml +++ b/deployments/functions/create_repo.yaml @@ -1,5 +1,7 @@ CreateRepo: handler: bin/create_repo + package: + artifact: archive/CreateRepo.zip events: - httpApi: path: /create/repo diff --git a/deployments/functions/create_user.yaml b/deployments/functions/create_user.yaml index 3f6cfbe..d7aaf11 100644 --- a/deployments/functions/create_user.yaml +++ b/deployments/functions/create_user.yaml @@ -1,5 +1,7 @@ CreateUser: handler: bin/create_user + package: + artifact: archive/CreateUser.zip events: - httpApi: path: /create/user diff --git a/deployments/functions/delete_repo.yaml b/deployments/functions/delete_repo.yaml index 554ae67..97a9a5f 100644 --- a/deployments/functions/delete_repo.yaml +++ b/deployments/functions/delete_repo.yaml @@ -1,5 +1,7 @@ DeleteRepo: handler: bin/delete_repo + package: + artifact: archive/DeleteRepo.zip events: - httpApi: path: /delete/repo diff --git a/deployments/functions/delete_user.yaml b/deployments/functions/delete_user.yaml index 0293f1c..4386964 100644 --- a/deployments/functions/delete_user.yaml +++ b/deployments/functions/delete_user.yaml @@ -1,5 +1,7 @@ DeleteUser: handler: bin/delete_user + package: + artifact: archive/DeleteUser.zip events: - httpApi: path: /delete/user diff --git a/deployments/functions/list_repos.yaml b/deployments/functions/list_repos.yaml index 071596c..be2be9e 100644 --- a/deployments/functions/list_repos.yaml +++ b/deployments/functions/list_repos.yaml @@ -1,5 +1,7 @@ ListRepos: handler: bin/list_repos + package: + artifact: archive/ListRepos.zip events: - httpApi: path: /list/repos diff --git a/deployments/functions/list_users.yaml b/deployments/functions/list_users.yaml index dee6c1f..4d7df1e 100644 --- a/deployments/functions/list_users.yaml +++ b/deployments/functions/list_users.yaml @@ -1,5 +1,7 @@ ListUsers: handler: bin/list_users + package: + artifact: archive/ListUsers.zip events: - httpApi: method: GET diff --git a/deployments/functions/login_user.yaml b/deployments/functions/login_user.yaml index 2a1e6c6..1ea6342 100644 --- a/deployments/functions/login_user.yaml +++ b/deployments/functions/login_user.yaml @@ -1,10 +1,11 @@ LoginUser: handler: bin/login_user + package: + artifact: archive/LoginUser.zip events: - httpApi: path: /login/user method: post - authorizer: cognitoAuthorizer environment: CLIENT_POOL_ID: !Ref CognitoUserPoolClient USER_POOL_ID: !Ref CognitoUserPool diff --git a/deployments/functions/release.yaml b/deployments/functions/release.yaml index 3b613a8..c6fd148 100644 --- a/deployments/functions/release.yaml +++ b/deployments/functions/release.yaml @@ -1,5 +1,7 @@ Release: handler: bin/release + package: + artifact: archive/Release.zip events: - httpApi: path: /release diff --git a/deployments/functions/reset_user_password.yaml b/deployments/functions/reset_user_password.yaml index df1d49d..86e6ef8 100644 --- a/deployments/functions/reset_user_password.yaml +++ b/deployments/functions/reset_user_password.yaml @@ -1,10 +1,11 @@ ResetUserPassword: handler: bin/reset_user_password + package: + artifact: archive/ResetUserPassword.zip events: - httpApi: path: /reset/user/password method: post - authorizer: cognitoAuthorizer timeout: 10 environment: CLIENT_POOL_ID: !Ref CognitoUserPoolClient diff --git a/deployments/functions/verify_auth.yaml b/deployments/functions/verify_auth.yaml new file mode 100644 index 0000000..af930ea --- /dev/null +++ b/deployments/functions/verify_auth.yaml @@ -0,0 +1,9 @@ +VerifyAuth: + handler: bin/verify_auth + package: + artifact: archive/VerifyAuth.zip + events: + - httpApi: + path: /verify/auth + method: get + authorizer: cognitoAuthorizer diff --git a/go.sum b/go.sum index b86c0fc..2f8aa55 100644 --- a/go.sum +++ b/go.sum @@ -121,6 +121,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/util/generate_response_body.go b/internal/util/generate_response_body.go index 33e9a1e..39ea600 100644 --- a/internal/util/generate_response_body.go +++ b/internal/util/generate_response_body.go @@ -15,7 +15,7 @@ type responseBody struct { } // GenerateResponseBody creates the response sent back to the client depending on the error message and error type -func GenerateResponseBody(message string, statusCode int, err error, Headers map[string]string) events.APIGatewayProxyResponse { +func GenerateResponseBody(message string, statusCode int, err error, headers map[string]string) events.APIGatewayV2HTTPResponse { if err != nil { if aerr, ok := err.(awserr.Error); ok { message = fmt.Sprintf("%v, %v", message, aerr.Error()) @@ -32,12 +32,14 @@ func GenerateResponseBody(message string, statusCode int, err error, Headers map var buf bytes.Buffer json.HTMLEscape(&buf, body) - resp := events.APIGatewayProxyResponse{ + resp := events.APIGatewayV2HTTPResponse{ StatusCode: statusCode, - Headers: Headers, + Headers: headers, Body: buf.String(), IsBase64Encoded: false, } + log.Printf("[DEBUG] resp %+v", resp) + return resp } diff --git a/internal/util/generate_secret_hash.go b/internal/util/generate_secret_hash.go index edbe67c..0261f40 100644 --- a/internal/util/generate_secret_hash.go +++ b/internal/util/generate_secret_hash.go @@ -6,6 +6,8 @@ import ( "encoding/base64" ) +// GenerateSecretHash performs crytographic calculations to generate the Cognito secret key for the +// current user func GenerateSecretHash(clientSecret string, emailAddress string, clientPoolID string) string { mac := hmac.New(sha256.New, []byte(clientSecret)) mac.Write([]byte(emailAddress + clientPoolID)) diff --git a/internal/util/post_to_slack.go b/internal/util/post_to_slack.go index 3d13457..c453e02 100644 --- a/internal/util/post_to_slack.go +++ b/internal/util/post_to_slack.go @@ -19,7 +19,7 @@ func PostToSlack(webhookURL, message string) { slackBody, _ := json.Marshal(slackRequestBody{Text: message}) req, err := http.NewRequest(http.MethodPost, webhookURL, bytes.NewBuffer(slackBody)) if err != nil { - log.Printf("[ERROR] Unable to marshal json, %v", err) + log.Printf("[ERROR] Unable to form request, %v", err) } req.Header.Add("Content-Type", "application/json") diff --git a/serverless.yaml b/serverless.yaml index e2363d3..e790f00 100644 --- a/serverless.yaml +++ b/serverless.yaml @@ -2,15 +2,25 @@ service: release-dashboard frameworkVersion: "2" +custom: ${file(.env.${self:provider.stage}.yaml)} + +package: + exclude: + - ./** + include: + - ./bin/** + individually: true + provider: name: aws runtime: go1.x stage: dev - region: ap-southeast-2 + region: us-east-1 memorySize: 128 logRetentionInDays: 7 tags: App: ${self:service} + Environment: ${self:provider.stage} ManagedBy: serverless framework apiGateway: @@ -19,21 +29,16 @@ provider: httpApi: cors: allowedOrigins: - - https://release.fw - http://localhost:8080 + - https://${self:custom.DOMAIN} + - !Sub https://${CloudfrontDistribution.DomainName} allowedHeaders: - Content-Type - Authorization authorizers: cognitoAuthorizer: identitySource: $request.header.Authorization - issuerUrl: - Fn::Join: - - "" - - - "https://cognito-idp." - - "${opt:region, self:provider.region}" - - ".amazonaws.com/" - - !Ref CognitoUserPool + issuerUrl: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool} audience: - Ref: CognitoUserPoolClient @@ -57,18 +62,7 @@ provider: - dynamodb:UpdateItem Resource: - !GetAtt DynamoDBTable.Arn - - !Join - - "" - - - !GetAtt DynamoDBTable.Arn - - /index/repo - -custom: ${file(.env.${self:provider.stage}.yaml)} - -package: - exclude: - - ./** - include: - - ./bin/** + - !Sub ${DynamoDBTable.Arn}/index/repo functions: - ${file(deployments/functions/create_repo.yaml)} @@ -80,8 +74,12 @@ functions: - ${file(deployments/functions/login_user.yaml)} - ${file(deployments/functions/release.yaml)} - ${file(deployments/functions/reset_user_password.yaml)} + - ${file(deployments/functions/verify_auth.yaml)} resources: + - ${file(deployments/cloudformation/acm.yaml)} + - ${file(deployments/cloudformation/cloudfront.yaml)} - ${file(deployments/cloudformation/cognito.yaml)} - ${file(deployments/cloudformation/dynamodb.yaml)} - # - ${file(deployments/cloudformation/s3.yaml)} + - ${file(deployments/cloudformation/s3.yaml)} + - ${file(deployments/cloudformation/waf.yaml)}