From 21afee78569fac7e8229ccabb5d214e0dd744d90 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Fri, 14 Nov 2025 18:27:33 +0200 Subject: [PATCH 1/3] basic auth, (std auth api) --- authorizer/basic.go | 67 ++++++++++++++++ examples/02_api_with_auth/main.go | 7 +- go.mod | 29 +++---- go.sum | 54 +++++++------ internal/cmd/auth/auth.go | 85 ++++++++++++++++++++ scud.go | 128 ++++++++++++++++++++++++++---- scud_test.go | 8 +- 7 files changed, 319 insertions(+), 59 deletions(-) create mode 100644 authorizer/basic.go create mode 100644 internal/cmd/auth/auth.go diff --git a/authorizer/basic.go b/authorizer/basic.go new file mode 100644 index 0000000..d84cec2 --- /dev/null +++ b/authorizer/basic.go @@ -0,0 +1,67 @@ +// +// Copyright (C) 2021 - 2025 Dmitry Kolesnikov +// +// This file may be modified and distributed under the terms +// of the Apache License Version 2.0. See the LICENSE file for details. +// https://github.com/fogfish/swarm +// + +package authorizer + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "errors" + "log/slog" + "strings" +) + +var ( + ErrForbidden = errors.New("forbidden") +) + +// Basic Authorizer implements simple access/secret key validation. +type Basic struct { + access, secret string +} + +func NewBasic(access, secret string) (*Basic, error) { + if access == "" || secret == "" { + return nil, errors.New("basic auth is not configured") + } + + return &Basic{ + access: access, + secret: secret, + }, nil +} + +func (auth *Basic) Validate(apikey string) (string, map[string]any, error) { + c, err := base64.RawStdEncoding.DecodeString(apikey) + if err != nil { + slog.Error("corrupted apikey.") + return "", nil, ErrForbidden + } + + access, secret, ok := strings.Cut(string(c), ":") + if !ok { + slog.Error("malformed apikey.") + return "", nil, ErrForbidden + } + + gaccess := sha256.Sum256([]byte(access)) + gsecret := sha256.Sum256([]byte(secret)) + haccess := sha256.Sum256([]byte(auth.access)) + hsecret := sha256.Sum256([]byte(auth.secret)) + + accessMatch := (subtle.ConstantTimeCompare(gaccess[:], haccess[:]) == 1) + secretMatch := (subtle.ConstantTimeCompare(gsecret[:], hsecret[:]) == 1) + + if !(accessMatch && secretMatch) { + slog.Error("apikey forbidden.") + return "", nil, ErrForbidden + } + + return access, map[string]any{"auth": "basic", "sub": access}, nil +} diff --git a/examples/02_api_with_auth/main.go b/examples/02_api_with_auth/main.go index 722d682..3826b42 100644 --- a/examples/02_api_with_auth/main.go +++ b/examples/02_api_with_auth/main.go @@ -31,7 +31,12 @@ func main() { ) // Public endpoint - gw.AddResource("/public", f) + gw.NewAuthorizerPublic(). + AddResource("/public", f) + + // Basic authorization + gw.NewAuthorizerBasic("access", "secret"). + AddResource("/private/apikey/hw", f) // IAM authorization gw.NewAuthorizerIAM(). diff --git a/go.mod b/go.mod index f68ba98..a3d2337 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,29 @@ module github.com/fogfish/scud -go 1.23.0 - -toolchain go1.24.1 +go 1.24.0 require ( - github.com/aws/aws-cdk-go/awscdk/v2 v2.204.0 - github.com/aws/aws-lambda-go v1.49.0 - github.com/aws/constructs-go/constructs/v10 v10.4.2 - github.com/aws/jsii-runtime-go v1.112.0 + github.com/aws/aws-cdk-go/awscdk/v2 v2.224.0 + github.com/aws/aws-lambda-go v1.50.0 + github.com/aws/constructs-go/constructs/v10 v10.4.3 + github.com/aws/jsii-runtime-go v1.119.0 ) require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.244 // indirect + github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.257 // indirect github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.1.0 // indirect - github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v45 v45.2.0 // indirect + github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v48 v48.18.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/yuin/goldmark v1.7.12 // indirect + github.com/yuin/goldmark v1.7.13 // indirect golang.org/x/lint v0.0.0-20241112194109-818c5a804067 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/telemetry v0.0.0-20251112162317-03ef243c208a // indirect + golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools/cmd/godoc v0.1.0-deprecated // indirect + golang.org/x/tools/godoc v0.1.0-deprecated // indirect ) diff --git a/go.sum b/go.sum index bb0ba49..2f32755 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,19 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/aws/aws-cdk-go/awscdk/v2 v2.204.0 h1:AJVkWUMwTnVVTDsvtLUx60mLieeFy7coKceyS8ZlFpc= -github.com/aws/aws-cdk-go/awscdk/v2 v2.204.0/go.mod h1:cx5A9p1ru79f3I3MqBp5qlJiJsL65ccPYzzjpHA0kpY= -github.com/aws/aws-lambda-go v1.49.0 h1:z4VhTqkFZPM3xpEtTqWqRqsRH4TZBMJqTkRiBPYLqIQ= -github.com/aws/aws-lambda-go v1.49.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= -github.com/aws/constructs-go/constructs/v10 v10.4.2 h1:+hDLTsFGLJmKIn0Dg20vWpKBrVnFrEWYgTEY5UiTEG8= -github.com/aws/constructs-go/constructs/v10 v10.4.2/go.mod h1:cXsNCKDV+9eR9zYYfwy6QuE4uPFp6jsq6TtH1MwBx9w= -github.com/aws/jsii-runtime-go v1.112.0 h1:7jusWZUgSTuSPLa2ZRv+siGuyoFSzFNk/TaHqlcFe6Y= -github.com/aws/jsii-runtime-go v1.112.0/go.mod h1:jiAbLN2Hz+7At3C59LsQyv8gK3HvfNYF2YFPkWLHll8= -github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.244 h1:5BRmhBTRsGEH17AWRAO9exny9UQEnPYNXdKbQTIbwqw= -github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.244/go.mod h1:1FHlu1VKVvrE/Bmcow4crPddJlOWhEXde/Zi4TcUhkA= +github.com/aws/aws-cdk-go/awscdk/v2 v2.224.0 h1:El9Ukcr4qje+Ufm9W4BjoBBeDg1moKyH8XiY7ZINo94= +github.com/aws/aws-cdk-go/awscdk/v2 v2.224.0/go.mod h1:EsSENvkUgROR6gLf8pGk/tRvNFanSdp7Gn5cLXBRyxY= +github.com/aws/aws-lambda-go v1.50.0 h1:0GzY18vT4EsCvIyk3kn3ZH5Jg30NRlgYaai1w0aGPMU= +github.com/aws/aws-lambda-go v1.50.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/constructs-go/constructs/v10 v10.4.3 h1:x2j8RzlBhjyQvK9aZ74C34bQkP+ORQHOn0ZAPz81l6I= +github.com/aws/constructs-go/constructs/v10 v10.4.3/go.mod h1:DIGkbU2Lety5CkEfL2MoJI2azg1p2xqpo8MXjp88qXE= +github.com/aws/jsii-runtime-go v1.119.0 h1:lqrlBOUxzthDn8Mtzw+1R1mu972KT8fFx2GnOgN4MUE= +github.com/aws/jsii-runtime-go v1.119.0/go.mod h1:67f+oydH0cMr//tkmNNj9QpKk02hNEEVu4CByxkpGB0= +github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.257 h1:8jKpNi2gOawmsXNWYfbppNmiMPb6RYj0HxVBKE63p7w= +github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.257/go.mod h1:WU79qEJ4N5Oaiy/cJehtT6E85PMvZHuA4JB3CST7oxw= github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.1.0 h1:kElXjprC8wkpJu58vp+WFH6z0AJw4zitg5iSKJPKe3c= github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.1.0/go.mod h1:JY4UnvNa1YDGQ4H5wohXTHl6YVY3uCDUWl4JYUrQfb8= -github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v45 v45.2.0 h1:d7nzm/qFsYWC5TPIayBGIWT/af6+bsmMDsYK/Y3t2ts= -github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v45 v45.2.0/go.mod h1:HQLZo+YhqrT439d+7LrIhlM/oYzY+EVNlAuRd20m1kg= +github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v48 v48.18.0 h1:6D4o+f8TexpVYDISnMRCeQYJzCDfxMJsGGNWTygtXOk= +github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v48 v48.18.0/go.mod h1:Mv/KtlUxCbyDI6hGu+YgEXn/nBsJ7WfQnUOw9zyBHvU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -26,31 +26,37 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= -github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20241112194109-818c5a804067 h1:adDmSQyFTCiv19j015EGKJBoaa7ElV0Q1Wovb/4G7NA= golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251112162317-03ef243c208a h1:gUx35lvbguAhkO/SMZOrCTxb5u/ie8hWkMBdlqvc1gs= +golang.org/x/telemetry v0.0.0-20251112162317-03ef243c208a/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools/cmd/godoc v0.1.0-deprecated h1:sEGTwp9aZNTHsdf/2BGaRqE4ZLndRVH17rbQ2OVun9Q= +golang.org/x/tools/cmd/godoc v0.1.0-deprecated/go.mod h1:J6VY4iFch6TIm456U3fnw1EJZaIqcYlhHu6GpHQ9HJk= +golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= +golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmd/auth/auth.go b/internal/cmd/auth/auth.go new file mode 100644 index 0000000..a3eb78b --- /dev/null +++ b/internal/cmd/auth/auth.go @@ -0,0 +1,85 @@ +// +// Copyright (C) 2021 - 2025 Dmitry Kolesnikov +// +// This file may be modified and distributed under the terms +// of the Apache License Version 2.0. See the LICENSE file for details. +// https://github.com/fogfish/swarm +// + +package main + +import ( + "log/slog" + "os" + "strings" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/fogfish/scud/authorizer" +) + +var ( + None = events.APIGatewayCustomAuthorizerResponse{} +) + +func main() { + access := os.Getenv("CONFIG_AUTHORIZER_ACCESS") + secret := os.Getenv("CONFIG_AUTHORIZER_SECRET") + source := os.Getenv("CONFIG_AUTHORIZER_SOURCE") + basic, err := authorizer.NewBasic(access, secret) + if err != nil { + slog.Warn("Basic Auth disabled.") + basic = nil + } + + lambda.Start( + func(evt events.APIGatewayV2CustomAuthorizerV1Request) (events.APIGatewayCustomAuthorizerResponse, error) { + var apikey string + + switch source { + case "$request.header.Authorization": + apikey = evt.Headers["authorization"] + if !strings.HasPrefix(apikey, "Basic ") { + return None, authorizer.ErrForbidden + } + apikey = strings.TrimPrefix(apikey, "Basic ") + case "$request.querystring.apikey": + apikey = evt.QueryStringParameters["apikey"] + default: + slog.Error("unsupported identity source.") + return None, authorizer.ErrForbidden + } + + if basic != nil { + principal, context, err := basic.Validate(apikey) + if err != nil { + return None, authorizer.ErrForbidden + } + + return AccessPolicy(principal, evt.MethodArn, context), nil + } + + return None, authorizer.ErrForbidden + }, + ) +} + +//------------------------------------------------------------------------------ + +// Grant the access to WebSocket with the policy +func AccessPolicy(principal, method string, context map[string]any) events.APIGatewayCustomAuthorizerResponse { + return events.APIGatewayCustomAuthorizerResponse{ + PrincipalID: principal, + PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{ + Version: "2012-10-17", + Statement: []events.IAMPolicyStatement{ + { + Action: []string{"execute-api:*"}, + Effect: "Allow", + Resource: []string{method}, + }, + }, + }, + Context: context, + } +} diff --git a/scud.go b/scud.go index 6fc366d..fc8e94a 100644 --- a/scud.go +++ b/scud.go @@ -113,26 +113,56 @@ func (gw *Gateway) createRoute53(host string) *Gateway { return gw } -// Associate a Lambda function with a REST API path. It uses the specified -// path as a prefix, enabling the association of the Lambda function with -// all subpaths under that prefix. -func (gw *Gateway) AddResource( - endpoint string, - handler awslambda.Function, -) { - lambda := integrations.NewHttpLambdaIntegration( - jsii.String(filepath.Base(endpoint)), - handler, - &integrations.HttpLambdaIntegrationProps{ - PayloadFormatVersion: apigw2.PayloadFormatVersion_VERSION_1_0(), +// Creates public (unprotected) integration to authorize incoming requests. +// This integration allows unrestricted access to the associated API endpoints, +// enabling any client to interact with the resources and functionalities +// provided by your Lambda functions without requiring authentication or +// authorization. It is suitable for scenarios where open access is desired, +// such as public APIs or endpoints that do not handle sensitive data. +func (gw *Gateway) NewAuthorizerPublic() *AuthorizerPublic { + return &AuthorizerPublic{ + RestAPI: gw.RestAPI, + } +} + +// Creates integration with Basic Authorizer to authorize incoming requests. +// This integration implements a simple access/secret key validation mechanism, +// allowing you to protect your API endpoints using basic authentication. +// By validating the provided credentials against the configured access and +// secret keys, the library ensures that only authorized clients can access +// your resources, enhancing the security of your API. +func (gw *Gateway) NewAuthorizerBasic(access, secret string, source ...string) *AuthorizerBasic { + src := "$request.header.Authorization" + if len(source) > 0 { + src = source[0] + } + + f := NewFunctionGo(gw.Construct, jsii.String("AuthorizerBasic"), + &FunctionGoProps{ + SourceCodeModule: "github.com/fogfish/scud", + SourceCodeLambda: "internal/cmd/auth", + FunctionProps: &awslambda.FunctionProps{ + Timeout: awscdk.Duration_Seconds(jsii.Number(5)), + Environment: &map[string]*string{ + "CONFIG_AUTHORIZER_ACCESS": jsii.String(access), + "CONFIG_AUTHORIZER_SECRET": jsii.String(secret), + "CONFIG_AUTHORIZER_SOURCE": jsii.String(src), + }, + }, }, ) - for _, path := range []string{endpoint, endpoint + "/{any+}"} { - gw.RestAPI.AddRoutes(&apigw2.AddRoutesOptions{ - Path: jsii.String(path), - Integration: lambda, - }) + authorizer := authorizers.NewHttpLambdaAuthorizer(jsii.String("LambdaAuthorizer"), f, + &authorizers.HttpLambdaAuthorizerProps{ + IdentitySource: jsii.Strings(src), + // Note: enable for debug purposes only + ResultsCacheTtl: awscdk.Duration_Seconds(jsii.Number(0)), + }, + ) + + return &AuthorizerBasic{ + RestAPI: gw.RestAPI, + authorizer: authorizer, } } @@ -214,6 +244,70 @@ func (gw *Gateway) NewAuthorizerJwt(iss string, aud ...string) *AuthorizerJwt { //------------------------------------------------------------------------------ +type AuthorizerPublic struct { + RestAPI apigw2.HttpApi +} + +// Associate a Lambda function with a REST API path. It uses the specified +// path as a prefix, enabling the association of the Lambda function with +// all subpaths under that prefix. +func (api *AuthorizerPublic) AddResource( + endpoint string, + handler awslambda.Function, +) *AuthorizerPublic { + lambda := integrations.NewHttpLambdaIntegration( + jsii.String(filepath.Base(endpoint)), + handler, + &integrations.HttpLambdaIntegrationProps{ + PayloadFormatVersion: apigw2.PayloadFormatVersion_VERSION_1_0(), + }, + ) + + for _, path := range []string{endpoint, endpoint + "/{any+}"} { + api.RestAPI.AddRoutes(&apigw2.AddRoutesOptions{ + Path: jsii.String(path), + Integration: lambda, + }) + } + + return api +} + +//------------------------------------------------------------------------------ + +type AuthorizerBasic struct { + RestAPI apigw2.HttpApi + authorizer authorizers.HttpLambdaAuthorizer +} + +// Associate a Lambda function with a REST API path. It uses the specified +// path as a prefix, enabling the association of the Lambda function with +// all subpaths under that prefix. +func (api *AuthorizerBasic) AddResource( + endpoint string, + handler awslambda.Function, +) *AuthorizerBasic { + lambda := integrations.NewHttpLambdaIntegration( + jsii.String(filepath.Base(endpoint)), + handler, + &integrations.HttpLambdaIntegrationProps{ + PayloadFormatVersion: apigw2.PayloadFormatVersion_VERSION_1_0(), + }, + ) + + for _, path := range []string{endpoint, endpoint + "/{any+}"} { + api.RestAPI.AddRoutes(&apigw2.AddRoutesOptions{ + Path: jsii.String(path), + Integration: lambda, + Authorizer: api.authorizer, + }) + } + + return api +} + +//------------------------------------------------------------------------------ + type AuthorizerIAM struct { constructs.Construct RestAPI apigw2.HttpApi diff --git a/scud_test.go b/scud_test.go index bd20743..41d5289 100644 --- a/scud_test.go +++ b/scud_test.go @@ -299,7 +299,8 @@ func TestAddResource(t *testing.T) { ) gw := scud.NewGateway(stack, jsii.String("GW"), &scud.GatewayProps{}) - gw.AddResource("/test", f) + pub := gw.NewAuthorizerPublic() + pub.AddResource("/test", f) require := map[*string]*float64{ jsii.String("AWS::ApiGatewayV2::Api"): jsii.Number(1), @@ -326,8 +327,9 @@ func TestAddResourceDepthPath(t *testing.T) { ) gw := scud.NewGateway(stack, jsii.String("GW"), &scud.GatewayProps{}) - gw.AddResource("/test/1", f) - gw.AddResource("/test/2", f) + pub := gw.NewAuthorizerPublic() + pub.AddResource("/test/1", f) + pub.AddResource("/test/2", f) require := map[*string]*float64{ jsii.String("AWS::ApiGatewayV2::Api"): jsii.Number(1), From 284ba73eb7774d101ac1ad5934a643491e77642a Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Fri, 14 Nov 2025 18:33:29 +0200 Subject: [PATCH 2/3] unit test for basic auth --- scud_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scud_test.go b/scud_test.go index 41d5289..540ca1f 100644 --- a/scud_test.go +++ b/scud_test.go @@ -420,6 +420,31 @@ func TestAuthorizerJwt(t *testing.T) { } } +func TestAuthorizerBasic(t *testing.T) { + app := awscdk.NewApp(nil) + stack := awscdk.NewStack(app, jsii.String("Test"), nil) + + f := scud.NewFunctionGo(stack, jsii.String("test"), + &scud.FunctionGoProps{ + SourceCodeModule: "github.com/fogfish/scud", + SourceCodeLambda: "test/lambda/go", + }, + ) + + gw := scud.NewGateway(stack, jsii.String("GW"), &scud.GatewayProps{}) + gw.NewAuthorizerBasic("username", "password"). + AddResource("/test", f) + + require := map[*string]*float64{ + jsii.String("AWS::ApiGatewayV2::Authorizer"): jsii.Number(1), + } + + template := assertions.Template_FromStack(stack, nil) + for key, val := range require { + template.ResourceCountIs(key, val) + } +} + func TestConfigRoute53(t *testing.T) { app := awscdk.NewApp(nil) stack := awscdk.NewStack(app, jsii.String("Test"), From 01d8bded40bdab89118fd51dcc8487a1a2e0f386 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Fri, 14 Nov 2025 18:48:06 +0200 Subject: [PATCH 3/3] add testing for basic --- authorizer/basic_test.go | 53 ++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 3 files changed, 56 insertions(+) create mode 100644 authorizer/basic_test.go diff --git a/authorizer/basic_test.go b/authorizer/basic_test.go new file mode 100644 index 0000000..33924de --- /dev/null +++ b/authorizer/basic_test.go @@ -0,0 +1,53 @@ +// +// Copyright (C) 2021 - 2025 Dmitry Kolesnikov +// +// This file may be modified and distributed under the terms +// of the Apache License Version 2.0. See the LICENSE file for details. +// https://github.com/fogfish/swarm +// + +package authorizer_test + +import ( + "testing" + + "github.com/fogfish/it/v2" + "github.com/fogfish/scud/authorizer" +) + +func TestBasic(t *testing.T) { + auth, err := authorizer.NewBasic("access", "secret") + it.Then(t).Must(it.Nil(err)) + + t.Run("Success", func(t *testing.T) { + access, token, err := auth.Validate("YWNjZXNzOnNlY3JldA") + it.Then(t).Should( + it.Nil(err), + it.Equal(access, "access"), + it.Map(token).Have("auth", "basic"), + it.Map(token).Have("sub", "access"), + ) + }) + + t.Run("Forbidden/InvalidKey", func(t *testing.T) { + _, _, err := auth.Validate("YWNjZXNzOnNlY3JldHo") + it.Then(t).ShouldNot( + it.Nil(err), + ) + }) + + t.Run("Forbidden/Format", func(t *testing.T) { + _, _, err := auth.Validate("YWNjZXNzc2VjcmV0") + it.Then(t).ShouldNot( + it.Nil(err), + ) + }) + + t.Run("Forbidden/Corrupted", func(t *testing.T) { + _, _, err := auth.Validate(".") + it.Then(t).ShouldNot( + it.Nil(err), + ) + }) + +} diff --git a/go.mod b/go.mod index a3d2337..848f925 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-lambda-go v1.50.0 github.com/aws/constructs-go/constructs/v10 v10.4.3 github.com/aws/jsii-runtime-go v1.119.0 + github.com/fogfish/it/v2 v2.2.4 ) require ( diff --git a/go.sum b/go.sum index 2f32755..02a0cd6 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fogfish/it/v2 v2.2.4 h1:hkBePGW7X/wDc1QCLG/j+/j47TG4obnozYsGMX51yMQ= +github.com/fogfish/it/v2 v2.2.4/go.mod h1:HHwufnTaZTvlRVnSesPl49HzzlMrQtweKbf+8Co/ll4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=