From 9f52a6becf44af7d8dda356997b792e92dd4c6a0 Mon Sep 17 00:00:00 2001 From: Marco Jahn Date: Mon, 17 Mar 2025 13:45:36 +0100 Subject: [PATCH 01/11] added apigw-secretsmanager-apikey-cdk --- apigw-secretsmanager-apikey-cdk/README.md | 99 ++++++++++++++++++ .../apigw-secretsmanager-apikey-cdk.png | Bin 0 -> 39436 bytes .../bin/apigw-secrectsmanager-apikey-cdk.ts | 12 +++ apigw-secretsmanager-apikey-cdk/cdk.json | 88 ++++++++++++++++ .../create_api_key.sh | 21 ++++ .../example-pattern.json | 65 ++++++++++++ .../lib/apigw-secretsmanager-apikey-stack.ts | 93 ++++++++++++++++ .../lib/lambda/authorizer.js | 87 +++++++++++++++ apigw-secretsmanager-apikey-cdk/package.json | 26 +++++ .../remove_secrets.sh | 33 ++++++ apigw-secretsmanager-apikey-cdk/tsconfig.json | 31 ++++++ 11 files changed, 555 insertions(+) create mode 100644 apigw-secretsmanager-apikey-cdk/README.md create mode 100644 apigw-secretsmanager-apikey-cdk/apigw-secretsmanager-apikey-cdk.png create mode 100644 apigw-secretsmanager-apikey-cdk/bin/apigw-secrectsmanager-apikey-cdk.ts create mode 100644 apigw-secretsmanager-apikey-cdk/cdk.json create mode 100755 apigw-secretsmanager-apikey-cdk/create_api_key.sh create mode 100644 apigw-secretsmanager-apikey-cdk/example-pattern.json create mode 100644 apigw-secretsmanager-apikey-cdk/lib/apigw-secretsmanager-apikey-stack.ts create mode 100644 apigw-secretsmanager-apikey-cdk/lib/lambda/authorizer.js create mode 100644 apigw-secretsmanager-apikey-cdk/package.json create mode 100755 apigw-secretsmanager-apikey-cdk/remove_secrets.sh create mode 100644 apigw-secretsmanager-apikey-cdk/tsconfig.json diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md new file mode 100644 index 000000000..2ab25ef18 --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -0,0 +1,99 @@ +# API Gateway with Lambda Authorizer and Secrets Manager for API Key Authentication + +This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager. Each user/tenant has their own unique API key stored in Secrets Manager, which is validated by a Lambda authorizer when requests are made to protected API endpoints. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-secretsmanager-apikey-cdk](https://serverlessland.com/patterns/apigw-secretsmanager-apikey-cdk) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed +* [Node.js and npm](https://nodejs.org/) installed +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd apigw-secretsmanager-apikey-cdk + ``` +1. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + npm install + ``` +1. Deploy the stack: + ``` + cdk deploy + ``` + +Note the outputs from the CDK deployment process. The output will include the API Gateway URL you'll need for testing. + +## How it works + +![Architecture Diagram](./apigw-secretsmanager-apikey-cdk.png) + +1. Client makes a request to the API with an API key in the `x-api-key` header +2. API Gateway forwards the authorization request to the Lambda Authorizer + - The Lambda Authorizer checks if the API key exists in Secrets Manager + - If the key is valid, the associated tenant information is retrieved and included in the authorization context +3. The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer + +Each API key in Secrets Manager should follow the naming convention `api-key-{keyvalue}` and contain a JSON document with at least a tenantId field. + +## Testing + +1. Create a new api key, you will need the api key later on + ``` + ❯ ./create_api_key.sh sample-tenant + API key for tenant sample-tenant created: b4037c9368990982ac5d1c670053bf76 + ``` +1. Get the API Gateway URL from the deployment output: + ```bash + # The output will be similar to + ApigwSecretsmanagerApikeyCdkStack.ApiUrl = https://383rm6ue91.execute-api.us-east-1.amazonaws.com/prod/ + ``` +1. Make a request to the protected endpoint with a valid API key: + ``` + curl -H "x-api-key: CREATED_API_KEY" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected + ``` + If successful, you should receive a response like: + ``` + { "message": "Access granted" } + ``` +1. Try with an invalid API key: + ``` + curl -H "x-api-key: invalid-key" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected + ``` + You should receive an unauthorized error. +1. Try without an API key: + ``` + curl https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected + ``` + You should also receive an unauthorized error. + + +## Cleanup + +1. Delete the stack + ```bash + cdk destroy + ``` +1. Delete created SecretManager keys using + ```bash + ./remove_secrets.sh + + # to check which keys will be removed, use the dry-run option + ./remove_secrets.sh --dry-run + ``` + +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-secretsmanager-apikey-cdk/apigw-secretsmanager-apikey-cdk.png b/apigw-secretsmanager-apikey-cdk/apigw-secretsmanager-apikey-cdk.png new file mode 100644 index 0000000000000000000000000000000000000000..6d167277c786854499e37451976482b7887a3f33 GIT binary patch literal 39436 zcmZs@1yoe+7d{F|mvp0q^Z)`%cMH-vv@moJB^}ZwZ2?)~h&pJ!q;)s+ZvX>d_cPzYWq%WI>cpn*|PP#v)`fmc@Z zvjTvBs2dd!}7lhZm+R)XMYoFx zrrL`!!P%Kqq|Y(j_uuu)3Aj%ozCX)9Kwjd5Su?QrG5=jp23%i=*r!e4hg|fE|6vFJ zH#jG*&ZHiLNF>=846rZMnskp|__gl!i@U1pg=S`04me%s=x8p2CsVTW{okqRcfJ5_ zltq>lH)qgE1ILa&S{rzTZ{U^bg})fI@@GlPwJ$fV#m$oq^oLIPwR~D}a4HV?ZH~CY z`=_snnEjqbjo+ZDnLf+i(;UzJbzH4VphY+K7cl7F#gP=o{r@hWAG!H>yu z)(2tK+?f+H$zc~AAH2~%Zb|WWciVfsky8Eegs+$2AdU|F!0ix$Q@c0&80*%^8Nu>r zDFVr^Q$dXVQl$F7U3@Qx8VT|kd}DC>jV9i%|GVE$%`kSf2Lolo1_NWuw%D1Ecio7S zM(>PSpC@^pcEm9t@}D^#DWfX5F19Ls67Rt{)&&`(d2Y%q=D+*Byuy4f>LGr#(I4}} zb&-KV1Lq}`2ucrb@Y_LO)qL9x{St!$e#DSbqZ7H!uyMRx%T(aHgO(H}w|T;6t0)G8 z)4|5OW#Ugm;E<)e>FADI!+2GL$^!7!N8$Q24Y^+*8IeTPO(cI88(%Mk5veKC#IK4` z&KSp_g38j7 z=VoSbN;lPfvA}S4M>u?vw!p85KblmGqS?!ic-MPxu~9aS%cz}@V)$cpYm*eOFG=`N zRtt7rnV$6ZYxg%%x7(ABkYNLo0?aeny4a$-yMrootLupztvXo_asOK;D)>i9|3pS9 z^>45}eLQ1sqEvBwn&I9)povv>vubbC=b}z2Xbt^P!bZmzE zn%6z}Tt{(#AI$yWMxE3tx0DbMI5rF+l~LQL13ULxb5xGiOHnZUY6oZjxjb0;`88>> z=u9(9IFPVSw4oPiP&L|gMl_fsNdXqHl|MzZ{$h2b{p7a>dRR4`R9a6vcgM=Y)kAjO zW=xu?&#@u+TTdOnd9HlpgZM5ucR#QMruR@E*u*z%-*-(L$j{Z05$Ir;%>1NZ_ZnYVU?!dl`3K=jLP5YQx7;fn>&H9HZKL3;vom%TLK)K7sLz&c2g%SmfXtC8mP%U)weDaGTC(*9tM3Br8!t^ zlZ%}1dFnW(T)R0%a?2YDu ztACKBEFS*Y*SK;cD?L@vr-6==l6vy9J0toLHz(%l<@KlaYSLn@x**3$(3#olJk4KU zn+JI09?EFIrG}S}oOWrCqat&CpHeIvpiKC_E(y!>7=BVlLqfN3XwSV)$&ZNQb z@Tp|bb@Z-OnnHq#^=N~C%W`ms_YG@oT2(@JFi+o*u}?RYvF-)x$(nM}onReeuKh|H z9r7|tbdn1fp$((g$dP!9TlWD6VRShsI&!DeN>TKjwm#P}=M_HKfroXt>u;RNB(p$3 zQ%BmT<13~)j9sxyClyhjuRF_gQqn*wz#Y^N6km4cWd|>3y0#M!CHcSO`hw=?=@ilY zoYQhYY@Ld4pG}v;9Je6ql*0FvUN{_+yM@8}sBju?^Xh5VyV5bDo8|_}bvvojg{xbI zac_w)7E|cN1=bl9+~#jWnR~4U-hZ{EKH9&*+MM;FTJ25b?nLmjmgyC@N0W15UV1v? zp>?I`(O8V`#vsyGj3Hc_m=&2*a$U1}e2`-Zsy{l|XJ3Ly=5Q@P`D&`^2T^q$dLQI- z+Xvytf48dA05~)!G*?b|^)NhgcBbk4V-d-nKl=yuDco3b+nPTiqjPU+v0(7RPZJtf zuSO|wk-s#^Q8(W0)M76VgdVl}-552huBn4Xwa|szu-+2x+k8X+ty!hWQtM!JP*yoU zw%W*1qu|z-g1a`vvM5{#t<|7FrEy-7<}*>7JtYcJ`0Te1r<&@>eC_BqFlgR=#8{-V zt`c3O5mw&lZdoArR%W{Ks2Zo0FCQ74k4{)td@U5_62na;JIDCZvb}NvTeh{) zJ+lE_WaQDMJnk()X)U%KL3w^*LtoK=4G>a#aaFO7pZjOHtGWLFMVKV?p;|Z$d=p^0o9k``Th@=Z~{l$qzLrG z@vK#bx#$&wUt6s*?jNL32HuO;P2IuGLlFZ9$o#wAIUEfi+%A5_^GHk%7Kia``yXU^ zS%D%4`&`^tBi5I+KJ%xq&Gz2AhmgCe1CQT0{yCP%E=`#ZCCn=e&le)$Zt!>06Eorc zZ$cRA7c!}{F~o3ZaBCWJ!gB$+g}MlcRWo8&k#xLVje@x)_()j;Qe0udN;Z@lO#J^1`v-QU$)9>P6@<_UeTT&vC?hO-j|S^OHDcHcXr||N)eEd7Khnj%> zKVa?GX<^5M({WR}@apRdgB?#NXlz8k_+Mm=`*D z4I-jaTdo|FJ0!{n5z&4t^>-V9AnUf=;`=Mi-!ci!CK6=443Tj1TlirXKq&{4p~Nxx z&#TbhdzCkc`EY-7V8#wGJbD@6kl8tYPSV9EfJ+8khljsuh$Iqo2zNaUj5VY(6Qxh- zi!F3y@;38}*5}e#e)(3mdBB75@8|;bP|bLbCo|4g-H~DCBa1vwns9RGQNbRLHZtvN z)!q0~0T+Z`6!bFi_sNI;1zF8DH^0k#8V{FQpe!+YW#W|g@$=q*s?g}p_s!Y@OP^~* zg4Vg^{hxm7?Z2#erB3#eTOd1eS4`DO^bZjr_{cdUHrT z34bEHly6VOimw^8-(mNj3+K~8OYDyWsr>PW99q1^9U7+o;)Z``IhYHWJ+;kO)hl4{ zCqR_V{D-Dso-6|auA}1U2#p-c6#$lO*8ZvusT^JBx0t)QDycifR)oEHTl+gdx2sgA zBxn5b0-8?VNbLfx;baQ~4&Z!5&k$UCYho(#{MzYN<8QZA!&C0STezl+{>djN%@8s2K=Qk5K!ar(= zzD1pMt>m(?i|pi7F_d+eKVZ*ddi83GfBDL=u8;a1!R>k#; zC{1ov*?5k0RA07(o*pb-xo2`;e@nsC-ez1?y)rHnU#&TNEX6pwTwyLl5jtMWiOQZW z`4Q2uGbX2dvrbfb!o|d}lasi_Y`u$Et$F>R!9r=L6_NJzL&2-x8-EkJ*F}KuG83t2 zmtY4c71=b6&?i~br#|;Y`d$a_@l6R0C5+YF5v#`3iYssZE<0nw+&St>-)ud_=Q+le zH|%)Y_-B^@t#|(t7tT2(7JZaKlDp;eLgB*qZm~1R_Fy&L2#f)t-_}LxZ*T1ZT*=%S z$7Rk%1ArQi^`Sey$FC4jH<<`Vjqgc82w_;;k34qxxd+n4bvI zb6G=Aus;oPR|GjScu8$G&V5}VS6f;Zf6mVgCC%$4#bn*lIrm`YM~s+!CFx0X!UB`X zbGsyD4kL1O?cYorU!W2O`1}P3RdN(P1~*AZcdnF`!rpk!K~gbY*?0_o-EMZR$5h_t zLF_yUySJl!4L15uVu*>$)aQG<7t-A?hdp%hl^Mc*aKLu9e;RxZJ@CotP&hGv7$dH7 z(c@LataAQEkTI?^5pC^%nO36);Ka}D_dK}!!$3302nV*yakkh=zX+iNDJHKhcFQY_ zSv<$@>q<;h+g-&Pjc3=~@PwrV;|Fmud-NbujA?WKYuuG@mtUp)`}&}D@_3r@oO20W)y%> z=P~{G`WK)1aCL?rEJ}1}EA=I@=moiC{TFkj?}KLwTW4lwItDm#k>{DLhc`T4GkNE} z9--eUaUUN1J%8!Xm)uWwwZcS1m!%&0&e?6@?;yXR^|Ax^?cGGwy;nvBC$Xr88DwH2 z9cF*dCFnwP``(6PEz{zPf2f-l!rTti>UGfS!>B7QOJP*aR?kAdxesG-r&nR+z=wQC z@G}a^l2r)NWI>v|p`p5n&*3hanaG z*i;1{eChd98w!MqvEmjd#MlqV4OJl2R5AXrPy+V9{n-RNQ2S718MdMT0L1YSx*SY$ zp^=Qs!r*?yND3P^)ukce3JoD1mirF`HX|eNpX3ePoYzDp0tjWQ3O`IzA{bS3%Ku*v zOCeYMmQ-}$x1!AnJpVolqsmd3qkk*}oN)V$B`_k|bW4oaO^0!4yO6CGYT)@>y+v#H z1JdxBN$NpHT>eT>%`ij-tHVIX)5-|kbXFo_RjL-BiK{pF16(T!|WDkIA$%M1#j>jR9DSGI1>0>JzEOy%M$VU@~R6{&8r`=iw$R5AB>k5P(=3`GR^HtBEOtu*lFrihsC!{ zD9xq;hO_2{0io7oL!}L+WvOWb?}wCsl<;3YTmvCQ2*Z%Y}X0E~Yjm6p4 zc(EA<_(^1$Ox*Tc<-2ukqS^5~V>avl!;_WNRpyy}m=l|-J{67}>{AuiaJd3tJKPQt zgJr^CMHz}om#xH%{vjw9lyo{$6BCN3)k_|)08Of8wf*@f8OTl*%W^H~Ei*5eQ0q2L&q-=zo zx+M%*6`h^ZgV|z~{i$qyVHkMQ``Ii!jYvlCK);Y?V2%|f9MLq1R zFm4#BG!;XYPG(Xd86GXsDmc8o@qcDNCSSX?v4I7>X!hQV-T0nU;Ih!1pF_%~-kmMx z^Tqc2qm}ZWILd=EKIa)YhFYc|xficdEp|^VIq6OAY4FX7LEgI5-Sr#5EaHNhUxV~Y z(@Ol8J416X?54`8Z*R`bb;}Kj322^mE`NQ45pZ+z!<>m%#%>Lfe6l3Z!?}g%*Z%OF zq_(8;{gC4v24|fd%OTSy6tl5}*Uz$@3O+GRvSEdx%LyP1$jrA&6ei}KXJwh|gou2W z&w!D}a-9Lo@?OM)RRso$87)w<+8Qf*|I9uGh1hR%{6P*K&wxRN(erYnI$YPKc5+R5 z3vwYB6I7CcG5vR(dYT1_1QVrtc)*P)P~?1Dd=7YWOFcG*5m_Pv`%4|XC^5@%DB&;! zwmt=~8E9Yx`s!zILJtXD=%bS+ubrv$(>Yf@?_EPwi`rIjor7L-4r~X3p-qCSyE|R2 z&{j*PL^NLSY+Rj{&Y{DKDuxRIRzdkVy)*HMwEG>G0U>adDNj;((0wLsPIqTIXvUAX z#Ak%r3_UD@vq#j8IuwTj=DaBK1b*?4Q3Vrd~ z(dVm-z@v;k#SXDoFVV13bzqFU+ymKhxLJSytZh>IBT5|`yC7g{gEM8$x~w%t1J8}) zCnF-?T9f;QAf(@q=SynQI_FC3hks6-ITS&z4g)WMe55HtNo6pK*fc&yW8JOwr;fG< z`mY!zGs`^;P+2oe07^WRJSLqrw%@~Bdw=J0oRWuj*c* zMvhr0;XBvS{D{pTAD%mg*T@f(a~F^ta)gkvzNA_a98KYTCk9=M9^y0Y2%LQ@ehX!#-0Ze3)E{2(23;C0~G zp$#As@_%d5q6?7)D+cOlm+F3&e@WJ4mzF+Op!BvoXX0*iv;f$AnBHDO|09T&`RB)L z-o%}wM{KC9O*(*cZ@>e?LJ!b0(H#rRW>WMKV`xUF%PK?l_=yZiZKVO#m`_W zw~tmlMGp_)OVk=|*JP7Z?UsIn0RsTlqK&HDRl@$WbWcq&*H8{Y4XW{TuKI zg?TFOpIR?T*NY{0mpaTrJIMtKJjC4JM@d+E-j0$ymZ5FZn5%cjiv&R@oDyPsRT<=~ zttWht$3Q@eYi4(ezs!7QuU-8_*+{DclnsKO+u@|nKX;7;J6lLSevOvGB&7nk0@2*=%KU2?x zsFS>|&)rwFwVsMJ36g256G6}RO;=2`Iz_oNh?SG@^C`+&#qdBT$B~WTB*A zDY=!jzldUZ>jbHgIm)}~H@dyj%tWUn8&r28ZPHqMueB_WO2WO8ECzfyHdTR71)cNw zv53V3O#DtD3MvGU-n>Y%%doq6o12^^5|+LcbfqTEaFS{H9s5HGNW>1^nQU5J)xXwL z3Hf#{NQ=Jq*-Y{GjrX+1R!j!YfWtwXMsOFvRkSU;5X8);ILh|rE#7-1U@`;~!(bBQ z8x--etXam5c#g(cGlx7-H8Fxr($*|WVq}F5Hr=F z5r|zCM1JJpnoSc8;JVQ;oJpS&yBV^g@uHDHov=0SaN-!EZ?jDv?Z~V8tr2PwwXXmA zfUdB%rui~G1;7!^w=|5h;hHTk_D07To3z)|i3^ked;*<-DL%OqtraOJGs$PaeJn-s zRLNZ%7IP~Bw2&<0PgC0XlDm`+U#y=z@q3eM0oy)IE3|FW#Iws#z+y&?>+MdmhGnb= zy-v6XfH3Y|4s{As3ma(7AR0K)MI7o(hJdF1AkRr<_B`IyDtwWu*c#CQU011I>+5gB z6~+Uv-lo>R`fOBWGAcEb#PS!r%$6novDg0(UP<7VY>KJ$T*_s_K;jQ9|cz#AAB3KCx(c2$-3otq!huf2#hYmFF_>W9%r zwJ(ZKzf$lUW`2hJdpAP1P=WJsG({QMXl{`cGS{|16B$#3A1-vH%rK)4E00TMRD~T$ zj#L7MX^q$NtzX+eW7B>Pl$pv8`J8P%Y>SBz9}cpWQ5~`=G7_ZKJ7yVyFTPbhP(S$U zC0ND12uXVfk)Wi4Lxvh3)^Y(S{zVy<=kCcQNytQ^(sy_cVA()fNT6KuvGbm^t6DR& zmND5g4cd%LSttcu`{pWY`rQIaP%Y1B1g621N3KnQmeQY3$KR=KRqIn(xZ=Zs#Bs7G zX9wt2s5W?hz!iOLtKFYg4tJZk5TM+vU3tO3fYUS}U4S(M@YB;-LcH*b%c{Q`qP=>hE zi2Szvc$MQR#=S#zT7L&G7&d%UnSQoK)iFP?XH%JO7BV@PYKDNLgZ%eXSfpo=Ryg8` zf?Rs13s8zA&{YxT6hsUR|A@B>@x2lX>;4TSs6ajgk)cql7RX%#6^i)B_|iZK=tXs% z@}y$~8R*4de|i10O0P`c@*(G}aN2FQ;AP=WmC8F#7Vn^jbqFzJ^jG1UY}H;G|_&?n~Z|WMO}GY3=Xq{sfl%Y@*6Z`8=le6 zP7(yMiXIf(|IwcPAR7u2C5fUF6m3Z&afTTIz_WU#q@w-e;^NiW{(_~Bj_9OYqQt>I?yeMo%X^S_XY3{2hK%?KZs{!mkJ`~;ED zY_)ma2IL6QYBKh;7-;yS=psOPv@CutI{)5V?JXtm$LW6Y+I3k`v80fw#Pqy%iCN`m zI-g!zDtTkTF$rj%m@tWAB_%G4c<;Us;WCOXd{eztt(XXYR?)U2-B{KmN8e_VhOxY;M&x%L_t3OSfjk;r4NTwl0wy?Jn#!huYrFj|0`D8fY^{B2bSM)-o6wE5gz>tsDTz%o5@>RSvdP zItL(hT!G8xpuDI-(mb>1Y^0!Mk-Y8YsJqyj%_wVOt(aIS-K3l|*9PVou}nIzIfd<0 z65Y|^quMG;8DbHgwRkOXSgGyzkFSt0?)n;&>)B8i1`Ec>G0&OSQkHuSMD9LliGdd{)hxo??=4d9#{(h7)5#gr zdFv+dM|3dRfxN0WkjFg8UiwjGE`=(A4)#4-!;$=wL03h>q7wZFgB+2+>?S%DX;BI! zRcP=M9-|Kh3-kRa`Rn3nHKJa(cco(463WOoYx1MXl53j8yF_H8IcX_RUNChggl*c3 z=(so9;iP2ohw(wbmf)Ynk%&M&?F6!2sc0Y^dt;%~r`zwG?i_Al|~`|1EorcOkbNUA0*bNxk37Z+g>=QpMTl=FSj*>T)>S^f@{f?~W5;c88F35~n0C?w1d>F`f{+aE) z2dxkoz5t_Q{VjIXe{o`q`_x6c0MZBpp}k{Lh>e!}xOHfYYs9w9m>vtmUQ`2lU@J-L|P^5iaaobs(VeXsKfo)_Xy1fx!wvUi3|R%vUsyWl6N2n?DQHQ zHR3q%O<4Xpb8JjLi>wN|)S4m&CT47Khx^aS@eNv%lF>0H5o?uvL(bsfU{tAEaXYSL z4}~aYv03Jw)4e&%8g1M~HlH}@*u`+&F(6-c12EGQ<)q~@8>f@K;+!B7S1umo`jDjR&^4m9Y@@)|*zLU^7v(Nv9lNRJXQ=-{!m%gZAZPa5TvBj#U6%~#k$fshJp!F?n zy0CwdQm>zDE&e<+KBWN0uEq3RPb@p?QURGiHTOc;wE6|t$OG_juS^C3{J7Pc9Vq-r zP)_&rZzb#!ETjO%kXjEc2>3yaM8g1BXkg~Z=vOB#gDUd1NR=;n?#*}a{rMQ6a4@@9 zNP*C{kmrSfMPF5F4$TDF#~^_vfqzp5(7XFOG+P$HV?TM(d-9kXnfm%aX0DghuJ8W( zWTZHpRHVQ?yyJkuPMEkzSEOd){1qk!P;8zVB!KarfKO#K0;uF*Wj4t>CqT_v=DwIU zsSZ2Ie3LlOs+1$v{4^UU*GIQueFFe~6RWM2`XTe6S#7x&Q(uZO;K$uO;Kc7^IR1f~ z;jiT0&a~F+U`g)GQ0e_kG_9wtk>p^m|C0IrFi;V_lFPe-@ARhC!!B4WB|+;R0NPM- zm-OMtZ3M&iIKHf7!_#W8K7&FrSd3sgDHQ@ZdkM&ybw&S^ZTI}{*G2)0PlPNJQBduT zdRb}V^Im(?euR-1@&`LmOAP#bhJ>&Tw@f$x`-NoV^-T{Y3D2m^=eK4Ik-YOOC0y4c zTo$+(glCTusfF|B z>|m@P-r)SEV46e2t!BFa=u^w0Sg+*{fqI=fZ1og-Vbq;_wt@n1#LX= zl!YxRh6FEFh%%S4bI`bdAR3nqNs=U6w)D;gR+IE= z)6xn7EhGxw=fOyO&^QFwfC2U?tv4jAV>NNU>IKl$nnOtIV?R2c$N z;?e&S_%DwN%`a{O10C=4_2sW}-pAf~+-xdUR)v^wP&%r8jAl#&u5*EwL=prKUh8DT z2^<_#C_eaK#WX7iqD1)48ZYrkv}SihMJSFDd9f4j=$3g=t;V#Pg6Q5lK z&;ImRk6w~O%U#69r34B%9jjr71=lVVL<2{Ys*D8nWWyW6Z71d;94ybWPe0Aw4>lHSrTVkP;h zI$5C#-2+q(E9Mv1C6{cGs+X;3evGPb9SeK8EWB%bzxyOn~> zgz^fpVEJR92pYXnwYYj)K-n00hr@#*ArFj%=)D9EwCL?Ypb-6A#5%5B1PHnQyu#() zCzPU2@5TZEFH@&>E_LI9&V6(+Q@JnDsdl zgt9AF=+1Q0F!sm-1!%>sFSIXciz2BVVW z#V@N(EjG43JNp)=K%NFO1XCW}o;8ckON>G%)Li0ffN#Wan?84{@-M!C*^tz>FUsPq z_iSeTN%?B=#>a->=^AQn4h)W~u~y13_Ie#Y!$cSU*wJS19Y#^T@7wHL!TVjf!Vkc) znMFScpzn64T6&G&=-Z)us@M1*W~np4-WV_c&tHoHF}K<9nTQ|5(9ASvQEOYATmow% z!_G)k%d|H;_PaQn(EtJm?k^5=?qvNd-{-{a$cLkjfKRRVjihH!y`f*v%8&`&)U6s^ z!>-NR%XC6O>+k;04g-NEPyQ?QF-nT{o*&~-^fYH_m#6}WkLdV7QF>~cC5kvnFy@mp z%>>tDem3U=x1(zU&$4VeS8|hKB)J;}Tz0;J!rjQmt-%Dx2PT@H9xLC8pgh!9^5Wr4P9k=bA%sOzZE&CKMIP0|F zCgk$*R4?>=2Wl-AWNO)SVytR(xqcZ_Cxn3v&=Ld$R!f8W|Fy@mnrNDwO8%nL@qA2n zr;{OS>HN#Ah-b|6pdI(mGi9nAq%;s`aW{g7$8BHFW^_J(m}2P|?C$*NOw8sI(4CNj zvZ0>S8A6<@S@ptLZ=GX&kvTo-sXltzKfuc>Tq;y>B^q^s%~$f9)PFc7zL~S> zW7Rz|BtT0TPkAD&CP~qYYR`rm13a#uEnQlOqsvQP@2pK=ELh&#v9ZqxU1LY2J>%Cd zyrsxI`{*rBZ4MntSt;7_8TkaYnY0!zIAC7}L7LDkO!C@CZy*S056C@R@}8sv6aseK z;be`YI#bw`s!h@f-Zk-I<)|LFiV@#3^bX>@j?Q1T2L8bmfo^n7A}KY+&HW$)S@dP~ z*JYPxWLGxMQlX4PN2NSn)PYK(bfTqn$W59@+)ko6iG16&^)` z3`z#PaDk?%YzI+oNOZ-A_>gBvzXZ>k+V~-}ms7uwy{W|; zgDHn*J>|}BnTdgtx|-KZFdQpYlp?Ix^26-%;K$D)j}Y2;oM{>&2AWz*+)f-{3Ryc= zO-6H6@|nFi1LQBqML+Ka+X)OLt!MlYJDctPm)uaWsqXCW)_U%X zdk{iEE2LAG*~15h?=yV2GvI=ZmZVwPPjtS-erm6Uh0a!=f@XR=vm?hAM*p-YIX{@q zAl|Z-9sEYmZunhWh8t$>JuZ7n5()a=SS0~Z#|PIJ50&%4#*BeeQNW1tpg%IqVS5x0 zIOGl!>*D1hnEwo7}X-%It{8KouDY|?;5BFA0yXW6*4cCpzn z=nqn@!#^(SN0}k`Rj^oGRE71tO*mC^^E*g&!FQPgH=_@X{^tN~E1&Vumi`BC$|>M! zhNV7~-pe;V*&nL%m07TYZokbxqpA`l7&9?bLsb=0)p*?T!W5BY%{^^&=AHQ$9_8c$a00XDAU?DzTzSm zmz*VH}Pp_0rcZ+x@*^ZoB#T3^;t&xSb z5hx7h!vWNEVq(FD1R3N~p2`97Hbt8o(!ZSpp7TNnPaMQ>B{G8Zrx96#)yPQq)oV4A zjuRsKGFvY&tx5F;mxVrpzXc8~+ut7WOx4SW`;X;dSQ;N`TPiW(?1GNRt8yh;Ok5=0 zF*dIc_QhXEu4lf@Q_9X#CXp&#Iq`^CEV!o$G(rB+1a|+mEB`VDCZs7QTkqDKuYH#} zIngmoB$1kGZ@b$K#nkh4KdvDjCuyn?5C7}}D1lp<0spHnx9>!#e*V|Kr-tjAkO{&| zB0!wYC(8u8q7>mI7h}|`cFAw*eD6fa2Djf@Hl=LRqNE?)^Qn*38vg&e6S)|0G9}0U zDG%(ycLSfPxMRQ0x*w7_kgN6)!al^Tir=SPq z;ou%UB@|1X2f*t=(wLMWNl!v(V|6}*L*XGd?bJih=|6uAT;1nG5M{z zv1c*kKtKI-ra2yZ@0xB2@I0>XsU7#-hs#Pzo+4RP^KcM3T)7{9bxwYr#1fHKKl!Y5 z*RqPWszQ9tDZBNWDBVT!>mC;G;qkh0!1Xy!P|zzP3Fzx5@uV1^N&2Lv2c~yTw#YM( zI2hP);Vm6$kvCfA_8Tey{iOJDx?tYF9N`-W_P#ctY&qN-XA|%lJ!~I!>uPk-3Fx{) zk^|D7kR7t5zh>y=vHj;c@^2-0P;})D*2CUhnK#|R53qtH&yxPfwcP;}FVMDL$+>%? z%%_hgAeM^{nbwKa%Vzg_pDKV=zFsD95D9P`LE!o?v`;4I!&faiFlL6vK#Y|4v>skf zX`nd6h{WT0R6{630m;yNNPPqMtezf9;d?O83ebt(%b@;UB%Akvyn7GiVDm9`-uG{d zJ)Rc@-bUyEb@`8{^YFNTy+_q26~H(+bh$c1vpd&;3OVOWBg5Z zjymIdbecgxx8TMS$Jq>EZ?a1g?rdzpwwNg$Pqrt41wuo6@F34=g<(gn>+E3ZCqNc= z7d{0#H?bA0-y#Wip7PpSU_jWVe{2G*^(0{}wDZr6Igh1$IrlrLMhp5kc^WvtfHn4$ zE)Na)lR_k3pJV&R>dGd|Uhn}j0KIW7v{9~e(JEvE-I&M<2W zN&plymUEBelEpsE5B}nEVNwWi z`b#f`$L|2MSuOjIIb|^YEe=-IyZJ)k_iB&obxk0FWnb);W<&DN;+2yfAwZ*3!S~eAJ$?jax1CR;iTWtBLR}#z(>*Xs}rXI4y;?9St!IOt+Ff$`ES&_WmTOm|H79p06VqB~~_5m}V5 znl^f36qP)`dpcHQ~&`L;Xk->%LdCi`}NRJk7=2S<}} zFcH#;$A-i&S12{y&sP=&g1CbVl5K`c+FbUJkn7vdm$Y1Mg+1o(71s#?yu5 zP=fmMnET|8{S5D1l+THe+>jwml<+pEFa`8tOFDPOCSNF1voj z@&*}&2|f;?67z*^ngc2|O+e=~QKC(FuL9d+-BY0D`r#y9vjlE zrSiRb<#@~e9<~Qw5mI+>MRHhS;!%NX0|@H(r{8e>aIw&=QU`=JZ$t8nI&MPuc3`>2 z9V^()zO<^p)Nv>rH$FaieZ7Xm3F9V~?JWE8Mzc169+V|P?CW7mnbQ{Z2U7r0TJ1TI z6G@qw(24nC7W8U#6>eWFO%Vem-=br!%YqVl)AN;iZbTxpEz8Vc%B zVZ(~E*ccg2{R+DTq;kIxf2S^6OJCk?@S5d2$6-(2jBSopQg1&$c(NR|Z9nxzKbrT` z2typo;Y7xt&Bbb#FqcKyAMIV$u1MEt3hq+F_Lr%g7^3wqppaCQ6qIVWuK{+GpLtnt zgf&4jM3B*R$IM?R)$3IftH?~ca6k~1cQ8`DInm6w*zW&utV%xlF*;<%G5wKdmLX~# zZn*c|m^PJZM_@}G`47jBCSdoUznqDsrdw2L@0D9IB;8%hlBQp5x+pyN1YXXk_u$=y zn9K;oJ$_!l3X%fYbe7~hF;}%fr6vC*prU(2Uw-xQ5u@vZYv=ZJMg?_hksu(FlATF?-K=g4juls%JLwGhNS`dK2R){HN&+Yvl`?HkWCM5w zON|KtL5}-tk*5%mv}l3X9{{+4v@?Dm#Qjmy$E@3Ol(*S|zzB<%4`5gsG1Z#!%=;|6 zVGFl-UGYR7?}Rxmr4wr^bl`FjdyZ;jOv)sk{7f=KOG{(HzWk;a)U-Ey_3%UO-XoKC zl?B*2K-E-D zI@uZgjmTkRW3P-0+8d{Q63Qr%3&(^I1+pP7t$)1^$YCpXtAgKYQrtAxb%n|)MffYZ zbEnOEZn51yLBN0TNPGcA(hlUcebn1Nm(Nx z=ia7n-Bsi{1mb74Z@$*Lj8)l=;u2P%e%ppJ$(r~lhOw~6~B=w zOX0M&{y%Yv$*MqlV{NA8vI`bi@fOb46C!}klr}b`U8V2mxq6UMNEbBP9gBzvY4V&A;O(0 zr0{WK@%N9EOOEK^V|qmt#hCWEO)UDRMRfg8sWkz za+_CgRu|KRr^w5MET&p}rq8c<8QUo*&9D@uC@1QX>OkLFILGEb1x)TRwCm>yuI60* zLH@N9ART%rj?f>N(_3b^lW@Mj3ME z%F7(xOBauScXg-Z1kQ}}hdzg3EHuC2pQMSgMtfE80C5>;|GUQ+^d;o{E)a__PX@`; z_}VEw1Z1SnQ2YW9IPBYiC+m3=)()}1h9woDua5J7E!_Ikoz75nx_}jH-t?R^ojl3Q z@{YCIL?tHZ-NyG!RgmMga0#1AU<^HDagwDqHhi}ZnN)P4e>{VhxW8n@re5H1Gj;B} zZRfcuf(-~w`~Gb3qMM97%u%dp!C7WFOM{&h$R|Eo3@Gxu6M0S6)fYra!iG})vNlaU zq!7J(=|3?isj9C4TO!fdLoy?a>`xk0iBs8Em<{$9zBd%vZw{7P&i0vuf0vrSD`pWj za;OOhT@r52`&xl)3SsDOS_n=U=(4A@QLc7P`ZrEJ1M+B376ZO%@U%$=CR2pY zhDtr22Y&Oxy!|BwcIG0`h1Sb|T>@~5EvDAAzN%787+i)G@(QY`WZ|6TFq8#+&Ry~rDT-hHH8EU4DayY7TBV9^`C@>P8*@kBMbTPiua2b%!CIizr@{Yl={VXP}8 zs7NQc|EPGv2SG*bBxb!!)UwA9@jXlr%WT=-ye+Cw7~A0FtXzlO#_$C}f=A_`udcsc zuJotcT2h-e+1QIzXgA&UnFBIWa3epUqQ>XrHyWT7vpzN8O&NKc@QJ`7R#?H$%60U6 zs(jPgfz)ybUV0L%HFOQ6`^XVCsQ3wg3HL`jN|A20wC9$Rne$-!*;8$*o# zp5gkZ7#@=Tr*t}fggB1`G2FEo_Abf)0N*0uK6v3pO)*Hvi>AM|#9@~P1ms=65pGn{ zDPv*wB!xtRQ0lKF+!>=)|I@ab2WA+`QndN0^D!ZRsgZ|Pq~$BUjV$82BdU6@10q8u zXfkAprI1hcdrl`im8(Un5D1IeX4n1n$&f#fB}{KeFmEG7%W{iXY3d~!(WI{^^U2(oW2;#ZpLeb;Z5fZ-!@9FCwJS#za^Iu-B zHC!jl6Uic@j=$lF7z1v4wpLzgN$!}fNu>$TfXG-g$zNekh3Lsmo3mcDP#QPm4#4>M z%0rED-4{p6Mg>QZ8@D@1OjW(->Rl>yei)8-MgYVZQxN5BSovVaSb;;EzNHhNRd=Vw z<;ROadiCozoXqyjjclW$VQmZ-3y%IQ9)^v3b1+9JBK~FFxTT5ytis;JXd%wc50)t4)+ctJXl8G!reUZgcyK z!6BI3&C%v_X`3?A{`Sc{$FRZ*?=8Idq+H9&sL#(_qx2K_TvwnY{2fPDq1$?NPcMn( zQGfnl?7jCtox$7hiy)%+PLL>JA&3^8AR&5PdRU0wdnbDDy+?>{MTlM!EL}wJ(R+y+ zb&q_XXPFjQuDQzRdQZ~(-q^XP5hNeI0`@0SJeV zD3tMIRV2O1E1Uk-64q0o-ezVxkk<72z4#(M06h3Q*KLBV{9{#1z52$Bd}*%ox-ba#;Q=6u);0Iz;RiBSVuZnf#+wo}O#j zt-@QsP1B<+wXDPh#n_CLkR790UimLSrLn1$&vU!!W_Qs4Mano4ZWO#-z$n_^xS8|K z>A?5!#;S@l>7ef`9f4R8q>IgCYayTCe``b#~g|6Xor9Op~**#q~v7*?P;Ol<^I=c6_p`T zxxsdC4ncw6tQ8f&Y=nj8_%++zNCbSh%cqDU`K&*!shYA;*{J|^sIJ-QhZWFrKK$_? zs^c5;{qRB{7y5TdugT}*fAqSp0lc7px<~>t)CXGNK)c>QMfRk=+&V=Z1i>APvc3gc zckX+TJHhYwwZ#9Waj!OJ%ynR3Xmy@@3dnzk_5}&jxhlc>P2>M0-lw`8UuuX*rKtUJ zcz2)Svj49P*CAHVz&z-r1NG=M503wS3GfG+V1Q;9Gll0IArPXsov9rE^W6V4-~Y2t z{%2qQk3IR{M)v>Cey8fo<%%M|>r69I2H6hkLVhhyU}t$B&rtzG4A2b!l5KNB!y$F9 zEBj7a|NkYN8xuOUh^hN>T)s`&=O}vlXu18~?{RCi*$pIMxaY+{?z5gnrgpZyG~G1i zu!^43>K&?nTND4>7W+pu37X22VA-i^Uw{;QlGElHc@=B0bauo1jraur=aWz=qqC>? zMaS9hr=aFB-DyjtDJ^wh)M*cR5agsIH4#H2gilUhQouBHe*l&HZ?Mt*>k@gWFSM#x zG3P;Gx9IaK#qK8lr3LkPOPl)*atx@NVSuXHS7pn30p0uZ7$@m?;_r|sRA>-74&fbv z^|Lr>`|$=RbM;d7XQ&?<^5y4H|MiE97K2h_=-a=oiS;$kJA$a|a=`-@L_k#+e#~F9 zvcx#Xnu7cH0pWI~tb)Y7#I1n?FCf{pFNK||Qm-MfC-(6Q5!!*ne-ki1gL&ado=oej zr)dnxmJL#7FnswmjA(F6?f_Id2d{?njWqp;`;&icUG8f9N4Cj9q-x$3k!h;0=TPsg z+28dK!*D2Ye{hn+-}-)z3;KH0uAz>BEgn@ufU~ogxKUN(+VciTLFPQkwQhq z)F1D;U=8PU|9@8=|IjS^DD33uD0cS z?=k;Kg)450#uTI{&a@;;QKLIE-ZF9cEi?1`ord!b-@k`13>L&9$nb`dF2Yh{Io$8^ z6hu1_HKRy}Qb|)3$ZdA`{0c6K=TkoOJd7~F>3gaTq`$EdZ5zGGd;Pi7^TjbAQ~nC;GGVSF-C`#MRQJApRbY=^ zt14owukjf4bquU2Je`{{5EY2=+DLtnyYNKr{MQk5iQ@BjcOB z9dXVp_oSQEO_AEd!ZPbVS;(;LG#Ftb7)mmPOLVztuHgft`>>#Kz`eEho zvqJt0Q7={vvdxBH%+)5d=mWWekMQ;vyH*AJl}q(BG)$A*mUuXVQ{$?}tk-geZR)zOKR z=Nb4g(Pnp7s@`MV)CKn3KuF;Fa->unn~@^{5&dEZpZy zU2c1vnFmf2#l1*WH!U0DlNuvL-%q5ZI8t2dmPF`J!j8PmbhRLaz8X~c9P2%3&HXmt z3+9w|Q%v^_^m1pUROPVe#4h4j(GT-!Hl^U^j`9CfOOyq=Vbndl`)<;v^7B%dqz3Q% zxo(WXu5#P@h6NWsT}AaN_4NIGb;pjhAolKI|8X&4uEY*U!UoMYTH#MV)OtzHc{3WD zZasq61lktiRScjL8;+)@K3pJ}So`cZ{Ic; zib|1k5_z7z(mBal5N~!{p#yKgWcn89i){nSUre_e1aedQTL+{Rzto{lWL%?lZd3uv z2Es;4xbV%tIk5eFMW(sD@RtOkoEN%*8|Y=LhWy*Cs4Nyy!_%No4@%}7p^zO$$-+4{ zT^?9J*YT^7(M#9-g08u(6)sDAytnCJs2Te;emWBz&jx3#!qcZ|;*wt=<=xHIFsC-n z0JX8l_ufZChUqyP-*1lP{p-)Cy8e_~sEzB^e3km<9(HA8qwzO-U^O|;v|qo`-Z?eC zak{*pa#xt0-1nE*r2S6*r4Kv`-+4G*{`D&3)xCOD{}Hq{mbfD6u}E^&N7k(jgjAgWymC8u zZZTkFJ^PI`{5mlYHqBb1Q%2C~dmO#3+fQ;Hzun#PbY;BliryeW4`>&;b5p)wU_+J1>HQ)_myEb>hs@H zVF1~h5MIlJ4F=q4SRzEgL#W(-yqnHYQgZvkR3Q-ZG2Hlx)65T!rX9tsws5O@+5$C~ zm2-}GG$;pAzeE>kXVUSHe+2D296!_86aIz?KWQ!UIvGI*){uZ|qOu(k?(`!ee`Ylz zk!Y-LkNs~}K)J|O>~d(rpyX?iuodk~8-|+Kn*`@*%bfU*V)|R^ruiQ24D_?Ugx*8<{6` zJKI|>pl&Ms-f4rVm?O`_Bl62VgtWhO-ZgYE3TIt4^_!FG2 zmUCF#RLD@ygI(#5uL%OjS+q+9NBuHUzA&A4?Fr$H)NE+)PWfO!AB-;d(9^_;SiOvQ zTidoF7cl;WRBM?WVSH@pXL5C!eqk};DC~KSedGCpaqRgYmnBRRFrD|Rug#jCQj6NR}Z3BhfFbA^lG0v{l}4I zOXG{qbf?sxpZZy-UpmS}nRb>`UWnRhQVTm$KItC%eh}Ghv_7{8n5!Hdh7Hdyb9i(; zueqJ7->;0N%WsaDL~Yu&WE459UI>B`B*mM=rx>(qKu3 zdvC?jDZZ$fJkk35|MLCTDCN^AlR=1U{f;-Th^%q|8#>gP!bTT)`Ek6r)Ub%9peKqk zaDK7XC&(YBg3oCAg1X`G^{GgYVv6PGU9%S`@XBlhPVuGrrgH)N!>6p1TOmcqGI=t| z%Luh^^nkk=LYqx#JUis~-16F6xUrQ6fI6fC#~4vxuMH=XMC;&m&U@hjS@DtPJay%+@~ky|q9Zx^+W)5Hc*Dyj53w<%7;Vk>jTP(XQ6z9WsybSD}9)ZR`XmDSb?H(8Q5EsMiSqn1%CH<&9YC(;|p%Hmld<*4>;M8tJ#?isX zAc1F$RT z#8n_GeKTXK+$i@@xTOILRJBQSrIqyXd6>_on*k|SeBw<1VU5*_b$s#gQm*~!4Eyg1 z(*b>)^D1j1<@4cuH5A+m`p7;`Z9hT&Uy>E#o3va@Y*wV)Gg{c3e5)r)xgCZ7*N+R|} z0|Ct9DWh7Z&9xS+EjSjO4VduSR(q`I$?47VZnGhy6-K;)n9?fj-G!=Y^;gk zjVH9R{C11<)#8dxr@VIR6biGr9h{MqkDgoVRiVh>+J;V6EzW>WjZwr`hS12-LoO$@ zEP{^;IW;fHk+!~|FlIzB4*Fx~(lB9qGc)hZoX)24`{c1G^(S}svKf*S`RM>0G+Bfn zvwmmS&=i<&b6=5cs**{99c(feHXy$pOlHR3w(|58k9>rh!v@(zLdIjl50V93$ereBgpU)f0n0V`HIE+zQKg{bvtQ3>0a#WBl-PB}4a3 z?3dfQpN5e=R1Y!E#ejbrGzf4|BaKL7^~{XTixv+v&fOiacj-aCZt!@VGC&1lVU+$_ zV!Pe(Dh>3S1X5BPv%Wai4@RrrKWF3meJO&$shnq@o??6z{+9>+(Q_)Ea-s~r5|&H- zFYh>oo9pyZf9k%3*3ns=jNrb`tl&0N+z!gnkk32M`;3xi%}zEtrSqbaq1|@$gi*V8 zR|%3WM)-!Xhm1or88a9Z7HR&oFJHEMLKP=&wI`&iUEwf0w%;Fq;6{$>YP{x%1PHkmF}eRB19$_$uI^eqbCT&Gcg#E|GiPWNuErnF`S;=aFF?U_zHI<^aY%*i+U$$U!R z=)bi9imb-92>n8S>Rz0}MwZQ-z?ElA1CF^69gf-%NMKJgmW;_(-ypf5FKtY&FsA_$ z9Sw{9f~OcHVx@o|Gxd%eQ*3oOCLwt?oIA;J0}L*9v;o z?yY)zt3fp!?Tp<%8}R2 z^t+uUEn`_&|K&9Ho#7Bpr2cx6es&T_&}BJ;?GGUk?^t^e|4f+ zKzU~U{=4Y|-I0ch+lwa^@HpMNcgF?<=F%KwXgfsM6!DdMP2pmN$y&;`L5Zl6GPclj9)ODESLIX++Sn9H zk&_dMdm^ZxbtR7`q=IJf)vY*blf%_SB5HlNg|h}IUH_~zZ@>G0>~0E9Q7Q29~W)m7j9MZv4y;(*^w9HeCwo zF1OUDpxHHE5?8c-L0|t28qj)TmY=9*?(XolXo9`GtyS|0Ww7 zN?*nM1+`Uo5jn@zF|b)kTd_X}3F_=hkCqrqv+(T0>ptAQNZ&g52VIW;O4h7oF{>4iT*w->V^i$V6S?XM%MGqhxBE> zr##?t(NpqHV%--#Ef%;kQQ4usQpNeOpWX%7d#Fl^q}fVoP0Bvb2UAElUVK=hR&hg*-zHWNa+|*m z5}nToyRM~q0LVL6MvQ~Xqz^p#q;lqvH+eRhVSRCwL^3V}R3UYn&#{B9>Ih}8<4j~W zaCJDzM#&q`(*5IuX2R?7EPBpmT>7!{A`GJ7f5kyiH0$#>r_pY{8E-5o692krcO+iP=hVo=ZisXe+@ z!Z37eHS0qBDf%GzK2|zNX(*X4A*;-t_Rom+{4S;<9QAlw8vACvj6W=E$%}gKD-u=x zE}jAU$68!2<2gei#vzyTi3l> zJoC(YS0V(wpn%@wi!}-A<3Eik+M0UmJdLVo(9(gXTGZWE&Zd_FC4Hbg;M zp{$aD(Qqq~`|$-vf@x_WH{-F>E`h}4w>R?ke`nY$k7c7Vc{f;RP;(8ke-q8ICSck)58J#1L=HcJjuzs)0chgkI)jTJx0>ufjVr8COc zKd(b)*EJlTp&CiQRQvbVrS$SzqM@TgAFMB-i>Jp1@h%E&V=?=}H88$+o^&E)LUsbDoDiw0I%LpfwKDI{^ZXkJ-C)vtda_9Okw?nPw z-43$rxI$)2$UE!lYn9h&pQv?*X6j3$Z$gLj9CjoQz1-8-^@y2W)>d^c{C{t+_A@g)Kg8-A-yG=pLq8wPvtH3}+xULSem0d8dv~9Uu)XuJB=JBhO`>^udzLiSH}|ike;d>#Qc9b| zoe?ytW?kNIzN~|1eby27YOCatn23KOYY!N3QPhH?CA+Nj@{bF!p{3Xjn5Pn~ ztAZ%dGha8s^^3O+v9*Ea=g5!og`m#{jwr(_X{e01`u5&0+F%=q34B03TB-i}8G4mZ z?XBpb4w$6hV@`iv;QWaDTZI8@30UsC7-Dk&Vbo`hn40R%{43%wrGe_2d z_F2)WDdns9nx~xRe*2O37=m(5@LoJf-Z}9cg$ae%a-1!7ss7G+f_J`D%c)x7wVXLIw4c9b!^Q}k7~5xxR{rkE zv1f>Gl5CYO$qXb8oG{`V+*BojS(uV4rgG5SUhZ&4QS$cDS$_^JuSSYY z8AvM|nQB0RnprywjzQ2m8^J_C)E>UUf2Ev6uis~{>$ti13bRoumjliCRV?WXRFuZ1 z^T$tYwpYDznGP!$(^W63TFIxeItto zzF18wiMCDv&7vCfc|8SKUZ0RsxU}+Z8A2}c5u=u5Z`$6?%YwD8`C9k>uS=g3W=f>k zPD?&`;&?#95x3EHw}w+UifVCpU_xNqAU^r_-3mVP2-cncNQ=z=;VM-A;=A6r@^2I zDz7fr?kc;<@tvkJ65_Z|Jt19ErA{st2_!v z1ENdGUJzj0=KVJi5s$-M4lIyPdaz4d>)zg8XI!2%mj-=>-ep6Lb3Zv{)|;iKy?uJE zCROe8c<~eup+HC5Q`*(wXDeO~IS{Gn1?{M;yyvNSeF@K^J;ZaQ=ZM5Zpxn;;a}=VYqMKu%i42$7qcmKK%OOGE)>NK%-(>HXV=#DDly z{3(uvRBtPo??!0IOqmoon5h+ zsg(faX7ZE)3p`q3qezm$Afu!CCN~Z==%@FlJM~z-IwU$+{Tk#m4aU9Prd?~~?8Ma% z5e1DOh@E*r}wVGrAptI|PmoD2& zd$aYMGhf+osRjJ|lG#?)`cu=mOwa+VITp|oYZ#*_|MAXL0F2q)%j^5EUqpA;$9RB( z9SQ)lPIoPyr=5UPt&%T^cCtU8dG&s=RRjpDV}P3Ycw-E~02qRd)MCEWV4fwz2^pW8 z4Unr>8Dz8bL%*1H0ZQ*Z_xzr?y)l$6wKwy%#;60O4yLj=W|;B9qS|bb5;l+)f2CpC zt7=MG$Bf`Ut&lW1&l1;kZ;M{_mM2lhftn8)ZY=+to$&Zw^>BD;!Gs!jH|?R^@|(L) z#-n4dH=fiYmrC99WwX^rTV^MZ`Dm0 z0_kX#&8bEX^8z<{tpJ`Hz>|LH6RIe+W5zpDYT*);J6Wzn=#p_}LcTLmL;sy({H30- zmq2Vh_b)Z$af-GRCl7?`sb2L<7{vn`-6HM0Xlaw@>#DUR??K;-{5>MJdD^hWvrAcW z31GsXmh0BX{0>J7&np7(O6%VYEDY@oX@c&DbaV6b)0D0|6YO9^p)|SqwRHe}cBo7= zWxo5-G9pJr2#iDon1ej89Pq}@0r>4NIF$0GLPy}H^2Ds_7$Tl07^L#ajPj%d={ygA z7D!9=CNNT*Y__Qt$#vP+!+Hr_$q1v8pQ)jPRCG6xumsVSd7bZX&en52ARSa0TTm;{Z-<#DRDWL^1|xb$ZnA4RlcqNXmqKzi1r2*}Rmfs@ zZWk9^aXcMVms!hYavED?PEWH+a{-_rNx#3>mCrmxr_$%Uso!ilmnLgGQgk1bsbIPmdf`9)l#YbSqdOF&= z6N9CxUKwIZ$zk{Zp(O)0j+YJxR{$5Oq?KEwPmh!$ryH}WM&qd5=0)hGph90LI!d{2 zjiQ0D+>ES<<8U4ywuQ22VG9#f!};gr!+J3fM4rTv7@)2FXIJ9Uib##IsNnNj!l(tj zQzC_9sTR##XL;~+-c6b;MRh4)+na@Ti2GBXTiJR^afzkncx3{-p zgp8QFT?j&HO_oO7hLb<5z28SZE`t92-|Oq$kAF6-Za?iO?uu8jP7#HHiwMH?6I)wb zI~F)<$q?|9ev#&lM=&%kWim1uQvP|W*pL^WM#Y`WU#R%sFPeMDJjaK*X2MGt2;GCZ z$6wY2mpUJLBlOFk#q2}Ftf9XOoi*INFNqOx(#X4I6MNmC#uM@J4<89)@^|jZXVy3! zI0WTZX*Xf*@E`Q-EAUL~tS^`dH=$mgTL*J%%sc#D)xkAmMid}jfVwx;wf9EI7Fd(} z1vu761?1d;@M}h0`>fO-FyzwC*PUq4koK;W8m$y>MhZs@%XdGyQK4E@2BugO2W@vl zq^uEM3<3p9u8utbJsv$Dh^he2`jxHtotSo3sSs>zJ$}1|SL*}mg5uc_B|y)1SL8Av zej@#uf<-hEWxSVFhC>pCA)>25y8=u-S6F;-WVyy=^kspJt-^K{AlYg- zE!(kug_rw#xIdXA%!+^iIjqHdEwuF1b7?tQ?7~i@iLYzwoqm+>?fTwg$)Yh5E{#Y} zB4-*W6)13g04=MI!sK1SjIQ$j?hYm$UJES5Y+3!A8!%}nJ6$q%O0DnTdxlk-jfH5v z`iR5hITSEg2XeSD;Cg~eDc>bh=*j4jNukxdEfj%L8Y%pN^e9Z8-<8``j0B;%2vup0 zZ_OUZj!~q@P(XO~jZ1^aC89-OR&7&ukRa2b6{=mQR;qYw1N%P}{Qk|=*&bD~W1gs_ z)gRY)_AXDE`VVahol zfi3w54C;;5uo{j69ctM{fxauRdg320sQUsIl_C;YVM4SOvo$cf~PQT4m>q!MnR^JDEH$r-$x}IfzT9)tV2zD zleAP|N}FoZV+661c#Wr!xLvZ)x zQj>VfuSHP~AqlTwNa?fJc9dqXQxnwKggq{~Sxmg#$s$o^hI86FJ~OcJYaQc$5B%eh zK3jh)FesQxSbTf7W4t<)KArVzKva{iKMY1+p*>`_=fK3zl8netc$rk8-=@-sA?H5> zZZr8}hb^zD{4eQHR}0%JR|f#|XB)4%Rn^oJOlpY^5|TXa*X%uxY&ZW_T6X z@VUEC7J?3m_ zVDml-zsqdnvKIFubvx@k#lW}-17ZGi+fl+Hku?!FVqYPb@Wc<@Q4}+LZ5B^NPv+`&BH+l?Ua3uiXt+g6 z$E{gwLb5OD=RhMdVvONXnkVI+Z)^DT-6US z$p!-G3)ktchAFaqhqOIB2B3plYX_e4gARG`1RJ?Y2_ zH}K2a;8<9_`veyjGgl0))0fRR*=f`pVwS3$$qFs=DVxcIfIe(P0>tQ&*&`x&*1Q6_ zs+z9OmrXK^aW;s03BIV`Z4_{E>;ueiJ~|t=djI|-@DP`JX(fZ-E(RRvg}U{&S-d%N z3-~1jVFctVK9VN;l7W!GY7p`{`dytLY!HejDfk{Pb+8yT3tXNb4EHL6D=zxmrHkB4 z7~iJFVacfV(yIN}@|I80hQDp$Ulmxve2)Y5y*;pBtv@c&Dx)^|d&vXQZ}i0KIcMj> zggP~&JHDauar@oz#QZO{C8~KWZ5zl%4XWg+%g?p7X7AvucK-ZG!Dg~QCZSglO1E=h z-h$-0sm+~Msw^bZe zaHcbO9jm>n9VRU2LK6!)azwIv8l2Z$ueog~N~o3An@+fmiN0F z_@oY1vojge6HiOgUVBt;E++lW_7Cmb#@A!l`-UG&E3(Y+6J}8W7pTzr+IexbLIFS9 zpYN~Ha^~rYrXmA0b6G&V7G#Pv+6EU62}`klKJMpidmg1S;qLfs9T6WC`8p9R-jYSN z5H8|8095>OObV3yp?R@#M^c7j_6m@YQa=k>ia-j5 z$gJKL5(=sxHb?}aV++AL803kdi7Z&nxbTv*cOVLzCM4yxRCm4MwHW>II}Boqsy{}_ zXN{;th}~WU?ckuJqS`Zgup-TuLwVdiJ<)sd*>zsjT201uL`6W=9RQ4nfJf1)|fvf8q zJI-y?JL=b@{1BQwJ1M*NlGfeV{IPM*Ev^{<-M1cmx3!tv-Ab>ME8E*SK-F*jKwLcj zMP5zR{P;}jbvElW&8yumu$8NAr&0Ga+hdGdBz52sDRzG^1I!y&P4hqcLO_gw7i`~VwtNDK&U&qqxHZd4$?VoTxETTJeJsf+m(&i@2@N6iB<2%#u+IgZgT#-e z(eerxbTE;SC{o_Ml2G4rhpTVVMYewi>4>RX3nG@G?LTY%hz#Dcj(RK?E-1JJO8W9- zYg$gT$<@XcFGt8N21{u;jq3p*$iEu_5f>RF?{?}CSMldSh=$oaHFoI7e2(-IUj`Mm z+VQnI_(mG!=GCVYyWR`izP>)Lm%u!hfBvFn(i>s(&C!wl`g+mvop~v*`*t4xI>$_V z$JRsFffe$Sd#4whRD$$o79_!)Ewv6p!PCS^bE(>Hu^RpPmXjyI!OEmx_>NOg>`Tth zBg&w1{We8T1tggms2aMx z$~N%6f0pU;L{ZELP6>p@W`>?-o|~M1O1D08Jm8`QY3;5*e+%PTY&_nQuLZcWTh;WR zq+0Z6Q^G+o18xM5^Qg%>ik37nw7HysRG=)iG&etY+t+iQCwQd-Aql{P7C5s%e;zj< z0Cq;X263!3os6w$L{$O9sCs$&(D~7k#CWMjbT@3P*dA~W(1eAB?E$?%=0Rb*>vk?> zm(|+9-<#8Sfe=hMAjecsDG0C1H?^AHWW?o3>BImKmx6L&oR~spLhgI_c9keKhlvR{VdB>?sg!y zzU1tf+TwFgtHK~_#x@MFBvac-!s)YkTqWRI806h|rk;2ghXm($o)N^o^MDdXok$ur z%IkeiOipZ4Sh`RPOVGdL)&s64(L6%?8STptz2b& z0UI3cGbPWNU7M;gVNuRQ(uJ4LFSPl^g6r9WAW&juyEhWnGZ+@x!l&S$Jvqp0VZqKMjjVbY zu;2sId)M5z<~=g>k}dB6$0ek;{bjFXgBEAN>Jfm{AF)`lUj*7`{lL+^MN zmqV2E%0mC+p|SJdkLVm3p7zZRcra5Kn=whGur>9b%zru~Pcr`~w%}xp`j|MB(VkFa?x9PtgGs6fcQ?Z)RoF z3-@N+y?Pq}xiWZ6Qs`yxNj|4ail3T~dBoeuq+7$#C0zii;BA!LoG%4$^#P*$bXmOs zjz&hp4#D4RQv;#Z+7sNxbQEaH7IU0wy6uqn#KT24_tOoX8Z*J43|FF}c&{QtU!_yB zr6+^Z(EaD@g3rGKUzFpX-WK>^EghR*8*<2pTq{qd3)H3GioJ=cl}Gb_oQ=YeKvGr& zKbmHTJyZLE%L(CvuCqt{C*+V%@wk$5s})Z%ZEWa-CJoV>?LK{-)cB38I^WCI@|~w`$V4=|0@%yiafGuXqPQCbc&X2S(2)_a{qSPSWqOj`ZsafxF+R{EzFXCN0#nt zCAYp$&&vcy$2UZ1l4|YeN{^RjnVTEyscb*Xqqcakr2ig2pbBKQe7qp~s=cXL{KISe z{waJe!Zl5?Z+9|z+YLs<@Ld@Sim>mjd6;l&&F2)~Sm-c6eD*)cj&85##5e>XAS%&| z#Ye}~g5tVp#y^Hp=}?TB213XmIX#gHw4N~-30%+!j>mvOtF;{?EHE)(>7Np*88O39 z3jA5}DnRT!v(47RrY!z%sj$Uqb3FUc?~DRbN^`J)?SJR6ir-KCpVh7XDIx2>{&%Sy zj3v0!H`CFK3f~14cnmNg9r)iqVtj-uQ_HcT_P-&8?jriIrIA&{iS6H_fzu5YPVLkt z@S_Ao`ch#}f{4*x-T$sS9R=i|P~p~fKPuhm10WmPPK%Sb7uRb%3Cs%1)IlS zw!|Iha&kdYdrs~zlp2e!3p>vVif7W0!`_=5Tq9kCGM9rJ2YIoSvR;zOYhD=jX9OeC z*(1M0`mMzLh&i+#uE~*o@1!6K>h}3FfL6Ob#ylmwndZt)ng6<@+5OKhnw*x+2OnK6 z27r_l8#wP=v)bghMgdJ2eSVP?Jm@;S6`=hPG_}NhZBFjb506~ z01tS~YP97<-yAVDm*8?yueLem69+07T0_V~TEGfk8~@$N^^0qBR?z$k4VodJYWnhk z<=Q6<&}6zkQec719kopFnoA=!Y61;@ zL^&<)AYVvH^`ZPB(58A1nr*K9M7Gg=v3fg{)7B{#w;8HDOByXX9_oMkt5`gWc69c? zpOD4K25pxR?%U?`@R+o!^Cyvc+{(rPxllRh-RSL~HLOp=`VsS2v2=>N+Mqb{$Wr}T zY}I0G{=0sT_Y)qUxe4?K zJQ;O6F*@U{#w&I6fE9_}=w=xmJ$tq^HXX^L!}3^I7VlMUiHgI@`D|J}jATf^IYMvx z3y>x70M!B3O7TEPu)J2#XMS6EF<5UF@^NZ;=S=kM(OSQlcXaE#kiB7$tdited3v?^ z9l>0~Inp?5a#uC-JdRGBCI4QSjEj}gy0_iQ{sgVUwqK|;4Q#&Eom}k3+~acPOn>bC z7_QJHr*yeCSG1&N+q;#cnFE~W4MkkOeVeAU-+$!|oh@gh#EOsX=D$7$zN>V4f;3l& z5*g?-LAeQ4T55CK8%c`7q5>R!tH_QLxU?h?E z{Y)Nd<$N<*RXSFUb!)1GK9BiZ5D8xfI1i*G!xbJBikQ`hPi0ay}Uz@(D)OuhlPa?fxAjP$k8ed}2K+QT~i(Kk3{moZlcg_nR)z*uT{d-Xv zzc`18Wn}8rQmoj4@m-yUfcGvjll!2=9qm3MflI^_k0SE&YbiGiuk8x58X>wiJ02V5BXx zCvoy$o^6H!u;}jvLp(tlCZ|^T^~h)a7Q*3dUz{yA4`|BRXEa@iOn;Po{;v^Ow{L>6K;}W`(f;!)0#R0u) z>>j^)P&Md@8;_!-ktZ#cIkiO1q=mZLEnFs=)Ue=2_~ubh?*#!8FFF+5n2pE~eBkM3 z6Z^+BV&&-91}FzjDBk%Nq;b-)sQT4&&~}5l{Jq!I7B6JZC+v5GIe!&xWThE9WRrKm z@idx{DJ(Yp$-_fsKI{4D-~}u47h498r^+fv3w zzs-~xKw?8$GE+_NelH66?k?A4TL&aD{X2PDQBpp{YfDLFN~P>efW8$`*T_vB`SS%9 zO%}06=l95%Q+p!r5WmlfvqfGTe7i57Y1Z80Iep?>wjc`Sf!#EkqH%weH^I^Wh{wjV z1OQ=`Z4ysJ;#o}{b9m7_iNv!7Ym+PZVZATEncy@PAbj{ua`?rjKD!)<5vFs0G^x&; zD!CVOv=+6NJf?xE#Cd#0x0&ivsN}Y6h}2qpj6>3wv~)LtR{Bex9u*qBt$}y>bEMA7 zFNsqho1JnJM~A{Ok*rJ^FBj#3Aa3N`7OGH?r<+9RghC))WVhlH8JFs-kFU?}_)(>T zAaGF0up1aT2sl;(nGi9<*5$b(HgbW^MEadIc|-R>gC9$8JXNd~;YWCJ(mwNkhb&Vu z1uKu#dlWNzl~2yEkXHMXY1GWvI|H|fTE8z>y1Y(jNLSInS!|XL@56DoS?ACRC+8|K zSi2FJ5NCJM&`=t)1n-)G9HT1!z~Bi4`eujapb-}9bxo^#Igyytt~XW50#m9~p`|AtgZnt1$q;^b=Ij}C66|F{lMk64h% zmejQI9 z5qnt@#P%*fd-`xiKqZP*38n!))?CIij0%=R{>R+f5XBe672XH9zXcqrv8-q!_TIoR zn3b1ML~sz*y@+wEZfuBJ5DQs1zBc2cYQC&T{Lg9|H_^KhY%%;c>obMd#XzK`GoQ?- z^v?WTQIYHa%F8jM?my~0GE+eLviQQIs3|jr9#u|wv@Jx>H@$PemAheL_>o2{az03I z{BelB3;G1gDYM7q7wvP|VIMrZjlkkw+jQmKZ&)KiBNzGWC3zZbB_yLVWsa$i2h$r8 z@L;!AiBidM5n?miM-tK88qe>QwBaD>P9QlxcKq+S4Uj6HOJG|je3b|;KQLn*AxiTr zBCxt{sRs;>{`Ie?f zZ@+aPabre057${Z)zGE_Fu7LZMnU%60V5Ak@$zitMpqr zJq^^2n4+emXp!VOaB%aOibG^bKnbJf%_jy^kpJyCE}}`dt4@nqR8ZjA6dDjjt9tlO zU#`sER7WnA7`ReX@O}2RsrA^i_R+J4=ze>L(1y&`C)!fks9r+ry_#UUI`{+vzTk*f z90;F$_657t|Xjdg(C4Kir$RtI0C4EovHeN1fFdI}k*d^=4M6 z&~^omHfXQ^BW^1Cml19)LRk@POt)WtpmJ)*52Z%kZlp`3*TE5k8QPtr^>t^CW&}NN zT2^^}Ht54Ui*brGp6ZcD34DAx$28W2OY$y%nHgQEN;FZ$HOU#|t9oeddart1#(tpD z@7&VInW|1R#vwrxT|)qdDp6X{B^^#+p`&|v5{_#N&&Ffc?FzJUulLVFO#oeVks)4d1n^GC+>9Apqg1)|>tcml}eArVYxb>id}|iMH*r z6pQOHq!xGOE4k$fK*LiRL#-Q6@^p}7qKC_zZVtdD9JM;nq!ZGMtBJh^inDLRhrY%Q zK8A*{DsVHs`Mfbz9fnzBo6uwBVG@9X1kU`q!{(i3&=)}x#+7}_SRA~*r9g7U(nisP zO|?sk=m5KPGoee+i#-^zjpG};Y|3mF;LZ_(>)Z(ey?eWj5VBx=4K4J}fF^j4s^3-@ zJSBzjbrA6i&=4h7)%BjS^hw4j?bOfyL&(ywEXkU2MxxY12&)cGiuPO<%94f{KFQ>3 zD0S^Ay$?z*8-Z-DDc$HQgvU_GG7oE%Q^AUBVsJ91loXg-D6q~0Yc!R(Q32|S=qsxQte*!;{-2})Kg&e&E+`LkQ zG!|n)&=-`ZDs`Aq!W04RZz@zWxL)&4`g~_5g!JdPz+QH@CIMB(B=fWjdon?18OwC! z!q_wfC?A|kpq9l#p>v_uaDz59OtD;aZ=?p2pDVAQ{q7q@-$1RV#7j23TTO~}<1DJ^ z(Pa;9@aFLl1m>V&Mh}v#AXbvB>Ea-%F6v-PRw(SjoMPa52jgxm10uM3nwkz9t`;&J zMmIF=UrB_QwG>lrr=M1y2w{W~s1LU0p!l!>ax=f#8_P5t@V;L#BoTY&{u?MX92WqpJP0;uw8Z+2~)4vOybrHM1wpZp%3_!lD?g&7diTf~Ha5)kp{QAA)`E zW!MZUKm-w;pD*_a5Cl*(j=&F?SA+l0n;-R@+($2o7bkClMI&WF5d+;?O9FMYF(1M^ zEupGXd4QaA*aYgO`$J2D;E3A}DJmk@?Vg6F^UHL#-pUDwftpl6K7w~&gU17y+{YV` zXZeucHtr;cVrrrcgv|Y40VT(lznZU1gCYrSgS-SQGXuow#; { + // Get API key from request headers + const apiKey = event.authorizationToken; + + if (!apiKey) { + throw new Error("Unauthorized: No API key provided"); + } + + try { + // Validate API key by directly accessing the specific secret + const tenantData = await validateApiKey(apiKey, event); + + if (tenantData) { + return generatePolicy(tenantData.tenantId, "Allow", event.methodArn, tenantData.tenantId); + } else { + throw new Error("Unauthorized: Invalid API key"); + } + } catch (error) { + console.error("Authorization error:", error.message); + throw new Error("Unauthorized"); + } +}; + +async function validateApiKey(apiKey, event) { + try { + // Get the secret directly using the API key's secret name + const secretId = `${SECRET_PREFIX}${apiKey}`; + + const secretData = await client.send( + new GetSecretValueCommand({ + SecretId: secretId, + }), + ); + + if (secretData.SecretString) { + const secret = JSON.parse(secretData.SecretString); + return { + tenantId: secret.tenantId, + valid: true, + }; + } + } catch (error) { + // Secret not found or other error - API key is invalid + if (error.code === "ResourceNotFoundException") { + console.log("API key not found"); + } else { + console.error("Error fetching API key secret:", error); + } + return null; + } + + return null; +} + +// Helper function to generate IAM policy +function generatePolicy(principalId, effect, resource, tenantId) { + const authResponse = { + principalId: principalId, + }; + + if (effect && resource) { + const policyDocument = { + Version: "2012-10-17", + Statement: [ + { + Effect: effect, + Resource: resource, + Action: "execute-api:Invoke", + }, + ], + }; + + authResponse.policyDocument = policyDocument; + + // Add context with tenant ID for downstream Lambda functions + authResponse.context = { + tenantId: tenantId, + }; + } + + return authResponse; +} diff --git a/apigw-secretsmanager-apikey-cdk/package.json b/apigw-secretsmanager-apikey-cdk/package.json new file mode 100644 index 000000000..97b35e2da --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/package.json @@ -0,0 +1,26 @@ +{ + "name": "apigw-secrectsmanager-apikey-cdk", + "version": "0.1.0", + "bin": { + "apigw-secrectsmanager-apikey-cdk": "bin/apigw-secrectsmanager-apikey-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "esbuild": "^0.25.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.181.1", + "constructs": "^10.0.0" + } +} diff --git a/apigw-secretsmanager-apikey-cdk/remove_secrets.sh b/apigw-secretsmanager-apikey-cdk/remove_secrets.sh new file mode 100755 index 000000000..6f3c8ea3d --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/remove_secrets.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Define the prefix +PREFIX="api-key-" + +# Add a dry run flag +DRY_RUN=false + +# Parse command line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --dry-run) DRY_RUN=true ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done + +# List secrets with the prefix and store them in an array +secrets=($(aws secretsmanager list-secrets --query 'SecretList[?starts_with(Name, `'"$PREFIX"'`)].Name' --output text)) + +# Loop through each secret +for secret in "${secrets[@]}"; do + if $DRY_RUN; then + echo "Would delete secret: $secret" + else + aws secretsmanager delete-secret --secret-id "$secret" --recovery-window-in-days 7 --no-cli-pager + echo "Deleted secret: $secret" + fi +done + +if $DRY_RUN; then + echo "Dry run completed. No secrets were actually deleted." +fi diff --git a/apigw-secretsmanager-apikey-cdk/tsconfig.json b/apigw-secretsmanager-apikey-cdk/tsconfig.json new file mode 100644 index 000000000..aaa7dc510 --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From f8670f5cfe84f6ece11769bea42cf0bdb5a9c50c Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 14:45:11 +0100 Subject: [PATCH 02/11] Update apigw-secretsmanager-apikey-cdk/README.md Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md index 2ab25ef18..7c309b09b 100644 --- a/apigw-secretsmanager-apikey-cdk/README.md +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -1,4 +1,4 @@ -# API Gateway with Lambda Authorizer and Secrets Manager for API Key Authentication +# Amazon API Gateway with AWS Lambda Authorizer and AWS Secrets Manager for API Key Authentication This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager. Each user/tenant has their own unique API key stored in Secrets Manager, which is validated by a Lambda authorizer when requests are made to protected API endpoints. From 1b436d002864fee554c9afdefe16f8a4bef22759 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 14:45:18 +0100 Subject: [PATCH 03/11] Update apigw-secretsmanager-apikey-cdk/README.md Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md index 7c309b09b..85bc1a617 100644 --- a/apigw-secretsmanager-apikey-cdk/README.md +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -1,6 +1,6 @@ # Amazon API Gateway with AWS Lambda Authorizer and AWS Secrets Manager for API Key Authentication -This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager. Each user/tenant has their own unique API key stored in Secrets Manager, which is validated by a Lambda authorizer when requests are made to protected API endpoints. +This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and AWS Secrets Manager. Each user/tenant has their own unique API key stored in Secrets Manager, which is validated by a Lambda authorizer when requests are made to protected API endpoints. Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-secretsmanager-apikey-cdk](https://serverlessland.com/patterns/apigw-secretsmanager-apikey-cdk) From 776cc17e3a527861c9dd25f7b4c06e50c7220270 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 14:45:27 +0100 Subject: [PATCH 04/11] Update apigw-secretsmanager-apikey-cdk/README.md Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md index 85bc1a617..d2487920e 100644 --- a/apigw-secretsmanager-apikey-cdk/README.md +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -18,7 +18,7 @@ Important: this application uses various AWS services and there are costs associ 1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: ``` - git clone git clone https://github.com/aws-samples/serverless-patterns + git clone https://github.com/aws-samples/serverless-patterns ``` 1. Change directory to the pattern directory: ``` From a2dbe7ebe8d587cdf8bc3db1e2c54a277639b863 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 14:45:34 +0100 Subject: [PATCH 05/11] Update apigw-secretsmanager-apikey-cdk/README.md Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md index d2487920e..ce687740f 100644 --- a/apigw-secretsmanager-apikey-cdk/README.md +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -61,7 +61,7 @@ Each API key in Secrets Manager should follow the naming convention `api-key-{ke ``` 1. Make a request to the protected endpoint with a valid API key: ``` - curl -H "x-api-key: CREATED_API_KEY" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected + curl -H "x-api-key: CREATED_API_KEY" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT/protected ``` If successful, you should receive a response like: ``` From f65751b00245bf1080eb9a220244ff7850b0dc5f Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 14:45:44 +0100 Subject: [PATCH 06/11] Update apigw-secretsmanager-apikey-cdk/README.md Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md index ce687740f..3e1917452 100644 --- a/apigw-secretsmanager-apikey-cdk/README.md +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -69,7 +69,7 @@ Each API key in Secrets Manager should follow the naming convention `api-key-{ke ``` 1. Try with an invalid API key: ``` - curl -H "x-api-key: invalid-key" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected + curl -H "x-api-key: invalid-key" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT/protected ``` You should receive an unauthorized error. 1. Try without an API key: From e01f25cf2df944f2ee4a2a6c2138a1b6b38e36fa Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 14:45:51 +0100 Subject: [PATCH 07/11] Update apigw-secretsmanager-apikey-cdk/README.md Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md index 3e1917452..bfe4ed1fc 100644 --- a/apigw-secretsmanager-apikey-cdk/README.md +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -94,6 +94,6 @@ Each API key in Secrets Manager should follow the naming convention `api-key-{ke ``` ---- -Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 From 62f81f74e436194cfbf7f1a37f0269f60e354d00 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 14:46:21 +0100 Subject: [PATCH 08/11] Update apigw-secretsmanager-apikey-cdk/example-pattern.json Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/example-pattern.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/example-pattern.json b/apigw-secretsmanager-apikey-cdk/example-pattern.json index d1ec3627b..906958c5a 100644 --- a/apigw-secretsmanager-apikey-cdk/example-pattern.json +++ b/apigw-secretsmanager-apikey-cdk/example-pattern.json @@ -59,7 +59,7 @@ "name": "Marco Jahn", "image": "https://sessionize.com/image/e99b-400o400o2-pqR4BacUSzHrq4fgZ4wwEQ.png", "bio": "Senior Solutions Architect - ISV, Amazon Web Services", - "linkedin": "https://www.linkedin.com/in/marcojahn/" + "linkedin": "marcojahn" } ] } From 96a60215c2f2c7c7f0fb7483fb987cee0ffb90c7 Mon Sep 17 00:00:00 2001 From: Marco Jahn Date: Tue, 25 Mar 2025 15:33:04 +0100 Subject: [PATCH 09/11] fixed reviewer requested changes --- .../example-pattern.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apigw-secretsmanager-apikey-cdk/example-pattern.json b/apigw-secretsmanager-apikey-cdk/example-pattern.json index 906958c5a..2a5b08f40 100644 --- a/apigw-secretsmanager-apikey-cdk/example-pattern.json +++ b/apigw-secretsmanager-apikey-cdk/example-pattern.json @@ -1,10 +1,10 @@ { - "title": "API Gateway with Lambda Authorizer and Secrets Manager for API Key Authentication", + "title": "API Gateway, Lambda Authorizer & Secrets Manager for API Key Authentication", "description": "Implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager.", "language": "TypeScript", "level": "200", - "framework": "AWS CDK", + "framework": "CDK", "introBox": { "headline": "How it works", "text": [ @@ -43,15 +43,15 @@ }, "testing": { "text": [ - "Create an API key using the provided script: './create_api_key.sh sample-tenant'", - "Make a request to the protected endpoint using the valid API key: 'curl -H \"x-api-key: CREATED_API_KEY\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected'", - "If successful, you should receive a response: { \"message\": \"Access granted\" }" + "Create an API key using the provided script: ./create_api_key.sh sample-tenant", + "Make a request to the protected endpoint using the valid API key: curl -H \"x-api-key: CREATED_API_KEY\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected", + "If successful, you should receive a response: { \"message\": \"Access granted\" }" ] }, "cleanup": { "text": [ - "Delete the CDK stack: 'cdk destroy'", - "Delete created SecretManager keys using the provided script: './remove_secrets.sh'" + "Delete the CDK stack: cdk destroy", + "Delete created SecretManager keys using the provided script: ./remove_secrets.sh" ] }, "authors": [ From b71e550af73bc97b9777fc4f239db882771de53f Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 25 Mar 2025 15:36:18 +0100 Subject: [PATCH 10/11] Update apigw-secretsmanager-apikey-cdk/README.md Co-authored-by: Ben <9841563+bfreiberg@users.noreply.github.com> --- apigw-secretsmanager-apikey-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md index bfe4ed1fc..421956949 100644 --- a/apigw-secretsmanager-apikey-cdk/README.md +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -74,7 +74,7 @@ Each API key in Secrets Manager should follow the naming convention `api-key-{ke You should receive an unauthorized error. 1. Try without an API key: ``` - curl https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected + curl https://REPLACE_WITH_URL_FROM_CDK_OUTPUT/protected ``` You should also receive an unauthorized error. From 943b7ddef55a2bcae35365364861438230af271d Mon Sep 17 00:00:00 2001 From: Ben <9841563+bfreiberg@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:07:59 +0200 Subject: [PATCH 11/11] Add final pattern.json file --- .../apigws-secretsmanager-apikey-cdk.json | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 apigw-secretsmanager-apikey-cdk/apigws-secretsmanager-apikey-cdk.json diff --git a/apigw-secretsmanager-apikey-cdk/apigws-secretsmanager-apikey-cdk.json b/apigw-secretsmanager-apikey-cdk/apigws-secretsmanager-apikey-cdk.json new file mode 100644 index 000000000..d81e104ef --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/apigws-secretsmanager-apikey-cdk.json @@ -0,0 +1,97 @@ +{ + "title": "API Gateway, Lambda Authorizer & Secrets Manager for API Key Authentication", + "description": "Implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager.", + "language": "TypeScript", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager.", + "Each user/tenant has their own unique API key stored in Secrets Manager, which is validated by a Lambda authorizer when requests are made to protected API endpoints.", + "The Lambda authorizer checks if the API key exists in Secrets Manager. If the key is valid, the associated tenant information is retrieved and included in the authorization context.", + "The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-secretsmanager-apikey-cdk", + "templateURL": "serverless-patterns/apigw-secretsmanager-apikey-cdk", + "projectFolder": "apigw-secretsmanager-apikey-cdk", + "templateFile": "lib/apigw-secretsmanager-apikey-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Authorizers for Amazon API Gateway", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" + }, + { + "text": "AWS Secrets Manager User Guide", + "link": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" + }, + { + "text": "Amazon API Gateway - REST APIs", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" + } + ] + }, + "deploy": { + "text": [ + "npm install", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "Create an API key using the provided script: ./create_api_key.sh sample-tenant", + "Make a request to the protected endpoint using the valid API key: curl -H \"x-api-key: CREATED_API_KEY\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected", + "If successful, you should receive a response: { \"message\": \"Access granted\" }" + ] + }, + "cleanup": { + "text": [ + "Delete the CDK stack: cdk destroy", + "Delete created SecretManager keys using the provided script: ./remove_secrets.sh" + ] + }, + "authors": [ + { + "name": "Marco Jahn", + "image": "https://sessionize.com/image/e99b-400o400o2-pqR4BacUSzHrq4fgZ4wwEQ.png", + "bio": "Senior Solutions Architect - ISV, Amazon Web Services", + "linkedin": "marcojahn" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "apigw", + "label": "API Gateway REST API" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "lambda", + "label": "AWS Lambda Authorizer" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "secretsmanager", + "label": "AWS Secrets Manager" + }, + "line1": { + "from": "icon1", + "to": "icon2", + "label": "Authorizer" + }, + "line2": { + "from": "icon2", + "to": "icon3", + "label": "Request secret" + } + } +}