From 65c9f4424de11d2e92f9ac1233533e3f5ef80b87 Mon Sep 17 00:00:00 2001 From: Marc Boorshtein Date: Thu, 18 Jul 2019 11:06:12 -0400 Subject: [PATCH] impersonation support for the dashboard (#4082) * Added user impersonation and username to ui * groups working * Added unit tests, extras for impersonation * added docs * fixed formatting to be consistent * updates per pr review * ran npm frontend:fix per travis ci --- docs/user/README.md | 16 + docs/user/images/dashboard-impersonation.png | Bin 0 -> 53650 bytes i18n/messages.fr.xlf | 6 +- i18n/messages.ja.xlf | 6 +- i18n/messages.xlf | 6 +- src/app/backend/client/manager.go | 29 +- src/app/backend/client/manager_test.go | 286 ++++++++++++++++++ .../backend/validation/validateloginstatus.go | 21 +- .../frontend/chrome/userpanel/template.html | 4 +- 9 files changed, 359 insertions(+), 15 deletions(-) create mode 100644 docs/user/images/dashboard-impersonation.png diff --git a/docs/user/README.md b/docs/user/README.md index 395253b34..fb838b613 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -10,5 +10,21 @@ * [Integrations](integrations.md) * [Labels](labels.md) +## User Impersonation + +Impersonation uses a reverse proxy to inject a user's identifying information (username, groups and extra scopes) as headers in each request to the API server. The Dashboard can pass these headers to the API server if your reverse proxy will inject them in the requests. + +![Impersonation Architecture](images/dashboard-impersonation.png "Impersonation Architecture") + +Impersonation is useful in situations where using a user's token isn't available, such as cloud-hosted Kubernetes services. To use impersonation a reverse proxy must: + +1. Have a Kubernetes service account that [has RBAC permissions to impersonate other users](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation) +2. Generate the `Impersonate-User` header with a unique name identifying the user +3. *Optional* Generate the `Impersonate-Group` header(s) with the impersonated user's group data +4. *Optional* Generate the `Impersonate-Extra` header(s) with additional authorization data + +Impersonation will only work when the reverse proxy provides the `Authorization` header with a valid service account. It will not work with any other method of authenticating to the dashboard. + + ---- _Copyright 2019 [The Kubernetes Dashboard Authors](https://github.com/kubernetes/dashboard/graphs/contributors)_ diff --git a/docs/user/images/dashboard-impersonation.png b/docs/user/images/dashboard-impersonation.png new file mode 100644 index 0000000000000000000000000000000000000000..4addd58fa729b81c7ddb1a2c378f8e80bde34d5d GIT binary patch literal 53650 zcmeFZWmH$|*Def*3dp7u1SO?gKw3%>q@@J}lb78XDR^GIu3a(9keK z;J;^B=io09uLDKV(5TU5Bqh{b^;Q!vd{FP-KRLhjxOIs|=8c@}9VYCIH*z-E84(36 z%1j|M32z>-R=gRfPTT2kqLHO?<(9p#& z2|~P?HiPKjTMV*neL7DSOX{yLjL9)gftPhYeR%Zwg}AQ3w<-Eej!N$p6C@vz8y`x?^qS(Eo zkxB>C0#vBL3>GW3lE{u)e~sJLU57@uKeG^LAQBfaF;Kf8kl7i}A8+2D8JnY;6=&QY z_1(Q%;@P?Rx8aK1!B=>>rK!9e(RI2GHoHX+Iy#cYrr7Sk|H$^>!}X!hPRj#a>^kK) zOq0FJnf2>DQ;zybF;5rnIqefWHu8!fSIfPh-vyuA_h%|N%S%(by%pSSyr^)rzq3ZQ zrY`^7yl^hm|#t`D8fP^gTD96NiN}Ds>Ttv1^xJzpU?B)cM5V_wK>(-zK3o zaP4&Tf1Qovwe0v{Kid(=S!d>zt(Nn~e5fGtNHte2r*tn;vt_Kpmj5E7VyVy9LVa5* z*v@xL13BS2DjBj&Ud_y>E0>9^oQ08L^Ukd73FC~Qo&ck z$oJYJS>L&Oevj15KM`t^#weBvX9y$5SG@ga>(eCBrb#o*h(BtwZ%#UNi_8yd>X+Fj zdhai3T(_VyWQakD$%=QyaDB#GXt=<6gMZT1&@6hK$5+tsbRnt9dF=4aIA>qe-0pyl zHIjBY+8HukeNzziZKM9gTexdBRBUqGdCYn~d>BQ(AbePaMo%!e7-veR?r|eUayrrCK zraTv{W0~j;>(PO6mnkpv9Y)>6DZ9`An4nfi$`5pGYX=j2j`x-xKllE-(9*Eb=rrns z+u6$Lt9RYE7azIrt|y|Wvm6podTlFj&4nwCo-3$c7t(S1d$g$UIbvY7r18jGUI=GU zDE8PlB9gDtxx#~5w`5EyTPRV8=X>aTYLY3hU~qRZW4eZ%_zVm6YV$8tO}Q1;qGYp# zQ&&88*C$Yu)+4Z-Hr<|$f1L@_O<0NPk&GP$VqeGA;okj}tLh;)f3}wHU;NqyenZaV zcF3yO$)1wg`V1Z`=i$>o-30*`RkIX% zpPl=M`(U*sw71V2)>U;#P~_BGaK8f6H~4h8l$G4UJM_R%^yJW?zsypzzruFvRWP;4 zy6AS#?fF>KB(5@xLGEYNk#eE5+c&B#|lQA+9JU;Z~k~<-uG6<%=7c9Ultz*8~sd~da|X|vcC8$5&g`$7&*?`8`tz+_IF;^ zbNv)V%GE&Goo?uU{f|+jVIC5S3+&HEe9cPoSQqrzeY`c_xi)z-e-X1(({a8d7G<-6 zMsP$QmN#_-ZCSuK)f3EVyv`~6<3>4~%%~48!uDugT63bdx(?@Ri z5q&-tHcaa_`S95bBr&31`!V2aMtQI-2eJtbY||G;!C|av#RxmEUN>wG z;$lp6RM|e~+w#Zp`CbA~l}X?%QLRGL5k$tbO2{p=ef2`p6TCA+y;51p-c{@|{|36d z@?d0rBSoHjkNY8VsOG59$6F8h6n(?a*RP*6q1c%MH~)2@Kv(#eq8Z_7V5EUfrupY> zZnnC@JNb0(ko!Nem}av((sT3FjsHDV1j%56emsV-x|v=Fs7%kjpU!0J9^^%Hggunf z{touZ*B?Fo9&&21)bAN#k+4jk3s;2koKw9_9+MTq`^TEa9g(FV6<)I*plq?9ZH`S3 z6&3QMG)aWSIx+K_)^kP-Q9vGZzo-dQX!59Y;#D}6pWCI9$6oZ|U-+q$B` z!QA5JzelrG;YpBTt4X7E#e@liQc{QSl=-VbVS*a$o}x7r5%j#- zX$v4`T@AZlywsl+&u!dBm&ZC9eEZ?b*LRsAi(y zesHqMf~aA`&ia$14Fb0)kpjn!$qCQH&6fEWS8c9G>pE+pa1+9A{RtPI1Yq7zB66k2 ziJ#TF?{M!w%Sif&xTNiutXQhuNtC}`CG{b)i2=P@risS*sb7ho0h0MA%yeY74J^c> zka~dTn{Dlm#(#frfb{rr^Gs7<{O**G7_n}jGzJD^vS;z$R=3#5LM!g6qilphyfBje za1;z@p{Ql^;Sz1itn1f&t_p04YkU;HCisR<|Y29@`()HN6Iz?J($ zm2X~Bx@RBB8?f{j&Q&jE#&-yB)O@B{WlnzXi1mAcA0co`#xLQ1FX43|9EQQDXuCty znC%+ztC4HuPIPa|@!rZcW(lFT&;QwvA985Zi_vOtl2^tm3S-55>MBvAmI@19iQ(g8 zNu9LhCt??k)u=pwNc3kb({A@U^5|9 zoqybX)s|HKxn{1~mw{~M&)X|Q-KFOJ43|eB*bR-lEhL&u*0`ah5>yohn-=xf0msa{ z7r;)LfIrUEt#+O-=@#XgZ3(TwP`p*3@lKoBKdRdOy<(z}$?9+s^HR9tsi7-VBh!uG zsGK88r=JE4FmS`Hhx)r7v6{g8aM9;Qiiu*PZW6Zp59>KSz`{E-qV=xq3S!Ip-~A0@ zwd%c(D7%+E+aG^#%kXP&cz!`)W3qOwuIuD@xBkC>rc3m|ssyINs`h|JmPMB}+isw^ zs!KRx0=BZ;Y(B5Bj{EWhJ6`~P`tk>;wKdUCfVpC6Lj)QEK0&mb8u!?!HQA`$*IG3Cv?o14<09DB=OXk_{&m5r@VFRvGzao)BoQ7r0h-bxjPcQ3WS zz@xCRsr*iI;6i-nA1cxDFd%nzOAxG6@l! zCoQoU%zaSDlE*|S2>VgJunD8%1DyMXFs5zhi}&HC^cE8S>$~H4+i`dc?IK9k>zA$v zua)*I-WJF_BkJ}0%!_9u9+D`@kyCvo37PI#Uk}marM?V@*2QBJf5{PbOi8=zTdT%6K|Nm>11K4j)|`Q(QqLXmcP<39mk;0Y3G{UAkCh%@73S2gh1%`V$w}C zaZOY?@pRwZ3d-071a)O1X~t@Mu@@CX9qUr*wl-SE$9liQMt7@Mnrg(@oI;;=n)v{x z62zS?6t9x$?6~?`-hVJIC?Ugp%Z3dMRn9~B8;^*G^K8@OK^huL8M(Q8qZBJOkKkG6J(^y6z$ zqpS-*?1-zg8>+O+ES$}?t@8>w>wYzmJN8JFUtlDM(aW<+_CF$T3CCOB{^Qn9&qbdX z+6Nqm5V2#|5(ra%k~PFV?#GQ6l{y4Nq%nGouM9rHG0s#5I|8kyb5XALRo4vz9&7nN>?1qMi_a{E^)jp=+ym!C#_ zT^R4=wb&#Pk5O887L9zfF=Yl74~m`Gi_y{)P7xx-rme4ODsk)^sL3lIU#B}^vODwJ zv~K~lC4KYyZba?RHrCV+RD0ln`5_VF5{i=YAV@^(xG)F-Z!M|m8NTSS1}!6Z87ocB zLW6~3;|j@D^_PZ!)1t#rWRyG-vDHX0b>CTTdZ&WtAFnn20_Ry2hpq)aZ~c1JO0hu) zF43PO^$kK%FMh_Qo9Q|lrINY0Q*&R!ReNZ4>8|xaG8S)NJN@G7W0r>0v!|g8jjW}o z1AUEfz^e;#TcxqR=c&G7Cen_PGs!;3=8ko5@vgP}4Zy#s(78is;;@l~@+!YgsoYm2 z^L%x)ol;ZuV3ns@@|{;AAl=R+Umle|`0WWk29`9{Yn7c>EJ1J{ojhKo6|8+$Q4(L! zS|-@q>Jsjzbp97r{5U~Q#*M3bUOVa@!{uGlo^xT+x8}4^AJSNf#SVYm=5hV;&|&)d zWs~lYWXyje%*XTSe(CskbKD!(9MpWOY?Kxl4RUBxco^dOZEb{&d<>UA^k4k_s3a1j zjfzx!gD~f6cPA|!^?9O%Fsc+csZM^qWPcTA*Z6mutqQPr#bl`&7VKL3&LhTxE)p+0 znu469hp0q%#~qygwdDr?1u$4G_qu(_cuZmtEQ@#z`Izf819c{G4pkVN;F508N(H@l zvHo{MVTNMogccv{pN0%=oecaz!u`X9WiF`6ZDUJKb`p_VF&donTu;m0q zP=`+5A<3>2CV`vo+h#n@tM_#P=~?`J*0&L}Rp5oIGs0GEFZH)XfOj{caFWC)AxEdE zO}Cmwt&MfYa2a;UFeYAlFk5d^cpf4Z zpY>=d7bL-|r3o!Mr_;RxR8BuyEwyfeR!K90$*jgd^xg!dmg#-8f7UJ0^diCiQnRp1 zIIs#$0mKM?vsKNICBFX`^}fc3*EXYb=E*2rOHgg;nv}=*-(C^o12ooS7xnG$3g;GjTGIm!$KGD(ZO!TBpyVtQnTJ` z*OFegcOsb6_sn_iEK1jJ=>2bH!tDfK@t>v(1Q!9S2?!JwOaKF5@^kJ>=7Y+|&ia!y zFUg5$Hu^UZ7`^7*o9w;+rPN}uZTxXN`;rR|{;6pbUVCB}bnm_Q$&pKXyiuuH|yIXKMchyGA(Dw4@(_vl^iv0t^%jLEt`(KRp9=>Mw{*aRyBR zBlO2YPRma&oLZ3YEkL6r7}}(INZPst>8sP7hHRj_9@()viXyC*^!-qB!M+PxWxxn!OP4cf#z2plZAL~cMk7WR4 zmKQ{ffDHO3x^i+MU_I4ls;>4(_Us+V;PGyM_<{asPz2^+a(7>D_;jssSWcL;_aC*x zF?Bu{l>6g$44-w}-cbFq@zoxRfvUhOe~)F#B8=#cDQN%+3B*M3+{Ru@U*E;U<|;8A z%9dS3azQ1rqtZ3hW(yTRV}jkST-_A0Q~PK}_^W$n^}l8={6?Y<3yL1JZ7CAZe;_*pq|K0FJY%AaiTXw$Oy>FCDFt>b-Z zd68_m^p>)78*|n$i-Yc$SAP!%0m7O5kiXqKL*;Y$=z@u3McL|hzfy&Ib8PW~l82D` z9Z|z(Tw&1(k$1OZi0Ebgo{A}+61Uu8;FT|?FO&5HRf^Dlt4Cjp139W%OD%^uid#^N zF^T>c8RMe$y+=Z%cNJZh2fidYkD8NF%DgxQ`*+3QFuggpl)rRrm+^q_B+K!CrQ{Uw_2v67GqaH7A3@lH2WKGj ze^fZzU%UYbivmYX42g|31(w8`1rN-<`b0bBI4T@}XTD}IX_(9!LcKjJU0qTWN9JLz# z>iU%oNa$8KrJoBNoxBdC5b`N7_>n(e>%1r9hsc1^TulZ&WW$e>3u46$V> z*piZueztR>!T>gl)l08QOb`WVUn$hD7uCd^mkT<7x;9II*5x@*?(VGnz0HlUupTp^ zBihm5m{ND8RYjKr3i4hAI0H2*2Vrs2B+MIGg_) zdbYujW{RC7`;OkgmEQ2>;pwL;Ax6X!_`>Ns7ZCoJct{1WiK{M2_$)xASW#QebPhbhV#u)VW)=)-s$@vy;J-e*zCMi3XH;c=I!aOiD$z{ZWD(r3O}<8^2Q}QW%`~3 z(K=Am!;{RPhW`o#Ow|JE;lL{b(I%@4?23+y5;p zP^-a5QgNiW6D0(s#9joQR%gVU!vtRw!IKdWGcTUHIBl%g_VO1{9c!1SV=9Y`jZRG# z-bwu*z4JA&reT$YOmO~E1paO;0SkQO-gDahE^6|0s@qbh6(Z`{R`R>p@TBbZe(Y24 z$PVBm^Q_0bbAVmP_%Jv3=}t9-2@?EA@4O2{f9p6_1o*Fe2{%spa4cf@NOl9EmnlI= zZzv7zDJl8d-U#olA3RAPvxNC4q`W4AMAFqV{`Jn=GuBj$n4$)36QiJo6vD)AxUl|Nk8S|F`2$ValMGU{-EBRrm5UfZnoX zk*y9M56?oi92JBJ>?Na1I^}<fU2F(6qV>$iqty5r( zhA|CgUR-_oYtvy-Ic03`=q<&*LTUzugU7u8R`G5?6iX+oxaV4H^o6KDUyG*%vZ|X$ z%H{UJ0?&V#4Y}}qN&x*7Es0OV(Yw$RTyY%wq&UQ|C8RU_WpVIkfbkP7bhAZc_rF?z zQ%7G4Pp1lbSgLsY>{GnulL>J;M5Bgc&CvM2dW$Q9~#vV$bHLv`69(fTneEZLoP3H$qcpy|DCuAcm)$F@_x~P;e5l+INr5Msp4RB{D1IH z-2?`*c)0Zci2V3?W**7AsVTjGJ#rXS*F-=2H(|jAPu!96Fw=ucr`)Q@gTi=p?0&h` zh-0|V!tsFgq2-M~oXi(^ojl11 zo3#tDkE?jes3~Xx4PWmgcoGjEjs|@!T17tlzaa z63kedTh#gR*GIv)-pJk}^g(L}{HzZFxqRD>xG;v4d&qnmn&k+dh!em+CGNfb2N^{U zEVw*{r8|tm0P*ld$b8d~u=37znjMoH32=V^n=c!Tvg0W=k0h2~B>Xi$>=P&QhE1^L zSTcC+DEz+vMN$aDS6+hO72O+l#l|4Of*KDORzT_Rm2iSj=NKv|!Q;4qXH+p@Q^o&S zEN9JgaVZFLCTap?xUg1d;ZZFDqZe4d?*Sja;N|{>>=!kp{j@wl`rW+>p?|cep9=!H z?=AQp3C>kcn7#y9*g1(XJ|q;rML0*N{3RI0D+q^1vd99zB7q;4&OEOmXk~&`4Z>l3 z1~Ibu2|W5mLKN8J4ydxuqEw*T=P={X7+1Ys)lTlQS|o#_JM84Q86U1WSg-aFs5&`5 zh=)Wv2Ke})1o!2fX(}wreTFA?+ra_nktOg$=KbZ-UAh+r)rq{8QRbti=5K1TUADaM z2b1wMKk(WaoTkK@-23l8`4Nx`^yL&nq#pu%YXY1SQnHx$XH0re`M|OF&ODSLWQ+Ny zZ6@0-K0Q7AyBvV%G{B$nxQyGsbgBdDd>%vuJRWOhnjg+TbE$*9@p?2vgKStTf(#xr z)fg;>iXC4E7P5P36L@i+gSCp}BklGgF{p=k?UovOEF`+bxoyqILP0JDDY76ch)k2t zI8^M{cRKOGSNY?9es7Q0)N^YJU>iZ`%P!%Ko9X4Dz};`P(uNLqZ&`ManLvuv0ol7r zb10Q9k_}NpAxO30fgq37@bxVy@x%ZdN6KC@^uL#Q?udCceO@=tuwR8_TQIcDthb>9 zG#uWwuA2r9P}*C#{%XPYopu>pG8Ey*nc{_gjy)ZQAH9t2R}!1RrMx-(ID7)A7{N6? z&&{UaekA#1MgN2wAx(TXCaNo9Z@Lw0m6!x0#R&E@3y^V-byO}SS^ALO3z7;V4Vxp2 z)Pl+t4{K7;IShhUPcRg^76H!bUo(N6aZo}ZaHsao40_vf;Fj+}WK6`wKW_J|5-h?& zGF3FZ7BnClt*zTf$43VT7E0YzuHSr3K;Pp*um{{j=ZI;twdnD$*$<@=6jq>svYom} zqeMqe|AGD~{e5G7uboIl6LoRF+HK1uJ}hE5qBV%ue^St4{yt$rIiih7kd9*4F*!Q} z5(RTHVJM9L7OtF~_u8E>72C35y%+WCjT}c&vfNuaBZFf&o<}7!GRV$9f)gzN_)?8Q zEAdCOJ*W04_Dac7KkSP^+@@_0DeM~wQ66o#a;YUpB#j%q_r71&Ax~qQLrR`a5%~~Y zO^~X_24v?qxM6nzrIuJw4zb%cLHUQ(V<4kWEoQT=LJLh#$-BW7RH&tC`~D& zt3-3?b~O55bij#>yJ2o{Hf30JXFxSmI+RkdJ75eBh;6+_JrO@qC~!GXk5)%53whI} zf{T4FUobW?4j4f{PyYJgs6w~nS55m4|mVNihmVNXB!535N9As^A&S-R)$$@al)0C^0A_Ox%B#L z`~$)nvXLzBH#Xldw8(Cl$#Z;;Zq@g<<+4APofA?u8SwAPkc%F*aK;|Ez*%!$+N{bH zqNy{FSx-uWi~2|{s0^y*3-jMb^`D?B>PI%@BrOH6vXX+c`sBKK1MzZ>kVG>GV(4)> z!k9A#7{sGrpMB{`OKhR8FAuce393v@)a5G|8>maph~LxBdt( zdoN1~L%(Q^LziI7rMDuCjdXam2JLIn56UdmIkbsPiNc69iB!)nX$!;xt?dPiPR-g4H`CR2+o4%B|SyIw*r`1q~v zel+t+o@t^OXU&hNAm6ZlW2!pO8*0~46T^KCTzCnXWH|Yhs|UHh_nvY3EnHKUeY44$ z$|IF)(j!eZ9sZYWO%&uZwOQ297K6e??C{|oC5cgoTOHL?eS4|0Cd3P7TKr!hIG0d+ zeP<;1Ioi=w&A-p3BR_}ZV=yH%w*lm~L!+zJ*L!l`)&;24%b>P4!k34ehGUUncj>nA z9e+B$p;#_P*EaIbh3W9LJM?LuMqxiz?26}}@ zc@_e$JLY$u!N2*i?g*u$TI=q#5{VK(k&zdvLPEDun z@iNwxwbX5rFWajl{rg%)26x6Xrs{rP4AG7nqHL*Q!9*j3BjwE z-I9-T&2AfX5F7vUWS=&aD9y@G@JsaUBM$IqdsP=8q*u%Nsh}e*1i!B`>JIM)J0FMK zg8{yh9WOt_a;YSHP){lYl@Mex_d_@Xv(#2i=R!oSvCZT>06;9Pa9o_JtRT0E{WG<*DxCp{_1P%x68;Iej!N<+O3cHzTk(OM9Trc9JnQz zQa{9ovJb)UOn{mI9X^SL$t=rJaKEUb%40xT`py??PX!)`OL1q!caaVBBx=0uiZpN}$72Fz3f?2T&bt+Ojx&5$#ivXXkBR?R7W4($)z%0;N!Ql>9~$#cR$>t4C0 zP-DJfCXYM|_?;HY^bZUX%8Eyo=qeBntgiwEyY&EM%tHlBA<&L3;$8zO#xjx5-9e7=Td5DpWVoaLn+adcndg`F%n;0sD9g4WI*(PlL!mO})3ftq zY;(}z&@pw4svwt$Rp@4n7h40*{{=|^@rLrPd1D*w7kg5jnH@kfJ`cL&a7^Y3)k=j_UkJ&ba0~1f8dSKC=+Vu^GIUAF-hccML(2ML>@Sviy`y?k}g07 zQC|t_9IJDRu>z*rC<<<)S8gXqyKB-jo@3S(v4)`{e41i=MZKT>XOv4!6lSxMYbHba z7ZGg-+cF}?7%!vq0-bmqnCev5JE-<8xU-KXb&sAXbK7A_#z6Yt@aaPO$`_)Lw!0L7 zH=SD%MG~uK!3~6B>|CsY7+-m>^}Ih0HhSTbgTLgOU^trBa!ytUlHHU@z?aGl6rZY! z%^C0e7S@J>FsxXA#SNQb1Ts*gU8yz%a?K?d8y64hd2}YYOuC^qv_m2Vc1D|nuUe1U zaDE46r{OxNZxW;tr#zO_SY!nnhWV!#IIcs+Urn4zM6rPK(9&}W^A!1 z@Rb$Tbzp~5WWv}@pk$5(;dtC_Ji$gBG=J6TJ(YRoJCxc_Bhgey`mq0DS6J=_Bx|~> zIC0rRF4h@i%s=rA2$?H9}7=s z2TcK;XrFr6(XBa-w z6*1VC+D+f8zTpWvgSpc5h9n?q=GxxRSGbfQ6K6VS!_d)YpJw~$B*Nq4WD?MiK_4JHgm0E&h@ELs_LuLjSIjrB&<|QHYj^g zs@r@1ntrLnr3RfA3A?3AD(#JJ+${afgloDQ`sG%!UVLKH_X$oYcJEU@7g1!IR4L=m zZ%e}?7w8GFIeJ5l*S2nPlG#BKJ@@c~y``W&nP$D!OjA1b5nX_(-pLc)7X$37D?otyJBStzB$7UC$ zAG!%|{vBPh?@<9bOkK#PA`jNWFP zw4<>7itkFqVn%rX@P>TUS*y=dXhyO#`3CVVBuTF9nLN-wQC>R!E&`ukAQ^{Q-bf%9p-gWa{Sa7m;DPsvpl&>k@QRfKE^|{RAsDis4 z7{Z$Uc($#4;n}_q<{BW#KUDTg{vz{YPRch#ih@r5NiYHuc$7jA-R7VMrUE+;xj2 zPbfhl{UBd`tc!X$Z~xTSx=XP`*IvF*GUM|KRHtNj0oVIVC@j2wN)p21F)xC{=c0B^2znlg8E!TyF*xfA1biCx4md{ZTV}04K#LWJ6Ko+fk za)i_Jl(L0(9!T)Q9o=g>X(MYo+mxnn+nl+=e)WBzyVoQ;a+jFFy!{EQ&E{tY<(r4w zwE9Ol(zfTc)E9*q2P~mY!^Z(naHynBvMR`d@8*WcS(?O?`ZQZ8Qhb9%=B#P+RW|}G z6@g_IAx*cuXJNX{4CNmFufU>5Ld*CSeM(Ig_?-omosjlAph_kwT_jaZV?Hun&2*v2M;L#ULx~R)xQ8{#Q%~!1y z@Z`ogXUfk3NH8hMU2mntW}9G3pS6DA+|L;=x%p#EDQ(xVWraI$Kcm4zDr;9z)3;DK zZ>Rlfmu*MX0=K0whkaiJoec3>2H_4H3ykupE4sw2;`3{MPg4 z!=w{&>shz@>)kAQIky|~ph0tC#k)BY@8klHiSeA{kLr0%y;)Q{Lm0mI0dohbE28xm zXFy!htxLCWytt0tLh$Gl@$VGtiMSNTm7if0X56Dc!b1&2L1#Q1&Tr;t z_;a|A`}d>b)Dtkrr4aEKJIT1yi#7_qvCoAV30Z~E6K8)x1uX_qW+j~`^INK0gydq_ zf!nFaJs3kx9Y~PW^<&?>PZ2~B*k1t19hi6#utJR zA~F#VRB6TMMd!=Vr$+bDX)F*=Vu(0c;y#ofRwvpoT;{k@^_}6X}u?$$081>@=EqK2)a)*pUdsG4#`@D);5f%dx8T9iV&D|f99 z$UdQ+ShpL2en@i%(B^@HEBPglgXLo55xpc*?(z2i`GxVSZeEsX0`wiWkprHVs(2rC zg|}@#)hfv*oa~0#>srNoniOAiR9jH*EWeMdyIB}^B?zLDEZM&=Q|zRA#MZXgx+`t) zTMhTz{Ppb_%3Xq8#u=2ilf7#n@`{5jqQaH^-YTUuKxi)^Rz!{&Q9{O<+-0wkZRMcfPUM)&QX_9AT=XE_AW{8SIwB z;iaiI$T3zaR`z%2<@IG>1scsBi zFVm~@tgLm=z9rMJ3?RSs*4^$UMRUfGnhjYs*(V}E^=sVigx+dIxK};_Xt1T6Mi^<7 zhN^Mrug%%DAt>hXRk>&}NGz`_J6{K2Z&_b&(1ZgD-ixd%Q?9fBtOWWc`31?l+_Owh z&4*rD6rL6PZkwLR8n45o+rmh_hu7C6N%ph#kb9s!s_Ls;HYQOUzj1Nmj9r)D!tgN^ zyNv6zHUmGutC+NC$y?cT)O3L&+gfe6cT2ok!zDEVi+rNbdt5(PG;0KY>zkqD)Y*2m zC-pXayd=p$0?VT3HqZIP;sD|h@8L?3#wx=H^_K)afWKcoeEoi?rU5xoh_cEJS)dEj zyPp+xoai4w+3JqYYO0s>OY+CBv})bUax~t>b-slLW436lt>ct|T}X#5b2Z*sOd-8x zYePi5M4o|!`b>Yd07Q?tjW_9VBXqTET#IVYjA4_MWr?-nE+tusWCf*2#;rnChZ`y5 zcd{IV3C}~$TqY{(DEqC7EtBX9#ZiB8vb9pf;6$Q` z*S@1eGnwgd!zJS=qawM%vt+jS8hVR50>fssBLwcgXH7y%Svh*ORY&2;cDM$Ldx@ct zrtR>ahlblN&xP=LKTU!C;GB62kYKKj4~;vWyogLxH`xbny9|R0Q6T(QM=4SdHrPX{ z3Q}BdXY#fhT_~LL<-J{f!{g6$^yDl4-Pm?EnTL@axpPUfaoWlg-jbFGyqNv7`$*p)sNisj$6!MZsed)cK%INrX1F5}b}sQI?hdRGnAn zVsE;`PA_Ei)i)QpQ@Tw3Z)3~E7T-h20|aF6K@qS6vf%uO8n}vr-y;%GrJ+c(dSw8E z!Si&<_fwna=*f?+v%Jz*{OB=z?=|*1>1AU@;%(h}2k|YY`aPqQm+Q6z-z_j`~lc?TWtM{1HED%usX zwI#50RG*DKvsVP>fA$f-)3RE;`cOhOQYSrT7lD)x`i4Zk34bCAia+K4IW;`4$cA3l z7=SJY3hl{m*wV(|#*5#1D0-kFMTT7xo8kAL0tAh>N-vTB2DyRibmT}H62G$*7sZOG z(WAS;@CFmSUbiiFBnb8g$QoZQJFhgNSja4wvteGJj%k%I?%3@vHdSUB-K!O_ti&)? zrms38jok!UQGoejkS50M_V0QQ9_Q(=t7Xq&Ivs2-@b9^=y~%Qc9+&z0E`S{yR!!Tf1KS^&yXLnL!%Zpy^pi7;#u z_NOBEJ0!K)F_K!h)oCriXlX@*{N_HN4QKjs!ER}x@&T=SB%3`6=jp9b;w`t=f(g%G_l_el(4dYrjwUY`xx4t!c;`9PxcJDj~7MJt##*G~Fr z$kpk-o8m2AMpvI&;7u+aSOu#L&Iw@$(RWf}XxswF7{%fVwHZh=Um*(nx!a@#=#nx5 z0SVj>p<+~`#SQ6;*n0qvB)N52Y(d_ybH7PpNY2QMJyh>V%P`WqIh|emf=JO_W8pT- z#cUP>e_nr8>_Mo?4lC0^m?_R`yCfT7;gb`!Y%KIlB~5)kI!wQw$eQf+ zD-@i$M^W*-QE9If#T-6z<_*uf9u_-rMcNG{L^9Ln3kD#PpdWzFxT!GcU*k@bZWJuL zie;BoZ1l--mJN-#;k$puzs)Y<5olN%Ek}06#U#xy-up?OT~n<0eKBa)aUK~=?9XOP z#d)A6Ig7U$^-%eHx$t^RZD9%B_m^GKj+y!Kxtft>5i&s*GTIBQ2;lY$3AmepkTn`j zB)J^XuOei&EyPlBR7)2WM0w46KI-vi^X$__=|&B1xNgN=AAQLX)%>> zG5GAGxuHTyd%NdSL}fx2jA6u1icJBE^;^9gUz2oy!XiCYz3;+Y4swULtF)Gtt)XmbLWtS;gLof?GsPq1~ zf-B-;V(2U6zxAFwYBV$^xgPFeDAuBPX$*~HX>+YDxSJsNeub?GwEqm`Z}g=k_SDtN zzqn6zlmBRE`6^w%S~{iCq&Ui%clGt}I}{K-w+gnTf&c^Yx!~K>*DxKo-bmJ=A}k6E zaqMe`?n@bwet`DMw@*Nhdr7=2lY<~+Guv(rW0ByY5k`k$sBx15fz+d3g#Oca;I#k?j(^;k==1aTY*mjisI*kR-}k2 zFbf<=o^k84XX`w6G)}}L9opm|;;mY~_wwC}r;JyDa9kiUD~Vb~m=)vQ>Fhkpz{{L< zEw#&Gn9urQy?*!kQY0-3z6AW<5wX*+?k9mGz2P-bl`{_H&|L)S^@)~i_V*{}AdOBdv^&ISvMeO*sPnuLO` z@^{rhU#au@A`?@m+=hyi@_cE5gIcpafE7W*9btWpBPujRu1Z0~8ha8j4{Y1C{!2XTE&|Re%VC$b4ATr=iRA%mF#;Xd{IRb~#?yWR?`TUyLfa^pt za#7RT#2AA3rDU>ek5FS8ud1s*9Cg&W9y;DhCp~zSF38B0dEY}J+0uqhyVMN-g_HwVlvV3ue2~cLJ@c6dIhT)*>YXY$ zxSFU52p|t=L5erHDVg?=!3GuIA<{r{*uS|zrw}twB~ZB!FdAy}@ONsfoL1uCZjDhF zN7CZDs2}0q!m+}c0~xulr51x+u3D)Zn+m{xm4kLW2?1;N4s;5s!NN|k>N|?j(|_hSqX3YlTch=;r zO#u>T#g>a9Zk?CKX6IniXc$G{@H_m)vEQl)SeztXe0IeT<&yJl1_<#A_2I74AvuE{ zR`STbK#_CCP%&`+qCqBQe5Nd)DNv-KnLW%>OC$DXg-hQ`;AHs2$0Jf+?3=@x3Rh{B z??ZE5^{%4#@`qG}Fc0_)R~4-#Nn&deM|@a)g#7%)oe?(nV3zi5ws}Yk<4WFcbXGOv zv_MT|?@D3FsJ+Lz#Lrh2fba~XZ3S-OVFZ`sd`wj#+#z9jpjk0Y7^*KZd`VJZ{<2H7 z7HTWB0pGGoss6RuO@le!V4X3R8o9wcvHe92%3k03^$&x>f%`|tas|dI7T=fXZie0V zamqH1v_CI9;Q~E*q_0DJ`MTg5I=O&dO7G+?e=7g~6YQf!SgU^Rwj`2*3$TjX7jo&G z52&w#tkeV)YIib9YNjFvU$3ASG|DafFpkzWISBvavNoiKQohB- zIh}2AMQj!t^Kx}LcH_n_WWU4ssVTR}@tA8uE17WJP$}A#v zmnWegR1k7z>mu2FQq^*0E$Ow|2&SL3+>@kYA2>1l%5M91GE%B>1pJs8?tKnLK|G!P-Nd#xa4bEmO_N>FcCuzf z4O-x(T6O62ch$J^S5q7P8vzN=Bk|5%8!qOHppRO#|3|zUZF*%S5C^ z#sGk;A7$y+d!q!XO*z6@QuEUAV?*ZL7E@n#z@<3`flqL_Cnfte#vMOYpo}fPXzHAA z?2oN0G<=C@H^tgmXR~K$>aEm@IJgSZ==|@*qAo5Er@N{Tg7e*wos%OMmLRvS@zz?o zjO8!UbbqUry?teo+PH}@gzT4?#0yMcTl-+#SxpqcW76^7RK0DxGQjT=ZLoC9m+U_6h4af&l=ZWn zmWnx#9~_8&50oD%pA8Z2GEt1oaPT$@a@*{8@E#E$WTPpd_c3;#nCz2m*__3ohIoGaiOP#m|V^Iog+9YW5WNprjoOH?`3&Sjq3!_~lDhO6IYm>>GqB#p zVmTKx5y@d~qLe)DJpDG zid(L^jDrMsrjD~WQ$9ktq)0rm(d|CQodd$HEzLB!Sfvf`#iKZ6)6Z#XKd6D;B-!zsxlyJA5thNgtyPBsKf_nbGuj_9 zvzhQnnL1&Ev}6DVMLtrROd$;UVkp;s2b5lHSHva}r582QVJeY=`5W-76dTF#JpA}X zyhYJ)nn8U0GX+%*aAIl541)#!7jJJFRb{)r{VJk@MJUn@g0ysZDM%}&Al=d}9U{U+ zq(MMw5J5mfK#)d2kPhkY?(#ive4h1R@7R04WA86}jOW7|Ys`uBj_bP4IF8>b)!0gp z(bW9CUiqKGK6w8~ByPXSssJOZ8~O%0l*!>Qz5x+J_~1XLP3n=@8=a4%igFSiZz-k( zZ^~lf&LKtxLqbS)qb~{$);$cg32{al6gGt~tSp5{lFEDhpp6;0(C)|ibTI2ep5ybh zeuu{yc{Dv%{QL|UNQLg4&pedPrCE(X?D7)UDqm*a1L-3$Acb}VZ$dHN;d|M0*-o+X z@m0;`8ih7>Ice34WU-RqRPeh8IUv-%Ctv4O&Vx8UrF@LWb z@y!@QaV5t;bydA@$E!SP4Pgh#MKj$4*Fs7ErA7OZd!HCbzI3c2P)I2KS-1bBbt?sh z33vCMjW2dH5A16$;tvRyp-sUtOx?rgK@n_=Jjpnwjdm8OLe4HUmNsV zesoU+>E^V?Rb4nMEZL@wat3i@n`bn0Dujj zsH-=kz|&QZ)7&ArURRZ6&-}}488O!y(!x9OI8eydPa(=CQt6jnmi(j%;HY*LOh+o9 zS*GU*!WS*dv^fX-FE?dIooN|pzbCmZ-epWf^jb0MMr)N@C@YB!`S|}mUVoZCw_50A z`2SR_f3*zGmcqXgJKK=^{A!0MkW$)!GGbCax1;HfyVRd=J^Etn;gQhDLuSf5-I@Wo ziIeXu%6i8@XuZ70>1q4xR3fyWnV1JiRB$JpD_|O3r5LHmLfFRJ)GHcqq@tkGZTviSnFJ zC8Aj+hoGg{hD6Zi^UrgOWaGfyA~bqUlZ$Am5ifp`+m3mHql&Q|l(4=^#=5JMFSh6cB*CH5R?Yv|6#WgELkRqdlxw(@_NBWBcBRMTU zt=p5^s&P%KibB1IDW@YRy|_Xx^LO3T+dkYR#?5HaIt55u22jXKgfP`#J&cPE?ckld z_Inm-&zSrz&{two`>0jA*d8&Te4{wRpW65c1x0IaPxN=kc9R16WlMbjMCv-uu}2I2 zKVvFqeP*8*&c*Z8k_Ahzd1#8b5-o-}+uQ;YZcUqMOZjR$e1Aleu*eig}VcgMB_ z+#QUwm1yEXx;o-cr z_e9Y1qTZCu_k+-i`f}Z^?)+p|CG6dRL<`TRdV6Fe*@V5hEev~%6;iS*GAm>D`br84 z?|3_JKM@4`PRW(uanV<3FMWe^vRXro7wk+Gh99q`7Wxd~|BcC=&5@Y=ge}I^qq84Z zp5=E|J}nuLTnuQg$VD+2?T!9WB<1NrO7QPwP$Xu1G!L3{-q#yp8Axd^pR^8{WYbz2 z5k@s23OxJh?Qfd>8WX%`T{OB=y~+a5JKo!COjS+>GupAc5p!FzwGi;z%a8N}5v1Vc zBfC|h%kh=k#xQStB)qF=D@?u*ovq_pAKhK4I1A6lze(pYRDupa)N@oJIR}Gw7Oj+t z0`G%dGcz^_kFUMcE^+KI!vag_h0xvG4~}Kq?j~fWkGu4N9$!0`Y9iG#q0mz&f-Roi zDEO(cI&H!4%Y%?0aoEqwGcrV3Awq`G!o>fH#2-_Xn|w=N8!B#n0_KSlk+>KAo@Y{) zEnE~s#sX8JtvQR<lQ?|;6Lp&#{!i46YW?4%?(-A>$Ece@E1Z6dnq=g<0BHM)n$G{Nr^kHgwdKPS z+I|D#PDM*SX)!ob%3mkk*n4bL9{nQf{B1>(!zvUhk?@PM6Z-s+)ZmxLg~|=(^<}|t zX>J=q=ek;GmtNh!r3dq-uO8{jMY`>oAL5|$n#ET>KE-SRMA$$P zgVEs=De>U&#?(}IHM37!*BzLxff`qu>4 zTdU}i6LS9Sg!~@?G7>eZI|@>!fAipfqIFr!;{E{iYhUp$kbdZ=&d9 zOdT796=p1Xep#@Y@+D+d+YEN=fKZ18>A(oDGB%=9vcgcio-$*y| zYqak0zA&QZ_?Pc)jc6{a1ojTztNSmwkevKjsuX6xAqVIY7;Fx}EVI)i=a>MPZ@~+wuWADqx)tt<${M^-+ z+plXtLSZW9T4_&nmB_i;bGF}FY*tKp;0W2A8_qW8Nr*eySvIywWTD(uPf9NoaCr1MRY5|b7C;I=L6f^J44|IbfC?5-5>H<9W(I7A` zi<wRxOcinYm-f=E0^vH-^%u%7p%!fo(eD?bc#u?BFPrp!R3Dpi2lH=<>vpHi z>mjbSf2i#gZ|=Z%IDNw0N)c2y{!@o=L~x(~Arj<(f>7$nfG%5A(j4XA>;}ZJ2;Z?P zRcB1O<=I6dTP8W&%6)xY!mwy2P5pSF5&|0FZDvOc zHz;MgNx&T)PDqUUmj;1=m8pwTWYHrXNd%lMldR}}6Xa`T$dYdTBVdB(h|nMSHrp1J zG@B6LWbV@n?$z%M>Uow(99kF}hvy^>UVL~NG{IXFo9=^UH-O;a358#{4BtU|)`8cF zZyP#)`Ii7G8jm=t(1jz)dvOGZmgZ5Dwg%ndfdTtk{h$Y(@n5j~g!Q@1c~;v8_I*?}W{5c(h3Vw}W37=<=MX6pO= zK8W;P7`&x#wyWkT>@g5Hoc*ObS>)auB2e^8mw8AfenVS*r3Bm|3-d|B)QI1x^ZNuh zgnS|mQc>D?+eH!$Zm4H)9Tg@*J zAW!uJeD7*NXbjvx3izLX?`6-BwJHezpTBP#eg%S{W~W7Nk0bz5?rCk*YGWAQg1agm z5K)7?%cAhni(e*w)J)PB0i3pWW4WOfetvtN+Ma$SfNSuyRWME1gVHJfrUEbJLOUm@(hxG^LgH@WUs07|JxKA{2cPKF>1$l5WW!8#7)x}jhY$vB8 zqg?IjQRK2Q8G?`UEd$?2T4CD8FnqQR19{aEU)(j@N>G&$ioA-4hTI53!9*H(+(mTm z;1GQX5l93#uD?OoFJq57jHcXVv~zI5RDjSRnDllHm;%+15n!$JNvtygcH0h_)lqTu zM|?0W{mGkyESqc#SOxL0<0T+wI^!#6c*hQ}({5yjgK#Mjt^XT%5DQN6I1t*$g4P0h ze?!wLlXxhAR{DR1fMgjL3KpZ^DiC&I`_^dsBEl*~8j&v`Mn&i_92YL%-~>8@PJ0n> zVL9QfEQpjgCFH5*!7GNH_;LGRP2x!HoTvk89c}>{99VAp{Yuau~92kNOQdy=KmC? zJWRUvR(pBbgL5VU>M;Z>+6aJiN;oqlqiU@Uvq@3{akS)mhwR)P-NLm)N;XJG2S$>7 zmK!g)gCk(lPErx3I%^`Icb)a7bp?QAOVD;eVb#u|56}gLdESTAB&wh}M_$%j03IpG zLXX(@^sFrah2bp3fyHE|gIa_2V88jzlv-#Bh9ov5ABuTm_sSy!I2hblvzL>Gq7lA! z1{^m3A#8h;Ld|Fv67n!ck^~?*A_X3#&{Gt_ZcQ1YUiRZ9v2|rYvkc*lC|iIg0iH5$G_EJ;8(O%~tsBpJdI(p3Ok^^M zIu~2DdQo>44|V1`f>|>sPc)T`ZW!Ud9X+MhO8k1|xd*eY+j2qEA(1M`S7=?BWm?s) zvrNznfB^Fo7W*UR81Rc^difiS7VYJaG#)#T<{J;Z{!@K;oV?N!`&wbJfBEE(v&#M{ zooHhx4+9w@62`s)dzF$uW!23C%qEa+&oJ@Gu^~c#%dx5v`FPGB##?Hfsn=5aX+l6p z#siA#rfI{m;XOx^P|0b;Wn=s+?j{OkY`o!f>~T61S$oA{T}bp{0Ig2r>dKXp-*yZ& zm#+~#gHAxw*1O%Oa1{UCT|y+pZE4B(8m`D->gQARc8VgQhsQFMMM@1EW`7P-kxX?o zBvv>j&}BU5P*nIGy{;?Y5uN_9Bl;*o-~H&1On}XHM=;$Py#+Z;t|`loOX$?vLZ79? zTe9{SCjZLQ9=k25CM9-HLZ!+ai7~D|GgKe|k&pA^?`B^5kUVQoay;g3gxWQ_y5Pk8 ziTnyEto{^S1ChW1bG*Is)ktm#H4)p3-ht+BXA)5*S)yoZe~XXaXQ zq%s`LR_{y&m=c8bia@mSi*KDki8D5i!(@ooByM*-yIiZa9UOQ=spc*Snw(nwUo?4L zIl0^Xhj-V~o$vE^+6sd5?%@4N6tr=_oMxqf+JCLYEBT&*G%C3Ot)t>Nk17#%#NItb zsONsKp)M}Ei|cV5BW~AN6khM!Dh~gtKeERq2Blx!+U{)q;PJYBsM}o7`#RyC2Scof z+@0HRz|%Z6BZX`%W{4f}s*_iuTR>`|AwS9l5L?`SS(fiON@IR8-g&fb3*HlL<@s+< zrDc9+Da`==_H({mTei29Z#F^$SwXxhCG=8{H72rr@$u&8=P5|trwUWSY*%1a&Xk2( z=7DGb%o_qW!g(Z? zEA+ZWxMi3xIETH@9&`<@k7!btlTh=va&2yOolkq8XAfxyP5LK9`x&hgkexhTVW=qE z8~xKNc>mU-#lg7zpfaW)9sTy#v3o$?puJK|;sy2D*==V4bZJt4N_l5l`SEb6Sm0r% zJ5T)BAK8&due}(*#t`m>1xv@t`WvwQ$$x)lMaV`V26zj%_O`%v8V~5_H3Y1Du$x;% z`pD8Abdw2Cmzh3#Ax8EKx8Oy18Iw+>%dk2m28tD9Dui8K>2h?IRTKs-tumpHPDt&V zDUAV+%-n3lK0Bybbx^lWQapJ4_2as)asw+v(%oD<){>aPuhW+dJP&FiYW@8A=QdEpA$pZsqe=Zowtr}l-8<(z4#@@ZYewcWoy#!5{|a{vDs*KWeUbog<$06usE`U}$Eq z6?^QKrSaU0{pY6$tSh*#S=IqE5z=ThqG&YZ+UWEo9u`8pM7TD@K5|XuTN=Mwzj^4- ztrdCvuS#=f@~SC+3NlzAL}+*2a9|lI*@p zIz-encA($RST?D9h3-{Nc!R($KNi{1Q#@og(z zdWAKg4_&&Gn z*#$p~ITt|9gMo=o;1k0$+pAXeK(MaK4VjR$Z`N z=$irj^#WA+?A+h$)udgc9R^INMA4?BupuG#Q)4meC=MKMQ=RzZUFU*90v2A5CpzUFLg(+wUf?v<$yqMI zF2qkFWMMt+zWg@a`AfuAT!V&Kv%f!YSBWUm&~xoqXDX!s_-c53@9y2ZF{n?})8X2S z52osWF>GlbDx)g*mGcg(jQ*tF&TQpzUy(DNA(tYxZf$(QKlCok?G#sgO%i zKlftaQDxJ$iFmE}(3r(OW1$!ui>8y!x0m({Uh}HRSCVFKXS=1uv9(L^HSgvcAHSBm zrvmbLlkUK?F{`yqMz558w*B_nft%uo2{M61JeH2I0#7EP)rM;albJbIT-MkOAIXIr z=RS|+s`16D>?jzmDJZj_?7Dn3AUw2;Ulum?lzeXeD`;AW{V+17XC3{30`m=WY1)`d zyybXw#MJPtYzF-e>5G{zr!>A$?WmBmETAv+J;P&*&DH3M-OQFm(!&UH z%b@S5Pqi}v?mq(-#>{B#>ZY<4e1cKe-m=aX1c#{p+Gj)lyo>j_fYi(H3-9A*-9)yl*+0vd&|?z?$!-TbPyh zgvE0_wmZnBqtndrh0oc}pa46=itHc-X3sUBu^V_l-qR(@9a|BNSX%XD52kk+wAdDf z9qOpakbR}i+jK8_Y}D$0$LO-3uO#(^g24`RlXgcqS4MJmXJ4b&YMZ8JbSl} z&h|(-v|Fg*n>5%I=x!oTPf*5@L`k7Wp7p1SBrzA79;7=)uxavb^pHxAj3-sUHriZ@ z+w7}%!!@Tm_gb#iXu8%R&P}_L!lton+vCSot-v30d&%bg%2oGUorq}BTgw+RQ@&nT zjKq2iPx%~XStvY--%5p)xLKNL?QZMQqY3n+T2fhJF;TBYo6Rh+$(e}B@~dPk4m)rC zXy%K`F`4s~jv8(5aGGn~H!5Ll)JXl|j^e6l8W2GKoD#1c)*I1t*v-P44C=y`p7@=e zU*x3*KJ=)3wGBKtQl1doOuwIEEwiHOIXY8g;5%il`g`5Q!wlPt!S}aj!O`HmLzPmL zD|%Vc)a2IRmHl7aFXb~vw4u>+4+Ey^)j5p1_a0HF`uRyY3A(q)nHtun8_n2tmKpd~ zghxAdHz>2aj%N!*wqI$gaBpZ{w}`_SZsuPnJUfEW&)3!7Yu+rWpp&lSK1mW)P(Z~? zSTcE)x!d1sJ>AKbb&$&}=fp$d54(SPAo}`wuE|UKINO)5-$Sy;iv)#}w#_qM=u7#h z2nGFEyPcRLu##(IIYJ}(SRr?KCPg3}uIZIDoLOfMdk-~_!XRX2 z{x!bC;oT{3H~`IY&cCcJ7#?y}Wk##d=9i%$FiF5Dp%r;S% zMo=@UdSmnal9hW%W>xszrD{IMT#pke<;?+#rSyyUVoF6s?p_sc#-TY4e5B6n$x;0Q zotoy|KgXpb+KqtJewq3kC-L4@UEg(iD@#L?S6d=5^9=W zy$_1!{L`{@v(1K%1_j6A3RS6;)y09mqu0~N(T(*PpFD5p{)q19nd9RMX&nk^Eu#Lp zq$|*Sj{MG9Fkur1Z*S}TehL>Xnb9%)L~>q+2HKLNP>ic@cYoGRhg|y5HM5MdFvQaJ ziz561oj|_4(<<}ZEiHe!DVioTn;T?|(TPXF({D?{-(r(8DdRMvBE3#NYHdms3G$|Po&F^G0Q_~wSf{^V-_WL*3{6|`g`bwoCD`Fm>FY7mD`w=68 z-<(p+SGXqos+WzJdS!q}?pt{cd%1J&_JZYd(BM=1%~SKwGRSL)VF^Fl=UbIdGHF1n zbGX-@`-c#XSiMr!r=QZ(zC1ZC;!;d8}#Br@%xLx=}e`E763j_2~<@zZ{wrqU^D?8S) zWcs;MPN)2?3dY(|9Q$3))hQ1vS+7V{@D+Y`%Ls!8<-wS}v9 zj;4_<;InwJP6^4pyxMX3?3VpRZrQajB|I_{^CP6|G7kd0?R)c83H=Sj@R3st1C_5y2QS=7qHROkExzzMx6!u5tHg05 zW?wAVs)P&dO!i`1URe!XG!gntO;g?rUa3OGax$jwY75f1 z9+CE04CRN${ijqv+L&*m=X|loVR=<>Cx(D|^um zP##=}`Kjf(1D^QcHATz>rM&OOI8Var3_-_(w3Qsq*ZxlFpBU*#BbEjGFZ(_th{W${ zn#l|CSwmtcI90>o@{TkLX+PL(M?L)f$Mn^;*cCU=rgbHZlHlWzEMn3b$y*$&;#BcO zP8%1R(z5CuC-&RxcG8SeRWTbq0$7{GkSMP>>Rf<<)l*ljp9y3#SU!#)2JsT?rw6(6 zF1AwC5{379k>z8*mp$lC(aU<`u9xQU{3c~6t~dD6v~+K5Mcxpf6}z817m&AnDWsdg$GLo^}x_q=@$q51sdca|Zwou#|q zn*$a$SJv7uw)(%kls%=V(f<;4pKS5IfLVsjNKA;xor_ZK#d=@3OCstfr~ZDw7a}E% zhu$wn-oZUsTM2r8GwlAV$dAgjdiGR{J0^4EIni#LC_*i?A9LcyiGTnZEO?iMNnHvMHSm` z*%3i#ZB!xn;pOF?D9@P3G0TL2ltrDRTrQYyWmp`56QpVJRvk4bN)n*7%#n>VKVZA;nL4u0F z`ulWyuARTq6hA@s`I`#w;c8dj>1JA+=bV4tdGU2VXX;M9h(IzI7t!49ipUzaz-Jqj z$R3eJ3I8w`y0B6O2*g*SNJ}UlI<2toeicg^V}GJkl^mt_;<+9#1@h6)a5y$IPdtV% zJa1SCZ&b1-`g+@=VSA%2Kjh`qS$2v0`604S>;74%2BD5+Yo?(jGTI6lO_Kunk7|%77N{}iocss z#kzj@z9O0YuKi0Imv6dGe^XHUf_Q+m9rg& z;(Rg{yuhlh6My_*_5IWdycAIh024*g_keKRE8h3Pz$_Mj0i&u0NoNPsi}!(eRRnU zqqSNv3R4ZVfwcGU-#333yG80e`~iImG@H{f)k$qE->TziI>lCrW_${ktj7zVV=EUQ z7LtjJf-t#YX4HKx3iN_^J8soW%gP4H$sw$hR6f;O9^+N%$!3#2ledU|P@_pjRJ05GD8D*|Yk^Mtx zTD$@HL<^MtSYWtcn5O&sZDGJV6oQj^r;c3b8;HQ-N#%AIx@8cQ#9cvc68wcyo{nuJ4be9B zSCNUH2u7pj!ezyG+$2W|3EU51#zbD##df6Qc8mbUmzr>h1uZRvpl!)4eORzpReV1) zGn0{Zabv2fw|q+W5MIvcfdP%;Qlw*8)NlTKi)+7ww&KqDY9v99X`QX`yJ0wi5vB&y z)o&eB%b=R8JdUQJmH3zppz(dN%vX3t)kHC3iK?)|g9J>l-y?lWcxAO+{h%3=QBQ(S z=hT9lDCD!GX5w=w0i|jlfNc+U}T`7&;iYrRG5<;3WSI>j>I$>Cjuw(Is&oN{G3Q zY7f!9D&ukt)3I|an1qfdsy$V9e1H6 z?dFyUtp$;bK-iguoOg+^+4?Y%Koji!Y8BEZT%5lZc=QU}Yu%=%JN+kVEzaZ>s`HmR z1D}P06;*DWunvb+FcRdo;Q&~U`$XwWMzN30?A|Gb|L96+ZZ6ZS*RMam=mW9cD2K~; zidc$13Y638a5gHi0tQVF-avDQUa`@vo}+_vcie`>1jeO97P86@yvF^4?*{9&cx$&Z zes`1Ue7S(dE*J-BPn8sGG6feR95w@U^(VFgnk{tqMP| z2xCfWz!5?1tY})fk77s&OTa0E93;kQJ`5z3OkZ9;Bo9R!0oUmaXoi)VywzU?uZT#dXbyDf`K!c}ym&QaS}&4#ZEKhL zv}3oa2IAu4GK|#;-6y$O*NUM?C;4Te;1K9>xk-*RB#DI8v(JCj^*V_6 zp>ihl0`MKd&HBJ$DhA!g+Zlh4N00yM+rSTVp;Iqho`e<_l#sP=8(0XfoIL6yQ}K`w zHCZ8j)L$|;a@;|YK<(pwM>Nbplr?{k4j(2}jmE*`z@dx5M*&)f-WOJ}%_#%$<>cg| z<`l+kIjKI&3qTnKd3DonyjnA$iaz=th8t%6l^x=VMi~9 z*gU86{rmAQS(6LKuM3OQ$G7)~+c+2>m!PKz1f(XV?k-PS91~9B)RNdY6qIoh1Lcd{$fu`mp8{(o1<~*p`nYR7VD>6a6 zxfkp4X02K+r5;Ie*4_+0^3p<=5L`4h8aApYl^{T6ZAF#N=@q>p=EtHAFEA`mn!R$4}FlVbX6^!P{g<>(>XxSKH?*x zw0q-@E%RfaNW75~>jTaNM&|`Rj09l4@V9evJw@G+N#bpk9>WgKdhd6Rk^sC`!9vdr zb>J{iqL)hv^k9{Q zZ2J`u;h{3f3?)BMD`sVXgVu7?{q_we!SJD}C&WoSKYg)9|DfA5(2U!{)=wt$q=FxV zb0SE^fq(P%tMKBcM!QFpR70iK`BkA6w);Q2JYA>M-hSO^MAD z7;7|=m(esrTu3%p(66D=!UdFxQ5nkmH^h~QVKqf%>@jb|nTZIwZ7b<;XghU3bW|Sh+FQT>G>${iPWBij^xS$>dT@Zquha8M+=Vep2=RRu zAve!u5v#J6h8&34_it>Ejt2cMgYi78(6*4;0_o31s%MoM=r>+L`i;->gc6l_`;v-h z<^Luc=H!sW4VBK4CEg22mDwn?{paIZ%1D$QEV9m@X>+ZTYbNBMa;3DqBc;d8jI_-3ng-hvSj$HzcpyrSB+1QI4CB!aehfr(~u4mmW~VSLMnoVxer zS3e!1Q+!umxp@DVa!MzuQ!8_6)=;Z}^Xe_NuLYbmWzzb~Fxp2B5t}(X{NQH77#}-F zBb#Y6-ACc`9gZP_*d;B4q7<9ld)uD<1uQc_*>sjOasCcbadFbS>zEpFTc?w37=4Cl zNR+jSlJFuA+(8G1c$-=h0*Ko7g%^pvAZArjQ=>t~!!R5L8VC}MT6JfHZhraDw}r%c zV0(!HMz3n#*IA+>E}uDLz&aOt969Zr0NPkX=`ogv=~8!Q*gKE~`7$tFz0IUF<;{cD zYls9KEtn}idT?(ZjnOk^%FTQTg?aAcyW{dd^A{W==YA$+W*8V& zm2iJzB$H7axmVlVTxR*{!UGlkr=YsYt4u7(Eq8C}Ju!8_BG5B>$jh()vYHV|BXSB* z)rvzli67t)P|4R3as{sN@S!yxG4**|8>D+;F}xpr9j*92Z%XADY(jXLLF7HfrvU0! z`)P(ji}4YRL|T}Xf-mE{v`FHrt}0`m2MI4bZCcggBIDQjl-Ft%fM`*%`p)mPOb_On zXppF6z$6T z4;X}HAEXG9n>#u$^*!wM;O5uW_Bx-4hl*va|4c0{-DG4%+SS!H4LUA$X9G|R<_!$6 zJFna|*-H%W`79BthY^FMdkg~?_Lda{Ryg?}7v!|)lXq1C zGm8laj`5)%ZgR0jzd}5>RnF*L7M91l5RhN|oz#@iQ^^eXJzP zo*#|&>lor`HS8Q57cI`^PXY~U2FBT&hhQgSmkqtn_LF71y`8`GOai_3=oR_&I*GaR zIlU|SMmjo4AnsHuzC4j*Vd4e(b=Nm{XS`Un4&1iQtf9Drayee}=v@N6ryq-MU@vQQ zzzFWEr=IiG)P1w#?LtNlBE}{r#6uSlzFggjzmOgSNod~PU2}c?l%b=6ibH%Q7n6y& zQ(zxQen-kQJ%UI%N%ulE$$3FKSQ#c!1xwz3fKgOCxaNB?rH=e(l1*Dn%iFRtUM^;r zQ6M~gs(kP)?fT?3k@p)Ha5FBVp+AH;-`}fhNo_*Ca=#Zt;`# zkSPfn2T5SuC#Y1?)6=u-(=_=3>2ccU&q|oa$Kz`tTQPDkYZ?IGXwiF2xd{v$ONXje zT$D5c9|sHb_{091xRX3WbrVItqr6TbZ;xqH5xu;O+D<|zzf0HOMlq`AFcJc);&FA) z_}V4U1F=ZKQ0S}hAXTD9{{0bpn;uh!kP!KEDL88fse`zvvg*tRidmpZGJFMnZSPZN zNj(d{Qgr=aYP{mI+E;2}rc@TSwhO_A-_1QVz32t=ZJYy$8&U*a4_DI`G}g&hd(5n< zs{&n?VJx`lEoo`#oJdXrI@A^BhSTGNt=>dQ49#Pu{?WR~$Xi9U^?Mr^$jG<|Ry8Lv zh;^i0?oBXvO>ym%Yh7sqSuAlptEXOpe7c`2RRiiac}CMohF@=zVz+}Jihz(b=9?p1 z`CLsQzk5rRbK>9yl!oRN8Iu94;WA~{-h#O#fWx#@%K`8w8ur*GdU0VHkf`V(@xdjM zte;c#l;!;^<#*O5XLS7PL7Wcr5Z#z#EjC zYR*?xRaK*6{dllsYWgfeBF~_4v9QU1iLNS^Vl67%Fw6nAf6IS)4 zf-V9Nuj@Q`5qN~c{v3^_EMjqZ($%FR#v-+Sxo*H#_vWY!hLn3PP7SB@>{TDVg|B_i zmnqq>v(hdW(-y(R1G$l@dJl=|X@5R_e^GMkslw75(tDq{8V_S|sNw2Qvib*`$!zMc zRENa6Aq!!k#(e~kmbC9U*x9GSR`J+<5BP+OhZp|)+0W2#7r;QiDU9DboY~>r=9`3y z7&2{5UGA4DgVF}pb0!FOpem!Dg46UWBQRC>_Qgq13VL|nAvrpWP5ICquF zg01A|u<(P)Aky%~j7yiwba~u&EnR`Oh@Pa4VIwE84e7Z3qxu8blcUu8aj3uO89piL ztBLc_zFBJvb*RAIbGzZxkIQzSoh|%w+J#dB;l)DdK(#;7WjUReX_#^YmdlQ z1XuY7wn?8LieKe6+I1aXE3J1a*64K<6z63o*2$K*eBbx#kfHO6MiK%Wga3BQwMqGe z-8Av}MQW?G>6&}fQ0~d{ReTqh|0&d;+B^-{cfylY<(669v5jsICw4OiwTnD#ZS&+Q zr26VtpJ-3z!%w2POYMHH8iV3+bkws}xhUJl!RcXm3C16tXCnH^7S^R>r*$)Z)pCELDOus1OVb`Zz0 zw-nn|)LXqif9h2tao!2d>6iX=*vj?T7TB5CIpDTccBKoi!x~=m6Rg;%*L+kesq1|$(Oxb)dQlD zlb&}`i+assn4MQRTAJs}ET0m32XV)vnxdwR+tT?{x|_+^T3Xmi!<3!e9#WDNnCPuz z7=(2}2>9Y9(#*&W&_$u^$Q=J_3XkNf>%?2-J}fH5*(!YeacBt3e0YVJ?U^@&iHRF$ z7D#Vq~gLDG=aWCBqrZ$^a?+Hvb!yrWuH|3xr*9ounYJoa{xWVo&R zIw(jfMcKXM*v^iO=u5V!_kN?-?&gu$HBvr1N!LXpp6AOwS?erlR&Roc_IjXp{HZF8 ze7*TYrad=*@2jYn69x4ik}^{d6&2NWXcH$dKYV0Tl_BB7`|_C#i`24=wsB;5MdPEV zB}r~$XV1YdM0#)`npNIH*^fC>uWDf#H5Nf+`^EFEau>HiJ&0CF}u$}_pH@j zMuA328*_XNC+NlS-G-;S=##P01-YM|@LJ=pBVC`W5-LxAGP!)}Z=t*RZ)-ahdH7$h z;GoHVHCT+}{x-K1eq)gSza(7`F~4hj*Z1?EJDrKy>x^wMv2{j^N#$uG?kB* z;<@vhr7&}Xq3G40DRe*NmL(v`fc?{BJ2lyWlr;{jlXmbS%*AYtzyF(az(f?OQ^PJ-t!X+8~tm;$@1Ix6h|eNxCQ~Gt)i=+1374 zP_jYZ(@8)l{HPUEqjgH7HC)a8u$eSyGdN4y_et0)XQZX17QS2ukDbvdRN`_DF;y=1 zI!W}fZ@R4x6r>K(6Rbb7^0)Lx2g6N3sxH&b%bMNjxsOoU3-=D(|H!hTf*EAdQG%~m z*&}aDMv+A3N<5%XbN0y+%a)+gmhrs*f}AT}LR@^f_mghPlmaBOVGtTTTFg13CAbj} zyW1T|k)GecWfO&8d5(sT+@~Ka9b@D!$-^b(RF$7m>N;lJ43VFS0TLH{>IHThwL9KJ zmaDf=#c|Yg3TVZzkd~*GReP@y8f*tDf$xvWTebkKXYYHInC0M8=(8_u&VuA2i%BEb zd4?&V;2W89_j+9g`1F*MxPvkw=lTJmYQFgcUHhM0##6&E{U+yIC6a}BxHu`5-}sw@ zoSOpp&P+wlK9{N^XDBzFj7 zMzWSKtWBH%mN(n2gI=WnpOQQODc8fkVW zAmP07MTR&Y>!iij<`-7XX?dzvT46(*nn7}r0VGgw<}VB!oa`@mw2-iL+g;ACjHl-^ zGuLQ^(r`9=NPDipgNyp4F98(vxV&qqlC+?2tr9ZzO(4THXFN>k85LuoUzY1mU0hr? zi?sJ|vIWjvls#9u?vvBkKk(u8t3=dsOU)OG>Qdg?9jqu7q$Sd;-jvTHX0lSa8nOQC zy?5+qt_h3}-cw2h^;mYADR(#*StRJiG!Q)T1B?m29^byn=Iy7)>w)JWbBM!Lk+txj zdPLyaoO+$W^SeYdpj^d0LJ!Az-v)@iS=8zy+K0km&m2^jqo9$+5MHiVcZusZh7iV0 z0(1VUB+BKI-_KQxKRTW)Lj&qQ_-=YkG7K6M>yfU|n=Hb>p&o2~w&^VYdGyOYlhR(Y z9q4t(HRt!R?&*41hQ!ZieY0|aWDd`N2nq1~%kmnm3@>O+y~GWDX6b)jlB1rWZ>VS8 zV>uLnI3-sk+?Q^2hNT{Z1Bo%Zqz%KO^0lLHSwp<&cY|d8vS~lLLZBYe#)`7=+I3R z^qrK+M0+NsChc8bQ{+Oe`EF*SQ#=X3gRE=fcpL)C_=)cCi9c3v0!8%*cxE}yZ>|yQ zO6-gMlfV%8C>?++p+`uF`Nc1?Q;DBulDJcRF6fk9&*I|Za&)*VgW>t#CK=I3@)x0@ z*O(nsv@uUo8huY4Pt1cvT|_1t{O6Re3Sp?aFmG*crU6AcM+ieb=rRE+w8D8UZwMK3 z)Vs$NmXjElUtGSLKOzdJr!@MaZ6Tc1j-L*{)OxT#RVi#~KU{cl0*LvQKYe_O{ z_v*=9ieT&E{rmUa(Lr7PI!EL?$+mspK3`p<=7g#B2xVVrta9HW{`Ds} zT_BlaND#K04&nr(RE~{jt;xXZ^^P$a1Z9MLcuif> z%-PRBKkd}>~c?91T}&C4pWk{MQqWPa^%+=N{O~;J4kF6 z6{!4~&>no*`=t`8nW5*yb5$mFoxS$;0g%Zbku4;+J!KA<0>9bypZM8M+lgXA&)V#P z$QR`+2p#Y7dp#L(i}|qQUfz*$d8K!DLkK74O-G7N2AjtisdEcF_XE(k&Qq(_RoZ^4 z-OeiKcOO#WO<|QeYG@WbxCUqH8dB3uEM8v-Ul!4#TC)_oyW})2=hG1WrRYKv&h8Gr z)DQ2x&MuI#6y^6fZ=Pei&lNa`s?OlKck|RRUh&noJ zHo!8*$hgNo26f1sDLd6}=kvCY&P_Go@S{DqjN46$zak^&>2@kTAj%uxe*MJvB5I3y zjhA1`*(0;3Q<|rRsHt;+uFsRt8`zKBc&OgN?&#XiLguzt2V|t5n4r@&hKRO*FR7-c zGBVygU@Vr@Ii^<^J`mPzSx%M}5b7JJ2)AGjc?-fl*`mV_zWE-#f|Mlp)a9(Nd48X| zEL3UEYmN+viZZP)$j+Xa$KTtxxkXAw_WTiVt-%3Q#IprL*Q+L=otVlr?Q`LBkFqoL zLAF;%v@d?t2)^e+JmFYBA{RtOU26-DBnFDh`j|K~yjOa+>PKWe(n8lX-AHhP^bM5}|(M>|f|$@GlfrDIHdq_NU$ygkHGs zcu;c1LZm1r5YzEXe7r4qv{EIm`Gs5E^Qb~LSgulF;`1*Gvu;kc#wX;AptW`>wZHrg z$&s{!V*UNmYfhwtu!gaKP5Y7vM_58w_W0M!CJbLR-}BDf<4^09+1ui6>Q8v4o$8C4 zu`m@W&}gB#nL(k3bSyR%#N}&@nL(A)`{ zY9>$8@hI(i%unXN^qht?j3al)mg|4Dch+B3c3->qQB+VGLAo|w64ITUMmhzgLt0=X zB_Satu|Xx2lS0|h*jk69uC_j^D@(!I1!yJhmb;)WD z{onkY{=j2fkz0VqIJG6|Yz=d9q{%M;q1*b;@yB4^J?hSzH7g@jUQ~RPLaTsd16Ga`1__dri*{#O^&|D;VzIFB*1*g@JP zBE^%|Tj3b+(;HOtG>g#@943kYqWc8jbdD@gXVg$Dec!d5;``8$IRKl%YdC)GW0ztt zV;2bgxchN@!7>ixwDm%9ihiW!1sE)tD}XUN{x~W<+I7rNPIfKh~C zo%{cw-^3k5n;WTg?;LzD+$R_r=|%_k{uR=IdS&}_5B+Vx?%6HrQe&k^QpMd;&%wFu zL|#;Q32#S_2Wu-W9p{fZ{l=?zIjEBBc;}YPMidqS5X}q~zt4rBKUi4_dz<>(9kc?c zSq=e#iS;vVyB(>uqzGkKljUcq52m0m9vgIL-wRpWtR*4)tU5aPChw*W9{aUl>{S`aV3KJGZVV*Ez40pF&k(vWEL>U= zOi=rrl|?~<&$8$c2`498m(6xC40v%51EP@Jg3pfSGJVT}ksk*)y8@|P41Rw|kdPNA z;NYD?-&rQV{PBUCkK~Q+*61szx#hv{SKkG2aaLNN{lLst6Fcs!blDKALD;uxn4Uh{hT=QhadTAa%A5-IM89-_P(K-=^M#LBU>Au4MvMQ zpTP&emr2GwRyvF_TbP+^5&4sfeZd2Ngs^zUok^)kME(?~ZsS@;{xQo~Ugw});_GfM z2=4G^XG0ug)4Ap*H!fdCPh?gYx*7LoyH`95W?_1y42Gy1lu>>ASL-nY!F20Fm96FUL{6}&n|6ofmdQN^V zk_n`l2yvDv=Th7K@rL0jPtK_rDLy?3K&~}*V_oef2-6@+m)RG%9p-0|n!>iXzA{Hq{lZ60a zGI>XYvSham(2{0k)g=oR<7lL~$ujPx{r-dapdf|dHx$`_jKD@rX)`$Z`lwZ2=`ql9 zrw=fB(6piS`dV^l=krDgjA^SVt9Gt%TUWb`*C+*|ZxR&b4px8Y>C{_(_aRA}@*o0b zG5f8*)EvY3Dx2h*lt#C>wG0hm6H*K*bpWNCQr9J+wj z$~0NoSRcX&6Y_hb9S%=r;14PD@o+MPB9BEk&BSZ=0DWpC{O<5XsT&`&fy_}j+~vb- zv>p-x(+83L-#8Ch>U|ibe<}kxIHZR!yEH(aTiehhbKLkzmB+VF;(LH(oSn-Cikf*P z&3lZ$B|+eb9*p9-xyL@kNw0LN%)D#!EeuEM8I}0nyy>qDAA|M=iKWm@u8Zmio{Y5< zU4$b+Sh)D!4K`|}>K>E@XwYG8nG?~t)xTPr^IB#??sXQcn4Up`7@r1?wmmRFR#S_u z`Tv%~4k0JFD*m-=`2yU;efhyuEITljRMj;H3`GW@TN~1yl~m$hpl>V#r9~@qd!o`V zZ(Smtu7XWFgr^bAM%VoK63A=oWCwnwUfE^&7{3$Q?v~g0HP^nZ^db5CfxRZM^LBo=w_tM$R)nbq~{tR%tm(xi&zc5p>Hb{()X=vX0)B` z!$J>|;p;9pwWBDY8_eYL@j+-EH?8u~ps2S=gIg$l?%lhmp=34uH0X(caV_wz!LeIh zvTql(N3Ydk!?BTsE_DguW@}XI%qs>Um>eGCoec5cY3v2a*76{_gz{@^dZRBR0gq4p zP5c|pL*0w(k$r5O;Y{&~mCcHn%2KB2PS=T1_-%FrPk8P6; z4-}r~Gw*q@{ErmboW6aH^c<^y@kEWw=OniYYv-TesFq#*E%yLEjtB4A=~FMf{~^XL z-W5TikRn7k6bivXg#&`H^7PP<=+{B=%Yb0C=r$N8xp>H99Se)B+;Q}4HSRXFiVz}2 zFBNrY(y|W&Y}V|OPrgr^h|}(p3&V=$_=inywLCEFt*;ld{!yH2gVlm>f)`}Eh?uU+ z<7qnIQ}`v^n{H#dVryqAPWk$^CW^%TK>w6cdB-Du{&CQJa}#s%`@-Z*?IM4^D8ekw zr>22;MdozmtbIo0%gnC>L#|(rX9uF_q{uf8xm-TQ;+5x5D6txo*7hOZmnl+f!vYc! z`y0ZqIbX^J4MY)n(AOoCtT`qf0+y&&d|P{a4$qtiiSWzvJ=|t}Ajq)@3hIaQtN-(l zA3h$Q`pMK>a{wHQ)AA2BhM_Qw7|eQp#?$dzkOMRS?o<30<1#<_g*+`a3*9p?NrK%O zfaDUIV9hjwNt#`0(qUgu0Ukwg32F8`cx5wq%L$b#=QeR0aT?qS#Xv_V8-)u+DS_=N z(`2h~2cW&a_gH2DHlpIe6Fq%))DbYlDd!|BBjcU0Kg4KDdW6Cl^OWEAJn5OK)EA_H$HRILiF++lAa3Gwt}?|s32T8-CPy0bJN%>VTT%Q3OPoF~aHcbjRG&jm^v zJ13{wbVDm&w7pf=TPh3GYyTUKL>KiZDNig`>G$TKz*g3V!&4u=V{!@nf^g_*mZ7%7 zd^O;eO72VNl6hM`!6Muz<$11?34QdxC9)I7*NJS%D@jq~Z_|U*gX+o07;s+t zVHZ9b;L4kkAZ4@}LaKDofC+FS!R8K13Vbl#Z_q0zF;wfL?%v|K>p&jqUoZ~X^_s=^ z0P; zuhVHog?%BnK5lW&Y&^z7$cSdLvnd)W!||`I?XPQwC!oZiv@Cu$h6kh z)`ELb7z=VX!}7fU5yk+7^Q^EA5d-i*+3AR>DFeWdsJf?B^1yV5r)49gE&-cfCU6nV z($y5hY{RAQ0sgMEpa_ozM7qfLjw2S~NK`2fP9xG1h}!hw^AoLT6evU=2t#_@De3;%k zJsLf~=y+=!t)q z)0;*d^Z-FuzI(w{oIo2?{5gLgrQllAt8M8A;-jVjrQJmAz0^G6a@hociZVF?qZKrb zgShr9D(1gnbT{C8O0ao&9#P=uQ>IozE6&^K?D{7>Nxp0n;nA5jhqT1 z=S;K5_w9W+nS^9%zou}}7~)PFoSj6);d(4pD4s0BawwtZuISWx{uQ1MsTvD01rpHVnt;CzMp1*p9cv|8WWue7RFN&p>3M#J5mL zgUqz5AYYrpuTQd&xe*{6w`~OoAoVR+oFTPGLr3Y!62ZL5H2F7Oe zsGLdNci9ODBbNsRw}qDZUqdybZn>0qVhs?Pr%SU3@LtrPbD5sRJ|2fg@w*6uH>^RIvjN zpPce%Ek*W`vkUod2u7Zmr$~mVlFC7v(s)RfAW@VzmCJ!n3?rZ>8C=$@7C#UH z_G>1cJ4b{ZSpd|P{f1))Q-a0zQQqK*fZo!YZd6@d)G>JYgt5PWp^#MmoN$Gs*@~Y6 zRW@X*Q`>?OD+9;&11a^RaJd+nP<*+1DmTl9WZG>9_kPJqN)a6%A;V~gZ~7KYEpN&1 zUXAyT1w%$aKLUfwQ_xP4OiVW;{RsnCZ?Vzz#f$QhBPvqI;oSc&RK7m;TOB$OF>zh7 z6?gzsacaW#Hbw}%oEWbGf2|R856YOn+hBpp(FQ)xvggiT4w-Lzy#nfk)Sd z4o~Fe;h@9>eZi70Rp=|E*%Q9j&~rrIkXbn86#Z=leQ_gDlHiv8MovLjJflMn zdpL@xf&!DE``s$nl9~9(S>Ufl|K)LC`dkKUXjW9hkaAp)b`yT?tAxxWy89^aa^Ugs zyh}FzI_h_HHTK0cBN!Y@?NW77ECVr{v6`>hBKwU9V;L;DIVhMT7q<A1?Lc@tkeA`ikix)y7L?0D7xfq9KI-4yTOZ=>lZ7{_B{$86h|dhu%G1s2 zX`;@H%tvskWqzR{q~)N~8C5KJG#I@iF*IRTb^i8|N@D_q@qH$|nE~gScZ-N8fa3SQwg^6ebIGPJqO zhS z?h=g`(QnzCLL zazDN#0e0oWM#`RV_dQ-O0|E`+UjL|2@W5CUR(#ZRDOw|5+V*FU-K~ndcE~xLwT}q> zRtG`~##88Bv@I1>OQJtV2=BMNLs^3n^rjJ|w}1X!yRm^F-}9%xI%gCUDvY#eV=V-P zNpXsN_;mZNtVD0zN!uOe&vE9P{=x4Nn}O}R{q4iA_dZjOf}pO`qi{rjAE7u=_VJ1( zMleO${j3LkOd@M4{^y|^K#Kfs{6s-|i7B3T8xE;0>14%vz|g;DPS)V;4Hru#9^DI5 z=ov|FxqwF8U4D$juGpc(A0)za3A!+RNxKqpOykA==mM`BN%M)zjOIb&uY; z@ADHcx2*oHK0QQwgIbt$!so4ILJxdcMj(&?_nqnPU%46U99t|+*#;>_A0fsh>8pog|`8j{c1IFCfaOXy;jseh%k*41ahKl>dk# zHI{%d-j{M8yc#-pcMG{Bq1D~`u`!vNrk(I-ER)D`jeJrC(-NL<6{@Tuw z4&bN)t7odA1}Cx~rUm*6(Q9lcQQu!}=PqF43|I9j!E>tGdv9A#6s`EbdcVe8wzMkR zCv8_0sk#R^>&vHE_0hB>jorK?oHvb;Xc~D#xn7Uv^yAGBc!29-R^qnpbnO55#HY#g zwi_rKK2HhgvN!EKDa^lpZHGthGhbxOwkh*bYx7N_z2{kz&+*Q&HhHKY<2)jN!sBl7aws$c-Vx4Y$^89zZHn2&( zL#_sEzXFNN@@ou!douib*F;O+w+vzTIBrWX`=dUH247q`XY0%y zKh`c3c%)VPG7^^&+SE0FNWqsAY79xcK}i#$un zRt!jg;XW}foNf+_szp4HvHMeX6%#{>jV0#wM<bf#y2glLi0GPt{? z&SRiN0R4pR`WO&1Mi!?^T*=r4M{<@XWrD8uF66Xq#Rp)_`ebPO#F240e!`}X^?^av zg6D0gA<-Y57MR~NsZHp4AoSVtAb6+knjRjLMYo-|_YuMq4j_@5@u*J0DfbHcdwSy| zj~PA0)VW&C>7WK9Qwd$N0SSYRXi>73%6j{I`ihR}<`i{x8sW}E{zo@$vcLljd}ll+ zz^i=w-jLEmqRcI0X)B;nGn@ikBoV1ede2vyLhrI$P zXOKc@b_f|CI*t1dN3cP0)|}k}S!yR=+RICRL*^%-NPGhgg%Y`Vy2A@G&THIT)zj&w z+CeX+&rm&VbTt2Yyf&hEvgElqVm-lgUw!$Woi1pIM^FA}_}&*cMLCWtJhFT%r`}`5 z>14m!sTrs*OH0e|_XevvR!ssSD7@=hi`htX+%*=C<_WS%9_l7B+RaHz8cp4q45qi| zqt|7vM{g9Ltd(xOx*-0ZwaCZHF9l*32Ujb8?1+J^UO&^7&b|WT;CnY+OrB5S*ZZ70 z@yFO3@KKAOC>&|B+8uv~^6#}k5yA^m=vZnk93m53&;x{lX{Gd^-#de}!LfeFJUxMA zN}Y;EGsSl{jF8QQH#>bVGaXr+_H$9km}Zs=bOm=DAVDP=z$`{y@O9N457&+F9X`}d zFXK3zEDDKt;?*4pMV%Me-+F$s0Siqv2rozs>h3|)Y)GN-fzuCZTWCLokDu{b7+qKi z2Yb*@PcR}|9IBqOx`d+i)z}Kh}DE z+qCNuQ-aYYQ;eq35?T+{hYzec>x}d72=Q31l3za%t6%-O*|_+JM&&059{Sx;{LKBr zmwfG8ox|@YGA>;+eaC(>l_4wDaq;k8?o}un0+R=fAK&wCRB0SYkSns#r9D|HhSiid zT%3+o#?P6Fzb6G>bo%a_xR5}4=XOcWsNAvVdD+2RXp?;DK5)`^DL;elej~cxz7j=II?^k22w@ky~DjMaik)>a##n&YJxV-k$O=MJ+90acRVyKOZ}F!PTrv3$(U zf9PXw4vyi5Vdkc>OZ5cvDLTZJa(~_>AEcynH-0mDK&5NSU8UBW2HPRwBMHDa&{a3s z*-bo*m- z{GyEvV!p^LdO*~9ycTDK0^e!b-9x~(0B)t6r*kWBJkK;kNaTlL%j8yoWx(HFDhqH3 z+22`z?qXhT$#+t1wlYyjWLMO;1H^KW<~h0g(=f70>2cqK`*LF?46VPG4+z9wzQ`+t z$A^ANeCL!_wVdO8;|30noRqkRV!s_Z-On5Ppp{9Q#%u>ErrVOr zA?;;>dj~5ljY+mU3)N{TC$YqeMkUC)rP!WusD3_c;@VIs78I-H>X^m59drWBD<&ZD3%NDR!`=2k?Fe!}|| zqlTmDtl^0Tm1Ramx%gpab6pf&HxUMGpd{!jx7hQ~Hbe&3tzpIX&=f`ec`vD*uYwE9 zwU~v{%o~iK&EP#NPj8p*A5ow9_@)BP%0me5K|szggT|)nE2|RpcgYqZFJalTbwjO7 zEm}C>@Q8byr`;eJn+V?6P@c4=EexyEEvN5w>LWDkL3GW*e?nBsSK#T6?wBZhL)1s> z9GiCgf@3oKtTpjmAYv%i&quer5{eEV^FgM9CsI0^Xy?5~ z?f#FY`iTJoQ6YRF_V-E81o~!(4aX-}vbs`Q`)3_Chxp~QrU{JrUrv!2T$rcSpNz|i zPkb;5lVG#`Jo1p#A(1%pbLXSpx2&QMh_f88QD~pkFNl2xmzn`Xru)OzNsy7wTyIJtUcVg(*9i&~X(@f6G{4!x1HgT;FQh`BwTv-}i+!?kk7`AI*%?7;qT z6Tt=a=W7|NV|JH3UyU-RO6%trRwSkklm$1%_`Z8dQZe2QXWx%HHR+y;@bNSmH$c-0 zV*Jr-i%L}mV4Tb2^;IhQG-KQkb8BNiUNXc8e&lc~!qeOBL+kbz7>Pfex!N-uWt!|f z&-QWA8hX>V0{!mfUe?CAor@?^HOR?N92{nci2P=+h~4y)N4~e(hgqFbT}IY2_5@lE zpLTc}kF12z%0faqIDWO66YZ_pcN#)TBZ>$=KIgXPqb}O^5>S%nYSrUKgY%N-((l~R zBNj@&20UJ6!$wn*XE_xO>x*)$O7az#;z3gNCF6NodZocdy{>ajy=X;AUeR9y7cMFx z*w@_kai)M1TOi6$V22=FX}z^B-=vTiPZ9WbgP&D8(sEps(`aWKrKa32V#ru3=|8p!tSXu3$*ok_e2QpFGnxy+UIyJhHRN{Q7?^0 zhyDlHNQ1qDRLEhWS7%Qfh0Z_x5*R>`>-EZ{?g7qHK2O*SZV15yfit#$;_BlkM|W9S zL26Yi>ZN#(g*4E4ysz$S1^TmnEXSnk~F3HmImCPyIS1!ntxldd>lq(3 z1+@0A?0>O$HS?O~%iAtE6Hv)&u)ZjqU(#P(%3>Q;A7?q;39JW$*>l?crqj&5j5aba z9jlxU1;_o*!&55n51=QkBII5uh{&l#!MA?zhqtdj1M|(x55<0mSXpsh#g;>~f&(W% zZEp1?3g6tG^^Dz+YpBlkW5zbgJ`3-9t}Y4Yxws}xa8{^pi}LIVhkbkvs$4$c-jNOfzQPgP6mf<4>^2#ID|Poud{U%I#y3Z02Z z-gDD+^G!<{<4o(p-n-wsv<{8BkKmgq3t^9rvgCRDBRiM8_jeJXP_~cj6F`W46YaP7 zMGReYpX#=Hv8_YorZjROjo1wMH@0m;@`+0D6VG2}d6f^H(2Vpe-?Dagyed|)oXk8p zBxj4K1z5JrE~&i1UaW>B#zbFso>5Q|Fkfqs(+cJ{5&ux@KE9iuyP(ljybOHC2R0V) zww3<=$*%nd0h#W_fV_mIDOu5f^Mmu>Ij4@{vzjR++|BXZelsUWG_sTot2s9sRbRQG z1j>gf`f|Z8d=1DLQES;JjWuaLRebNT&5XZ6d?ei{POfdW@;Ttiy)R}_|L!8>d$-&> zj9u5uJ!VAKkz$gG6!Xsmg(ZVHYb;t2vE%qMM?Ncg%}m&`lU3WBno~%YAW}Z=em{DZ zht%^EEhp<5bQrbfrF;cBwy7;?w)6gFT&`T#R)ptlv7DXx~bNGK5XWYne}xUW`NC&Cl3^j_&~^XhN8X_xo5X^Bd0n^ptNAHVq^PqJKU9ya8Lk=>6yn8R(fGJrZL9s4E}svWbm>hlihwU# zhto0g=*)OA=&rX5HoC0A4!r*DtN#cd&By%RDBlM;J)AS_QzxF=gMb8inKR6vJrSd= z!RA^(v+HKAcw1U_UmfRh`qmSh&?zCV5nJeRFNJK3n`K}fr`NwxR7DK($FT_dn~(a& z(nl42J%nYrdMOs?Dv{wEQmq4j4IEs>Ia4u8F%p^6M5*6*^JUj2cqG7`CPtB9V_whYhzN#;M*kv-^}@@hkp* z9Dj)1##Rroqfp~d=9mX=_ljvy6`8K`;A`c=jg_#wnQ*eq)f=>e+DUU)+@NXWRT}X4ACn(4J?xh7$i{tF-*{#8A>8Votc${Ge0<2N$y2$DZZ z(8AE_iwq(N7RAB zQiJ!pR%0jw!p~($gZsleF~Y9D76Qt_bHh$B*U6=`ze`4f^pfu80uu38LpbZUU7kOz^LJRTx!#8ILNkpX@9H@b!%p0%ADdPJ>q}0|Dzvj5fZVti&nM|xoi|NH za-Fn-YV-|I9VEOFcD60OR_bE(%io-hpI(CCoJ1UgFLudi$Z}zkiAG)1@a*SdL1;HT z6LWCeTY<}L^3lls7*H6hu4kv~CQ&!0`11v?BaBJD}qJ4^Udc+eV!Oy{RdsvM^7o|ULC!d`G!(c>TO1;80_+AG5+~T1Y z*i8hoh~SeYGPr;UkanKkCyLR3Qs&1tf& zSLdBKJ?1jM-bk6x$r0wF7Oze2HGZd8@*|1ndiBI2z~=zuVrC6Cf0usgmx_u!WyM#M^^A1Fq4{ALS$x@b*QL(Y=?CepT(JMX7}dySaJTETaGSF#Irnr5 zGF#FpqX~=Jkq^g+&jct%>2!7qBAnpAL8Bi~LOFKEs-1&oH(BGH9$5hDuA(72Rm#hLO2eci$xLmR>JphDzJ$kzEcQ}_qeHaI8~;IY5^46oml tBT%<{A*zl9ll*-X{{P4RpIVT)D{?HeHK|B3_zmz+PFh*2P{P>%e*ri&1myq# literal 0 HcmV?d00001 diff --git a/i18n/messages.fr.xlf b/i18n/messages.fr.xlf index 7d28b8a0b..f6706bfba 100644 --- a/i18n/messages.fr.xlf +++ b/i18n/messages.fr.xlf @@ -2597,7 +2597,7 @@ Compte de service par défaut ../src/app/frontend/chrome/userpanel/template.html - 26 + 27 @@ -2606,7 +2606,7 @@ Connexion ../src/app/frontend/chrome/userpanel/template.html - 34 + 36 @@ -2615,7 +2615,7 @@ Déconnexion ../src/app/frontend/chrome/userpanel/template.html - 39 + 41 diff --git a/i18n/messages.ja.xlf b/i18n/messages.ja.xlf index 2c1d71e65..2db6b8f38 100644 --- a/i18n/messages.ja.xlf +++ b/i18n/messages.ja.xlf @@ -2423,7 +2423,7 @@ デフォルトのサービスアカウント ../src/app/frontend/chrome/userpanel/template.html - 26 + 27 @@ -2432,7 +2432,7 @@ サインイン ../src/app/frontend/chrome/userpanel/template.html - 34 + 36 @@ -2441,7 +2441,7 @@ サインアウト ../src/app/frontend/chrome/userpanel/template.html - 39 + 41 diff --git a/i18n/messages.xlf b/i18n/messages.xlf index cf52c027f..b3d2c4112 100644 --- a/i18n/messages.xlf +++ b/i18n/messages.xlf @@ -2224,7 +2224,7 @@ Default service account ../src/app/frontend/chrome/userpanel/template.html - 26 + 27 @@ -2232,7 +2232,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 34 + 36 @@ -2240,7 +2240,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 39 + 41 diff --git a/src/app/backend/client/manager.go b/src/app/backend/client/manager.go index 4bc588959..3be2266ed 100644 --- a/src/app/backend/client/manager.go +++ b/src/app/backend/client/manager.go @@ -50,6 +50,8 @@ const ( JWETokenHeader = "jweToken" // Default http header for user-agent DefaultUserAgent = "dashboard" + //Impersonation Extra header + ImpersonateUserExtraHeader = "Impersonate-Extra-" ) // VERSION of this binary @@ -311,12 +313,37 @@ func (self *clientManager) buildCmdConfig(authInfo *api.AuthInfo, cfg *rest.Conf // Extracts authorization information from the request header func (self *clientManager) extractAuthInfo(req *restful.Request) (*api.AuthInfo, error) { authHeader := req.HeaderParameter("Authorization") + impersonationHeader := req.HeaderParameter("Impersonate-User") jweToken := req.HeaderParameter(JWETokenHeader) // Authorization header will be more important than our token token := self.extractTokenFromHeader(authHeader) if len(token) > 0 { - return &api.AuthInfo{Token: token}, nil + + authInfo := &api.AuthInfo{Token: token} + + if len(impersonationHeader) > 0 { + //there's an impersonation header, lets make sure to add it + authInfo.Impersonate = impersonationHeader + + //Check for impersonated groups + if groupsImpersonationHeader := req.Request.Header["Impersonate-Group"]; len(groupsImpersonationHeader) > 0 { + authInfo.ImpersonateGroups = groupsImpersonationHeader + } + + //check for extra fields + for headerName, headerValues := range req.Request.Header { + if strings.HasPrefix(headerName, ImpersonateUserExtraHeader) { + extraName := headerName[len(ImpersonateUserExtraHeader):] + if authInfo.ImpersonateUserExtra == nil { + authInfo.ImpersonateUserExtra = make(map[string][]string) + } + authInfo.ImpersonateUserExtra[extraName] = headerValues + } + } + } + + return authInfo, nil } if self.tokenManager != nil && len(jweToken) > 0 { diff --git a/src/app/backend/client/manager_test.go b/src/app/backend/client/manager_test.go index 03f00322d..1aa12b5da 100644 --- a/src/app/backend/client/manager_test.go +++ b/src/app/backend/client/manager_test.go @@ -305,3 +305,289 @@ func TestClientManager_InsecureAPIExtensionsClient(t *testing.T) { t.Fatalf("InsecureClient(): Expected insecure client not to be nil") } } + +func TestImpersonationUserClient(t *testing.T) { + args.GetHolderBuilder().SetEnableSkipLogin(true) + cases := []struct { + request *restful.Request + expected string + expectedImpersonationUser string + }{ + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{ + "Authorization": {"Bearer test-token"}, + "Impersonate-User": {"impersonatedUser"}, + }), + TLS: &tls.ConnectionState{}, + }, + }, + "test-token", + "impersonatedUser", + }, + } + + for _, c := range cases { + manager := NewClientManager("", "https://localhost:8080") + cfg, err := manager.Config(c.request) + //authInfo := manager.extractAuthInfo(c.request) + if err != nil { + t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+ + " %s", + c.request, err.Error()) + } + + if cfg.BearerToken != c.expected { + t.Fatalf("Config(%v): Expected token to be %s but got %s", + c.request, c.expected, cfg.BearerToken) + } + + if cfg.Impersonate.UserName != c.expectedImpersonationUser { + t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s", + c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName) + } + + } +} + +func TestNoImpersonationUserWithNoBearerClient(t *testing.T) { + args.GetHolderBuilder().SetEnableSkipLogin(true) + cases := []struct { + request *restful.Request + }{ + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{}), + TLS: &tls.ConnectionState{}, + }, + }, + }, + } + + for _, c := range cases { + manager := NewClientManager("", "https://localhost:8080") + cfg, err := manager.Config(c.request) + //authInfo := manager.extractAuthInfo(c.request) + if err != nil { + t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+ + " %s", + c.request, err.Error()) + } + + if len(cfg.BearerToken) > 0 { + t.Fatalf("Config(%v): Expected no token but got %s", + c.request, cfg.BearerToken) + } + + if len(cfg.Impersonate.UserName) > 0 { + t.Fatalf("Config(%v): Expected no impersonated user but got %s", + c.request, cfg.Impersonate.UserName) + } + + } +} + +func TestImpersonationOneGroupClient(t *testing.T) { + args.GetHolderBuilder().SetEnableSkipLogin(true) + cases := []struct { + request *restful.Request + expected string + expectedImpersonationUser string + expectedImpersonationGroups []string + }{ + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{ + "Authorization": {"Bearer test-token"}, + "Impersonate-User": {"impersonatedUser"}, + "Impersonate-Group": {"group1"}, + }), + TLS: &tls.ConnectionState{}, + }, + }, + "test-token", + "impersonatedUser", + []string{"group1"}, + }, + } + + for _, c := range cases { + manager := NewClientManager("", "https://localhost:8080") + cfg, err := manager.Config(c.request) + //authInfo := manager.extractAuthInfo(c.request) + if err != nil { + t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+ + " %s", + c.request, err.Error()) + } + + if cfg.BearerToken != c.expected { + t.Fatalf("Config(%v): Expected token to be %s but got %s", + c.request, c.expected, cfg.BearerToken) + } + + if cfg.Impersonate.UserName != c.expectedImpersonationUser { + t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s", + c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName) + } + + if len(cfg.Impersonate.Groups) != 1 { + t.Fatalf("Config(%v): Expected one impersonated group but got %d", + c.request, len(cfg.Impersonate.Groups)) + } + + if cfg.Impersonate.Groups[0] != c.expectedImpersonationGroups[0] { + t.Fatalf("Config(%v): Expected impersonated group to be %s but got %s", + c.request, cfg.Impersonate.Groups[0], c.expectedImpersonationGroups[0]) + } + } +} + +func TestImpersonationTwoGroupClient(t *testing.T) { + args.GetHolderBuilder().SetEnableSkipLogin(true) + cases := []struct { + request *restful.Request + expected string + expectedImpersonationUser string + expectedImpersonationGroups []string + }{ + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{ + "Authorization": {"Bearer test-token"}, + "Impersonate-User": {"impersonatedUser"}, + "Impersonate-Group": {"group1", "groups2"}, + }), + TLS: &tls.ConnectionState{}, + }, + }, + "test-token", + "impersonatedUser", + []string{"group1", "groups2"}, + }, + } + + for _, c := range cases { + manager := NewClientManager("", "https://localhost:8080") + cfg, err := manager.Config(c.request) + //authInfo := manager.extractAuthInfo(c.request) + if err != nil { + t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+ + " %s", + c.request, err.Error()) + } + + if cfg.BearerToken != c.expected { + t.Fatalf("Config(%v): Expected token to be %s but got %s", + c.request, c.expected, cfg.BearerToken) + } + + if cfg.Impersonate.UserName != c.expectedImpersonationUser { + t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s", + c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName) + } + + if len(cfg.Impersonate.Groups) != 2 { + t.Fatalf("Config(%v): Expected two impersonated group but got %d", + c.request, len(cfg.Impersonate.Groups)) + } + + if cfg.Impersonate.Groups[0] != c.expectedImpersonationGroups[0] { + t.Fatalf("Config(%v): Expected impersonated group to be %s but got %s", + c.request, cfg.Impersonate.Groups[0], c.expectedImpersonationGroups[0]) + } + + if cfg.Impersonate.Groups[1] != c.expectedImpersonationGroups[1] { + t.Fatalf("Config(%v): Expected impersonated group to be %s but got %s", + c.request, cfg.Impersonate.Groups[1], c.expectedImpersonationGroups[1]) + } + } +} + +func TestImpersonationExtrasClient(t *testing.T) { + args.GetHolderBuilder().SetEnableSkipLogin(true) + cases := []struct { + request *restful.Request + expected string + expectedImpersonationUser string + expectedImpersonationExtra map[string][]string + }{ + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{ + "Authorization": {"Bearer test-token"}, + "Impersonate-User": {"impersonatedUser"}, + "Impersonate-Extra-scope": {"views", "writes"}, + "Impersonate-Extra-service": {"iguess"}, + }), + TLS: &tls.ConnectionState{}, + }, + }, + "test-token", + "impersonatedUser", + map[string][]string{"scope": {"views", "writes"}, + "service": {"iguess"}}, + }, + } + + for _, c := range cases { + manager := NewClientManager("", "https://localhost:8080") + cfg, err := manager.Config(c.request) + //authInfo := manager.extractAuthInfo(c.request) + if err != nil { + t.Fatalf("Config(%v): Expected config to be created but error was thrown:"+ + " %s", + c.request, err.Error()) + } + + if cfg.BearerToken != c.expected { + t.Fatalf("Config(%v): Expected token to be %s but got %s", + c.request, c.expected, cfg.BearerToken) + } + + if cfg.Impersonate.UserName != c.expectedImpersonationUser { + t.Fatalf("Config(%v): Expected impersonated user to be %s but got %s", + c.request, c.expectedImpersonationUser, cfg.Impersonate.UserName) + } + + if len(cfg.Impersonate.Extra) != 2 { + t.Fatalf("Config(%v): Expected two impersonated extra but got %d", + c.request, len(cfg.Impersonate.Extra)) + } + + if cfg.Impersonate.Extra["service"][0] != c.expectedImpersonationExtra["service"][0] { + t.Fatalf("Config(%v): Expected service extra to be %s but got %s", + c.request, cfg.Impersonate.Extra["service"][0], c.expectedImpersonationExtra["service"][0]) + + } + + //check multi value scope + + if len(cfg.Impersonate.Extra["scope"]) != 2 { + t.Fatalf("Config(%v): Expected two scope impersonated extra but got %d", + c.request, len(cfg.Impersonate.Extra["scope"])) + } + + if cfg.Impersonate.Extra["scope"][0] != c.expectedImpersonationExtra["scope"][0] { + t.Fatalf("Config(%v): Expected scope extra to be %s but got %s", + c.request, c.expectedImpersonationExtra["scope"][0], cfg.Impersonate.Extra["scope"][0]) + + } + + if cfg.Impersonate.Extra["scope"][1] != c.expectedImpersonationExtra["scope"][1] { + t.Fatalf("Config(%v): Expected scope extra to be %s but got %s", + c.request, c.expectedImpersonationExtra["scope"][1], cfg.Impersonate.Extra["scope"][1]) + + } + + if len(cfg.Impersonate.Extra["scope"]) != 2 { + t.Fatalf("Config(%v): Expected two scope impersonated extra but got %d", + c.request, len(cfg.Impersonate.Extra["scope"])) + } + } +} diff --git a/src/app/backend/validation/validateloginstatus.go b/src/app/backend/validation/validateloginstatus.go index 391948337..2aecded19 100644 --- a/src/app/backend/validation/validateloginstatus.go +++ b/src/app/backend/validation/validateloginstatus.go @@ -30,21 +30,34 @@ type LoginStatus struct { // True if dashboard is configured to use HTTPS connection. It is required for secure // data exchange during login operation. HTTPSMode bool `json:"httpsMode"` + // True if impersonation is enabled + ImpersonationPresent bool `json:"impersonationPresent"` + + // The impersonated user + ImpersonatedUser string `json:"impersonatedUser"` } // ValidateLoginStatus returns information about user login status and if request was made over HTTPS. func ValidateLoginStatus(request *restful.Request) *LoginStatus { authHeader := request.HeaderParameter("Authorization") tokenHeader := request.HeaderParameter(client.JWETokenHeader) + impersonationHeader := request.HeaderParameter("Impersonate-User") httpsMode := request.Request.TLS != nil if args.Holder.GetEnableInsecureLogin() { httpsMode = true } - return &LoginStatus{ - TokenPresent: len(tokenHeader) > 0, - HeaderPresent: len(authHeader) > 0, - HTTPSMode: httpsMode, + loginStatus := &LoginStatus{ + TokenPresent: len(tokenHeader) > 0, + HeaderPresent: len(authHeader) > 0, + ImpersonationPresent: len(impersonationHeader) > 0, + HTTPSMode: httpsMode, + } + + if loginStatus.ImpersonationPresent { + loginStatus.ImpersonatedUser = impersonationHeader } + + return loginStatus } diff --git a/src/app/frontend/chrome/userpanel/template.html b/src/app/frontend/chrome/userpanel/template.html index fbdb25c60..de0f38942 100644 --- a/src/app/frontend/chrome/userpanel/template.html +++ b/src/app/frontend/chrome/userpanel/template.html @@ -18,13 +18,15 @@ limitations under the License.
- Logged in with auth header Logged in with token + {{loginStatus.impersonatedUser}} Default service account +
-- GitLab