From fcefc8d4cbb61a1e52384ce7cad774422c7a47f2 Mon Sep 17 00:00:00 2001 From: Philip Sahli Date: Sat, 14 Apr 2018 07:53:04 +0200 Subject: [PATCH] csrf debug false forbidden (#42) * develop csrf debug false work #23 * refactor to start heartbeat process in test * fix test cases * don't raise exception on strange heartbeat message * update coverage to 4.5.1 * save artifacts on build * rename screenshot files * format tox.ini * create png artifact in test * always copy pngs to artifacts * set to always * code cleanup * fix code change * temporary work * deactivate cas_logfile * add username to context, move is_authenticated flag * cleanup and update to new python-social-auth * ignore png in root * fix dict attribute error * add selenium IDE project * use postgresql for tests because sqlite database gets locked * add postgresql container * fix test --- .circleci/config.yml | 8 +- .docker/config.json | 9 + .gitignore | 4 + cli/tumbo-cli.py | 5 +- diagrams/AuthenticationArchitecture.png | Bin 0 -> 57560 bytes diagrams/AuthenticationArchitecture.xml | 1 + docs/authentication.md | 86 +++++++++ minikube.ini | 11 ++ requirements-tox.txt | 4 +- requirements.txt | 4 +- selenium/tumbo-selenium-project.side | 1 + tox.ini | 16 +- tumbo/aaa/cas/authentication.py | 23 ++- tumbo/aaa/cas/middleware.py | 65 ++++++- tumbo/aaa/cas/pipeline.py | 3 + tumbo/aaa/cas/urls.py | 2 +- tumbo/aaa/cas/views.py | 19 +- tumbo/aaa/pipeline.py | 39 +++- tumbo/aaa/templates/aaa/cas_loginpage.html | 8 +- tumbo/aaa/tests.py | 32 +++- tumbo/aaa/views.py | 2 +- tumbo/build_release.sh | 3 - tumbo/core/communication.py | 25 ++- tumbo/core/executors/__init__.py | 19 ++ tumbo/core/executors/heartbeat/__init__.py | 63 +++++- .../core/executors/worker_engines/__init__.py | 2 +- tumbo/core/executors/worker_engines/kube.py | 1 - .../core/executors/worker_engines/rancher.py | 6 +- .../executors/worker_engines/spawnproc.py | 3 + tumbo/core/importer.py | 11 +- tumbo/core/loader.py | 4 +- tumbo/core/management/commands/heartbeat.py | 31 +-- tumbo/core/models.py | 5 +- tumbo/core/templates/default.html | 4 +- tumbo/core/templates/fastapp/base.html | 2 +- tumbo/core/templates/fastapp/base_list.html | 6 +- tumbo/core/templatetags/backend_utils.py | 2 +- tumbo/core/templatetags/fastapp_tags.py | 1 - tumbo/core/tests.py | 5 +- tumbo/core/views/static.py | 3 +- tumbo/tumbo/dev.py | 16 +- tumbo/tumbo/dev_kubernetes.py | 3 +- tumbo/tumbo/settings.py | 102 ++++++---- tumbo/ui/tests.py | 180 +++++++++++++++++- tumbo/ui/views.py | 16 +- 45 files changed, 697 insertions(+), 158 deletions(-) create mode 100644 .docker/config.json create mode 100644 diagrams/AuthenticationArchitecture.png create mode 100644 diagrams/AuthenticationArchitecture.xml create mode 100644 docs/authentication.md create mode 100644 minikube.ini create mode 100644 selenium/tumbo-selenium-project.side delete mode 100644 tumbo/build_release.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index d5c112e..671d51d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,10 +24,16 @@ jobs: # - run: bash bin/create_package.sh > /dev/null - run: pip install tox + - run: mkdir /tmp/artifacts - run: tox - run: pip install -r requirements.txt # TODO: coverage should be run in cotainer and data directory for output should be added as volume - - run: tox + - run: + command: | + cp *.png /tmp/artifacts + when: always + - store_artifacts: + path: /tmp/artifacts - run: coverage xml - run: python-codacy-coverage -v -r coverage.xml # diff --git a/.docker/config.json b/.docker/config.json new file mode 100644 index 0000000..52be0af --- /dev/null +++ b/.docker/config.json @@ -0,0 +1,9 @@ +{ + "auths": { + "https://index.docker.io/v1/": {} + }, + "HttpHeaders": { + "User-Agent": "Docker-Client/18.03.0-ce (darwin)" + }, + "credsStore": "osxkeychain" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c0d2dc8..b15a6ee 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,7 @@ secrets/* *.retry .vscode +k8s-files/ansible +.docker/ + +*.png diff --git a/cli/tumbo-cli.py b/cli/tumbo-cli.py index df05f41..23a13b2 100755 --- a/cli/tumbo-cli.py +++ b/cli/tumbo-cli.py @@ -324,7 +324,7 @@ def projects_list(self): print tabulate(table, headers=["Projectname", "State"]) def project_create(self, name): - status_code, response = self._call_api( + status_code, _ = self._call_api( "/core/api/base/", method="POST", json={"name": name}) if status_code == 201: print "Project %s created" % (name) @@ -332,7 +332,7 @@ def project_create(self, name): print status_code def project_stop(self, name): - status_code, projects = self._call_api( + status_code, _ = self._call_api( "/core/api/base/%s/stop/" % name, method="POST") if status_code == 200: print "Project %s stopped" % name @@ -921,7 +921,6 @@ def stop(): for (each_key, each_val) in conf.items(each_section): # print each_val # if each_val.startswith('b64:'): - # import pdb; pdb.set_trace() # each_val = standard_b64encode(each_val) ini_dict[each_key.upper()] = each_val diff --git a/diagrams/AuthenticationArchitecture.png b/diagrams/AuthenticationArchitecture.png new file mode 100644 index 0000000000000000000000000000000000000000..4a25dd22e0aa56e8351d0f21e3eacf9182d71440 GIT binary patch literal 57560 zcmZs@cRbbo|3B_*~=j;%7YLrFtqD})e2vdPXU zWhDKcr|W&aKcC;{_WkE_h4Xqn$K&~U+{fc7!PHonmWrKSFGnwChmynmyrPI&M^EFN^(zDcfN1=`*CDo_UMqn5P zs2N!_WJEO(2vdZHk8%2F)bc>gpI?1{wmdyN4*~)Lueo1qIBp5;`RZ4B`pCWNN%bQo z4MRgi8Vmyz3eiwUApX~v`9BPLD73~n4uyijNO9Qz`BKS%NJnjgxBRc)CVtkL7yLde z{(pY)-;ZGka{u=kFw$)XgbA*O?*F_MJO_OD|Bgi+QCBJC^h>R6q^Zk5IO@9Izwd@= zK+T0rbkB+flo&2t*7W)*;|3mBCqr5u%W?LTTyo;Iv$`?QGGEB(@ICl+~zYq>9K$H5vU#%$oi?nJ^7?J)C4Eh66kZ1_eJQjjB)L{dWmose_-2QxjhzN?M){ zt;X^B!2eyHDDXo~lxem!Sj=`dC?0)?yZ`*ZOY2AcC>=_l4ZIeN!H|)UpZ~^ebMXFK zHiu!gKPOtAtw)0X>mxCbte-r-F)Us^iZfrZZ0`390e^zFf)4(f40+u@{o1Jsw_6 zSo`*+|J?0KDR$K``Ixx4(9xKZ?T6ZTg|EK6ZTaHnrNXIOcU+mRQhC(MPk;X!jpK>X zYxG^c=Ka{A%{W8Q=wZa~W3!P`j9ucZJD<-_HTjGE*!m=!ZESYtRm6{v*888_3qN$l z(cjo#`>?-WQ8=>D6$ifsajBY!vZ7J`_(1L!tIGEQSQ1tWb=kLBqzc@NB=Ot;A z0%gD5c@6yn<;EdGsT^>gb!g3dWLdT7`)Br?ZF7*c zQJ#ES#ILUx6ulRr88s!kspd_7>&@%2o2uc*nfh6x+TT})D$)fELS|+kP#o;cxwrIG zXn7u6Y&2~gfK7AdkL-%2&6IM|%$d=w0K4bH`B72ZZT|8rFQ+E&B?`NikdIM&8y~Ch zWqzG&Q#0mpa1HbreW*=suZtQQ)IKXI<(FM@wZX%(#zs1Vvc@K9q>W`&R0d9Jr3E7C z<2k->!~=#?$Aunjzin6NRP?KiMmV}Zb?S=sTOH)eiM`LRYSJwp5b^BV`#yUzWW=PK z?x*Hp*_Ok_yC=V5*z9*N^Ggj>7~WePSxZVxlzHRmC$v@X)}M>MJ^d=pxIh{6@zJG{ z(FzlbL}roZ0-hwNtt(4`i*FKIJa&x8V%E;uz9RHxiLDxq25pMoa@n44oc09C9 z*^s;TpRQ`F-d-i+qex=mD-V188EVP(;?_ips8!9{$Z(*2^i2h3vJ()( zZsxb&I%l5N%3G&l5OwU};MJ5fr0If_zDR&>#acbGd>2KN6d1&W;KOsxkx|msMa9*n zlp8(x<{=WW@%j0Uu44UrvhL+SHYY6h7T>xQzjl4cxG~)*8+TRt*VeNXcPV7({+fnc zUuJQ`T*L!7yqQL--b~P>z;LJSk1EGe<=wU{JKGbT>z4P=-O&5g5?3Q7+>>;s%Oy&_ zU6yo-PHAZ3v0Vu`XQO!vzCi;|U3!{mDw@0&NaJ8x5_VHgh|ZwyJhR8Pjh!wfk<3BT zO#xr^BTtWnK31AVt8I=}&@Y)+n)1eRGB7Zl^O%-@OsyU`U$uH z##n;7$87k4ow*+FwE~~=^^LJA{BHe&3!e1{BB4LGpkzp2?T*7W>Mt24s=Qkd`Jt3( z$ep@tDLiU}E)RLusC>2JIsB3c+%;z$5BGX_zAG~n#17d9f620OvdxXtWUQ0OLX^!# z{>hIx`oiWxc?R1LHl3?LV!&dL08zrL71MQ=%xiZkYIF>$jU)6V#u<9qJC}-l2RI5+ z!j8V}v0KKAjz{rP?k6?6gm;Bfj8NqtY<#@y)@9SaF;ZsGETMWhPgcOcjFk66wMn+q zFX#tZ*LCv>E#H}bCA}0^ceZBLv+Z~GkKOkd=1ikV$%B4784ctqWWYnhHmB~P9mA}B zn!ca(PE>&ef0%+LKZ zV@QX_VXBNfLC+E2Bz5|MzL_RhY@AMyVSMV*rD~+uNxKb2ar-f@N1e+Ve?mddv8E3< zBb~iYO$x2w`!V70{WvO+(n;Tj62*AA-qnf!1k{`wcl_HZ=4ewa4HiY>S!0KgLk{*v zVR=&{vVvW{il8C9AseP5$UW4VUTf@AIvOb+!eWj?#66v4_P4hfU7U=#lme1^wEo!4 z4NLa&dWEj$2j((&_Qxr6BA5rrq3d8JuLY}Ld%Ch~j72);mq;{U-X52m4aU=m9;ccZ zyR@a025O-85r}j|h+@teYSI7=tS_VExz6^J?bkcG;zgB#o=$6B@>*P(Aa_1>q9kKb zxB) zPKk8-O$|kLE3oZvzxU7|TQ7di^5RFjO}>w*X@~0Y>cvz{`r(D6&`v#Eu?6?hm8&uN zE1U{Gb>4ULgOC~RJWI~2pYjLms#6u)sb3EdD!r2`nt9J~kwyVMNlxj}{@OTS(Qmnz zVPoJzDwg|54)hLCWU<}wqwh7j7v*WO+(PjPy|oZUhV0AjElRE zZa=i5@kcHYAb7IZe?ENpVEC{|h@eI=Bq#(8A{fav$xUEMcMz8l&NX}MqZQw`RrDxb zt^2D7INoThyu2^vSi!T&{;i!q7JA(F=k$S^Rum;28s^Gj&mQzJ^ML*$)h+hd$HSDp z;w#?U(+b^)i&VjMYt-=LC#}bi_*2O@Sr%+Jyd-0uH(i-P%?)$tX-+-Av6MABfew2e z#+#UjQ!fJ06LI~Gn=$}k!-?v+o>ZQmb`%bRCoNeXtFmaWPMEtsd)3~>1JZSgFiB+zAJUE@123vDML42frJT})W`XO#|!(RnobE4 zD|PMT?;T^l*iCw z-wV2{lVRGbh?J=px1Mo>Ab3$D^{rS9K|zq*k0wYE#F$@bP^Xs;%9JzK9UokbMT^)oAWu;*P!#%Rqm1_!)6+#dn zhI&KkeK7gnC|mpt$7n8FlASeLBfKf?nbVg3mv%a(-uQzaWTGAs+Zo!N zq#PrB8}odPy4VBpk1k1>$cN^Q>9$ z8tU5x#bH}yF{XZ1S!gV(V>0eBi{aH5H{Z^3Zf$gRK_uxl?-p4n-E~LV42uK|INjzS zfWJO&B|!0vPF&96q7^bx%6yV!GQ0zDdzv*;H0CSDRCB?x*X1_ehqoE_wa62BN`*1? z!Q`oEZnUvX+t%J%K09xxqn3N2w^d2u{n7n&GxDf;ZNM#dH>|D+!X~+*pa6^9eU~Z1 zwA1a;?%=~$&zYvMDSaZEu5VciRp+QLR8a|eLOnHE-*(zp_ml7 zW!Y5}Bc0$qOqp~ZsX7_cZ7k_L${>_VfKOMDEYbROIFjwD=Q8Qx(=$k-8F^JvWKv%9 zPVIDc&NdHedr6X1_Y>+Ek(KdZ;uh8n86CsALP>*7+0c|Ojs-y!o!I=V2)6yWM*e0> zUTXr|0xvAhjAIe0bqD#{z_xpJH(i(;7K7-(lHH45NO*GtIh&|K89eXbL8q*&Fv$=% zz3oTwa5)7ntn_YsWi18W4%7K2K294jz95I)OSTK?rP|_sz<4tTaee@Pi{|wh%6XV_ zoP#DXQjc`}%(!d>nQRS9jWx1WPh5u%z7nS-ETtN-&619Fy)o@ha)PL)mt=LN+}rEU zz`nUbxax>|5z7h1zi-*==E%EEv%?iH?WH-%pc(6RPJEf9L+dfDjzj@#NZ=?qOW>)A ztO?w&sPS&LE&xc&KRM87ozj0XV>Dh#c>-$}YCytnkD7$~DDD?SyW z#4+Jbj7ed(j^t!K6_coKu8U;kIsyS#o-QR@csj<5D9}qiM$vaVNC|y8G(skB+^utL z@3iq#eX9KqV;Y@OYB9jGw+CC&5N4(7yyXb%ydK9uNygy{Rg*GZfP4K!JEllIpAnF7 zQI(?kSgeVpM?=X=DReHZkjp^m;WXZ5^UIw0QAhi2zjC~fNPWjE=)3k5nd2nS6j*e} zebZ+0nqH!3LK%bU{=J|m`hsWVyX{5aW$ zdH_PYH#w3_wk%nbaZK^ztj_0`cRtxrinQD4YxJ^yV^;r$P>sV4%hg=&g>x=k_PyYV z-0GX5&*fiM=Rh#~!?@}?6|H7vYjl&6hZuh1P0u4Jd*eBw&C$EhJGzQ$!q?cA*(b5g z(duHirWKgqD;V}GOk*xj1jVuZj=udW!{Qr5(urtxs}>7{7he1*_+A6bF&vtU7It=y z<8h*wn#|-R|G|u4#BE58<$3mp@6Jc{{33BYX@7=Qoj>Cx0dv!gAni0`{#Nvu>}Z&^ z;_YF53H*$cd`GUo`>wFglXPtIUfxq|l2m7wf}_3uDd`IK{I&g3!`-SVEugA8g zZEe+f_Ey%o{g6dm$zz1>u#Y3hX~yr`*YncQFH_9QS~vM!xPUI^Z>GI7OzFDceSM<- z@kf_mq7_0jy}qDE#wP*_a@5s+})~+H0HVwvK%$yVa7E1+Kn5OoqjEP2j1gHdFBWHX_5fag_ytk(Hr70lz z^SrtmjacI$GsMqWDIWC6@uK&L1#9)bz~+?beS`uOvWM{=}WKsDEGONEDTl)Gr+~6JLd>qs6ErKcql@S6MCzs=im+Shtwkd++y1 zHM~48Xo(3oLKM4jXCUwPWOQF^G7fau>9Bwg4#6g4(VY2m(dK1T6-h_mLJP7Uu&44U z7v(116E^D5R6XFO1q%x_tn*NGPrq_96-J6a^vq=yVTntnX~A^n?|bgkgNjbaG?|Buw)GPKnQpwEPxF8s1sK3v2OTyrfCJ(HlxG`q zgcU^x4e1KxF<rT#dof6{H%!d4KdvRNmkf8}9j4IB&t0o7y&GpLU}jzQaK(euTC z#dxFL&Wc|6x#yC3IBrB`kS9M#6Knh@cBR%9!*Ok}Xc?0+5=y38^7>$VbZXtR~;|ru-d^>^y9u7UU;7NirrH(F6)2{`|eax3Q(7BQ>g1SX-irBt=@iwTSh zQ>sC`Se+Ekzsd_hRhMN?zpuP6*wB#nS5>xQfU4_ht^K>c)x>e zO8OSX$2^4zbg!Gu-z^x>ZoiZ=@jH%QMC;N-qix&!0!)@cc9NK%vV#5Q#FGlZx<3H1 zq*6VKH0348`Xzu>ju7R_(rVQi_0b{?vPbqUia&rNkg2N93{;bik;8!P6@WBL180Nd z-o3jock9p_$a0m}KRje+V9+@F_QhoccRG_r6y##4Sk|^aJL#mbQPe(3L5rq1g>pupiA4FXkCP z(Nv1eHEFDOyBmEeOu;^Id%75C0Tu__GgZE;gDEeGvcs4DoY7*fIOkyGb74MF(L~?( z*%Eg87UhP;v>fNJ-(am61QMc+AabY5q9Q=q`Z#dnMp$=57!Jrxu|Or$t1u}n2RhK3 zN22HsAr^gpFeVrY=}Q#6y<=vO&G`JW)0m#<hs2Tz}v79I3kk=|GlK{Qd!o!ygPVfmmxv~G$` zQ|R%z^heFV=~#Mya3*hm$}mS8=*&I2qRoG8pftQc+f?x*UD}B0Hiqy;5}b(QBz3T( zH*Hn(rP5MPeDAX+pZW5wRKCmue3z1r)6u*L&ne=h2_)>2b2mn$;Od9QKo+%9bC%QAhIo1b5v<(LCY>b!I^-x0n4CCA~L=Q8{4+2(uq z&)?~bbnnZ2SN6y*4bOV^iq{_$MKmRS`G*o+UmHf{F%%sN{-ot6dj>R!V*b7Q;Rx83 z^(aoZ&m)O?SL+W7evIzO4eRjMfm*Dp| zC$2>k7o-oSM9*EiI(Ns2(ptPEOu@H_YtkZxjS-g?Nn%3M8H%_G?c!0|?$18EG=vDi z%ZDSEzJ6|4eJW_2*UL?YR_PMM;^trunjFh)SQJrbCMu@zp3e=Qs|4;bExjQ;oN2o6 zYa&|scQhVl;skQmJ`lLPc~IQHets=c^xrTAvVWET=J?j{!(TgXpQ`uPKJcH`W?S>> z=?wq@{TUh(J^1~ zTNm+O{F7f=tj@}H1{2CUP$BBlvzhvEU-&ODFhY3cES1HlmXuxnx!t8;w{*Op5@>bB zry22*tc1rNx;2)Hl*ZYI|F}eV6cyLr*iJ1K`tvp&%avDNycy)~>Dc^W0U=%u926K8 zzUOBboZ2zEyZXXkZH-W>;D-_DD_s_l)YvAJkVZb9Hr>2LhgJ}d68?3tlPb%343Q?h zaeL5=;wuM}kVn+9VB#Z=Gr2icG!ys>Luxx`zPFZq(~7lt_($z{aFed|AB>@a!@lO& zl5Xjrcx@3g=30@i0!%qZZUR)WAQW{LbY*5+A>*;hY0>%j$! zC($)%(wgfZtq0|gR7h6;qZ!KU09f4Vax42ITN*c<_gCc*)yTM)fl3>Lcq8AWn%$rG7 z5Dik;_tgsH{D;(R5?*%GALsK7&!RQ#t%zVil=LMUPV#+v;}Y}UL}8|=i(ud7#`+~X z@;AMz&Ea=XRe}?JCOp+KzF*37@o>1r4UxY`2)LfU%~+#9PXvc+Lj!Mo6Z*aMXLg^j zhZ+ib*QP&1C2Dh`q($~3MuOTtu7jRxaPEf7{YA>Mv}GP203dZI}b!|eOwg@e&m%H8yYx? z(9cj)6BtU*`gYz!bh-VJn2Epy5Me#YM6@qz%(sqQ$2?KC3k9DAy`Z{R+0iMB348EYQel ze?$pdYz?oCTQoOYJMMc`3ZbEFvpj5t8-8X$&&C~=}V7SkZco8vVY-<5&lwYBR+ zw;b{WAVRYgPK7lv!gCQPK|jyqceq&Tn)a)U2LL2f__{wwPP{zJYkuH1kAkK2bd!21vot-!q zmJL2dL)1WeBGfZls_rZdIYSR zReTp<01C>LUJISXmIe?HOf#+F4?ub8Mni_?<*w_ z7v?0JcS)1a$j18el)ljhaR@+hlbC@X&flLuomejvh)0z&@XVzc5E0gOq*R*2ue@ER zp1G%X1+_)~Q6=KYh?70tD-9l8cCQ1H1r2lC(WJ9bt^ozB*J`VOFB! zz4#rFm0a}6(Q4szD)SxK+Ee2g^!2W(q$_|2tJ`%G8N3+r81_xU89x2pc+oF@B@U6l zv$~SEXIl(FVR9Yn?1-kkae8v}JZR7PG$AQzIgC6H4^6Lg>t{8#0WOeSC_CYGUGVp7 zgO}B1WDBCNqZ$?f<9qJdK}zS8?!`Y*jrnJuo{tF7%?%wiQJME8W)Zq=nMXm4=V~DW z&-Lui=%^Ws^gXyB849xx+tU^#6PBNWapQjfI(RlQ|6;lJmKwMOY zH&tmJq=3vwSLH`Uh39p_$$HO%q*?ss61MPi9u&d?N!qS((kS7LP%6<&k4iTQ;Jg-r z&W`WNefrWaDI1tOGfi1UlU~(F4zTp5!HLSjYeNM_a}mGJ-kGe^Xy{)!xm?kw9D_!d zdH?wZ&XZc~NHo|2C>vNhoI&Bsykxa*wiH;cRV{nIa&UU!Jee7{O2|dG{_HQG@AhYj zRcsQ7*kvaoaDC+MhbL!zElsEbMM3LG+Ejz*lTF*?4VuzN;J1ljdM_Li!V)=NMS!+E zceGUlYE+k12rCGxP(BdL1tHX;RllWMTTi|jbPk>ZngZul0ETh}FuoO1_Bo}Up>r&z zrFsmig3OWhJgRwca+3u$(1~%@ zt|@b{K&3yBx;?tiYe7>Geti^>#MV@j-;38=kYjKr!DaVX%{4)LizjD}PfvgCtOJ5{ zjmy3*BEN+KYZAd#|Abc|scWm>dOAOps&HXM z&M`p-q|WqN5i@6+8b`cHK{ zGOjR{utP$`@w(9VvrE-hNnfoU!nHwm`4Y-G&zmdlLNRxQJsxIRu>u_+9%PJ^c?Mac zZ+*Q4B=?t$_T2M9!%vQgas^yv+?kSM>LfholNvr(}*;4bn`m!0GdO z5_XE+^xaA{7Ra>8vN#Reu6@v(Q3fHS7!I5Z*9Apxn7(7BXx`R5JgDtN^$~vAxkA@~3zdg@) zZdtBq2MA1N00kfF^1=sw@ti1T&>2w#0!f;IC@X7V+Lj$0)b}bsQGxK`u4DkYGU4XvL&A7rLPThD;)XOPwb6AEpu7oyjTr1gRz%)t_niI zYn*S>b1A-2Bzc-E<3{x-SxR7lzCnwO2_cC5Q)ZB}a(7nUHeh@D-9>{lZGk(pTjYle zaXdD3Yc6ZU#m_zGR5w7M%^aF`1}AO^dbj*z<%SzP9a_abfR8M00lJwQB|K77Ozb`F zWgI4|LTKO@zESM8kF%sBY?|H95$l#JIBqphN{VuZ0ji7*YVequ#Ny33craMrDI$YI zQ_`|Ci=DdxI*e{lOI&##kAVhEFN9+|?(u3-%x1-YpyrFhHFRRAs6xm3e) zJpk@C1l^RgoB$rj6w!W>H|rI>%FCx zY;G^?sD`9}2h3R33bcp#JH|AjcrQJ&T=-nHpb^En(M%g8u1*-tSL(693nEmS0{N$L zwZP@FDX}Pa(hl!lY#-L;K%O1~oaQ|K4xD1H@M!sts}OoJy^;~u`W~St7bJ6@5E0M? z;Rn{%dbr@p{$65|Hv-Aq#zK-;AWO2FEy-;Q@6y20t(GWcLP|3TG(5#RZ6saND3Fvs zfNn3NBfzadQu#i3PLqHb#LYx3D_6EK-ApHOVNsffN#hP6uQ%4%ij{ULuxLkjJ4})G z)gVrUPc%;uf>{fR>+tF}R+?(MUm|n98MdY?J*{Ev*v=#Gh1I%;D2OK2Vz;M32Xqm- zIX{vwMUyyk2hkm6;aUOcThp`Yp43OC9ba<~vPMjibw=S3f&-C2uP`(;#fo*S%d;vA z?2gpfz0UU5e#M|Hi*lnXCb^8`W=`$HkyGcAIL)Gv1g%;ov}m-={1K2T4HbM>mS0|3 z{&-w}RBxRBi`4{Y^_FhXqDSH@gpUr|bnvJdOu_8Ls`=$y(tE)e_`9XpM3G zg!sW9nSFk7m>_MTnPIx&!9mL@1s-Eba)>w$1X35>mG3Y%w}9b1X`a~5^m!;TlevMBji zd;LKKC@T5*yo;?!hsh(|wWO&!-Clad|oaoiYULaNa*{e(91Tof|*0zVF-v=n%2uR~j zVT8=&Awokj{*Dub?|@`MS8AGt9+KpZoQJ(&SMWiHk`S)bNu?>Ee0tBO9$uq9|Z=n+v`V+`gZ zr)G6sjPYK|nE1^0K8FZcuV%ilmPB_bZ@IY=bB-V}=Tzko()+G18GKUE5l%janIn^6 zKcYJB;#}?U%_u9bifP9NE{YfCyPjLm`Biz8rY65wb@~xrV_!-IWT9;YJlj8Af4i>Q ztEL{skn{^mxJ~WC6)Wq!xW}CSi+N}^zGS#UU z7e$*2gW=7k;s66OTj-2A!t)@P*GxpDrqV{EKni{q`R6pnoj~m)KPO8fNeiQmk25Fa zCIthYFqGy(Vn@O>aOQ|xegRg2C`M62WW5-k*s>rjP`g1{xj@ojEEi$ zWijbxU;=OmSw>f)(0%#MdZj;K2E8`6`ysiYGy9PH)iG$ej(d@W1~}^{v0++epLxtY zkUDqUX1v?cB(bv>Y=7Fqp$Zap2JxWp; z(aQSvDjp$#*MVHH4)NQbV++pzwHrVz(3kpGo&!WnMBD(D*{}iBIsVb8S8t_uFQ)Dc zx__~9$nos8K?WhI4h}LCts;ra(MP&LX9$J!DX5!YdvZc%AA>W}T6Ei<1Dp{jNq(X~ zAqU!-9F1D=nU*0)2VQa;mS_9YkYW++Ad$dm zRj!6)gWu0CD1xyu6FEsT8s6K};T9s?d&$iBb>|IJd4GM?RGzrHnoCudr%7Ywn6urY zrmxB*IkaL5&uX3BcuBkVs-vFU>;C9}HU ziSn&dr(yGRK}o*9FFb@-Mt{yJ(@ZLvuU8SX$0WFDyJ1vCFh^-V7*sCDMt;cUs1P8!DVU7e58%~G{5%q40 zUM3Porb5?{3@H5bvsFbz)el{goooX1w|5qI%SCnirT%{2vL@8L+&|OCZDB)dM{H<8 zOMhJ9<=P)~7Z}qWvjyF3tHGUzj+^XovtYpfG?^9e1&}ZdyX3QD7~j0w^OOb#8@<}I zE1UVR#9cp&8}+>ct=pZ+HRWlDEL||Di^Ul?C=q`Lg(ndm=r6{jgW$Q^k2Z}M(G5L( zi7e8Fy6TKMMnD20A?gZHj7IzQ|E$<2$PZ8Qf1l6gfkt=y=lR6eaK?w<)||p(g#Bc` zlJEhlC6|tl_g0KL(#dBOX;^!4j1#~%HQ)rY9^$+EO zSmx$veTqTb0`Q%B0z5V$0i*k4{g*?mRe+<)Drs*Re!O1{I3*xF%&B)n;U_I$f`Bwn z^c;bs7b0E^lzq*AFAf0ZK!vL20){Znok}nur{=8#NE)zm`taQjN@f<8w>yIXEpjpn z0(oHWDd??NPz5csXNY3aZiJWs!2V-(PFnhfr;#}r{l)qiX0E=6#2Ly&$Q-nz{+VGf zU1j8Ey9Cji2PV{Iis$7<^6*mig!SXfm6gk zDFbQGvdQnp#(3@38i^DxB^_duI8c2}LA$?lz>~mNX$0s$qXu|i4d@9=*Fg?5q*I4= zc6R1swp5EbAAK6uNtnX#-ZpZ97={LzHl7d!{;shT|};o5B|5#1Edg+@9cp8?FgM6mWuM>VRnwrY$F(m-v~ z@L*lXoz}Fw zQo}}(QGRRND}P|j)b4++v?oFUz zQYi8Zk*+sGkYrLDPKGW*+!rCSip}&eIqXx~9pI0XA9@m!f#EpJ1qwV z{rM4Pvp}j8acB+O|Dekw4UaP1rBQFa`SFn%h&6d*b_HhQ)dXw%;Fz{i4RduyAYgX- zl2iq4H?B^qYveLaeFfT?gk94`7E#Oj;IZ1PIe~^xe^vc-Ui+Ij+79%tP_jbKq9Kgo zKR>w}K7T5L-J!ZoQe*QJ2UnUkYkp0F6&~(x^d#hCIFlJdLdXmF#|i zi0~s`U$Uz0d;L|a{+a8$&SI!Ao1}fIh()>SpVQwrc4k|Eo*dCf;3ezfX#4&1^VSXQ z2l{ARUKXGqh=-DznsEJjvZc%?)Nx6kfICOS97F|F0MEDv$Tv=vkjy}FhoA-W^E&fR z>X%;!oL!a0<`r>ia(c|Q8iAvwOuVl13#$ywXyF#jk*N{tW2b*A>~Ub6mi$cU#2rE$BpjEK6FUfS^YzpKXx~ zYqTfAY*eQ8K%uGb-atdRA#ZkFCe(n3)?3H5Kq;@ZIo3<05XBfBc1?*X9+L2gW5 zy0|{uqBNmhsGpFMvXU1U@?(P`KA{QKeP)(=YpbiSC$K>=;Y`BE_(+OItaT8ujx&{m zeYQz?=_FeZA4Zc1$5JU%!ot0@8EwzVZYaZBFu3KPhX>r4&33+`EVQEqgnWliIT*0a z#c6W1L{|nu{BpgG5QhjiPTDn z7^ax$J+%f!O#5RcngBeRV+FmF7VwzwyHxNrQpg9x3VB~1Ruo1!KOsB)uKU{gZKAho zJ7_|wGA-6@j!w<|ScWEaDy;00Z;uyG`=WC?^_x=JS00!BXaRM~%f z`d;9#1&pb&fmtYHoeSIq*31Em)YM~Gwp4~AS4U0M+~_I4u@Ev7NP`nu&u*aPLgF~! znB5(f^zkU|c8kS&&B43OB8&3)_DjY%u_~akwr`m0_=3FY#7X)X<#_IIneIv^7F}|i ze02Nu9)_4ZQ_mL?oGGKRek0@G`RXyjGF>430(Gbpmn29s092U6=E~V!pcjZ ziiQ?{b_Te~vq4L=9;tgD6Z)n6W`kF`1l4yHg%@tMvRo^9^bST7Rv; z+^+^1j5?q6>Gz(L7N7dm_TKwQu7MVcNxA#lP+;lClU1Q!2(s461m8kzq<@Qj`kJ z^X7+#-Mx zhrvZYdl`AFg()ZI_UA~WUYdOkfYJ6l%tH2`rzYiQ817mUKzHyDlwSg% zoqa!o_8&OYHpZJA4TE58>G<5Hf<=M3Skp3`HnV}&2vGlh^{yu4N!L)#_}_SzGRTm5 z{E^6u<&yE0${~1f{Qn@dk&M4!7PzrQFneX^jT`UhYd6fhU$B#cGW^d6wuUK~{PZ+A zl>)@S4BX2|m9Wf_bsyaI#>*SI0!510aidypnz#pBTi62twYeaS3q0>h@4e5)f9u}L zhJ{W89VsYiTz2^9Xidh&N0Gxt8v8SWb9-jj`tFof{s4ljOw{D-K*=o8ONm}5m%$g! zkl)ao-~P!LbNc^NO9|#GI%Nyv6U%&$MPZp6W2-`Qy7s{r{4w`vcg}hBw`Cw(>UROh2=Vz?JC9m)6 zt>M35ITa-C)5D3rqe=l=z3$aZE2E%W#sajC-2g@0mY%%f*&@XR8j}v?c&<96OP`uU zfEF%XXf7r|yLivLlKjoI$cKXKW3e#3#d_K-)+9b^j92X?8bzs2n6K_Aw zy()rDJYKr;1BgTB0C>VLR4lSu1o3#?74g8lH3HTAd>NCFvDX8`dop{;H2{t9+G6o- z-u>&{$sG64#1?MDu6s>BYsUm4$c{yhx(zy$aQt55fCpq&nP7n&FN1>_mpSM0zY zT5)LwUU;@?G^|(`sVns`XrYt5FO8p)-ZzV3>C zYX~5rrg&b~c9!kyP2>MGO2UZcu+l)A4pXRcDf-O^?4rbhqG<)BS#m1oqH60WjLm0| z4tkI>j;A@q5+7v(O?)Jgg&l4^!pc}-i zM7>DdCFr7hD%X|N=*c?L0A_@TWwq7WL9N~#NuJ-|htx8a0=MPS9Y@S)LpE+a5CkoN z3RgwEy|mO#OGQs#LA0(qsCP&^6A)H@N8j84K<6mT#p6zQ-7bMUu^fEvNxdO|^I!>w zN(f7O5oKc3j+SS-UU-y)OBCk^eY6H+4p~+3nf|k&_DxHs6h<5;w2*o>?@`z_ z2c?L|FJ6!yB*{{p!df2@SWJgZ!XuP=5}jWmx799<3cO z&`xl?sTVRvyrIZqWe{Lg^6+xM*@C#+3w~j_XgY{Wr#y}>^H1UM`v(_@^_AG_2pZzt z>ah<6;Q&Cn%3xQC?O#Ao_K6ML{I?e%mlmYQ=fwUyDBufafTulMNNoj@`rAf$A44_Z z45OLALW*>sDcDoYy9$UhU1X`qf0l% z(xC7QBah^&+=WR)wuae7i{9lgjS-2B&$fDZ(itvvF<5{Tg%mw)2}pW!7S19GF#(e# z325VxyZPpb9B`B(z(?pVgshk4a0&@^bYVvc>L^q5GJS1uH+4lBCh`Zkfjva));i;_ z+oAeyUl49J^pzp~zeB*-j{h?R(90FYmhf&@3N=L1NIA=z7Xck2r%J&X0lw2<2lWrH z&VW|$V6TLG1F5ws*0#UmFKY{Z9POBPMGvQ+!RWy(-apkAyvESIPzW8lg4q2>LNLL7 zOy(GZJ-Di+hig3irPwr^&~GL5c@eYgUp85ipQVSxb9{1n4{gZ=t3aWmC!;a?pGx2- z*Tez@s(*Ea>eZj%)vJmhMPGmq<$07|x@A6O3Mw3u>WSauzjzA-#YYTntUo`xL+U#1 zSDU{<5&OtM@c>H<3Ux*g^bR#*m_JF2zzj{&`KL;CY;-4oINocB?=}r8 zQjb+y@xj5&glqfd_W;kQt5p|=A(OG(+>*R-om=k?PGiuADf#!#{`hxcS8mEi+y;BA zFw+|p-Vb93#i$5^u5wl+rX4UoIlCxsC@4_>9C2dM{)$;6#szv(CTkJI)4*n5Q;z3TB&dvR!8NfYQqIK2%j)j43ronA-^Kl$3f+UWD; zA*g;{SC1Oz9pB1fUj!fT-)I{Tj+v@mf8ucM*M4izF?;Wj)DRYBM3FH|5T*#LG-{f~^4-NJ5z9*IWH%RW)k>H&4;Pmw`Sv}R7H8LVO)Yr{j zAErp2u6{~$c7Q%_^#oX8^eF-QXIHlR#VopGV7dU@KK%eJmMTn!AW=8TApj5XRvGy< z+rL{RETx!Z293h<)wxdiDf$Gn2^i$zW`#!!zFiY#9Nu%23TGJJXnY2y*jl+Z_W-C@ z(zF*-0o1sMVZ>U4K0RSRzQ4VR;4eM|an?ItNZ(z4#}sn7%L}BOOwe^)9*ik^Pi)Qx zEhWUiQ}F(4W+Gt1tHWQwWy;W*@d429Z@FghD(#+uSvQ>ybY45R=>|$AIiz#6A6cN? z3_oxE+$!DirMHW3E(U0|a+RQte{v0-Y4YEn4c++wQW>#n$nuGOi;ZiC1bP8L>~q%! zkhl-knPA#X%aI2&f$N8ROP0V+CjLnT(7l=1db}ZabF7l#%lij8)jSG5Z}yb!n*8j~ z4uX+c6etJ7nrbYo(yi~je|%dY9krly?Fq$$lqYg09p$6XUQ8ykY23pHG2N~;X0_k^ zB?#b!9rb@iI3vdAv0qgpPI5tsaqE4~;8DuC90g&nmkQc-yi(@~S!l?jPTB`@mK4MW zXiprcK)YEaGnsmMr@tT_T+<%`UEGyGzfoohG6g(fY6l=xiNwU?L9nktY3Q5qKPC#)S{e*1vLF!dw*YBE0(4h z{6T^^;-5U=3EPFl@emt~UV6;*6;Ms0QFnl@QtrLf&5-mN+=Dm2%X;=YD93oY5zzSE zP3vT01^Q_%r_#Lm#Uw8s)Wb1#<+JnJ@*!^YW`;Cwu*mF(E)UmcOFlDeNspD<>b^6o zE1&Yq(69C``Ha;V2Lf8Cs3kZxwXVN_0bEYhiL_kzC2owC3p5W2A6`h$Z3FEFk1jtG zjN2sgH+f0x$DBXbK6ITumsl|mggVX;d>x7qL+q{x)#uYLB}RR67*xA^Xwaq<`*VAy ze-;vCS#F%K4|D|%@JBrunkfOKaCauvy!6yrTkr}>9OjOzcmZ0&#-6#Gw0tbP75O)L zmR8@?s*tT+pf8S&KaveUTviSL{>xN!|F%d-)h@^LupvXy`!6`ljM5UUwtr>Il=bP; z8nn5$7V$+*z^;eS0mYW~P7OFG|G@kVXsuJIP8xKpCK`d_)HR}G4*ROk2(V{Qw`#yo zv$~}=r!I=0P3=WH!^LDEtP{Et)EPUwx)cW1fMuJ?gZ53jCOo@0bj`QouvSC)<6Ne{coE2i7&Mf^y+!wf(sb|bbSzV_jBBrY ziB7U}$z0 z7!V~DC8V37LlG${6-DU=$pJxnKthm4P!K7ll?Ek6q#Fd029Xr@8t(i5KJT-iz4!5c z*?v$DKj3w(YsGo~YOyB8^Lq2wd2Nh%$6<< zz+fc5jHY`C?W$*xVI9##RIy4lhem%Cqs3*5WCs7zIiC^}{CY&Xd%?l$r2s-gidX?r zhl?u$FG#B;?L(u0mNX+STlcD`DgyAx9dk-s=%!Ty1i_OzX+-7_041bkU`W_675fvv zlY?z=a?iIXIrE4#kr%uf*|5ybkHi`MS}cv@_{Mq)*G#?rxD+6mU%LQBKYA_J7WPCa zMdZ%X!fBZ;Hl9dN^?c8u!)?nm{ab#gqwT3C#GawOTfS1G=Kyie?ueB}SRL?G6U-@q z2!$Nr{2v^vyl?A+i1!MndpSVrLtMi{s9tm_le9fCn-2z{;syWsI{}AbZp>SsTJIOs-p?Vq)`1M1|agtia0&;o2e$k?urO?bK+FQu8Lj;X?We z)SG)>OK!4}NW^ghg@MJ2WTBZ9mp{N<*J4FLtJ{suJ94hds>lofVEPa2AO{G;bKd9t z$bn%*wHBIXOV7(fyRYv*{LR|?y*k(aN-G5|cu_Xemt8A131jq4`nlYzcM^e)b^hGI zxn#(zr|xufO)agZ5=*JgBy2t#`RlzQ?sV3;rxm39-)hq16TfynY$1P|2MWPq^w8jZ=P{E3^{t3;$~b5fTVe$3OndlX zzcL_U>_e9Z+Y*{eKvj{u^~^-^8G&wZP!9EG(5}>|BoEuNLnt#p3C0P!PI$5Da16=9 zKRVXQbgfGfw2cR1Si&49tUxfjVdsm0HovnGf3z>XH?4Cp4HT;TRe`G};ik{4j0x~z z@bwe%E6~c2;|#MP{N%te)wM%qN2lSW;~?wD(B?Cdpo0r!@WdE=Zac?GWRtp z{n+xoKsY(`I+CJGzR8VuvIr0p>}vVb+qb}0iiRrt9{xy?>L&ojhrrrx;1>@< zOU|(V@`uG6dpwvT{62oC=sO_8^Bc)EE@ z*S?!%ft_h@w&W4>&(R!pe%hA6EeTv1P~Y~}5_T~)g~=c(M%OzZumXn9nmfpLC%!&A zp6c2ga!gPm!4q!0PIdh&9qgWs$LqH$2=ruEZgKk~+$@X{9PpV(3Ckft)6iMcKEVG;*8#L%=qc%( zr|wB%Y=}{u)c}tSY}tk90dr&<2x&TD&pB^Y%ea?A#JhCx`o%i~z&^s!d3zFU_k}`c zADk_TWN0M6LSX~^2QiEGi;~Dsic)863q3Ec3Fy{*1wl9*QZUmQ3DCK{5* zrZMUI-y;isKnabFzm^4f?2_k?qAoPQSZ&@gynVhGtKLfd4&G^tEUHDo21XRl6O{iT zBNbi4XTN59f z_hKOIym!;q&!t?!3qu}WHko1;cbGEeU;?B0mXhTb+&M1DctK6n9dCN~E2RW&=zAP! z(nj4HO)UNaG%9{REWeH!U_G@0%SKSK$@rfV?x4{4|LpVNpGwvX|6m9-2Olq5kqoZ$ zQ|Wwg`S=aO-GL)q@jY`Jv*+J?P~n{zd~6^&ktrqwJXXBN{~wP98YM-%+7@J4eFe@* z7FFge2W+ZxOzd{DKaVZ#58n*d5x*&Tr(XgvhfLEEX*_5vJEB;o0jx|dF*2n9!-K@p zmAQniUPv+A`K`)-_o(GnzYWk!`V%ra{(8<0^;S{<$Mx$h{U@M@&U`TfqyqwD;@yd& z{s+#Ty&HdqQZ+cZh=5;W=-nN8tswNATCzTf8^WSf^HK|G-hF#5#APr~->*Nt+?mRQ zeK=&r2Lr390Avx0!Em`W0F*mvHTW0wcEV*}d(>(FrtW!ZeuV=BRIVXe4gk*?b-VlXKkFcKRW*M&7Dy-_bY9B71}bWLM^5c+ zZhjuK=5%;8n$QyBO~WaF8W#=n7NaB-3x9wylt%(k=3GU>2QzK`0|u(Vg+)Ri3TpRY zrSWc;a}7w=chD(ndfoG*3rRrpTSCY>=5@B*_D>CAG>VU^O#3?7 zS8u=3&s|Jg*=c@|bXA7zuE3%7EW|K|{t?<52y+1`%nNcCrEb1948wD^fq(ayfC3=` z-=Ka5Z}l87eKZRHV*#3XP|*Xbf}KCVksU~UQi2X|%ko`+hCov;SPHMPbfZz&m*ZAb$$fi z0qCm~3-F!@QLbXpzj4(k+`9zk(m^%*efSWLpAM?;Z-(XiUB1D*Ff)b1#L z4VUM^-{STC&;D7%K&L7@5~cB$0}w8Qu{RYQ$T3j<$#POU9GrPA4E-~%5^UC@=Pbnb zd))-?FqfB4z58i6WuPt)yCXFeGO}9@wR-;(Siq2hxFzrhL4VY%b3c~tO;9tk)>G6z z1igw;EiHNud!e)t znivIpVYTxHhZgoDxs~w+zmMoeT_7w>Z9LwnuX3Ea4vY_6vLO2L1t|$j>eUAbJi}u6 z>SR;n7f|dD6dB~%15E-wZjuwM{AmAtjGM2RVhJT$x$s%|9_B#$t!AE+Q zQMHQ&E8Rz_t9@6UmbPl4Qjg#7a3^V_);x(@nY5JQe};@KE-(RNiHknZhz@)~!j&Ab zx{elrO-f$-d=Fs6IqxiDzGTVE-vU&F)3uoxp!R+Mq;j0d1W{+QB>SWe!LH?6W)#B*ab@+X_Fx^O;!8c^1x0EA@O4SW>;a9vhk1)v^l zH0StcD z1H$`X0OPpZmy!1I?9{JQ_b)&L@jcg7>lVm{6A;)8RXs2|-3K1STCTMvyglQ_M}MCe z@_}-o1c`e1{SCwYl6tSrL7>VSOAJ0Xca}mgvVyHS?ikpyjDYCjC(y)agLQ78q5{DK zhq4^NX?~wOG+_HS03Z*!KLalU?r3jS9glYPAA<)o4{Qn|6kNa~u0ZpJM?CQXo*tXi zFM%?=tN-k9nJbfcT+a7*mt;GpAB8`#V}1p?E>pj>sEBt>7S1bgKYQ~yT{81cG>aal zar7bxD;^PD9R@?_sNXRG)*2YXw!8lhVWtLAG{{2et_Z#Z;7##TytCm%#bmL>hl1MxL^R!oC0$>PDa;^-UEDx_*q5I!h*reW4h(t zf9fkas(^AKbxXlGJ`MoDc;Mo8I50Qvvab%kZq0;X&4TT%sPX!+9m7&{DL(0~*r2u0 zq}ktSIB^#_jV2H&NywS~A-h@Jh^kQPJ{F&V2j~px>M&bhcW;2&&x60acG;cP2rMWkaAtj2L4!I zpiSI-1D0P_gBwjAQ~kmh-THCoTb`RQWnmi{L^cPspW}cte)8%j3 z_;_T03oXGjLa_dPaSg~eU5jXC82fgO5+UphvDcyoc-DGayUT3a$*F4< zyQppUv-~!sPAOeZMH_&;3t-gDrxANz>WH$|xPbzuJN^lDi3xdSww=_|3F_3!{^;g! zCSTm(obd#UG{_a!-O!vffET*;5p~hvl3d&cWMEyqLHl-KS6SOr&<#a6Vh8w#dO^ra z45#diES?#ne3-Q%v=NN@6nIAGG$c zB=HMIkJGTLJZNcs&^Lf$=UOq}u&mWyreV(>Y&`Zc2Q`L_lAQ?&3Kp)&R&0BIc+IJu zS_2P*{K>pSOqoWEmP8K{ zqN7#r%6L&rC{4mLbS^h9%-@0BX*Rne4kIAm7<}CZ2(W zR5sR~1g0M=85T?J$`G$^@Xk_JDF8y=MX4(qQmq7& zHFQO6nYYA5o4p&{4Fr<;tujP>nR!WbF?})`Q}SY;OD^BF_rxJT1&wF#+{*nL**`l-;k}RdqB>905f4g249maFxYqcq-H)B~s(d2SA zC(UkX3c=o#1;JCuqn@uf4XXskeq=)4vOf2n<_3foC;a$4X{zdWhABDb;NN&3d3nzx z%I^_9b`h3TZNsbi)7|Y#lF5FkZj#Jx zL)MAn3$iiM`PE0i^N26#4m2}#n=kRoJlP8J?0KPh?S9{0ksk`iiMmn((B9VUy+|}< zK76;SXY{}TpfB>#=XFjzWML~YQ<;n~UM>T2FARu&LK**!WvVIhSlzJrb^Vc~kZFBv z)_vWxncgoOEisNSrPc3yZe%}G4)+@Z=HECti!p$dtOtB`5JVoWK34Z0o$%vo*8X`b zpswx%<;)j3nhI-FYkEoK&TunI!NQU#a_ zbklIQ6xD0vs+&)aSk^7-Qd^AjBfjlU3Ea9U=;f$+tEjBgAnbqp{$d#DZ>r_swDRpd zyz(9_i2r9yr|m9{!}?02AAPxt?A@LO9%G$&Y|8kD1%IsdRNWv$hUUO)u|3Cc;p0~y z{;arFd!dw_w&{}$6zGdD?5E@W{N6#WGktaF`}5ZaKL zxSaBdLlBsB{8f1k6x{BihSzrp@pSgUTA6Kt^B1({OFk-DqkIiPk4;JEcuV{5})k;Cz*odYAf5WZ{G zY+UR@C%F6Fj}vTpYF9=7q8wRgp8GPqwFAhhFG~1t5p5Y@Ih}c89Ck;Nbzc8cpIVqq z*;-sNs`K3CPTp=fBg(o^)Mi@@?6)_Xg2_o40rk-8s1i;DAAI=Qs!OMI*aeM@)ZzEBrnClU9@c z@P=*F+06;rP3cSWc9NjuG=^sB11V>8W8+DRBZNbJ-6MDT2#lc zZ<<|Nr6(#%o=pFDWqy4)$t)5ns}4jLdW|dp1uV4xGW>_^peg?GKXYa#L$|j62p#P& zUi@P5iIBJVdAbdPZ&6^nQOJ`=Z@RDl%N_tD)TB$#-x&4vv)gi!Nvp&`(Od+ZgbWVi z4#647y)wI0yb%EA#7|o+Qa= z8Go6md9AmoKnoi5xMyE;(&8Z;3PS^70=U_Ya5oRH!oIA++;t3>#2c{0RIO`mw%$+aE909wDcR9 z=-AnzV?eqT9^(Ujo<~*NGAMo*qF!xj$GHN*AKq1gGElwiR>{erO451 z1DaF~kl%#>?bwhI6pRMvvl>1qFjLm=VPE6{NE@^VjSxQH12}CMs5oU(fm%aWbNxr8 zc3&c>Bsl>cAQT%QeCMM!K6`CWRU>Wi;4G+-#6lyaI^xiHh5_RY)~qk&EgD6?i5Yk} zdGM-y_7MXMgHBfOxN7ToidQ_g3=8|YnKx2&^Cb~sS{1+&tnjuYfiZwP1o|cpm*RJh zGXI*dK;`!u=uhx*>)^VEN@4@AtmDd`v7EA+AP?odrUium3MlBS?7wnv?0^`N8|cgh z#=k5h6CXedT<&tFUo7a;b%Dt_2gIgrZ31L`03T~5p?3<}A8l5d=fG^M4;Vju76JZl zk{uZ!CD8*p3^^cQ&Cpe3!_%3Q!Zo-J=nZ&=c}SMoBplDJKatE^0DUAPu9eoqZyXG- z0hO*o!q%WB9cZG^PO<1ERFd-VcLqnAL^Oj_OZzHKk;?S%Dwq9&ARk&}Ik2<+epNMi zP9oFcX0xXA-|t~Ue&|VrD4K&8r6Hx)i4CN2}Q_VYeKw;D;2jFAXl8x{$tX>Gt+a-Rjv zg)#Xjvw4H7DXu3^C-u!z8FcLW5QMUVRR=XGbeu|n+;9V*knyvB?VP#RC(oihFjJ}B zVENI{Byu!L-IIiGBI*KG4VoIDU=fZ~okL_NBay#nrHPDp&=4w2Q1irFGYN5=L0R`S z@MfLvgB-phV6?b;zmFZSJBi_tP6nhRr+O$kM$%^o{d6iDL{l1pPQ3`s=0|ICK#7A- zU-_HUB+3P%2iYdP#er9bdPPk=?3*J z>eocI$og32Q_H}=9^=3n&LC(Kf9Y1KHr~f0`4<2LhM<|p%XEH&n{he^;n8y-=V1U~ z0aBDppA#?q1{SofJ{ct^*QK7%Qb6EeMC?QiB~r>VE-`8DS3uy6-}rC{fRTt@3~G*< z0)H9#1`vzUvhao?inJk;LjrIqiNu`NF90Ac%0vUCDL%%zi*gLaHHy43Dd!HR&&MQ& z$2fW;L>VdH@#(zSqJffiqyw-G)z&FhRp>BM>b}&&l5C-4JhWpDjY;Z$rU zq$lAigq&pF*A$w#xw#L-ye;4r5F9~2C6uNm#}$~gfdMZcs1?L8q1s3~(>=N(5qco% zRlF{T%hrGw{a0Np-Lb_-nR+O}(36mCIzpu_3<}Yiy#mMA5`gIWZ~?8z^%o;je!knB zfW28m30w#?nrb}%3vW=WpXm|^x*Rp-+BV**5q>#(Dlo|jgk~6P#3~`;iF2?m_}HOU zZhx}+!>2S{_ws5$HiX4SEp$$~4)WR?`O1g&y%U|6DY#o1Ow;I;w<*B~hwkuA||s|_S=6JkAH1^l;N zSj1qy_ao5@PQSr8A@Jj5N`VPN2iMHWh>D2^(1Sy_L(8V0Hug@9DvKd+9_3l*w2y-N z&Bsstm4gP9esZjkGeYPe*xV8<>L(SyA-Ea&<1^Van_K#sBn)d%W0hvhGFgF$s|;|J zeEw|ui-QhIJ8x&UUwVtBx!zyuTkPwRb0}B3Qh3^5>-8tFI??R%YYKfWC4XnXb;o=( z>f_&m;cZklx)~s0Yih~prh6He34<32iwRpUwg`|YvQWtd?Ad=O)&d5xA0X25;wwcO z#zZTNAb1yw6~u&yDGtGj%^ux}Co1fwTc{WVztr?;1Z$>eTNZ}4CW0PmKI zNBB2(MM%m8L3SXLyT~{jeV0gp3_^ggn~E=cqo9Ul4MsPwpSJCf);hgKH_IAt54me0 zn0dZRWOPG~6nG^lPTEd2UxkF7fr9K%-0E;BKm? zj&Y$*t3DhxYsrs_d7G=%xeG2VUBJ!ydGQo)cPw+=VmEUhQ28Ve+1@Ng!*Qs(AbUq#t@U+MZ zGgI~(kD;}N1mz{7^=H;k(W|d-;o{lz^H8wQX$HUI=C5Ips+TtfgjOO6 zHr?rsURyTs!5pXlB%$+t#va2eerzn;sT%vXX$;}oI_2&>ty?m~E^qdX*9-gnjUq4I4nDmcXY zQEeze>r%{JBUhpbEX^H%o|RPgUGX_vVapBD*Ipws^_k4+liAhg*;;tjc7l{Lp)A>Y zyi7EQyf;_TElg*a#I2DRd7s+;x67T15c%C;I6m>9W?z)^GQF;r5Cb8q7sR&|tl$VG4ScF?^?qgM)2BzHWz9Ww8DIl46?A5y zAd@r=A(d(wZ<2J}zi>OQ5{I=zys#t4E1<>VseHBxlApg}ykL}ra)v5nNo3_<@O5Vsd3 zfYD$aydFCOtzmGSjO|uU)}<(&@H7><2A@5?#4#DuKyB`+_2D}%o^@H~`@NNaR?PAe zxNi`eETS~&OSHSjvt>v&_nmZb{LcwTFw3o%Rv|uDpGLSiw>F0~oBY=K)bli21A0gg zedZPmkpVfKV@QF76p~e9Q@U)eii*bKkl*Kke=#Ex}xi^ps<| z_0ixxv0!vrfBrCUwyY?LW5a>Z6sMPX*E=DTjWSkJt<}}N(bvOSIP8sy^1+wyvaku> z@mnv@H69a2ICy{Qg9&};!AkA#C!RdAYR?oZXuCirDt$va{oeY7#F%J3XTRwjz5usP zXi=EtZWvxyJ|_zFKq zx7DYQ%dNw~&kPb^Qz5){+5E7K&qRRUnAFA^Dp?43YmsF!N$tX_PSQBK)wE2xb_==A z29vRRUH?UA6~{oM$Qko#ogzhia?3u{fiqd*{vBxE(41UMlhRDv@@(DHAAOe2UEbmg zEW^?pg5{2Gc08?nF^Gp5P2DtLEjrl5I4dve(hctKRNT}4h0{#4gVfN@yFOK=ibAe; zm2~`w$#GVWP+WzQwh6neSd|!8JJV#C&2_9R7YBDcCTRBhuFN|wddyasRlMZaA`)(E zFjKm%MYm2GF+y_rX`6td_wx4X!r!hbqYoxbk2kW&dooQ-^;Gzo940efmW~!&lb2XH zibUrp)hDnye=CShH;Hk-T|6_Kuz_y=r~-4oV9%bDiS$Tcr?5`9BZO2gs6FKi_GU6~ zfext?R5vgA$1;SZvLGl-dc{IS6}R&F__`TH>_2wZ3giJAYbo4Gc!UQ-kV4O$w9um7 zQDTJ#k)9S1Pqw_)yP@&y=8tMxAtBA?pIOy|~O2`_Uc$ zyUWVz8*wO$u%7tc%N|W}RslFToNW)d$DX#uc(eO|S_wVYE1M4TDcK0l$znYr-l)0X<{CKbs|zqbNX9g3t5!0@Pe^2j^g%UybfMj3ReZAeUEh)lVz9{p*N z#d>~(*QB&ge^b<#O(rmlC@h!ob32O=^2fwCQglO@VBf!t+bq&y-M)0CWqPoh-v?e0y24L03}q@F}}XNB@k>s!*H zb;)0ji;S;_N>YGy!oe$VkPnu@C-~e%)Mv)Z!yOl9mjL-FkkKN~!j?w1DcnVvd=pCa zjMqliCu{dfNIU&1aa%|mugU%LOx#$47w;I&VSr-7w>LKhcDLXo8b@9g%ARYZU->Qj zpH^sAGNEnf>fJz!BcG1!D$m}>5mihG zu>nJPD3?fk`)X(3lfV^S#6b3L?y_o>LQFlSh_|J`{mNrs(`DYk9Lt73VKK9@$r<%3 z@S|FV_!QAQq794dyiX@Dw=hwS!yv9zzvw@T)@+H4QjAO&2S?GkKuxY`=sXhPuS)AZ zu(9_4n2}0hOmg+0xkE9Ot44S7oyN_c{y4cKer^oXfu${MzTub$F46Oce(>+kkIyoP zu71E-LG^mO;(SX@v$t+rm#MO}fom{%@w_8WnZiBBjsoHGm-0@|)k9L*{Ykea4ow^Q zGhT|xZtUt^?#kHhk6xEl*XGy$-T)aMSB9-7Z}=3Ng^@8@M-V)&)q+uo1QK&OJo)jL z<{os?#P>7vXTM_gI8MM;in+m7K_j6v?sxh%){9s3(nd*3T}!|&wR&2sS88Pq3f5Ll zQt6x*O@8-z$HSPG$jvJi7P$u~UU`SDIxtm>>JvulR}BI+5i&$q!W{?G(4)WWpnWya zxtabH_&DdJ*8n-nYGFL|^Dv_gg+`1Px#e>2vVUL77--C*3cgZdU%JqK+v8^fWtb&5B#N0oHC#x{=!Xniq8;|xAMG-U=mWhyRN52MQ z@9Kn=fz^BY|3k|PY@l_2XV*UueYlNHTzudg&vdjviJ*3*-4?W1a=MRFcy#}oqH=Kp#*h<21zqe2+D_F1de?Dg=GAiKQbo?*wL1J2eD!@XzEFl<-FO<)5ihPtwQH*A%b zqGClzHHcpXPCg%qwPt{-A!?Xuxm)gwlM+^QL=Y`(-|d}ZOD#_mD~H=}eEmqmMn*-? zNKI4(%fHuHlAa=ehqENc@0N)JxU=#{Y5m3(hjLc2WufJgZd{R7&D^l+B(^ZEuv+zV*|10Nd1^o{ks zB^J1qjU~*5=pN5e+{KhRKIhj*!uotoR57l>6x5E24g2M+&K>YF;+Zu5n4(U3_#)hB5Q5m)mJ~V$#h6d5JwcZY?#qCHv<2BEWcx?h)BKi~(3%k=a_nzO&Qnt`u z-CH_dsl9=l-P`aT%ECe7f}(TM!3Z~ayR5i$Fc2g#JfjV{=#Sz4X~ZhwC}=-cF0;k0 zUh|%$IfatF!F>CB71%q)A-k?zp+fOQU666#dB%*rmDZ!e>(#yYBwS5d2Bvdvw>+Xs zYp#m9)_iXvTh@(7S)<5B^Z_u|e-2TLYMDGhyf&|`D0hzMfW@$cU8A$L#t^-7tuhxU zruRrS5}g#yk*?%4U{Y#H;oOh)zCB4U;n+E}ruKh7S8+kFGE6SoG-oU}WmecFV4G4Q zI4LWRGSjS;-{YhGO(((k{I7^GL{Ch9&^(rmf|Dhw(L2aC{fGcF_B)^} z%MYWyB!=03k<}fKwy|4b4Zkqbi?wA-MQUV+v8fvI7rRTQn7zvikWYYkNbLA}$^O#A zgs3ZY4Cq`>#M;j)=&LDnGP8&KA;wRv6m9K4N2)8NJ5k(mx6dvkf>_#h&0H?_^Qc8S z${!Sw_6bc&`WuK=Te;1OL41~dxviP0C9X(bmUvgmIwp0PfR2}XhIF>E=C`NWCZkT? z5J!CI>+g(VyKiGOU^vgUE@&55v507S))e%-+vj4|Tbf{=R^u}F#Qf&J;1bB_c;W|3 zC)_3ZT9}lkyYDa|X4Q=<2@tG9h%c<9-4$+vQ$?f_Gls~Y9<;W_(cXyG;te>a2czW` z^0vGNaVO~bQnE&pQnMgT7Z;?(Fl?2nNY8JQQy;%OpuMJXgUBlL|QL}>DvBCgga=*|4mL})tMCZUr!|6F(V zht_-#s^j3b^x`>5bdhfT5he0CgxY1QWj^$9=AM9yG}nFEtBH^)+XNTW;zyHI#+R<< znD{d@F;!Z%L2?Ks!WC9oZBY?${LXr_3!47aD#FcYHOz#j2wS780Dn_Zm2`8()OIc_rM4q9i z+3c$F6i&e1!8SxMLm95n&b?6m!)x|4@zj7jp;S6GjE#hP7kck`xZUg@$XBSl%g#~S zpG00oOvwMfwI`w52wsS9lMp$Eu~jF=++s$341L54@t+XX^Pk)0js8yd)~>{up~5Z4 zqOE{n#%_Y9T&RXhU*lUBlk{*&lOR7M@%q;R}&#lyMIXooa0lPRI_fYKD<4K+(L z;dinqL+f6-0^C773HNzIzsSiZ_JeUH!)O#BsQnFwqk2(M3DuW{#jrT5kdI>d?IH z3-NTlXkplaC|#4TrV@E6m47qc{m}SeCkevm;+$m7q*T0@h3%POJx>V+s0AS7YJ&8} zJemy@8#Dllcr|@j`{Pg=)0pv3y8TP$iOvQsKL`WuvGf5mW!9mex57L^#Q!9_nhR`w zRr4mKn175^Jg^q4wpvgXh#-;U4M3>P`-C)}ci&e)jEr?_g<3Ne*~lk-^knZxrAuX| z2z50|;<1DXm~Dp1bEsp-o&(4ES+n-dC>Pya$d>nQhmw#NmZ^AqC;{KACM>u}a2LT* z%Wcgx8$Fmw)x`2{paJt~ITlLXwsBMCpTxVnDg3Q>Zl37kDYPm&QIF84bbHh2! zBnd6)X%RLz8y7T>i+mT}V_J!WZ0=9IbzbmX^sZo=3keG1j6`Q&Q}K({C!h^tMw;*h z(bas`na}^7sU?8w4@-%Fi_Iy8wVlPEQ%RtaYKCpx}LNr!7o`! zK6<-YJFlkv^eplIHFtl#CtHaWY0k}GBU?sYl{I&jCm-AmFi$}<0x?U8mV~MSz0K7P zmYmYO`+`fs^~5i9Rs-fdE&8PDAJgf{^1`;WnHt_^X%LO8ZL}i0Va?rrO61B_8_+T6 zj|M#?=?9Z_*?fX(bb2gPbNX%^QpfCww_WH(Cm^TNX6-vvq+Wz1hfQm0?A4Iq+x5k$$$R>9TInJ_ z0H7^F#UnE$cg4ukFKn;?*`EZVSq;;uG{pIE_9Nlob=R*H61T(au-%rPww<0)PkRYD z9!ppMB`YpOqd0$uRC^Q?dPuvdD8_=Bl? zzSjOrdhypbRC{6%S0KoZK7e&1%BDC9|7}`7g)R5Z51?YVC<{t5Sdb0nb3H`2;%-LXi_4Yi>Mmb zTU=el#$+X4@EjmgeBrY*-R7&oQPm4QHi08$dlC+>kYd9mDa)GPKig*~>;_}&MlE3} z^9<+iBC&Jbc}!@`SSw^gB62v2l<>T6dUkIaXEH*AZYF=@k6L)QDhS!=&5-(L{|&CK zboT;G6#=DoucMs})Sk7pg7H};Dq;y#R#zz>uv~X9LebIhyjVcA7g)}9G;@Rqazl~! zXSCq5D0jw+d?fqU!2F$<&CAW$k`$>e>2bC3X@Cat6^bo{(!5Xx+55zk%m>^HhJ09Z zEY>PTVyyh@J#KBuzqPk1#&1Co5Mseg;2OFsI&k+ID^l)ddQs;mR+c4%Uc5_M;^gVF zA*W?Qzk#W3G-I1a&)onwE87yl11o_yGYRCrkKUHJdIFuHzyFFplLdP>^G?+ko%4?Y=lXm{vzR?(?7Jet)FuF+b$wxA{X)o~%qG`FP03GVj~h-H>FV z0_L8G`{N2)%q4yVcZv?E7feGx3Fi}2*BX^Jl))0Y(QM6cwrkx4rm6|#fWG`*jAljo%J%|j7>3FVX-4sohmaS*bGZ2uA z;_V>V2`y{M`1+9hMC?`T#%(-YFU3n+fq(g6Vafu`|jf;z!78F%Vl36|=n9g1uOch1F@GtYkvWuxl9w#=OKLTNIH=_2h4jndstx4!4)KFI@H>U9s& z15D`pwFJ{lB5JKDukZxZGGi+#+oUaLKOSS4UHte$zwe#{)H3xsj~7&%^!f{MSZ^;e z#r3}m2sly-nV_LmvxXj4*=$JWm=Ah)rA5e8d1sL)A6#itkA2tZ!Q)K23@SKAF0Q~6 zpU;Spfj9!iqpPklyCRd4(2X8gbDD_vNdbzGzAC&=#J?1K_A@Uo`j|MM_%V`^u!J9? zn>%)2);N^rcGDD;>Y}{RSHl9d&8A*l&SXJI0^~G2Iq|Q;uEr(xig)@@-;O%M z9Hmh*ni4ViB{t;{PsSYVlL92vg;>asz_L&3!cP;XRZ!*a^Vj1cX?8tNo@!hB#zAG| z?F}{QGZUWGd)kQ&I_U-9)9z!^rt0tZ(zw}uPr7KBH`P2i`_r~S$*c@s>oiXAvVTQoZRZ|9A^?@Mi@*e|+>9$b)f?014*Sx9JzbJWb07qa(Z zOODuKTB-}e`q0nA^K6WT)}Dj&Wbip{C{>{H*GgP zd!(T&QD7&-2qujBZSdl$uiWB3L(Qif9bcY^`WIbYaZeqxAJ@J#^5!m5NRU5XsMz>R z^7rBbq<|LdzMLLfK!C62pOO+(^;H(*{6s{cq+=@vypU@BfbL)~RuU z>Mtr{e{R8%4GW&3XUQzSs{OBNs}LHL_N;O*TPVDo9Mzj>2CnGzzD{8H)~L zf;^2DgbR~zleZZ=W9W^k9KG)bK)jXzlz9hTUhPnQc4)ZO+(Ggg$1c76@5_(jLB}r3 zTDDpay;WYOV3kDdm5C3UbI*QkI4<;l!8LlUM&5{;&YeB49~U!~Io?VVX(HIY4-}dI zED-Kd2-gh8H%B0^?>yl#cO|}5DxQDRYQYEg5Uh!x*6SEd)88#M3CNfB6e^T1Km>4sV-jgEWJn!V$SAK9y{g@q;JdEXCdsE~R34TY#Y7JvCld*Yw znq1cNYGI3A6xsUJOC;Km9h3fb3T9&4BfSB|DamS>o= zqNck1TTY-de9S3Iq+B5hpFLH46|aP2S;_WMM`a|%kd{y1zIO$LPax86JR(=V@;v*q zdQ->vygMr7s-g|j$o`j0sLjWcu@y%t{h(AkIgDeE2C@|yKJ zs-M_CCN!Ij>?$LIA@dT2tQ=k3`_fFp@ZjDwPYJr&Py1VKrwWv(hPJnU*>Po{UQc^q zy|+7&rC&zqsq9Ybs+IFOWB`iT%a6;&p9Lv!XLg$?jx_x+NzihZ{uB?X*RJ|&H;p7# zFN;RbRU~3Vo{9gC8fI;l&kPZbCUC@%UL^jks|1svUf`Lf(`VQFxNYTDxCx{ios}gK zj~{h;tckDlzcrQ=r*V;8HYM{aU~mj5EgnWWxW6HXNAEo}Sp4G#OyVK)_kSI9mQ3~ z8{N-aNU1CcSI?Q7x5MUlMQw^0sAYM@4;RT<;OTj-B|f0K24Uzq2Q4$4je*3xSL?2r8(Z^buRDqjY@#CTgj9@pKIIy(20)}X-- zX2yX$W@o7vW5#K=R)jm`-+~;|al1I~zU-Pj6{Wy|~tj!@hMe?yit>vpZWo)tqB5^*&cNAm)#JxM}rq zvkDSZVRI%lhSwJ=9M(x6UxVv%10o#wti-d;SkX|i)SA9x*G&~lOdXARI+s|mCP1&#FIr8MGym>b-^ zot@J3VMPt`=g}B<_nj={vHMTvYY%5+t{c|HFkc)4SH?ONqfrm}7anG#QNjnf!BPCK zYd3_O#Zx`Y??o%Td%jgTRc{!IclCZ=xbOt2oE2cY)NuNuWUNfl_jjX7fd8`4=hue3 z{N6s&o?SiV{TeBqna`l5=kzGIQYix>0{=WRMmKSA8eiC>bX|0C!6p=rzW{=`t=$7~&79hPEI?y z27l!n*=-g4&G+ih+*uebaP5`d{}Kg)7A&Y8nXY>Ke0#>Q(;dCyy7e~U8`|$b=l`|- z#+m!;hi)vLA>XI?skabgS6^zhwA}C1(AE83vwM$exRj^%`2EEY3yc!Cp4I1%&(Ub6 zO%pZ19#cF_o8Y5u(I{I}>%R3C7f?NYuY|OCxHQoxiis&$ZvBVLV9xr?rpIyf@U=h< z%TQ8W{bm5IM3)0@cKER#Myc)XSMtlq_=x2z;SW;zUQF>DFCr#|Uuq~#@7y)qS`N>S zm@B;QHtj$5-mm&bvFWv+DPfOIKHL+!I8&8|!$Pte^``_d{JgVy%){UlJx+OuU(64O zcTJf;Pkgix50t(A>)P?LEC*#;!Kc4`>ihNDOP~I}^4TO4_&S;R&lx!+)4y`X;ayg@ zx3j_j`WV*xlTwPi_m-EJyM02PlJ-BIyn%i;JM{^h^!)KDgeUzu@7Ve{5C+WFQjEOE zVGW7oo>_yl$*~^b@#l0P)Y|TA>Q}=`W1-j8XQwE|+Z8sd2TeaJZdX*Rc9)HdV`WK-WrR#cx-P}K=MwyIB9RX>|y=pB39s(mkZqf$3C{AU+c71xpN@! zE__HhLE^aDXE(of7Df8RLqc;4Mfub6pCxpa7gnB~T>BWo8?D`Vf>@EWvKl=`DCJnj zXNk+8p?i1tcp~FfPxKbo|_Ju_SDOEWmTI~g%I;Lq2J`qWB z=$9ACkGu4;C++`Y?5)G1?7OaE!J$zpNs$^9qy%ZCb4WqDk&u=SL1|Hu8e~wAP(VOH zx)BitK?Eu3PLYt3j&~3D_1w?>Jl}g9-}}#XT#C$`=P&nOd+oJEWyLRiv_Q$dxnLQ0 zoQQmm`%C-$BJ7%xLNo$fzriBuQA#geOl+6F)1gOlwQYjG*|jm{>#MqcmCp{;)STUq zq5szjmTilAOnj-ZfbdI5Fo3K$=Bz1i;XzJzIUI&+)SlE2#f4v{F}qW0dh@z{M)`=Z zMa}Ui@>2J9>OG(7d>zTwr0}+sqh(q#yJZqp3JU*Sxvj!~hJ&3*8y7S6lTw2s*!SmE zruLRUcTpAMe{Is=n?*H{_@)0Dtw#xS8(q`5pT=QJFN|&~wYB!hT>po*5jS<=7CyJd z*`x=xZHsag#5>FJlG2?d4yfG)vAd`fsURbI{r6^EJX$x68txy9Esm~cP`I?Fd~ohp zU%csD4J%f(3gCR4EsjVp0B%MKjVVq^9B5ez8N08)f0nuq>*ft)r|7=Ju$4L`eXl){ zM)$tS)-ABG*cLl__M$aJam>k=jh!gZKh3*;oDIlx0qT;A{AMb+mWXy&o3ZF)@6I6) z+w}1uqN9<@wvZFi+vWlG3b)WQ$+|5pifb9+XB^ z!Gu}W7a{02lq2zq7m4GyAie%Vx4nZ04&a?MNsp!6ue06jGabBrg+{lf{5FWhioNsE zbvnXor5A4I8jd?_gd~rChRdSH@2fyE7Hn?l?LZe3?0l6B$O7#zY?o@)b*i74D?rv| zmyj6QivP_HH1*SaKF|S`3@X$><^HIacswFw@6X-``T34x&rQ3w_8&>cH8rvy zt5G>32Z`%FE3f05Z$Wp!`s=8!RCM_4G-JCxlqN=Yw$1!?vhVE0$-^E24|c{j zAO)o^{QJtYGhSk~TK>G~KNwm*0r`S?$-;OR7&8nxx=nrp7~m(v=l z5TLJn%&PbUgWDE7M9`FR#(O|Heh!Tc^rE>0+P`|Tqej8uuO&HHtp~`R*`VFC(RlVk z>&v@oM$RTaf5u%*V|6P2O_Y0=W}jL$0Fmb!y>{%rngROEO)jYyNT<8%39p8|<>otF zAFA@Tx%S_59(QIZy38n?GzL*BkuImcC)cQG4k60$_^a?{j_Qz6bE}+qT=-_Egw}b< zty^@0_P1J7hbo+sE_t-FYxPnC=0>N$v2USK;aF%UwqFf#J^v zoe7-HsoQqHp#BB{U&?022M*j2LLfS5{ZJ53Krl}5%84yKnSR#k%E5Ptb(0sI*jiH?QtyUz2`w)nTvtYq- zUhD~I2e*L_9GCRbRst~3Wx;fgjesezcrsdM3+~%G`yC=K!$={=zBOl7XrXuxjn&G__jbm)Yqo!L%{`Jm%i8~w_^He_ z){KyLsj&*Lt3>9F?~$KKsK$0)V_CJI96L?dBKz`<6n&P***X|zzZK<+1RQ&Q+Obe; zKP!&vy)RaJ7+d)M;lu5*KN}s;0><~TQC%Y6nQjyGn3M!oGHH54k<4or0V@?OB5Q9` zT+#4gKR~^;fSXRjrvRin^}ziEt89MFQQ3!$@T^Vf?gi5X==Y}4eoa2nhB93OQ!n#k zsqFhk4S_Y~&O<$Awi%L=`@b^Twv)z+6QCGk9oDj`;9PK#DuhY~&J`^Y6iRM38sZu{ zHa;f~)Z+^sLs9*xFk`)}hlStPy{X=Mmxr`_dGC-+v~Qd3Db7yr$}_ZeGamQE6A z1Fu1&aRUf{s$Y=KZJv zCd&n{&vCLSU2fpc=^7733%iW48IJAdD3YrvBl-Yzse6ZH*=T|1c^01}1Q&edTP9Rf zm2^>p$W^`@B%bKAbv<6pMAb1exS%3o)hY*#MH)VDK+XGwV2RV*OE+KAH z_YwHz?7MrpnK1%JXb_`xHbwK+Etj$;W$kJzFM|9l*(Kkq63IVWCM*ZSoqB3=iQ3aRe04T<`lZ_!8 z!%jsP3lm<1)~(jI6P_U02=Il2HakQ-TIlZ5CS}4ws^(g^A`Gp=F4_U33HAx^^byo( zUgYcYQieQsmImK9mD|#A`vlf1t5UGudxj2grS&(0tS8X^N*EGbQa3}vky~dTU@cI^ z{uN#O+$(o)Z|_Q(Hn)gyU9+Uo-<}BMbA2B+xD}VOev4*Dz;T>Ny+PgriG)8C=M`F7 z+v$OUDSsP2bJxM?4`hF2mu|xlIHJ~$)g5DjXL~m3sDeqc3|7Ve_>CKMNETz?OcgFw z5jDB2zjnh55Lwjwhw_b-f5z54a<6?*n!`7yfVK3Fzp1g@5>|FmU!h znSD2d#GOvqGFFwO#vpu&7g0jV**vQ+c#UL{62zWf>6P1;p?Lg>-PoWc*vGH}DGkF{*)vVS_b zktexNf^*Jvd*C5e2fV{s8xBPacnz(Fhy!0q$(>)siof7|Z&%#Wu^_~WQETV5+tpgxGK6=@A`6wb7pZCB? z`l%hf_g2gDaD^H;r*lWYyZ3yiRtPP7^3wI11?aAO00&SFBwd?Ue}iUZ7Bo2NWGbDr zSUH9kpYOHQyrvx0(FLY8I<7TynCgWW>FIo+!WSQQLCWwtV~a7I8c#@&T3OY(R>K@Oh+{?N*(I5a0;(5ju@4$UQk+)`+!l=qM zm6TCj_kGPgJ)Mx_bJ9zqw?Md-3tR#64&f?)WC-nIU$@q=b<&BG_1K`+I2QR?=$I5) zF~7ga?ti?uFc(g#c^zR5Vai57gu(YaagE$7Q?6TuE`K~ZwhKhOPtX)cUG|&Au{wSZ zGCAM82h6U59%mvby|ja*PBc4w_QSYn29f0b?rmQX!NTg)unw396pr3sDipBqVx|*u zeFdUf%>_l!`o^DnA3(-W`h6U97z8ZT745ebaOehpJzb~&Yy?;iUOy6%3YExbjDLfA za$Bff>;09+n1z40DV3LWju2bLfO=0}T+KBr~6vd;ZWj z_^p}7f*VY&8v-F5d)skNWYiKri{~SorFCI2=}lq9||BFRzx%hKSLJvD}^Ri7=G)P7Znz6pYVEiwtBPVU4{1cXJTiw7$Q{;gk5aL4*fSq3@lVr z&ogGPuS>Fzusgs`oM>mI!$OYJBbn+>j8Blx!<8oQ3_7+YxdHY(-IJ3CnXf6IQNoeq zj{72aUX`LLA>5Lms2r?xle=GJY8M(5r9IKk2m}G5)94M_VmOZW|0aM|xf%pWq@0Fj zcG2CHCg{dLVC!5#zRg0Ye91kWh2`+L@3s~ST2RCJ;z0q4mLjz1BTr?TOBMLM4*^r# z7OYQtvQU&PQj>6dUh{|!CB06dc0T6jMk_0t%=z-v{s_=@uqG3Bn$;#9$G^FT)|w0* zx{uuQy31eczSnDicH8h4vx$D;bDn&IsONlqg}lG7q7yH!b8w*^6KW3<|L>#j7C4Ue zkA-3m7bb+;E`AtRfsm=eJi|_8exy`;}J zI8bC^k>)>7H`BC<&25$tG;HNt;3}4F3XGjAS6Jds6-`^vG8WZ{DPFH%uOsQvXXsu? zJ*X|Yv{tw2c@^b$JVyLCB;P8g%&tQK(L_P@`!cjWv8#lR97`Q6-w>OBVLkhl>wR2k zM~cnBv?6*=Za0$oxl?JZ&CguA?SDhIji{@o_Yf$j!vj|uqaXJqs0_;dEoLt zhbZ^uAfd!|(Z^%Q0_Og)R%41+NEXu|Lq3}UUa=xQ&!HUouen;=BC#>gUD=dWeI6HR zsZzXoRBjjscec04(|fT3g%#{XA+u*LUDIntlf>L(2bJvo%hW}1X`&I)sI{Z+E~1tv z!RT;N9Cck_Fmxv%3UGI4GPOY-s+@?d<%mN>WWewOA>zv53&U_wQyGiH!;RQ`^NQd4 z5s}-K)?H|5#zgVfwo&nr_j z{Usnb-sn2=7Eo)KiBsCu2Oq^=2unF4pQBMd-{rD6g@ixOaFPf`|X$={Rt5` zoD(ap%Gjl+?|;Y5LqFfzpz*H#&4lpY{sKDDWHy-E+h%G*I$NVR+5IH&r0k`~;S@og z@lM`Y^4t0OrGkq%G}s%MBu+K+>FI3wW+o|L;Iu5?b57uwgsG(#dDP)5Z~wJ`1g35RG888!A(`ux_{04)h4}`hJ{jQ`=vI9?nuhz8=;wsi4=J|9Yx>XeU3Oyk zT35$Q><=xGZ2J5QlJhvy`eRR~j^D*Gc>bbu2R3Qj9 zNPLL!Mf+Fbq*m(eh;FwZ;1qJA-l@8(=isw#VI-j0Jk323Shl{+N3 zi$SODos!Eh*h=sd# zGF56WfTb?hjFJ}Gkr6&a?cUjbcgN3)#5yJr~CCP9a z^;eaneJjrabM8AUk@?qc zzmJ~7nXGY_Ak-8S6IGUk71$bQi;H4@oGf#9nxZN3Wz`rr!M3<7)R1i(HGDQKxmQtHGpLCRx~? zqryp7Bt1yV`q8V=6Gx-nafzM+?aU09dG%M!sgV;vKi%N>GGn8IR+uRzROr%*M%z;e zi`P=-(+!%PN>_f?moq6c55XqWE+zLBOM~Z<;Y95+WbVbrqA{s;Dnr+ka#jXw)Xe>M z823INT#l2au|R_5U(4tSnc;@da@@-wG4a+d4N#WBev>>Z)G4?84w?+6>5_esbpX0N zRs0$2q!=!J^|DyjxK}O&-Z82a$1S;Tuv#}~>fb;0U0tbVCS@11~s>{p1Rr}po+fJ{gS9f$sEbT#!1cBszi^3_5fL#N12 z)XYzuIQA?0+edq;$RmA*U-p-|6$Z=hM5X~lbpqI~HpjcHU znFi#f^|G($W?b-YFjp0)$;hMe~fx z-eUWFOMU-XBNIJH{cvA%AC*}bwLjKTO4;1-^>vB~VYT!3B+sYRE zoWZqP7S8{hi~WTurhp2Sx=qP#`abLjKlkM=^0k1iUsk_abqxUT!aqlkgct#f;%RSR z@ZgIs``jEw(+;8fWuF3wOIt4*m}0XQw8^~>Keqw^kWho>Y8&VW)8YJbFmi6SASJ;6 zMOj&K8U(Z7AjY7hJ_{AZTbhO9uv2=EptOiR=E7TYEjO+gGv(wy}2OL&kPBN{GX9W*sNdZpGCYzTexe9E=c9q z@NgW6e^BegQ!5`5K<48*j%2`aC(h?=p_lDCvWuOE9$QSp@1(auryi6WPc!!tsJ5ku zFq{*16|3%6PZmU-7yrd9kLYTeIS#GsP^2+Kg2N53hgr>f3&UKD#A@8q`#Q}vPo*0g z{(z-pVRS6~LukAKgyPnx7rAC(Ncedr$Sn4iEpz@rh^nY5Hb>R#1Lh7MV0xQ;lJ40l zwlQcOivl6gsLFVT1v<)4Nyh+B+O*iYR`*TPdx6XV)&y@3V5cMyr)__ym;W?;76N=d zai3)lzzvITu79DllQP7E5Y}o&iOTN>Y4|iY%LS6BDy)R3^C5b#!N11t{Bwv*>VBOp z<0Kz|0BO7XSH`PgS`Y_fJH*xoMaKhpFDmQ7V8hN8{6~de3^j=_hSiw+O+`E!d`NqeuZ>i5q%!uvehx_NTA zY4<(y^8Hpr6KJ2GU;qRrur}wPnQEnVv8Yhh*Vt2~_xZz;=jJ5 zEM{N(u%Bk1@##c`>ms-?pP&y(|9Xgc_imd1TbVOhbB9lk!DLU153r~_dy*+Dwt)l- z3@c?s(v~|7bRIHkNH#AHhn;zLeH^r5d#QJ*!-G_Cj?`a?c9RhaZmVIq<3FF5P=Blb zk`)RS*Jo#7SQ9{>l!kJAec%L?e9jGFQ#P+Z<7U|Goee2}2igqpj(T-<&fEzPj=A;y zkXh2c%ZRUO8#bw}3c?figMK$X_Z$6QtuYq)M=D9YW?3LT^C)mIw6SylS3VHQ?FvRm zP4~do8*hNSl|bPaH407fkMW-x+=Lz@YehL43L)U9qSozieaV z+L}mlvn>%ixQ@~WpBpTvUAKu%3)?T+yn$l`{{Xh-_@vBD!Gzj1pY!Lpch%TuF5LDKL4FnyM9} z$7S38`zPyMyIOwKK3)g@#m8Q!VO{gXh8rceH<2MbIL0>Ne?cdgL3Xquhb+8%GIL!? zR52l@j$G=1;F6mRKYFsyT%Mx8$l_kOO}M4t0vXwo(*+!>0$>e(A^Z$hH2esJ)um9< z5LnO}C57RFul%N;v_vdGc_kj_EI#eYgU9&E?&DQtL<=`@$D)~uz^y5YJh^*VKD5}< za*U9hun)v&(IuM9;kO?%(Mmt8@BS7>9!fK#4FJ#G$CSLPUnq;IuQw+?<|qF~{`;jo zC@*WIPpRN4d~UgB=$n<|TH9vG!R-1y;<&JR4iKp8a#H;S!y2jLKMhTpDf_eA=(s*T zEYu?1277XBu_JfrmsB z>1Qyt8XAGKbf$^@y~31=T!z+7Tt3{Yi%)%5hHb-_Y0Uj(L1p@0cwoOwNHHmcu)3!B zV%8mo&^vf6m!?B2Ulqs@O4;{xmrL(XhACb5jafPTcrk9zp^RYt6{Fu|$lDKP&Y+z@ z*d|Ou@lhU^>`e*%q+B@lWm8;|KL(?D+?8Jh24(IZ$KZu{TZa%*BRVJAI=4WzgqAg| zJ?Y}0_T^`^{7r_pirqxdzH+&Q$RfIP7ZgjzgC!K$;*gdoB5sh6#el!tqjI;b)!XQy z8wS{0_9OO|O^88;=w+_#1Lk$^jD0{C>ucGW=GY6xf9Il~#Z?IjgdmrPN)<65rDV4k zyZ9Z~8vMPoycqa(O66^20M{1E8fN}P=qF3zT+I7$cXwiUe-^GX!XCW8HO2sMGVHs$u1d~$l5;UUL^fb#rjY zCxW>T!*_0$sT=)-Z!dD`3-Y4Agj&AFQ8sy8*cj?@aC<5G8GIX^gO&nCV{hR{gyS(UqXHnlToXgMT8ISbf=^(%G`K|Uy+mki(kBeTXAs<|45 zZEQ|S?GdNO;ZwmSCM};alp=4a0_ZD?vYCf)MbcHcdXr2dIDiXUe;ZjV%OBE8@H|Kn2$)H4TRvi~wbCa7NVIOC7a8t4B(-zhvwc0hHzbaS!e^ zwJi2*UgXs-v`n!O|AodhM12^|yZ_kVM;1!tGwi`u=SD*(13Zt#Jqk)`rj>VGPBon#p8S#8MB4IFX(Q7P9lz z<2Z?evzRjoV^LozVYhSJDobcrPfG3h3GK}9~G+=ade%9`$zkCFltao`fidGe-6YpcfYVX zi$}N%cI+OpW$2LIbT9n0G7q#q0oNOc^3@UCgYC6zvC8(@(%eLdd5^^x8lRppA1#;t zz_^Ko`jIi6;gs2}h~O_#M^NWGD5q#Kgu4sQmD`R}Eez_*DBaK^13`@p_D{7OcrkO| zO>LyZo=p-f1`A~UhLtH|^qRvk3S~xozFLX=y80m^`NVQZV6q9}A2K=^>K2ni_T^Ei z(Ahh8NH8S*k+xP)Zhs^=h)aIP`p#Ic^$pZTDk|FPug4`b~x%HQA$7ynGJc8{1c`5N3ota&Gk@ z@8hWVm0G<$*-A~q-SO`5l#Eh!+(%Jn6cQ&t|*}F-0)(ych8r5{M6(57Q&uqs7 zk|?5?maO_LZqhH9oXWm5@$gn&et#R7+)p^_N!h?)Q53)wkV&vZFVWO)525hpL5vA z!W@lg)`N;#+{3%r>gtZ}>80rl{mVpU0`tFmo+%bf#CNVLrO+I4&xyD^FW8uiV!T=I z^&oPTdxFQ1*+TNiPD2!pE z4l(2=dKmtEQuWwhuRdmF-O+qFmNm{9i$x4QuDit7tI}lVJ2pr0@?-wopKWSf`hn@bNY)!_b=DGOl5o9w#0qvY4B|eR<^5mol(j&7gDZV8D+^IC`;ID&ec>TkX04t6a5~vd>H$?wH&=;I8=HuOI_bbhvsoM z2s7uIclo3}++R%NfrS6=Ek|X}80gtbceT8NM({;QrMu>9wzDCa6V20-{tYA)6vW-A z-&lRUaX9kl#Sf;2b^JvpWE64w+{z%!jdVt~ug@N^EFxti7-G*GqU(&RCf)ON7Ov-+ z=G;arYsO9iWZ`33^mM>xB;Vgb8Y_we)khn)RgatOM7$wAj&x6g2avD-0Tmt_qBU%K zW;uHn_8DxtvvzFL@&Bro50&4E;7$>`Eq;OFJ(7j>jWG_O248&7gRT_7JzuKCT?z@G zUk0SY^!{KZBQ!N=i74gFks#pb+}8|!GUxL3+a7+gxj+eXJ}N`(J+;_16n(o2WBLOWRF$mq&yCOBDAvJJfav!4t$27nNqJ+=jtr(ih=S zP!t|=LT8jnuCGK^YBHVjvI6q{>FLG8eZow{z5H=u2eWz>A5+a|rDdMxJX+dpv!6#f zjTsC*419U?&2MqLt2Kib0ODNyKANvLexm>}r7yj6?i9Zvn|Nryv?Y7Gd3F9hby3ti2Ivhg@RAeqR^RoTHB{{&~lQr+bA9<|aG zp(G#%vYCrxNYNSew=6FG$h`_Mqhsq+p?_&|1s=RE>?Od0r(!Uc@pUbpJVRs#HgI-wuBbpe)Mk z7laIiw5wLmj9hR@mHJn~`o9MfR0--$3t(4vs~0uEYOhKM1rqO(sC&}o-utJp(|KZkk$kI4th-o3E@$92ew`!tw!O&S5}Ia5T? zG&CL7{r_23Vn<0Q->#CFCkaTUrVsS)AC*aNY<>RWKUTP%Lx{YH6W-F)BnKNnRei=4 zVoj(Jb3t|%o%f(T8mdMF4*dl$bx9Ixg$&aRDD&}_xB(Y0|NK_t0Xxz0peoc)o+Edy zExf6-W8nnvnK+vo@;K|w!DCm`4f4tMI!I(Z0HvW}2n4eV4`|~2))YaK4He;@3MT`o zcbmv*u0iO4hFHns_;6FJt*tE{%ggH&ogsAqeSP&P$m!A!4<=49*m`O15w@5uGU^uA z_fD_<&}IJn%bps~yL3{4)uUd$8qFfKPO(y}t^x8Cd?d>KT}^6WUg8t&W*si=+;-^O{5k znTL9d;ERw%aM@3WS-{B1NW1)*snDpog6olQG{9^IkZ~Y4BSD$-7RYbf90x&dR^jhX zyJmZQKf#IoyZ)t12GZRzDZGEhnBts@zCjWA?)cU9Rj1m?d{Kp+n>oD=K>(fUye0c- zz?rD-na|1o;(;`*u!(!|d{=fsOp#E*3ToX-)m64o3omsU5ysZdS%`r}B|yJ%3-8<- zwqOdthln1yhOynNUsvk>)6MMz(3nS;ECHRI+e*cNDp4h9=DSALBxMn`t zN!=GLV9;mJi^^dAEyN(fVmmK;B!6B^Ax8W+2i;A-;u0Tj<8l=cpAnZYWo9*pZZ(5; zKn%`RnkU@~nEET~dT{VyTOK`DTB6uQ=4Jsq@L1ZrD8&wC`%P_uN~ShD@d#_3{*W}d3?+7oVGjhjk!7Ko&T$J$ja5L~wfz z+dMowKaUaB`d$Pk3{AU+p0C)Yr6;NxO8`Nw?~N1s7EVM-ilKUppzYcjEV1qgY}lP9 z@rSB;)M!5KS+4eK_Zgej(Ha5o-@5V*pisxdBV1F^^+^1R){~=wpo_O)Y=x(ZQfkb^7C?Lc;=8#lU)Y3;%#d)nao%`oMc^b zW&zw02^AZ58+BnOVHEd@1GJCf+%cHt&4ClOFs_L8U$A`&H#yHM5l0ff#fdPD#3u+ob9%R5tE@9UG9(}(W zcSKXT+kDEFp~R9q+) z(*w8kYuJjd#u{|2l=nVOzF|dXqAI&d{!W+R6csOR3ZL(dLJGqq zD!Abk?IG;a-_MHhO9{x-nA%#Yq+TDt3g6p6VH3O z-2=8b);{vb>Lo~=4k0zjH4Qh{3BkQ zEe$X7)~_|)Ik=_!?(Pv~`smQ7Hlf#^nr^UV@qTviT+@x7oB0SaAl!{$t z=DFQV7eKA4$ecu+4!bDGu!{mw;Io8f>#dgbBnLTUwA~L|dis~HDtSW2inhM1V8-_( zHt6tk^%Lf06Z9?c+hYsHCz)&xVzk&*TY3i`x^W<`M8rOQUWQXb%gt3_0=QnA+ zXfhQF+6zG@tb^-6M}0D-&j8N``8vc$%TN`QP9}#nTR|=D(WNeYHT+eb5WEf9KpcIdaF0@&DV=7|@M4Vu*^KLIGBXEgJQB z{>jf;2=e0I0f?jWGgU3qAKI}pJJg$WTCat-_`_x2B6-^3kU5P$6%;KL43%g~6@XBZ0X13drzRa5p!xGvcljW|AfSB8A60jgirXG>W9UW#r1>i#@CqinA< zSjl#@Oot}vsGo&Xm3ozL^3 zB@H2vzRV%9S(y@B`pxEId;~9#W}KCLB$lWe@6#Ah^hhnE)spfX!6=F%bWEWA+^1^x zc)Uk4h92K$NHn@QF@(6{sa10nx+s~2tLcGv3N9MnsE)_Y*@Anh!i zscT%g*{T$8Pd4%neLp!pY*t!Gv_yQvw4u+^d!uXq6sV}7$(B9d%BZ9wKN&I6u&LM$2~|2aI$lq+`O#PsG$2U_^XOlW;~%wEph|&iBP@TSHNALYt9szG+dJaz+%@wis^3 z*-sa^Ke;~e9tVR8HRn8+z)*b6flV}Xb-778b_FglWcA)g2++bpr>oy=dgPl!RZXeRN}(?zx5=MI_V0$Z@?cz8AOguIXFF+GeQ-`!5a; z0yT>T^uyh%=7vpI4X>+AwAxya#-;GGoBdF$U5*RZ zm&{x4Cfxr0Y-5KE&Ld#pap|^1z-Q0$s+0YrU$#yD>j#&A9oEak67n^4)WZHQ4_wUk z&9?)haBwc}-#Nn*mh#KLZL&k_b2c2%pEZ86bK*LL!lVcewd$HbB7TrpAJCRwmzFeZ z{hsa0c^K3upY;Q2#y-*-cFj`i z{3gupGWcy0r$gDbiPjm(PKZ7oI@bR=bl0|QyX*prbtU;O2j`mY;=`*o3=yY3h9^QW zXCP%`74q0J;$*O9|{7_SMNz&Bk0n8VBpetfQ<~IyUn!Wn3`BHsutUGmYqWzwE zhr=qsgwI+PvH`>aRUp8&&hr%lAxZa_+L>I3(d@J&!SPX2(`0lw>8O`kDQg@nb|B)T zRTmuIbtQGyEgcPS)%(TfZ5!T)A8<%6d)vUyCj^iJA<2>*6HNRdy8(|=2uG5Sv%*Y# zh!Fe2Y;R6CDa^5j@0GCmY&i>jmr4S)pPSP-kB1}f!8A>#6m~t~fG5JHLt84(#c{7~ zCGJya^&z(E6{^f6^ouY*Z-2x{rU-v~xUhw3N|mtf7!~^;eCfLTe}9Sp98SafFx=p% zV8fnA*e!PX?7K0=Sa_~(YfKx@R(hee#%GcI2|f(- z>+Ys#`*{};wS%%BPw;!*)CO#B*!rhf#=er!&H0+J^{V2@8^ZMKv_TZA#H@=Mx zAfw=sW~U1?@nmHpq$H`knKKsH1&#gNlTe@18B}!#>wJs2Eh>zOvx|R~JsleA$#ose zNKGs>Qu`+2LfuhmKSqmfDq?dRR)Ay)s(2vg_r5j#iH&UJrBkPpqtiojIL}(C5i8Xp(fl-7KxX=@GS1|Lx636{!GM~`9^e=b>w$g7ME^p z<4N=I^DMkUvhY;t(PR^5;lbPV++px0UwkAp-ubuGYvT;3MNL}yGC%7WCT9tC1~&qz zOleFDcq{$@vdd7Ho-8~qw;vL4@SPtL@E(owfX31!`!a`SVk2Rvw_cZpZ{pne$(xEj zE&jREPfrVoAi4<;r4FoyYVB&D^B5@7dsl0|>z5MET<>bGlV^r9$Khe1|J>);*+Jga zY^zl9xBsqtIPk?LoOe+hk1Y1?U_$?_uj#YT)aUkX{c5g;l3N~hP{VEpY$rEGRMGZ( zLWSty5>|COj)(x?l*)_mg!P@|`Z(;&!@#rCW@RB=btU+jqQC!M&q`B6PoaE3&E~t~ zmN#xonXzp&&;_#twW#c;7uABZcaX?GhyRzmnS;=KPb>GhK*U$cx| z-}jNYu_Pb$%88u-Ic#9UAh|;Ly@8h0HPpEPWzaR&+b_5g5k5{h~^| zv1z-_HIqFe&-S3<+ty29&u$H=nLS~RbA``Rsbc*3H%kz=<>}g2buenkyH)X8#VTZW zW!V1Vy}(xHXbqLcF^$eylK-f4oP8+0%DyP*T>U&>m-Bb!Ftae6PhTFTRO-N0_XSQ2 zmUY@C!l%Jpi0yfo1dE(;q}X3^)YuVkT%xBk$~DYY=e>@Zk>rU(+xuY!dd9%${DyKg z>#bt{go1PdBM%0lr=&_l+(ZA}an0lLpm2%~rtD^zjs$!OPAu%CN&bxCR93PJxDj{( z+3G{wy^0xY#Ul6WY!Ztow04GtKa&}*e?l5bSSQc+y=Ob{S~2#D#NAgvwCD+3;fO8STXm+BoCT`HW5 zmji40#d#DP`H1}&7QRXg*|1*V zS|#Pcl~RPCVoP~y~0gx9hOusD&Lz&R4@PStl8BuiaFLwSD-o0ow~z>NLQ+ z`2z#Eay!%yII+aRe9griU3IN3{tb)p?u){wX0Bc}4f88D`f$Gjx`o(0pj%4~0CyX}hIIgCxiMPyLy8Oh z>=SNlim@*Tf=QPzmdEj&&%*o~n0q~cNkqE2v2jWUg_25}(10nWfD@$?EtD%vfqB>i z?SQ#!?ww-q9JnI1qg~1tUfmfjb@xG)MTkcx@uwxEIx9r6kY)KB*_DG<9m8+Tm zQeoA1@zlXvOssNHl8ZWAvN#bC4W)b#HkPZMiS1{d_jQwRkIl9QuS7ehuHD`f`2A~a z$}xhx3b5mDc-uz`<-i7Dxgd9))V*eU0+z?Uc@c*mWEjpISV5to;hnbX3O6nWSv+py zTGL$c8eE%XOB{yF^Zq4dI<(*yCAEMp2Ek#+tc|?~s7i;6Sjn#9GGIPj4LaOHKaUr1 z-+nIEk-~HoT0*8Pd}x` z1}jW9^EBtV`_wiwJ4*xK_-zrurV4JESnQsZvl>Q7J&25G)EU<-vLHCBpJ^i$Lw`>6 zz4o&b@WN#KE^yB@m+U?Z72!MJPYVZ0|IW=sNC%DDF^gi`_Q-cxuF#meg}6`D00bRn zT1+HH!e*d4zp`uo9Cm_k0&LD9TG=rT>dreQ-Y=fM70P)0JDEKTMgK<1!;p zK7KeLc)}GmEqeSEk7h`2P!r=qxm;E8S%w@lfVf?=byBRnm=>xcJA&apBZ|gor|^7 zH1cbAxAqNjapV28loYNva`<;$Tnb;lc)`J{kt%DWSbOxe?g;{u59j8^kSQT4sqy-U zN418@&rX2+*EIORJN-!MG2cw`W{y_+{Zf8&$%UYky@Sq^numLTcjkY5#3YAI99Mf< z49QZc;G#0p5jx1N1iS4-1iCSkqL}1IX;bXDYv3{N?dDJ6;lc6E>Co!eD}Zgn`6_FSDm)A_$g zwO&^m%te$ht9%Ln{%9LKJ9XNb6Rtz|#r)pk-eysMU2$<&;PGJ%;aQDI^;_J0d`Y1$ ze_FQgPDf~{y~Q1{46`2V9~ejhJ53=835kxuLBr3&>?V`Is< zn0eP_Of75uIr-JAe!imp^2$nL z>WNPR&x^D)-cJ&0WF`sv1IRws*B|hmmxrrHT3VFIadUEVQib{k`f&UVqo+MFjC8?c zV`Cm~(4^tqkH+pBT`>L8*(uHns`6}Vy>?Hu2%nncjSf6P2(!&lTw}qve(%EH7OLPC zq4KL)sU@#tW9s3Um=1P&C#{IGRQwFb#f;%kJ1Wef8{%q~)f}RF`Hj>_C(FnHN5^+I zB)q)5-}2<(M&%0ed5rL(8MrnWtsXp#U)SZcTlXY<*Y$C@xvVXsI-ICZt-O|YXLq-H z_YiOOaDU&1h{7^okC}Sw^Vg#FBTB{`0r;pT0p*KZYCX2gZR&|>%(0uDLv4xpG}wv?~Nu}*d0lhwrj;UW1i zy=rwNAu(PsZor>q!BB?~CN9!|SZ0ihrPki=x^>C!+W1JtLbm~ljNzdBfR?m^T%FrX zb|sC*sVOV}M~$tmDnzOsvi-q^fEUq^8c(+Y!S)nWFP$=2@0M!|EUZpEd4^Q@o~75EmJ#*;x6v!Elko zhg|jh@9J%m6@R6DR7{d#&d%HO?b&OVB^qYad%`?*|2;aa zmdjt|d0kylS{iyRV8Pm>ou2ouE$SxVcCiQeTy1XDhJT1Q zpRZ|?X8DQb(d{h}`_jf>RhM9vr|KDjhh?^)P+c?VZ=) iVxUx~QozjgpK7Vzbcts2EP0aPdrDG0jq0ZbjtDPNjCdu0vQRIiEJMUWoIORLvr4ACVAkAEmURLHWuMlDhCvB3cXZs1gwI3+yfP1M4WrxhaQoG3lO+jry70ae54LQ5/+ImreqJlAVc4pT2WlT8Yh/IVnpyNo1TlHR6sgIyRhetSsTkucoYa06SCl5aXebkaz91BWcI6PiMYGZWfsXTtmiqo29aFP/G8LzhXqyG46rlilMnuaUrHP5vJHnz8o/VfMSqrnkixYLmJKXRpX/aeRPKCGs+rV8naBMyFaJrRp3v6W1XjdFOesyIAiqEc8wW8t3//3uQa6NvSl5lG+ExBhn5N++LDBDjyuYiNYXbgG8bsGWGS+5/GcKi0XZVxRmJGdSv67PyzDD85wXaCW92xnOsgnJCC0f5M+A+MvrC0bJE2q0hOUf8SRC8S8+K1TPe0aUYa67Gzk1I2JBpiSkcER39NqokpL5jMgSMfrGu8hWEEotSSsOvbAqv2xswh2Dqm7RsAcvkAOhtMN5PfdGF/yHVIddNaKrpho+JOFTes6EkCeM/ldT2eqDLmpyzqQmd2xRU5gxKdSWgsJ/1kQ1XBWluG94B9dbvW4a+a+5+PcrYmua82aKUkwFyslp+Yqqmat+hhWglGOZLBLKFmROcph92tTetu2kYRNcCfTth6i/Bqr4t+z2EzH2Jm0ErhnhVZvZ/yBCm1ZTOtx0ttpEQdY0kW8ZSLkzSOdIdZP6FBLoYjnOddiynCsFyxRlkOHntjewWUk59Q2l8K3RYUVwzorGkx9Exeax9TrlYyNfA+nD+vMf1Qq00Wo5ZDYruIR0m67F0cnM/eDC7CyFKJ4lVjtLYjSdlS2QshtBF3hDTnKk6u6xkEO5GpSnqgdZobyqaba/Yla9yDhwZbl8k2vH92X5AVHMRY2oHLPVvlu2DKy2bNsEpn0fZr+mwfk6ompAWa1BjtrY1aEbwVeESj3H2b0RAn1dzo6NcIyRAwPLv3PdzcQ7/YmTJ7mLmpuA+yjWtm2KOLLDadlBaFsKgfcGtyNw13CuCde/MArDNS5xmpYbKINTlN3WnLIJoxWrNLeBaVxq6xoetebPcrUtDmrHS8502ypzjwFMQ7FXrmsH4n4xzNDuLSyQcqtTuvGo9+sC0QxyFs8fJ9uxahZtDV+MG8OMvugVLlcZsnc3n3sKnxOGcA+XOBMy5GabwhzqNE8UHiDjVpeXNZx7nJOZuUF7vwLFwprMLLIws7gHYmbu5c+YLdbT/0rKPchT4V2NfwCY8vQs8gzC0+WptmjvRPcmSVAhopqvSPq2AYhu7bYj0PTabW4SgnGTnbjSpx/CUI6zlcM5c7nIzkSjySEUuWySD0WuuxNpP4yDNoQrAnOsa+gX+t3zWI9G+dyobUBei926J7BbG4/UrWQ28xIr403DaQg6R1YnGcR5IycXtI0sisKdhNGNwK7+pxNGk1JMYJYJMKuoBUdmDswXzR63eakj2KOnIcQ7AgjFVHoGiBY8lGjQV3gLUJwGts0ee1N/4xLqVdQhdhlxXzoQxG0iFIdgJw5o3cMYjLbCgAkinjZaPzas5GNEw8dYocmxvpUw0kaTLce/F4AmSnS9oMk4aKP7uwIUtXmariHDKDedwOXEH3o8FwNLPBdY4o+wh3jOzFTd/YT5nPC6b2VEfrFi1cO6yJbAsIV1fYjVOwszbvNisMcbHesTu3kxYPFi78KJgUjT+zje6cWM/qBfNuvFxhark1Bf6yTUHkrbf1C+K2FwSkh1rvDJYm+dE1OdDa7r9vYjQ6ePiF0pBsFfRiCMOJxpn3quFjjDqwIuMsxL9THmICcww1Hk8ytUHXCLwwHFnd/ac5wXZALtEgMAu3Ms7knd43j41KRrnvRObh4vmA/4Os4HHfmA6wSnI4ZreoF+znm/4wKz8vaK0I5yI4+IPmNx5uvc0PmAJ7/nYyi9HPl2SF93Ay9LDL8ltbwLvIJIy/v54THgZWaatdg6crRMxZZM8/6JgDbRliDdmCjSMpGuxrF7gSxLKtJCwzPhgdF+sGrf5NJYTZygktVsu+G1P/A3LHAagwDszC90xzpPxzpL7GODOj1BfBQ3Gg8DLIOHN13394mbF2hxa+zEx23eUDtXiMJum3fogCvsOX0QeAYMWEzyI8CAdoUzcodDgcCkjz3ltddswSXChckwyYe8vVkXKgyKLo7BnMhWfE8h0caLB73wlW0IcjDk+XoGQ8fOvXxlOyE5FRZjeT7WG8yFxg6j5blS0QwDNocS2o64hLSGwpCT0xrceoEz1q9SRKcZr+LsfntEP4lT84BpAosvpXhfuIgMdZ7wkUIDYOR13TmFqUiqaNV7vVwPLuvoKN3vIUhXR0kNqSewuMrIHOfvSuLqA5IeBG7kYZUCmiTBKvA+bhOaEZt/sVTNAlOGZrbbvc76A4sazsTVgMnVLN8lfAQtAP2bENtmOJcWTH8OPqYW9E/Pxs6AWjAdb/gxteD6+l6Ih9OC6Yi9j6kF/QQ29obbCyrH1dBC9DG14Ovpvii4Nj8SOJsezAOt+GPqwdPVMOBmMCHpO0YvhaGIdxSJnYb/2vFxaGGk1s+M+ggM1Lz9HOENffXB+Lp0z2Fc2MsHpwdf+9cjv3j3dd+aj/Zz31eOPviMT+cl4Vizt6MvDvPi5v8Iqbpv/iMW/9O/ \ No newline at end of file diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..aba47ab --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,86 @@ +## Steps + +### 1. Access Resource + +The resource is wrapped by the function `cas_login`. + +The client is passed to the inner function when: + +* The user has a valid session. +* Base is marked as public. + +Otherwise comes to 2. + +See [code](aaa/cas/authentication.py) + +### 2. Redirect to Login + +Follow Redirect Response from 1. to CAS Login page with query parameter `service=...` + +See [code](aaa/cas/authentication.py) + +### 3. CAS Login + +The client follows the Redirect Response and authenticates on the CAS Login page. + +See [View code](aaa/cas/views.py#L26) + +### 4. CAS Authentication + +CAS authenticates the user against a configure backend. + +### 5. CAS Response + +On succesfull authentication CAS response with a Ticket for the user. The Ticket is transfered to the client as query parameter in a Redirect Response `Location` Header. + +See [View code](aaa/cas/views.py#L67) + +### 6. Ticket Transfer + +The client follows the Redirect Response and calls the Service with a Ticket as query parameter. + +See [Wrapper function](aaa/cas/authentication.py#62) + +### 7. Ticket Verification + +CAS make a HTTP Call to the Ticket Service to verify if the ticket is valid for this service. + +See [Wrapper function](aaa/cas/authentication.py#60) + +#### 7.1 Verification + +For the verification the view [aaa/cas/views.py#80](aaa/cas/views.py#80) is responsible to verify the ticket and response in case of a valid ticket with a JWT Token. + +#### 7.2 Authencation + +Now the [Wrapper function](aaa/cas/authentication.py) queries the `User` object from database and calls `django.contrib.auth.login` to create a session. + +#### 7.3 Response + +> Now comes the tricky part because the Session must be restricted to `/userland....`. But Django supports only one configuration. See [SESSION_COOKIE_PATH](https://docs.djangoproject.com/en/2.0/ref/settings/#std:setting-SESSION_COOKIE_PATH) + +With the following code we are able to create a new Session Key. The custom Cookie-Path is set the the session, so we can later process the response and overwrite Django's default behaviour. + + request.session['cookie_path'] = "/userland/%s/%s" % ( + base.user.username, base.name) + request.session.cycle_key() + +#### 7.4 Middleware + +The default behaviour in Django's `django.contrib.sessions.middleware.SessionMiddleware` (to set as Cookie-Path the path from `settings.SESSION_COOKIE_PATH`) is changed by adding a Middleware, which sets the path from the session's `cookie_path` object. + + +See [CasSessionMiddleware](aaa/cas/middleware.py) + +### 8. Return with Session + +In 7. Ticket Verification the client received a HTTP Response with a `Set-Cookie` Header with the session. + +### 9. Use Service + +In 8. the response was a Redirect Response and the client can now use the service with the session (he is authentication). + + + + + diff --git a/minikube.ini b/minikube.ini new file mode 100644 index 0000000..be1ffbc --- /dev/null +++ b/minikube.ini @@ -0,0 +1,11 @@ +[kubernetes] +namespace=tumbo +context=minikube + +[site] +host=192.168.99.100 +password="aHVodWxhbGFsYQ==" +frontend_host=192.168.99.100 +ADMIN_PASSWORD=hellohello +ALLOWED_HOSTS=192.168.99.100:31999 +SERVER_NAME=192.168.99.100 diff --git a/requirements-tox.txt b/requirements-tox.txt index 8793554..676d237 100644 --- a/requirements-tox.txt +++ b/requirements-tox.txt @@ -2,7 +2,7 @@ APScheduler==3.3.1 bumpversion==0.5.3 bunch==1.0.1 configobj==5.0.6 -coverage==4.2 +coverage==4.5.1 django-compressor==1.5 django-cors-headers==1.1.0 django-debug-toolbar==1.6 @@ -38,8 +38,8 @@ Pygments==2.1.3 PyJWT==1.6.1 pyOpenSSL==16.2.0 python-jose==1.1.0 -python-social-auth==0.3.6 social-auth-app-django==2.1.0 +social-auth-core==1.7.0 pytz==2016.7 rcssmin==1.0.6 redis==2.10.5 diff --git a/requirements.txt b/requirements.txt index 5e75bad..791e139 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ APScheduler==3.3.1 bumpversion==0.5.3 bunch==1.0.1 configobj==5.0.6 -coverage==4.2 +coverage==4.5.1 Django==1.8.19 django-compressor==1.5 django-cors-headers==1.1.0 @@ -39,8 +39,8 @@ Pygments==2.1.3 PyJWT==1.6.1 pyOpenSSL==16.2.0 python-jose==1.1.0 -python-social-auth==0.3.6 social-auth-app-django==2.1.0 +social-auth-core==1.7.0 pytz==2016.7 rcssmin==1.0.6 redis==2.10.5 diff --git a/selenium/tumbo-selenium-project.side b/selenium/tumbo-selenium-project.side new file mode 100644 index 0000000..5b72325 --- /dev/null +++ b/selenium/tumbo-selenium-project.side @@ -0,0 +1 @@ +{"id":"4d72f9d6-642c-48c5-a1f6-7e7aeb8bf797","name":"Untitled Project","url":"http://127.0.0.1:8000","tests":[{"id":"af7ee453-9cb4-492e-b490-e0f13fd2a0a8","name":"Untitled","commands":[{"id":"87a6934a-4c57-4546-beb9-86730dbca9c8","command":"open","target":"/search","value":""},{"id":"1ef312f0-12e0-4e7f-ad66-72c5e7091d3b","command":"clickAt","target":"css=h3.r > a","value":"228,8"},{"id":"6b7c595d-d87c-4112-ae10-16b3a26945f8","command":"clickAt","target":"css=p > a","value":"32,9"},{"id":"e3823f62-1ef2-4d88-b4e9-64321965faf6","command":"runScript","target":["window.scrollTo(0,670)"],"value":""},{"id":"413e0241-369c-4580-b1c0-87abb56fba8c","command":"clickAt","target":"css=h3.r > a","value":"151,7"},{"id":"bf254ee0-50fe-4432-b6d3-59604a257146","command":"clickAt","target":"//a[contains(text(),'Top 3 Selenium IDE alternatives for Firefox & Chrome | Katalon Studio')]","value":"322,0"},{"id":"bd0de052-00fb-4095-990c-c354fbb0956f","command":"clickAt","target":"css=h3.r > a","value":"234,5"},{"id":"d8f6a575-d83c-4d76-bfdf-5e398e046bd0","command":"clickAt","target":"css=span > a","value":"312,12"},{"id":"8ebb1e1a-4157-4ac1-9682-ea4cfeb4ae7d","command":"runScript","target":["window.scrollTo(0,187)"],"value":""}]},{"id":"5934e323-878b-43aa-822a-be33c34d77e1","name":"tumbo","commands":[{"id":"3f240f0e-352b-40d0-9dcc-e511f157abea","command":"open","target":"/","value":""},{"id":"7b5cc7c0-6ef6-4631-9889-4755c3542104","command":"clickAt","target":"id=inputUsername","value":"259,20"},{"id":"0b197145-5947-49b3-9101-fda5850c0b0c","command":"type","target":"id=inputUsername","value":"admin"},{"id":"a3649189-1560-47c5-9ee6-9d4ebaea85ce","command":"type","target":"id=inputPassword","value":"adminpw"},{"id":"857e133b-07f0-4367-b055-f642bb835972","command":"submit","target":"id=login_form","value":""},{"id":"281055ff-bfea-4676-9f54-c28a4e322776","command":"clickAt","target":"id=inputBaseName","value":"294,14"},{"id":"956d99d4-daab-434f-a47f-cd52c7664a9c","command":"type","target":"id=inputBaseName","value":"testbase3"},{"id":"334b86ca-ee3a-493a-9a80-218d37af3d8f","command":"clickAt","target":"//div[2]/div/h2","value":"190,15"},{"id":"8cbf16a1-c51c-429a-bb61-53b2092cb398","command":"clickAt","target":"name=create_new_base","value":"56,22"},{"id":"6df1d46a-a80a-4dee-8264-04ec107efa0f","command":"mouseOver","target":"name=create_new_base","value":""},{"id":"7787534a-355e-4a15-910a-0ec28f5432ea","command":"mouseOut","target":"name=create_new_base","value":""},{"id":"b00d6803-f261-4b5d-8266-e06e15f83cf1","command":"clickAt","target":"//a[contains(text(),'admin/testbase3')]","value":"88,15"},{"id":"896ab499-6250-49c6-ae6d-ed15473e471b","command":"clickAt","target":"css=button[name=\"state_cycle\"] > span","value":"24,2"},{"id":"2bc447db-973f-4251-8821-5e6b286251b3","command":"clickAt","target":"css=button[name=\"state_cycle\"] > span","value":"22,5"},{"id":"77a55534-9e16-4f6b-b3f2-687294eac53e","command":"clickAt","target":"css=button[name=\"state_cycle\"] > span","value":"4,7"},{"id":"dae26f06-0c0f-4c52-9b1a-0bd3579cdeb4","command":"clickAt","target":"css=button[name=\"state_cycle\"] > span","value":"15,11"},{"id":"df787a1a-7d63-4910-95e2-97dd6f62c9a4","command":"clickAt","target":"css=p.ng-binding","value":"705,14"},{"id":"91addc7c-a7f9-499f-805c-456ad9255c83","command":"clickAt","target":"name=state_cycle","value":"21,19"},{"id":"646bdf71-a936-4420-ab68-7a6db90a025e","command":"clickAt","target":"css=button[name=\"state_cycle\"] > span","value":"11,10"},{"id":"26f6f82e-c2b6-43cb-90d5-ba897f320bfe","command":"clickAt","target":"css=p.ng-binding","value":"741,5"},{"id":"11d91c58-00f6-419f-bf81-6800ecfd04b4","command":"clickAt","target":"css=div.col-md-12 > a","value":"283,11"},{"id":"5f91b803-55b4-4394-98d5-bbd4aed8fde9","command":"type","target":"id=username","value":"admin"},{"id":"6167394a-6f67-4edc-932e-47378e1d6628","command":"type","target":"id=password","value":"adminpw"},{"id":"f1e604a5-ee8b-4b51-87f6-c23e2cdeec99","command":"submit","target":"id=login_form","value":""}]}],"suites":[],"urls":["https://www.google.ch","http://127.0.0.1:8000"]} \ No newline at end of file diff --git a/tox.ini b/tox.ini index e490c35..23c96d5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py{27}-django{18} [testenv] +whitelist_externals = docker basepython = py27: python2.7 deps = @@ -14,9 +15,20 @@ setenv = DIGITALOCEAN_ZONE = a.a.com DROPBOX_REDIRECT_URL = aa CI = true commands = - pip install selenium==3.11.0 pip install -r requirements-tox.txt + + - docker rm --force rabbitmq || true + - docker rm --force postgresql || true + + docker login -u {env:DOCKER_USER} -p {env:DOCKER_PASS} + + docker run -d -e RABBITMQ_PASS=rabbitmq -p 5672:5672 -p 15672:15672 --name rabbitmq tutum/rabbitmq + docker run -d -p 55432:5432 -e SUPERUSER=true -e DB_NAME=tumbo_fictitious -e DB_USER=tumbo -e PASSWORD=tumbodbpw --name postgresql philipsahli/postgresql-test + coverage run --append --omit='*migrations*' --source=tumbo tumbo/manage.py test core --settings=tumbo.dev coverage run --append --omit='*migrations*' --source=tumbo tumbo/manage.py test aaa --settings=tumbo.dev - coverage run --append --omit='*migrations*' --source=tumbo tumbo/manage.py test ui --settings=tumbo.dev + coverage run --append --omit='*migrations*' --source=tumbo tumbo/manage.py test ui --settings=tumbo.dev -v 3 + + docker rm --force rabbitmq + docker rm --force postgresql diff --git a/tumbo/aaa/cas/authentication.py b/tumbo/aaa/cas/authentication.py index 4dc7f33..72cbdab 100644 --- a/tumbo/aaa/cas/authentication.py +++ b/tumbo/aaa/cas/authentication.py @@ -30,6 +30,8 @@ def cas_login(function): def wrapper(request, *args, **kwargs): user = request.user + logger.info("step:cas-1:user %s arrived for URL %s" % (user, request.get_full_path())) + # if logged in if request.user.is_authenticated(): logger.info("user.is_authenticated with user %s" % @@ -46,7 +48,8 @@ def wrapper(request, *args, **kwargs): service = reverse( 'userland-static', args=[kwargs['username'], kwargs['base'], "index.html"]) - proto = request.META.get('HTTP_X_FORWARDED_PROTO', 'https') + # TODO: make it possible to use https + proto = request.META.get('HTTP_X_FORWARDED_PROTO', 'http') host = request.META.get('HTTP_X_FORWARDED_HOST', request.META.get('HTTP_HOST', None)) if base.frontend_host: @@ -58,14 +61,20 @@ def wrapper(request, *args, **kwargs): ticket = request.GET.get("ticket", None) if ticket: + # if the service is called with a ticket, verify the ticket and redirect to the service + logger.info("step:cas-6:ticket received in CAS") + cas_ticketverify = reverse('cas-ticketverify') cas_ticketverify += "?ticket=%s&service=%s" % ( ticket, service_full) host = urlparse(request.build_absolute_uri()).netloc - response = requests.get("https://%s%s" % (host, cas_ticketverify)) + # TODO: normally with https + logger.info("step:cas-7:verify ticket -> request") + response = requests.get("http://%s%s" % (host, cas_ticketverify)) logger.info("Response from verify: " + str(response.status_code)) logger.info("Response from verify: " + response.text) + logger.info("step:cas-7:verify ticket -> request -> response(%s)" % response.status_code) # read jwt for identity username, decoded_dict = read_jwt( @@ -73,19 +82,27 @@ def wrapper(request, *args, **kwargs): logger.info("Identity from Token: %s" % username) logger.info("Identity from Token: %s" % str(decoded_dict)) + logger.info("step:cas-7.2:verify ticket -> request -> response(%s)" % response.status_code) + user = User.objects.get(username=username) user.backend = 'django.contrib.auth.backends.ModelBackend' auth_login(request, user) + logger.info("step:cas-7.3:verify ticket -> request -> response(%s) -> remember cookie_path" % response.status_code) + request.session['cookie_path'] = "/userland/%s/%s" % ( base.user.username, base.name) + logger.info("Setting cookie_path to: %s" % request.session['cookie_path']) request.session.cycle_key() + from django.middleware.csrf import rotate_token + rotate_token(request) + # user is logged in successfully, redirect to service URL return HttpResponseRedirect(service) # User need to authenticate first on cas url = reverse('cas-login') + "?service=%s" % service_full - logger.info("Redirecting to CAS login %s" % url) + logger.info("step:cas-2: redirecting to CAS login %s" % url) return HttpResponseRedirect(url) return wrapper diff --git a/tumbo/aaa/cas/middleware.py b/tumbo/aaa/cas/middleware.py index 780df58..51380d2 100644 --- a/tumbo/aaa/cas/middleware.py +++ b/tumbo/aaa/cas/middleware.py @@ -5,6 +5,7 @@ from django.utils.cache import patch_vary_headers from django.utils.http import cookie_date from django.contrib.sessions.middleware import SessionMiddleware +from django.middleware.csrf import CsrfViewMiddleware import logging @@ -74,14 +75,74 @@ def process_response(self, request, response): # page will result in a redirect to the login page # if required. return redirect(request.path) + cookie_path = self._get_cookie_path(request) + logger.info( + "step:cas-7.4:set cookie-path to %s" % cookie_path) + response.set_cookie( settings.SESSION_COOKIE_NAME, request.session.session_key, max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, - path=self._get_cookie_path(request), + path=cookie_path, + # path="/", secure=settings.SESSION_COOKIE_SECURE or None, httponly=settings.SESSION_COOKIE_HTTPONLY or None, ) logger.info("Create session %s for path: %s" % ( - request.session.session_key, self._get_cookie_path(request))) + request.session.session_key, cookie_path)) + + if response.has_header('set-cookie'): + logger.info( + "step:cas-7.4: Set-Cookie response Header set to: %s" % response['Set-Cookie']) return response + + +CSRF_SESSION_KEY = '_csrftoken' + + +class CasCsrfViewMiddleware(CsrfViewMiddleware): + """ + Require a present and correct csrfmiddlewaretoken for POST requests that + have a CSRF cookie, and set an outgoing CSRF cookie. + This middleware should be used in conjunction with the {% csrf_token %} + template tag. + """ + # The _accept and _reject methods currently only exist for the sake of the + # requires_csrf_token decorator. + + def _get_cookie_path(self, request): + cookie_path = None + if "cas/" in request.path_info: + cookie_path = "/cas" + + # authorization step for service saved the cookie_path in session + try: + if "cookie_path" in request.session: + cookie_path = request.session.pop('cookie_path') + logger.info("Got cookie_path %s to use for CSRF Cookie" % cookie_path) + except AttributeError, e: + logger.error("cookie_path missing") + raise e + + logger.info("CasCsrfViewMiddleware: _get_cookie_path for URI %s returned SESSION_COOKIE_PATH %s" % ( + request.path_info, cookie_path)) + + return cookie_path or settings.SESSION_COOKIE_PATH + + def _set_token(self, request, response): + if settings.CSRF_USE_SESSIONS: + request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE'] + else: + logger.info("Set token for path: %s" % "/cas") + response.set_cookie( + settings.CSRF_COOKIE_NAME, + request.META['CSRF_COOKIE'], + max_age=settings.CSRF_COOKIE_AGE, + domain=settings.CSRF_COOKIE_DOMAIN, + # path=settings.self._get_cookie_path(request), + path="/cas", + secure=settings.CSRF_COOKIE_SECURE, + httponly=settings.CSRF_COOKIE_HTTPONLY, + ) + # Set the Vary header since content varies with the CSRF cookie. + patch_vary_headers(response, ('Cookie',)) diff --git a/tumbo/aaa/cas/pipeline.py b/tumbo/aaa/cas/pipeline.py index 7315912..a88afae 100644 --- a/tumbo/aaa/cas/pipeline.py +++ b/tumbo/aaa/cas/pipeline.py @@ -1,6 +1,7 @@ import logging from aaa.cas.models import Ticket +from core.models import AuthProfile logger = logging.getLogger(__name__) @@ -14,6 +15,8 @@ def create_ticket(backend, user, response, *args, **kwargs): logger.info("create_ticket pipeline for user %s started" % user.username) # workaround for creating internalid + auth, _ = AuthProfile.objects.get_or_create(user=user) + user.authprofile = auth user.authprofile.internalid = user.authprofile.internalid user.authprofile.save() diff --git a/tumbo/aaa/cas/urls.py b/tumbo/aaa/cas/urls.py index 897028a..dbcd0ae 100644 --- a/tumbo/aaa/cas/urls.py +++ b/tumbo/aaa/cas/urls.py @@ -7,5 +7,5 @@ url(r'verify/$', 'aaa.cas.views.verify', name='cas-ticketverify'), url(r'^', include( - 'social.apps.django_app.urls', namespace='social')) + 'social_django.urls', namespace='social')) ) diff --git a/tumbo/aaa/cas/views.py b/tumbo/aaa/cas/views.py index 9c3acaf..1bd18c4 100644 --- a/tumbo/aaa/cas/views.py +++ b/tumbo/aaa/cas/views.py @@ -4,7 +4,7 @@ from urlparse import urlparse -from social.backends.utils import load_backends +from social_core.backends.utils import load_backends from django.contrib.auth import authenticate, get_user_model from django.contrib.auth import login as auth_login @@ -28,8 +28,8 @@ def loginpage(request): If a user wants to login, he opens the url named `cas-login`, which renders the cas_loginpage.html. """ - logging.info("Start login for accessing a base") if request.method == "GET": + logger.info("step:cas-3:start CAS login, GET -> return login form") service = request.GET['service'] m = re.match( r".*/userland/(?P.*?)/(?P.*?)/", service) @@ -45,16 +45,18 @@ def loginpage(request): base = Base.objects.get(frontend_host=host) username = base.user.username basename = base.name - request.session['next'] = "https://%s" % host + # TODO: make https configurable + request.session['next'] = "http://%s" % host if request.user.is_authenticated: user = request.user - logger.info("Next: " + request.session['next']) + #logger.info("Next: " + request.session['next']) return render(request, 'aaa/cas_loginpage.html', {'user': user, 'userland': username, 'basename': basename, 'available_backends': load_backends(settings.AUTHENTICATION_BACKENDS)}) elif request.method == "POST": + logger.info("step:cas-3:start CAS login, POST -> authenticate") username = request.POST['username'] password = request.POST['password'] service = request.POST['service'] @@ -62,10 +64,13 @@ def loginpage(request): password=password, redirect_uri="/cas/login/") if user is not None: if user.is_active: + logger.info("step:cas-4:start CAS login, POST -> authenticate -> is_active") auth_login(request, user) ticket = Ticket.objects.create_ticket(user=user) + logger.info("step:cas-5:start CAS login, POST -> authenticate -> is_active ->return 301 with ticket") return redirect(service + "?ticket=%s" % ticket.ticket) else: + logger.info("step:cas-4:start CAS login, POST -> authenticate -> is_inactive") return redirect('/disabled') else: # Return an 'invalid login' error message. @@ -86,12 +91,18 @@ def verify(request): token = create_jwt(ticket.user, secret) ticket.user.backend = 'django.contrib.auth.backends.ModelBackend' + logger.info("step:cas-7.1:ticket verified -> response with token") return HttpResponse(token) def logout(request): """Logs out user""" auth_logout(request) + if request.user.is_authenticated(): + username = request.user.username + print "Logging out %s" % username + else: + print "Not logged in" next = request.GET.get("next", "/") if "next" in request.session: del request.session['next'] diff --git a/tumbo/aaa/pipeline.py b/tumbo/aaa/pipeline.py index 2f25402..dd6735d 100644 --- a/tumbo/aaa/pipeline.py +++ b/tumbo/aaa/pipeline.py @@ -1,21 +1,40 @@ -from django.http import HttpResponse -from django.conf import settings +# from django.conf import settings +# from django.http import HttpResponse +# from django.http.shortcuts import redirect +import logging + +logger = logging.getLogger(__name__) def _is_member(user, group): + print user, group print user.groups.filter(name=group).exists() return user.groups.filter(name=group).exists() -def restrict_user(backend, user, response, *args): - if user.is_superuser: +def restrict_user(backend, username): + """Pipeline function to restrict Base usage for users + + Arguments: + backend {Backend} -- Used Authentication BAckend + username {String} -- Username + response {Response} -- Response + + Returns: + [type] -- [description] + """ + + logger.info("restrict_user for %s with backend %s" % (backend, username)) + + if username.is_superuser: return - group = getattr(settings, "SOCIAL_AUTH_USER_GROUP", None) - if group: - if not _is_member(user, group): - return HttpResponse("Login forbidden.") + #group = getattr(settings, "SOCIAL_AUTH_USER_GROUP", None) + # if group: + # if not _is_member(user, group): + # return HttpResponse("Login forbidden.") -def redirect_with_ticket_to_service(backend, user, response, *args, **kwargs): - response = redirect(service + "?ticket=aaa") +# def redirect_with_ticket_to_service(backend, user, response, *args, **kwargs): +# print backend, user, response, args, str(kwargs) +# response = redirect(service + "?ticket=aaa") diff --git a/tumbo/aaa/templates/aaa/cas_loginpage.html b/tumbo/aaa/templates/aaa/cas_loginpage.html index 2b86ac6..a462a8a 100644 --- a/tumbo/aaa/templates/aaa/cas_loginpage.html +++ b/tumbo/aaa/templates/aaa/cas_loginpage.html @@ -43,16 +43,16 @@

Login for Base {{userland}}/{{basename}}

{% endfor %} -

or

+

orOR

-
-
diff --git a/tumbo/core/templatetags/backend_utils.py b/tumbo/core/templatetags/backend_utils.py index 99abf72..4fbf968 100644 --- a/tumbo/core/templatetags/backend_utils.py +++ b/tumbo/core/templatetags/backend_utils.py @@ -2,7 +2,7 @@ from django import template -from social.backends.oauth import OAuthAuth +from social_core.backends.oauth import OAuthAuth register = template.Library() diff --git a/tumbo/core/templatetags/fastapp_tags.py b/tumbo/core/templatetags/fastapp_tags.py index d817455..fb41bd4 100644 --- a/tumbo/core/templatetags/fastapp_tags.py +++ b/tumbo/core/templatetags/fastapp_tags.py @@ -24,7 +24,6 @@ def iflist(value): @register.filter def replacer(value, arg): - args = arg.split(",") return value.replace(arg[0], arg[1]) @register.filter diff --git a/tumbo/core/tests.py b/tumbo/core/tests.py index 4f28c16..017c62e 100644 --- a/tumbo/core/tests.py +++ b/tumbo/core/tests.py @@ -78,6 +78,10 @@ def setUp(self, distribute_mock): setting.value = "setting2_value" setting.save() + #self.client1 = Client(enforce_csrf_checks=True) # logged in with objects + #self.client2 = Client(enforce_csrf_checks=True) # logged in without objects + #self.client3 = Client(enforce_csrf_checks=True) # not logged in + self.client1 = Client() # logged in with objects self.client2 = Client() # logged in without objects self.client3 = Client() # not logged in @@ -326,7 +330,6 @@ def test_execute_async(self, call_rpc_client_mock): class SettingTestCase(BaseTestCase): def test_create_and_change_setting_for_base(self, distribute_mock): - distribute_mock.return_value self.client1.login(username='user1', password='pass') json_data = {u'key': u'key', 'value': 'value'} response = self.client1.post( diff --git a/tumbo/core/views/static.py b/tumbo/core/views/static.py index f0e32de..76d66df 100644 --- a/tumbo/core/views/static.py +++ b/tumbo/core/views/static.py @@ -152,9 +152,10 @@ def _setup_context(self, request, base_obj): data['datastore'] = PsqlDataStore(schema=base_obj.name, keep=False, **plugin_settings) logger.debug("Setup datastore for context done") logger.debug("Datastore-Size: %s" % data['datastore'].count()) - data['is_authenticated'] = request.user.is_authenticated() except KeyError: logger.error("Setup datastore for context failed") + data['is_authenticated'] = request.user.is_authenticated() + data['username'] = request.user.get_username() updated = request.GET.copy() query_params = {} for k, v in updated.iteritems(): diff --git a/tumbo/tumbo/dev.py b/tumbo/tumbo/dev.py index 12d6e97..d17993c 100644 --- a/tumbo/tumbo/dev.py +++ b/tumbo/tumbo/dev.py @@ -74,14 +74,14 @@ 'token': os.environ.get('DIGITALOCEAN_CONFIG', None), 'zone': os.environ.get('DIGITALOCEAN_ZONE', None) }, - 'core.plugins.datastore': { - 'ENGINE': "django.db.backends.postgresql_psycopg2", - 'HOST': "127.0.0.1", - 'PORT': "15432", - 'NAME': "store", - 'USER': "store", - 'PASSWORD': "store123" - } + #'core.plugins.datastore': { + # 'ENGINE': "django.db.backends.postgresql_psycopg2", + # 'HOST': "127.0.0.1", + # 'PORT': "15432", + # 'NAME': "store", + # 'USER': "store", + # 'PASSWORD': "store123" + #} } TUMBO_SCHEDULE_JOBSTORE = "sqlite:////tmp/jobstore.db" diff --git a/tumbo/tumbo/dev_kubernetes.py b/tumbo/tumbo/dev_kubernetes.py index 8cc11b3..92f82c5 100644 --- a/tumbo/tumbo/dev_kubernetes.py +++ b/tumbo/tumbo/dev_kubernetes.py @@ -87,7 +87,8 @@ TUMBO_SCHEDULE_JOBSTORE = "sqlite:////tmp/jobstore.db" -REDIS_METRICS['PASSWORD'] = os.environ.get('CACHE_ENV_REDIS_PASS', None) +if os.environ.get('CACHE_ENV_REDIS_PASS', None): + REDIS_METRICS['PASSWORD'] = os.environ.get('CACHE_ENV_REDIS_PASS') #TEMPLATE_LOADERS += ( # 'core.loader.DevLocalRepositoryPathLoader', diff --git a/tumbo/tumbo/settings.py b/tumbo/tumbo/settings.py index 44f6764..4056e0a 100644 --- a/tumbo/tumbo/settings.py +++ b/tumbo/tumbo/settings.py @@ -51,13 +51,13 @@ MIDDLEWARE_CLASSES = ( 'aaa.cas.middleware.CasSessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', + 'aaa.cas.middleware.CasCsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware' - #'core.middleware.PrettifyMiddleware' + # 'core.middleware.PrettifyMiddleware' ) ROOT_URLCONF = 'tumbo.urls' @@ -71,27 +71,36 @@ if os.environ.get('CI', None): DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3') + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'TEST': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'test', + 'USER': 'tumbo', + 'PASSWORD': 'tumbodbpw', + 'HOST': 'localhost', + 'PORT': '55432' + } } } else: DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': "tumbo", - 'HOST': "localhost", - 'PORT': 5432, - 'USER': "store", - 'PASSWORD': "tumbodev123" + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': "tumbo", + 'HOST': "localhost", + 'PORT': 5432, + 'USER': "store", + 'PASSWORD': "tumbodev123" } } # If tumbo is run from an egg, use db in $HOME/.tumbo print BASE_DIR if "site-packages" in BASE_DIR: - DATABASES['default']['NAME'] = os.path.join(os.path.expanduser('~'), ".tumbo", "db.sqlite3") + DATABASES['default']['NAME'] = os.path.join( + os.path.expanduser('~'), ".tumbo", "db.sqlite3") # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ @@ -127,14 +136,20 @@ 'format': '%(levelname)s %(asctime)s %(name)s %(module)s %(lineno)s %(process)d %(threadName)s %(message)s' }, 'standard': { - 'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", - 'datefmt' : "%d/%b/%Y %H:%M:%S" + 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + 'datefmt': "%d/%b/%Y %H:%M:%S" }, 'simple': { - 'format': '%(levelname)s %(message)s' + 'format': '[%(name)s:%(lineno)s] %(levelname)s %(message)s' }, }, 'handlers': { + # 'cas_logfile': { + # 'level': 'DEBUG', + # 'class': 'logging.FileHandler', + # 'filename': 'cas.log', + # 'formatter': 'verbose' + # }, 'null': { 'level': 'DEBUG', 'class': 'logging.NullHandler', @@ -171,7 +186,7 @@ 'propagate': False, }, 'core.executors.remote': { - #'handlers': ['console'], + # 'handlers': ['console'], 'handlers': [], 'level': 'INFO', 'propagate': False, @@ -250,6 +265,11 @@ 'handlers': ['console'], 'level': 'INFO', 'propagate': True + }, + 'aaa.cas': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': True } } } @@ -301,15 +321,15 @@ # redis-metrics REDIS_METRICS = { - 'MIN_GRANULARITY': 'minutes', - 'MAX_GRANULARITY': 'monthly', - 'MONDAY_FIRST_DAY_OF_WEEK': True + 'MIN_GRANULARITY': 'minutes', + 'MAX_GRANULARITY': 'monthly', + 'MONDAY_FIRST_DAY_OF_WEEK': True } TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - 'core.loader.FastappLoader', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'core.loader.FastappLoader', ) SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') @@ -317,42 +337,44 @@ PROPAGATE_VARIABLES = os.environ.get("PROPAGATE_VARIABLES", "").split("|") # social auth -if "true" in os.environ.get("TUMBO_SOCIAL_AUTH", "").lower(): +if "true" in os.environ.get("TUMBO_SOCIAL_AUTH", "true").lower(): INSTALLED_APPS += ( - 'social.apps.django_app.default', + 'social_django', ) AUTHENTICATION_BACKENDS = ( - 'social.backends.github.GithubOAuth2', - 'social.backends.username.UsernameAuth', + 'social_core.backends.github.GithubOAuth2', + 'social_core.backends.username.UsernameAuth', 'django.contrib.auth.backends.ModelBackend', ) TEMPLATE_CONTEXT_PROCESSORS += ( - 'social.apps.django_app.context_processors.backends', - 'social.apps.django_app.context_processors.login_redirect', + 'social_django.context_processors.backends', + 'social_django.context_processors.login_redirect', ) LOGIN_REDIRECT_URL = '/core/profile/' SOCIAL_AUTH_PIPELINE = ( - 'social.pipeline.social_auth.social_details', - 'social.pipeline.social_auth.social_uid', - 'social.pipeline.social_auth.auth_allowed', - 'social.pipeline.social_auth.social_user', - 'social.pipeline.user.get_username', - 'social.pipeline.social_auth.associate_by_email', - 'social.pipeline.user.create_user', - 'aaa.pipeline.restrict_user', - 'social.pipeline.social_auth.associate_user', - 'social.pipeline.social_auth.load_extra_data', - 'social.pipeline.user.user_details', + 'social_core.pipeline.social_auth.social_details', + 'social_core.pipeline.social_auth.social_uid', + 'social_core.pipeline.social_auth.auth_allowed', + 'social_core.pipeline.social_auth.social_user', + 'social_core.pipeline.user.get_username', + 'social_core.pipeline.social_auth.associate_by_email', + 'social_core.pipeline.user.create_user', + # TODO: fix and add again, document this. + # 'aaa.pipeline.restrict_user', + 'social_core.pipeline.social_auth.associate_user', + 'social_core.pipeline.social_auth.load_extra_data', + 'social_core.pipeline.user.user_details', 'aaa.cas.pipeline.create_ticket', ) SOCIAL_AUTH_GITHUB_KEY = os.environ.get('SOCIAL_AUTH_GITHUB_KEY', None) - SOCIAL_AUTH_GITHUB_SECRET = os.environ.get('SOCIAL_AUTH_GITHUB_SECRET', None) + SOCIAL_AUTH_GITHUB_SECRET = os.environ.get( + 'SOCIAL_AUTH_GITHUB_SECRET', None) SOCIAL_AUTH_SANITIZE_REDIRECTS = False @@ -361,3 +383,5 @@ CSRF_COOKIE_PATH = "/core/" SESSION_EXPIRE_AT_BROWSER_CLOSE = True + +SOCIAL_AUTH_USERNAME_FORM_HTML = 'login_form.html' diff --git a/tumbo/ui/tests.py b/tumbo/ui/tests.py index 38d0a08..ba48a1e 100644 --- a/tumbo/ui/tests.py +++ b/tumbo/ui/tests.py @@ -1,24 +1,86 @@ +import threading +import time + +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.core.management import call_command from selenium import webdriver +from selenium.common.exceptions import ElementNotVisibleException +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.keys import Keys +from core import models + +# from unittest import skip + + + User = get_user_model() +def heartbeat_process(connections_override): + + time.sleep(0.2) + call_command("heartbeat", mode="all", verbosity=3, + connections_override=connections_override) + + class AccountTestCase(StaticLiveServerTestCase): + @classmethod + def _start_heartbeat(cls, connections_override): + cls.t = threading.Thread( + target=heartbeat_process, args=(connections_override,)) + cls.t.setDaemon(True) + cls.t.start() + + # @classmethod + # def _stop_heartbeat(cls): + # cls.t.kill() + + @classmethod + def setUpClass(cls): + super(AccountTestCase, cls).setUpClass() + + settings.TUMBO_HEARTBEAT_LISTENER_THREADCOUNT = 1 + + settings.RABBITMQ_ADMIN_USER = "admin" + settings.RABBITMQ_ADMIN_PASSWORD = "rabbitmq" + + connections_override = cls.server_thread.connections_override + try: + cls._start_heartbeat(connections_override) + except Exception: + pass + + # @classmethod + # def tearDownClass(cls): + # cls._stop_heartbeat() + def setUp(self): options = webdriver.ChromeOptions() - options.add_argument('--headless') - options.add_argument('--no-sandbox') - self.selenium = webdriver.Chrome(chrome_options=options) + + + dc = DesiredCapabilities.CHROME + dc['loggingPrefs'] = {'browser': 'ALL'} + + + # options.add_argument('--headless') + # options.add_argument('--no-sandbox') + self.selenium = webdriver.Chrome(chrome_options=options, desired_capabilities=dc) super(AccountTestCase, self).setUp() self.admin_pw = 'mypassword' - my_admin = User.objects.create_superuser( + User.objects.create_superuser( 'admin', 'myemail@test.com', self.admin_pw) + # connections_override = self.__class__.server_thread.connections_override + # try: + # self.__class__._start_heartbeat(connections_override) + # except: + # pass + def tearDown(self): self.selenium.quit() super(AccountTestCase, self).tearDown() @@ -27,13 +89,13 @@ def test_login(self): selenium = self.selenium selenium.implicitly_wait(20) selenium.set_page_load_timeout(20) - # Opening the link we want to test + selenium.get(self.live_server_url) selenium.get_screenshot_as_file("a.png") + # find the form element username = selenium.find_element_by_id('inputUsername') password = selenium.find_element_by_id('inputPassword') - submit = selenium.find_element_by_name('signin') # Fill the form with data @@ -45,3 +107,109 @@ def test_login(self): # check the returned result assert 'My Bases' in selenium.page_source + + def test_logout(self): + self.test_login() + selenium = self.selenium + + logout = selenium.find_element_by_name('logout') + logout.send_keys(Keys.RETURN) + + assert 'Sign in' in selenium.page_source + + def test_create_base(self): + self.test_login() + selenium = self.selenium + selenium.get(self.live_server_url + "/core/dashboard/") + selenium.get_screenshot_as_file("b.png") + base_name = selenium.find_element_by_id('inputBaseName') + base_name.send_keys("testbase") + submit = selenium.find_element_by_name('create_new_base') + submit.send_keys(Keys.RETURN) + + selenium.implicitly_wait(5) + + selenium.get_screenshot_as_file("c.png") + base_obj = models.Base.objects.get(name="testbase") + assert base_obj + + def test_start_base(self): + self.test_login() + selenium = self.selenium + selenium.get(self.live_server_url + "/core/dashboard/") + selenium.get_screenshot_as_file("bb.png") + base_name = selenium.find_element_by_id('inputBaseName') + base_name.send_keys("testbase") + submit = selenium.find_element_by_name('create_new_base') + submit.send_keys(Keys.RETURN) + + selenium.implicitly_wait(5) + selenium.get_screenshot_as_file("cc.png") + + base_obj = models.Base.objects.get(name="testbase") + assert base_obj + selenium.get_screenshot_as_file("dd.png") + + selenium = self.selenium + link = selenium.find_elements_by_xpath( + "//a[@href='/core/dashboard/testbase/index/']")[0] + link.send_keys(Keys.RETURN) + assert 'Runtime Information' in selenium.page_source + selenium.get_screenshot_as_file("ee.png") + + button = selenium.find_element_by_name('state_cycle') + button.send_keys(Keys.RETURN) + + time.sleep(10) + + base_obj = models.Base.objects.get(name="testbase") + assert base_obj.state == True + selenium.refresh() + time.sleep(3) + selenium.get_screenshot_as_file("ff.png") + + def test_cas_login(self): + # self.test_login() + driver = self.selenium + driver.get(self.live_server_url + "/") + driver.find_element_by_id("inputUsername").clear() + driver.find_element_by_id("inputUsername").send_keys("admin") + driver.find_element_by_id("inputPassword").clear() + driver.find_element_by_id("inputPassword").send_keys(self.admin_pw) + driver.find_element_by_id("login_form").submit() + + driver.implicitly_wait(5) + driver.find_element_by_id("inputBaseName").click() + driver.find_element_by_id("inputBaseName").clear() + driver.find_element_by_id("inputBaseName").send_keys("cccc") + driver.find_element_by_name("create_new_base").click() + driver.find_element_by_link_text("admin/cccc").click() + driver.get_screenshot_as_file("test_cas_login_base_created.png") + try: + driver.find_element_by_xpath("//button[@name='state_cycle']").click() + except ElementNotVisibleException: + driver.find_element_by_xpath("//button[@name='state_cycle']").click() + driver.get_screenshot_as_file("test_cas_login_base_started.png") + time.sleep(10) + base_obj = models.Base.objects.get(name="cccc") + driver.get_screenshot_as_file("test_cas_login_base_started.png") + assert base_obj.state == True + # driver.find_element_by_link_text("/userland/admin/cccc/static/index.html").click() + # driver.find_element_by_id("username").clear() + # driver.find_element_by_id("username").send_keys("admin") + # driver.find_element_by_id("password").clear() + # driver.find_element_by_id("password").send_keys("adminpw") + # driver.find_element_by_id("login_form").submit() + base_obj.stop() + + # assert 'Not found' in driver.page_source + + # @skip("TODO: implement") + # def test_cas_logout(self): + # pass + + def test_background_running(self): + time.sleep(2) + + self.assertEqual(models.Process.objects.count(), 6) + assert models.Process.objects.get(name="HeartbeatThread") diff --git a/tumbo/ui/views.py b/tumbo/ui/views.py index 4217404..f7097cf 100644 --- a/tumbo/ui/views.py +++ b/tumbo/ui/views.py @@ -1,15 +1,12 @@ -from django.shortcuts import redirect -from django.contrib.auth import logout as auth_logout - -from ui.decorators import render_to - from django.conf import settings - -from tumbo import __VERSION__ as TUMBO_VERSION - +from django.contrib.auth import logout as auth_logout +from django.shortcuts import redirect from rest_framework.authtoken.models import Token -from social.backends.utils import load_backends +from social_core.backends.utils import load_backends + from core.models import AuthProfile +from tumbo import __VERSION__ as TUMBO_VERSION +from ui.decorators import render_to def context(**extra): @@ -38,6 +35,7 @@ def home(request): def profile(request): """Home view, displays login mechanism""" auth, created = AuthProfile.objects.get_or_create(user=request.user) + print auth, created if not request.user.is_authenticated(): raise Exception("Not Logged in")