From 35146ac904c328c97de992250a54db4459a14d8c Mon Sep 17 00:00:00 2001 From: Yevhen Kolomeiko Date: Thu, 2 Nov 2023 21:49:15 +0200 Subject: [PATCH] example_configs: Add bootstrap script --- README.md | 4 + .../bootstrap/bootstrap-example-log-1.jpeg | Bin 0 -> 52304 bytes example_configs/bootstrap/bootstrap.md | 254 ++++++++++ example_configs/bootstrap/bootstrap.sh | 465 ++++++++++++++++++ 4 files changed, 723 insertions(+) create mode 100644 example_configs/bootstrap/bootstrap-example-log-1.jpeg create mode 100644 example_configs/bootstrap/bootstrap.md create mode 100755 example_configs/bootstrap/bootstrap.sh diff --git a/README.md b/README.md index 598aff6..86e789c 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,10 @@ front-end. See https://github.com/Evantage-WS/lldap-kubernetes for a LLDAP deployment for Kubernetes +You can bootstrap your lldap instance (users, groups) +using [bootstrap.sh](example_configs/bootstrap/bootstrap.md#kubernetes-job). +It can be run by Argo CD for managing users in git-opt way, or as a one-shot job. + ### From source #### Backend diff --git a/example_configs/bootstrap/bootstrap-example-log-1.jpeg b/example_configs/bootstrap/bootstrap-example-log-1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2771f55fb5da8bf1ec22b540bc3ecfe64501fbb3 GIT binary patch literal 52304 zcmb@t1yo$iwl3UwumpDq?vUV4kl^k#7Tn!6Kmr79f;0q8aCZ;x+7R5`-6dFFC)sD8 z>~qh3|9E5k1$39rs#WV-bAEHK;>XO76##~;q>Lm01_l;D1N{N~ms{0njY$JAmr18v19y zzF_TuGh8LKT0}}RxJhy;cIL4Z5-ApH%ov-+0nq7xCfJYJzrF#0vftkpx-hGS0_c#< z*9Nm%ERT6YDb(xd?u2mIdi3yr_5AA#<>Ys!x>$AxR>N2dRHqgTxu__d`6Q0mnKQ9j zWnb9BiFYaYv1K9)(ggr&*aF}OK49%p{~87(jq*e*;8#|FvrYas6Gj^M8h0Fa{Tj&36Z?pCT4O;g>P2WE%isLoDw z{o2L9xTZ$ze+Gkl^9 zJ-uO8{x#^C2mq|z<@ihq0Q;+oo@(IY)N;X! z8&5-tnVt-Gcvz0Vs`6|&LhVCO-Ylspom#=j*7@0a(wASe!HQCrvqv-j0@Q*X2Pbj# zlhc+;ZlcgF`1QGIvScPNM8w-U^c+{&VukGx+A0nXN@@$~S+Sl0=4F}5)$G2mId5aB zD8t`N|A{Q;F^SFCB0bl;RCg{L^U99CvF-?BKEXX|4`PV*hPlR|e}O*9k!9lq8BXl# z?_qy_4$VA6my$3wo-4M^OAy`tQeTwXeG1rR2Q;LP3D)UFxn0fbjxG~r6)Y2gXxyet} zxK&I{2O^CKQVKR8|eoy9C`LDr~0E?&_ilX66O;LH!rC+9c@?zyb}{HNVt!2T}7geeGG9 zlp*tIr7sr{VLMQcUL!Ice#E9Uf11pcx?J(eCz*V<;2yLblkcHU~)hjPSADzv_ zV&t^QI?a4M0kXq|$^U^W<-%`^aClo2PiKZtZ?6;Jg=q6AZ4;X2r@-c` z^T7IsDunM+LLQC7VfHoTy(3O#d9FjA;pTc8wk}Cc2MwR!?1!T*#i=Dh0$%`|zHOZW zCv835yc69W8zvu8b1grb+6l2UeL*Ff@I>aqqf94^-RDp#?wc`v5);T>&XpixYRPN^ zEEVLQ`n0Gm6=Y(?p4roh#YRok&xI&}E=_}H8$P2A9ND^oO7Mc!cn!TegebA7%Ry}ZFID&3kr%W z^Rq3(>)f6BYTek|pX&7eXV>3lG)Tb7*c%{Yc7mqLAY|)+Utdv)#|TV?t^MEo<00jE z3LD2kK`3{vFW!b^Txf`ZQ#>7fR z>9|XFIhg%|D0RxRDj)Ow!152t02uO%d4nX}ZwOJIl2MbZPnq{Ha}k0KZ4keiT5+Yn z9*82q+V@hywjLOWT)d1S6GEH|{BtP^@mhE@Tm3c z$SB%12L8HuzDDN08Pq{ul07fh!SJ?mtip~V%5`=vY<xP zZ<6Fo;Nv)e#ci>suMx+nwWotjoL0-r0{%-^xSWYJ4K_2C%uHWhlc=31@}HY}3J3 ze=?)h_ILZgGe14A7Z?CoSXiiA2Lp=)1Av1z!okBL0ALWI-X8`gmNGdWB^5QzOEwWh zM+&Y0sL%BT>ZQRT!2SS41H~I(!C0ijjv&hzW3D~1s@K+H%s$xZ)GZr7{chhn`y2tC zQ%7mbHU?I=yKups40c2iA)QNmP5EwS&}q1WqgZ~{qE%5?JA_9(q~CMySo`Quanl0~ z_C93&uIHt8)H6X_(if}pt7rV?ix$bI=>Ca2KJU`e_@o)8*N{=E?(LS#C>k2^G&c8^ zkI};(oXG_s%1C+Kgp^3xSI8}qBClj!;Y=rLP7_zgRMlTaz+pTf7N9)KD*=s&?Y)0Z zlDzP6wV}jx62oA|9zO7>#@KM4So2&3x3{z_nJ?R7mWgX|5be~7-*uPTPs{nK2xZ$@ zYR+!ia`%oYru_q8y5iMr4|`mG_zsAC%V1NZMZx~<_f8lYRX0pM?*r+>zyzVnB|?(Y z^!O}U$F$4)0b*7$)pFK?V(i>OPV3<`$)7iudoq2Vy8(+ki-;S$1`@SPaB5mVa=R5am<5t1}RnT{A~!7!=`EP zB?MSs9#r44=_!`Oi1r}*Rneka7&T(A9hIlM4d(A=*$~vYRSa|&1iH&-Af8k3fh3VH zis(HoR+!J*fU6A5VuGURJg6v}>S<{NHg30XU$IQzQLSX;M#$VlV#(z^Yji=KZNTk- zs-7Ye+2=k5_Iy*cZec0R*So!g7(F|VvoDDC_n&j;IX4arG`lDImI@YJn^QxaM`7`GCgm}(+Ng1|o|omd4Lo`I*hp0S1qcImj>Tl~B(x3DYe z>6q{QpDkVtx~iP|2jY?k#_u5Z7uc*bFQ%lxQ=O;m*&zw>G#{TX;`kbn#}^ZgmD_hu zkf&!xKGS_Dyx`no!)z<+Qe`Q`EOpyEDNQWKivQNV-`4GrB^$%B@q7HMvHuBMi@#x8 zp2qL`4kOsXa3b=UdcrX4>REcAaXA=)I*e68w#n>mf0A>Ya(n~esq3CyPXiyiAl`F| z&`mOdNv?3W@v(Rfd2zigE#|L_6XBO0!v?dxKLD;pp&~6SVh|1dLp8*zw5@Tsyj?@Xe0}Z3bmM(E7eHolBOZ8s)n1?sOgMlN7OC&i+0`mCi)5&L=##pi_?|U{ipUyN>;{D2*$ko)a#d9gEUKk z=M;2}D9eFa^=YVSFo_8-_ep|dC?R7u2ZD)<(>s9T%lh$(aE7hA`hzQUt`gxPEjSwW z7|U;bPz{t)j6>x2sT5yZ2cA0W)kT8x(NE9?RCTCo2YD&!6KifNv!@ldb?fYXq~6+p zh&geJ5XhP(W-JU3(DSb^Lr|s$>x=7g(#zik)r)575A-Se_%R+D?(Vg7mcmvH{68PP zh=BOE#&?@cVJO9kVVGgULepM>y)g|rHGRSG#sDmo_t&p9!8J=B8D-WO^C)djy2 zoMT#zoTeH7!n6P98=={T^0&mxt&hc(AKc&m(%e|FT~q^`l>12QJ-DUgnw&mstNUt5 zYEf>!q@>}PO4|Ga*y6vngF3(I-Qgb(T;02`^G$%y^Zl@`dVz1x(yHS>k-&cOnqe<> zvnbxj=I??GI^UhraL}9PBKSX%+7?JwQ17L9p1Q1N$rp(xevz9!Sxr+G*;*MJ>sKdM z?h&mO?3!2xhuaz_Bcr?ueb{z>YIivxX>$B$8c`R@W<4mZ_6*o3o_&?WJ9mjcSN_-o zm&TvofM3yMn4aGBH?@&y20~I$YpW$|%#dpxi$8jnAgZdQ1 z7#~HBLS@DvBT<}Q0#&@{K1Te_req!C-mA5NYd=Es4-C!-+HTg^zG}9sLMiS&$Mttt z&pt;_E6J)nDJT1mjH1yJPe<96LQ9e4mB^t~h1+R7w|aTTau2(YD&8b~z4&=CLQD6# zt9@R!S-nhRl8a!3H`egW^tE zCJP@<9W$KW(EXO>Y^a40Tdd0MJak++i~zMB!{zD7S%6&A;^Q51K0JfCpkP61Wia)} zaTy>beBFvkNroyNhM!a60?kR<;&x(a{SSbXL7H}>2Sd!>I$P2|w|l&?v@|qAtg&vjRRo8in}@|zpv*pfI?(}H&Gd<|%s2|pGItKC zsfvwy?Nc)^RgH)meT}<28BzDP6irWCOCqgCo1jQv89sHGH@OUm{6xTTQrkFJlQMA6 zDgc{NN8vCRD%Oer<41}dVjh7!!cW%4Y-;U404h2;UX-^kWzJYPihI|ClA~38;ySba zQu_LN6!zuD0{jf*sr5Tw8}w534evcWYUZqaqA%*bC)y|0_4V;3_f*Qj&@-46Igy0D9)q@(9&;X zhB2@*o}0HSH`Ap+Fs0?UfBn9qxXNe~HppJ}=)>&e=PHlB)?IvDE(P5{17!YsB;uMx zr@XjR_h$_HrbHgIMCOy0*Pbx1>S7K4`KWQ0WzxLY=)1Z#J`7GPFK5OpYyaP$x4i%5 zdHb5M{!kkmo6)BEq2gXmU{xJOq3!nW*o$7;=icaf>X8+f8tJi5`+yJl-I&aRq4Dz) zWBv;BQBp0QI7!rL7_paJu7so)c?Q!trta3NzF>r*@JknS*ti1EL1p1xW?jgz8bnN- z0BWMVHw=1C-^tiR=@LUVaK6VmI>+^j=O|mNs0LZ?<1h2`V3*KhR^D9iAR%68gR@KW zaV&RMc@vB-Q!0l!Am|n<>+77Q3wC>cYUa}tSw3w?0n`^_cpayM>2Cw7G}p_HZw>xg zLD}ZBLCv7q%Pe#2oU_g%64B!#mU??!t!Y!4+}^IL_R?%Jy8Vtg_k)CerwPa$SodJw2pL65NcFx^*of?CY-^yKkj_F)Ff z)vnF=yd`ymerk`tx(w!Z=EbR&;49y-Ib^ZmTMS+rQ3DU&+Go)m{m%*zXc?$+1IDI{{-Ha+%&>5jVdY2;mG?RV7nSG+)dJL@~hC1t4 zM@rc@zSOy9Xz^`jt55E?U1SPBJxZ&x8KQK#8ACGpOC9bMr-EUOFIS9f(*Qs&7GJZQ zb_NABqHKC0^7z~jW13RtIduIRKVh8u5(g~TqZ4>7W4t8moM7pkNS^I1E?e?;veANz zmQSeRZYjq?3S{Hsvh28Y-Qew}VEnn`bxuMZadn?AYLNwVG>8C2Hl`n)x=%Jmu7uEn zPc4pP4hl%dp9E`U{%ssqaZR0WC)Hx-JgJ8#+Uf&eHn zyXdcWC*m*_IH*F0Vi)1PtO$CND#3ra~?GsBu@5~$~18knI|UlJnGt)SNXY%Yi8 zNVn8kW8~G>?=}#mK^Dt0Q#s`$tJW#|5mEbsbh$-%O%yP(`N}0&H@U1_prE6|#td7r z$``bJgb zz)S510Alls>)P!%i7JGWC~V&sO$!MztR~G=;R;Wpi*dim$5ftB%@XF@OAiQd?@hK2d{+nBD^(nDBR|06Rw=)o*9noK-m`5dyw8p(zZR4 zgYqj3sLq7t1IX}q&6xF&#zS2N;YA8D^;mHQ8q-j$3q00lymu@%v8CzS*RMQu*A-{ zN$D0#z)fVm!RJ}I(i|wv{BkQhErwBlmbdszG12@|Oj%P<7NP-lKxzDO7yWkl7x(3w zO(s9r_<;APu~#Q9eu7Vb)9g*-7pyMD7rMq*`jyIH@?E}A(H#}ltDs{nV`Gz`P&&ZPA$ zu73>*dHL0KXPgatt28MA{ig+GfLiXNH#PRN5E+n?{o8A(F+Y`gf#qg(UZfOEW6*oG z&)cC&VQx&ir3)(74Slzd8f}}bJKN9vmoC}IUZsjPGpcG}hWddbA4e$J+K`tujQG=fedcxrMi+%v~@Bs$7CI(4A9KekVw;QiM}4Deqxum&ss?dS;GX zB=~bzu_RTUA_<8x+s;5@9_%^YHxIFBuTVB@HAje(Ia$V@y*+_piNeg0z5R#m7sQQD zGbsG7R6&Adth^Q)O$MwWT0s)m6BvaVkbvuIIj()s5)-cM6cZI!*gG(NfiLRQ?t-St zuj&~?z9LQiXMonMuQ)zKqM^wtAvO^&FQ2KoICdg1G%oxA#JMs#UWXl#pb07FZso4@ ziAuqsKjSQaqY>KWzT5deQc{Re^}}fvX6WHKsJnvT!fm&$Y5AM5lq>ypNJcr`%X`Tn zY~{|$r%sDxt0i|C{o^5BONzX^-|7^vBZ7LN`1)C^2ES9p$ zOG6_OF-NBW_FpK0{fUwXF?M#rYOW_@7@k3!QZpU()OWtV zzCLHrqo?v--PF|cy|{7^1yX2VU*GhOrO693z8vD|$Cn$CkLz(fDcT-B@vdX?6( zxg_rs*+;Hc=k=Xr!7G|dc-Bj$RNwB zf5VemOchue+L+1DkI_H|t>e}h#{PiTs;!;nsYfD|!0{LksukMKFkan>T$j(0!08;g z?R-AHYHhaP&h$Ow73bOVrnT8kNtC?&DY*SYsF*IJ@$>3b`H0Gr@|C+=2C9PzA4bg{ z^frj$e|xMjtRK;RmAP{Fp$K1W<#BaY2(@Z28uf2(&KvF;P_5&cU?I2aZ&-ylg;#@YuMD4Ucl;!(KxZZQ|4sVtbV|)`K z`EN~p`~ns)OzNf3EbM(?q|freWs6Fa_ZLE!k3Z_pqGNC@w3LT`rh~chQt9;>l6wwx99iy2C zIl>=$qMf8FEO#=EeTH)}7CT23dv<1zIQ2)Zl0Oqn+-q%BTTbDl-IC!W+;#Ru5pmrL zLHy5Umv@OcZOGDJ@6QYmM_29aJd!>?Oz&dt@%}8uH=6Z^$Cts^%J;>Ps~_4sp#8_7 zPUT1`xFrhYPohlGDhTbn+t?U37Dz?0C}J4g1 zlgVuHVvdbm!dwm)(0QYkDBTV$ETm>HT>YO$U;Wx9rL5GQ(Xx~loEFX&{H<3|Rig}j z;Q|8-4}*mC1PKlm9;$7A9%jP?U}00RAzqn|_ z%@X28&!~d$48aEQJ z{z~>iP^us!&K|$k{?5}lA?wwMbbrDEi^B*>77iGQ6&d{v#%TQBD`I?1fgjFxv`5?I-|GmN#HkX=Gc=7Xo1Z84u9$atD`0orYu9`~ z8RBW0{P7DAmet0grn~pD8(lOem^uQCEypDJ<(B8(iAI7ijyWZU5uvVY#hElh*Ui zSX5=IMY0z7fzBq~K5>j^bh1D0`fNU0h%-14JJ)Hox%H{-?UJrgwe$DJ zv)6|@r+2~%e~WzvCX1h&7SS!T&1*~sf*nXShaEcZh5I(?vk-D1B*9Wxnb*B^lVd31 zUexAuQVTls5isGm6lQtmq;JT0I#uR>0Hk-O2kmq95+VDopGguh<;dd5%OCapFip0e=Pzo?YyZ_rnX(r5NCaKAL3tXlCJEhjvy6-M{|fbvnSX%L`IV+q;AlU zp6q4BeJ7u2Hjzha>9NKJAFIwKo$zw!j8&@+#6x}iJ@ie1hR0RuGKewTJ9Kw@ydN`; ztJE_s@Z1=^Xk`ICf@4TXJjD5~@d65sWLJAtb5ge{yE}XY0 zlTz!I5uX?hp;kB}eC@e2k^_WBfT$?jV|i1m!#CifB;Boex9EJSN7R^x(vk>= zjSkza!#2`F+WIytzA(~YydB{iJnEDnIg=uGkI@ToDvs-`di#wHa@;{wK$9dzRs(5z z5p07C;>7I9UK-#71)YhefKtFM)cTTxbhdA5_PY!;?h_%Gjbx<c{+)gbqbS6J zPn~z{+bZXqvcC z;RT|%;9JjVHF4U77q*L6C*3TP{^>DsEcH6e>3ot5jFmLc@%w$}=R@dYk-I81?iV5( ze6JP^GnJ?Ya))9&^KQ=Z^RkoOh|NLlozJB-{04ukO2qt}WCQ^?YO}XXGxL+6d7snY zigK%%qmz0f7`6}7Mc1#C^;IdoHCiMCwO)QQP=^a?v~@j9;{zGBwMNqfS%G-=9~4v(_dz8`v<`5 zm&vi&jB+9QWo@Ff3R^bIM`#daDUX1ehDbb8lVi@=d73L91@8{SrL7pJa<*xdv{;qy zjJ9(McbSOcV^240ZZKCrfOG;b#gb+-#`i#Z{%;wqIqokalk7UC?oS8UniXGXt$i2J zh3;l%UZM(PBD+ATuE&DIVtAF(E1BvY4o)xW+44&^&THw*r{2yLH?zm#-bs6rVpeEI zn#{d64qYCrJ4N~+NwZ3pPMt@}nQAP-id$7$@uP;VSi-&RUAFGRQ7Qc3!{mFvPw6>@ z)vwVw-jiMl%^&%)Y20dax~|~mqxvid2mb(&s7R1XS~A{Yn_k(osi=M4%GN%ck2?Nt zI9_6J-OQp9EoU+bu~`Q3o1QnRSoCkKP1Kb+*}l{q{&KK*^CVnB3EGp;j1F zbe8~ypuh7BV_Ok=sIWt|JNKo@@BN0}`R_G_Xh~w_M6$%8b)H>rFP9{uN;%wQ2>MCw zeA{Zy)a%rJJ-Wcdbh}t$Dp@G(A%iwcH+k=}v^lYo5I-St9oCW7d#-~QnC7Y~R;Q4B zo%O;a7jaQ|g?-{WXPkssgd3}4i<5Gcx$)kQnmIAzvVIb_ymLS8*JfvpaH9ky9!?WGO)vpY#vQ%2$o9zC}jl{ozF;j1Q!O zO8}#Xd(dg5Z;YQ_GU~6mE402ok7c%Y{qab!dlXDK%Hz9zeIo|t&h);K=FO7g` zMPO|{y(Wnpn-^i-Tqk1olnp;Z1K5@={~mK{Y|(v+91==AZHAyd^O%*Pc^AF9x_#5dy^*{w3m2L zzvjD4qWOy6oL!1NH1fTp$50m5OI3AcgCBsI^*Daqi4i7b=cl7B1AghY&L3owR`fM1 zK!T6{+P!oaR_SIF`0P?~r@Un}(uf5JKLA^uYCV%UEVx2*Y2$t6aM6Lh|7oPVZl9cv z*T-=(voDwlV1~0;e${-xXbe}=d3k8Os6@*p+R|)z?Ncc!3F$jnxUs z$2N2I!e^R;ZY%Ca0br86^?oKiJ%TsG+KocRzHD+I`?O0<*&JwK4$O{p`+$~pyB@z4 z-%n~y(@70X^Vr8|G>Oa6HZM-|@UwJIs++ev{^uHv3f8T|@xs#&6?ZqZVS3wAoxdO> z_YV=f24EJ$o?X;I*ftyClE+Lp0*Rb_s> zbhGJ*FdTmHD$*pLy0KS{#Dt34YtdsNFkEIe_KOCiTOlcD()G+~R|IuzGw*;h8;z8@ zV^4^8Vg~1(TCY3`S(y#D{lhUxen(49u$ZP#K=O{^%B$kd55U0kp3$0nR4<~qNpR90 zja6b$@IR=+=3wqtFZxJYWsD=rGErwx-sI?-a3sAU&(q$qD#5A=sOKP{xfqKBSGVsgE% ze81bk`^`Ej`T1-jpWh5Wc0Zy$zrE&-6?uLFzC8=^M@26ANVDeixaJ2>Qp!x=VW-NV zZW>KkxOB)$BjW-|i|-86rIFWjHyxtX!BYl&oesx6)p62ix3*oM-y%upC-^u{ zI8XJwS0DQJIMfH-y}i_##+1BtVzC$%IT#xQ6LuR^q_5Lr@whyPxfyy>p(LnuH$^-lH(q)9Iww~ z?hPFs=L6r$o(!gD%|^kf(7(U(`iq`|8%0S19Q9`!(+1fZUhR1?tj2n*CvXk@%M=#g zC3FhLoYFgiMv&R!Ck!XiP{vJ+=5Px6fL7u6GtjvN(Z81l!#FDd~+jIZ2@=n&(D&4C-=B>}o=?bh%FyYWvdi|o^?>?hx zyiGtSqM~KdkR?&2+c`~dIzF|(!__KG_m!kXRme5@~#-AeCGYu zos@}#=3lNX)Iz4{eCdy8g2~RPkMM|;QbXU+o?GWpkBN+#njS)ZLnG%V)$1#xxAr!M znuh3Q7QJn4^KrN*BiDOThL2VOE(Y^X^B+Kb`W?+h0%W9y9=$Yu`H)fNtoPdufTlM(V;Q+<13F_nnf-L_pq3O(rQLi4t@v=Uxs{oR9^M4=W35_G zFI;AIX}X^V%3JtzBEwkKvpCT_>Pm5+SwH5bxA*l$dEhN<*#SxEcKy~AT+U0F3r0Yr z)SN1MdoLXUCWH$g?ICw*8yBbCrcsWuKV1T~GKajAm`uaf8)6IasU6r39zb&w|neikrW;heO=Pj z#$FxqF>pEIb{s9|#UJ&~f7wY*3&m^NzN4zt?5Txja2wZcIUzFLpc zi{0E^qzgEhpYFw$Q9l3R>Uq|SVPuZiYH1Qrp&$kP#%!zptoZ5OS_l^!8fLuB4*;g% zV(s<6_&wyo-(f@@q33b9*G#F*xmg`)bic|!0~&u&638FniLETDfnxoL)0TVR zmr}R%1!L{mwwX7u=^2GW%%dB1y~+P%KR5Yl*3Wei&_)TqHW#^<-S~dBLWQN1U+?sJ zOEM}yXt~)U{+4??_hUJw>#!hC-Rcz&V^2WUckTh+8Pd}}(So5dS}T4&;&R9@BMHN* z&zQ6LEb!>Rm>gw#B~|GmkK3SSC#Z`=Q=4^K$9(!$eA z>&jG+u8Flln&lUNCpfbSM{GtHpQX09Z=9~zB||Dd5ZP!3I|WCQ#5^=6ZB^&7kX~i9 zEB6>_Sr@VQnaXF~SgI%)3BxtD^eMP3rIVCieL@iR_ri|S52V{=R78EC2VO&SzYJA2 z4OPhy>^~)xy6k0u#NKDdn2TsRI!tjN*V4Zl_DYkf*(5C+@l_x>)=+u*(bTG-%UJkw z&2Ndo+H)>5I5V$N?W-3a!0ChzS-zPuCOff6O)+s~#pk5FB|f?=W}aO5=s-oF*lJVc z%qi$OnpSK!ocg;TfOF&b^t9u0KK`tHdN3}TjnCEzioQ5phxV!5tviKVQ*lLo5Wo7G%%)nJ_f?~3s$eB?V`|qqvmc`!*9>h& zc^A$%8EUl1jV1@%>Dpy9YYC=qy4)y}V2@-ew}UD+YQueXIhLDkXNM3F!zPI5kdY6t zvZF*zSpRdmnhXYfeq1&raLi>4Bpfx5?I_{;(EubqE!AS)_mBZa6}f_FCQK+*j36#?qOe z)Fqp!$$XhSh*!|r6OuL-ZZ+@!>2ilMT3frqms!S3C10&6Id*$l-Lj)(pag>+isms| zr6;1YEf+%XXgufrZq}l{imUgWpao|=5nG6G;SC!er&{VH%VgAl3hpBwUwz;rBZ?{Aq zssdp6eDmD90$m8__O?-foTk~+YuEa#Y)?CNlb79FcuylD>qKlpV+mNYjMfw`sL^um z-}cNLiRxD-W7rSodi#m>puJJ{x z3BKKd-gS!>@`8ebZ&=NlDr7eEVM3iGg@@$1IeE^|81MEB3`i<3$MCko_Pg_PvFGYP zf+R~XPY0FB^3?=5`TbNTj*qvb3nMmpDhrpnmnv+f6r`x6pWB}0Ds`>yF+eZ)^(yp& zS>7T=Keg^Jqyd{n)9&M*Ic)8tu7kzvwY$W|*pK+vA>q6QIa4|{Ly?0C^Ycyzog;D@ zW16(cH0y&lCsduu={kjRiZ#z-1!K0a(t2pW01asM+U4>WKfJv&jwO<<$@dCFDoUhj zMH7->(UOr)rA~~dDQi1jeJ@wifo?nN@4{7qpdexXZs3pr78;20yjipMsIY|oZ}2Z5 z0fJdL27AZqHbF{Wn_izaRgxeh(15YXSff}BgZ7}Cl%BDdS)y1vjE8Rn=pI0@0ggJC z9Q9pM7}5J)4km}>j>L|)8QVPaa_&Qq%gAY%jLlrS=v=Qua(h(j?g(lK?${8pYR>@! zW2|CGq36bVStDbwq@!G_fzswlkrOayu@__+_zTfuy^WXNn zvX>&6K}$tOFE84o3SbpqJwm$;T{DDB){b**2k}C4TKJwJAyxf5So_jpeK~wW;z`N_&t<&$=6EP|UoQNa|geH6QcGow? z2}EgT*5)*>0epc=3+~AEY2o0iGs@so$0NR5;PH2k06bNMj_j+L7tlC8CIPUFP^==Wo%Put{-l}C} zFr0Qz(1;TZ#}B?*^Erth-^>13#ZNGSFalEV`Q*zN) z7RE~MG?tHM-Z4J_Pi9<{R3#*{zY#cVn?)Dd)h(}vYg@a43$(#wfGs2+r+JcmYOE42 z6LSO+=4|4qGLU=%*pxELIZdI;-e1rZRNG>x^PN$*P*`A1b{qR5jDmhNm{8oT=J>6p zHK9u(Y1>g3E>3b4rF+l-&SxjyqYt8!Aj2s*8r2`reZ~)ag)IQKUwaEUZ2D z;Xo7WXVRpn{i;311~Ja%0&iS8mw%CQ!cx_ouUYG@T6RBUzOj6f*bpK#rB z^JUlE5dI7^6Qu-5RudG1LUILv z!!36Kb5k#-5|$wCY@r2%!C zvq~OZ5Xoq;b|e0}-_4C-^o5FA&U@)ui4z1NadmHCR=-0umykSmQ$G{??0TO>$(qlp z3jc&~H40%So12g3;hU58ikchftKolh*P$9NE?R_`@E$C|jrr(QkNbk9WUtJ30@haj zi6F-9k#nZBvaW^s)9-jU#zQFS>t|q3z4<@hA@VN!G9poKEjV#Lot@(z&~5!=v3#NZ zIi?oPWo>VWC>0C}Yi)ExTiKPENP#WZY&*A7*!)*UdP9mBia=vQAaA7L3u7rn5(2O2 zSN}IQ4GoKdp=Zt8u1&;+7^Ukrv2ZB)t{7GTS>z<)M+4LV|TIv6pOwSAmyh*=F=36SvG$QXCR}1a( z+wZz-dO2}gBY`TRAz{)Vbn#BXsX(?ffF zAaG;-i7>(6!N+byWrf^?t4BTOdf06V_vbxqS|ZEGm>;vQd)9F##Dh6Sl}^*3Z(s%a z8fahheC%0Gx@Kk-AuIk*K5SgD3nvo&=E=aIF-&uB2%kVUu&pSW3yr(HR^##wZuFZ^ z{Uiev?C9;w?2-QI_gg76KL8Jl(Yq}SIU!yv9^jx7cv!h>h6SIBK{s{jZ$nA0?Yf+X z>9tF?&T1oml6ei(PWxomn0ZLR%{QNv7EgNVap(%D%>^FB9O_SdnY7BrU~mdlzDCT3 zcB8*;Ev+FL+K3v4#HXqw6Z7sU54NVAKdmcEiH}7IOH`r|iS%x^I|OMZY{*j*moI(q z;X~Menvo#f$TPi9{ATt4u=dtbdG$)XFcgZr6?cc??hX&G#a&w53lu5t?k>e$i@R&l zQrw;5MO)~%p?&q7bMO7*ThCss&7LGXnIw~$-^@-zax2hoxCmg-CBioBzWa@7>p=+TzX$G*f>y#K(EeIjZS zx2b3Dn&%3Wn9(FvSaKQWg+6)@P|C2f#o}5oUsILMy;BBeEAGpUyTZYh8)?&hRcrD# ztyHJ>_YkY0!OJ~T6>2G5_%^mzuQ3Dut1tpknCE%G2BlTVysoHWpj2Tjh0VkxN?A@Z@|ucyx#hG_OQ!U-UiJJxO__14 zrdnGYtX+h>QY1(Nr_hks4TzF2t^PD8x|kHmA=0^N^Xxu-+0huO7GHXKh5a|X zkLeHY2F8z(B-Ar#EUo#`%~i;lUAQ>D_EitKr(PI9L#NA-U)BD2*bVj8AdQ+T5YwzE z(Nyo5B<0fZhmz({p?AyJ%K!phRZmrt=SF=gXm+bmKgYsbJ17Srlw7)QTWO5{;O0M# zGvw%yse-|bgc%NOlTbI(S5LNBhB<>aH+SUGY5>oat~SqQQ|!~{Rc@--Ci;<>I z*~L!QJ@j(MPfp$HZ_fHl^(?BQIwoMD1;!CN#0@!PUC0s zp$^5S@>8leXxQFvXuVTw=&OR-d)OFbSFI|^%~VDY77u? zK8HteaHpW^M5%y3Y)m5*Z=8KV zK8Th!*rQu3n!&Hp#y&0oOAkk;3WD|4(8tPwbt|>$;S1OTQ!;Xciq)~rh!DauhEfg% zZoD+31e&cP7NJxxWeTukg*^A$dLw?x-~D0Jd-t&N-!!07cbzpY2DvZDfL`ie4rMS<=w-%{40dSc(u#iABY+Ne5T#M;)IAw zKOZbcHUkRqxsh)D-uX|k3Hx1WytE5;K5HeBNtNZ|Wxg;lt{e178JPSiP_zaBp}o^c zV%KpC*Vc)w-*RUbesH8e7=6A?kmDgk@TwpdG;eZ{b4>7`jW~@T`FkBPH&kO^Y3C}) za)z(gG0Q1lIF(EF22c01YG3>Gr$~9j;xzYWUqNU62^P^v)Okz}w3-Xniv}Iz=*&8^;u!39`<7z| zear~e(Jj^N=L3joRlrn4-|4 z_&m#Z+;SQGVt~>DmLhh5cSbx;^(5&N5Q5QPYUV(z_^hX^{yPQ;BLY1VA#eb9oUX3b zF>!U(XNs00a|!UM{T21o6-Zm08+;Z)PNGn|_19u0WnmV8j2STq{Y+}>%@&caTqNWX zUpd!0iDTNT_hNBCN8mk6%+a413ZL3 zRx8|%*XZ^$h_lz5uQc^Q{G68QS{5m$1fRCHvmQ_e1-`9chUF;%$ z;+OQ#jyM#Y1F1Iy4awE?Iq4Ao8MtW|!7fuajvXTuRAjp|_Pz)6l1?3w?+_bqEcNU% z@H>L~%!o?KDYeyAyJu$~rYvg>h|RQ1zF2o~)AuXL7{es)2gKiTu~73(tGg-e(8Zd$ z)aP-k67!A=UwReATVpwtd`ZgL(daZP5|zGkc4-_uQ*|%`0#fA=EE~Q$SF>As4+i55kl?&*z@h4Uk)M7uC3N(5k+-o zW)D_xk~)h0s5f&xbX)ORhASI3jeg?!BvKq#9y~A4#KucP4j#+s8Ad>NHQ$I2h8%HLe8WgY999 zNx+UCkdEPVlxR-Zt>;`l?Ne9hsC)UMQ7{8NAbBC-HUK|z+Hw8%~ji^k2m5hjbp>%l{EoR^yeIF1!b(tAy<4=sjf z#ibLGk~EmPW2q~S?LBDcPBr`TO*PRC$`beA5tKziuRbC(lQcIuXrK0J7G~uwIN_x7 zZR8vZyNYKA3KnZ#R>pVnErt5%YMYN@L!OW9IvWo$rS^F|ghF7yKg?p+vy9F~2FGWc8LSct}Z8<=L5e^6M-6daM}oq7sq|SPSoFr1hrW z>fF!63WjTy%JQW%T;Pm;xjVl&cngO2}fQ*|jmJOl6=oUjg>wP@JWHRQFtdV4knQEsxU z)}~Ke^*V{9wa`2;K-%7PwNCL8jHH?OAH#oxy3K($wE0`-*!ZP$*lEjazqv-v>>%W0 zR#4EQ1ir|X@P|kyVs59bESV9vn$?KsM78C{25pYfOL@^mq$&}5a$wqk4` zBC46?nrW%7Qrhhmc@+^JmTq2eX=`KEu-LNw%gORwT(;H+2!429WZN+>n3q>0X@kn8 zat2LQGW!$ERsYoZQfu`D{sDm_gI{aNrr3NY>W;e!P|XA0A7XEznU8)2z9A?3_=Rg< zq#$PvZF{Ojcj96HZ_Yl1*QE<%X&vsH@o?xfv8)6SIh;$b8(OJ1ThSd~i5MOx?Ap+{ zjrI0*m^-Qv9OPGze}er4tKZ?L#SJ0+j-3xDKx=@Zc3Lkm%9$cE1XhMNwCsKHx~{&$73AbuJU zRjLG>bSV7lcYF$-ZzUYDdgXFF`hq^d#|Rcu?@S;{$(eV_%4cag`Y@{b?rGG^`C|5K zPpUW1`aDspeg~}oE1ZjO^^cKqkMz4+1`a-Uti1SvZO!xUW9+i>bTQF4&nm4H)?_oQHQ3vi0khUan<6h46?Jmy~on>pdvbld=}92LZKS=ssP~ne& z>4AS%H~=k!TD8eUcC@Fu|i*C6y3mnfBM z+X+Mt{#mQ5%~kqsoQ%@>&d2LMaj}T#0peha7P;LW--3cdA?d3SF}F$^@@oW#90r>G zYzN)$mBp`M0HB{huPi=>KQv>rdC+CXo!o~*{u2!BpEd3?`HVJPGZ5z-ddqp?9B{^) zQdgXVld9an_ja)ZBSvMIA~a8zoQ$zzvj@{G(<{z=!tBj?Os!{_ikvc$h#kY^+jzfv z>NT~roM~Uis+bBJ`G2mI{WeMZ2n%kq%nfmrvmHbM)#?g#qm(J};W54~?>VCe_pPXA z+3vWXg=cS#xUYK92fiDmgtmWP(|Eigh_B9trre|U6KoTY&3#E5^>s;UCg#S)@k-Zw z0H{?-VHs*d%hvXgf0l4e@y2nT~2h^7)sQ3dK;P2nUD}PJ>`elR~OMe`vS=WQre4D9%MK(?AhZk1&patFE zB6Ijf50#;fCckOx3Kslr9qTVM^xYCabfw1rR~ywtm5TEfQyc*m!Tq#X4F*^0LmU&d zkpy~nN?y|$|83`r+6bW^oYWxaLg!N>N@tmS$l%@Y5#na_S(=4_+TSTY+y~x_!byS@o2xJnu zL~2*K@qdtKj^Ira5f}MahVftRhbF$5zR7ifT7IyS!#`J3m8HPJwXpLQI0N8oW`1&Y z3|Of?6r}RH@A}sR6#mKsd4u$VNkrRZIm4Si$LD_d{+9x3TfsYw?GEs!9X1CP5?%FOV#zv2u^JN)&RUpweQ#8cLh!tmTJEI{E`Ro=SeO;FW!xX;zh?x=!#j+7-|A6KDb&{ z8J0?LvN$W40M%~hNBiurPhg@`bW(UdZN5~(x?yj9J(<5WnpPd5K6ask$)Np(um;E; z#mIfOt?Dn8>epW7=3NCZOa;wZyJ0u53``YF>-t*YAz%a+yiq*USn{?bkCccMzKow+ zlb;@*jzoLG-FEZmCpc`l1!h54W}->UR|CMOeJ@df?dXBM6oDNSe}8HYj!FW?EUaW` zkKz-fm~(nYT-`OvB(!c|_vcQEf+WDCU5ZE3Aut%(_z5-=E*t-zA=#VdeGZuCgt0j9cRft3v~TT$v$ChY#owJfjF*Jpe-U)NgAb?P6QBx{BS=O29Cd(tM zYlZfCP}nRoZK{a$p2u0G&{})+>*J>H74VSP3SYunpKHFg=;b?4+%XJ+x5h%- z?zxm4@LYA>VAsb>_!@nNKjV4QyyrzUNz_JRJT!RMgXBB^HtTBwPcDjQ^B8wRyDo5| zYXwT@VlBST8bWvzb`9@ZbYf>vkG>pJQC54KW^zsX&{rsJR^qUhZ1|8hMtbsUgsB?g z+r zrh8se!6tG>WL-%DI*n6xxIFz*soUA}8D^{Wge=LqDRu{5Z!5Pb@LWQM0uMD1T?Syz z>7n3k!y-;NRAa1(b&t32IA+>&j*Zp!t=Hm{t(#?`LH7ECQPks&4U@vHK5EaO)vV%l z1n|1Ici4@^_#x4~p3Bg6XhrE)knB2dD5 zt#khcOdLAk9kf~zIu%uqL3l)~xRDR{(x+?Jfqf<-5%PYTPFEgZXQx$eS=z?87;a?N z1|gbE$p?mK0=I&Gt*Y?IFUknj*!&YFZa4LOx;%-;mmRNluwaj~9+2AEKz>6}_*a!F z58#h;2iRtXg942nsmk>1{dPa4v|y4fTLvsrDPe4hmN0k#%&QXwba5CXZ9Ul>i5g5o z)M6`^oV}|)eCyP5_ysSB`EvHYTY<`hgT@$6qec8*xaok=v zQYom|=T}suWT3(-vNLWB35u9nj<`?i*pqQR-}z~>AEc_tgwx4Hm}PQwe5q|+9c0am zyyp}7@IH-Y!ZhsuP5H|CD+*=32&YJ5Hpx7_n^CZ>KQFwX2L3-YfLy895OrQ zsrJd}sX)q=+AbJH}J{5b_}Tt_W_WIC^RJ1gQSj&@2yA%(;4AB=iCOoAI)HVX){# zy#i#=a8?``R3t-=czr?HZ@Ap0w)SbEjY_agW-AT*$oMxPdGw zSpWPmwG$PhZv(uI%0$muqGBnQ&`Y|(R{R25J}+z~W=Gj{*mJXoL(re~hQpdVL=m_S zWdZsIL9gTfLa$aX!PRL`_%W|BwfM`IWmP+Gbfkh|(&c$!cbJwEDPqQZ-qW+-6C1(s zWFJPF)DgonbS11JImIh9$g}JRN!1|S;Ap{L5_W;>ypkRSQxwZePYI_l_A3&sg_9Se zaTUeFV+_Flls#ChECz?hbRck5p(mDWb9O;V62l%G1*K!O!|b0ja~8faj}Ro8uZSI9 zY)D9h^&MuT_G9u#$wDEIZFA`A9@#In{YEwQbur*d4M;qPJk{_;5Eu9FD5A>y7^K+o zZQX`1AV{AtUC^(aSRFw&H4^qgkzgME;(p|#_$F^;EV%6lu)>MI3d~fC@Q+dx{>?yj`UplqeMZ>h{l#CR+ z(Gf$alx1(5?dSHa84l63o%KE3+iOo%r(rYoH}<)k*u;}>4@V$F_M@7(j<>`^E#w`K zm&H(QpU-p~^7gaj&siOEdG>=#Cb8Q}_M*)~62s)gqEHq)Dz-q6;`Ls0_1dV7&EqrSwM5MD-^e582cmJ*bD{e5Ur*QyhfgGhE**B zZhBsU7!=((Y)k@5K@*VPW`>7uulgy(xB9w=L9nK)E*ME8<4}P(h*8)OuCQ>0Vp1Zr z9PNRqP-64Jni~?;<*hOoz^VfB$c2ZQ*tlkm)qAGvX1q(#vg7EXTIj22EIF2vMLpii z04#Xgj&BImAJSrdW&Fx;JoInvLt>-0rxB?{L`w$pN;0E`3`H%(mdlm34<_oZD&^)agjt^D^YzdrcolBI;Go)HNaU@=|Skt=^Mtm`B z5TIw@(PL2arsq&^(nHD62Itd2yL91ihSu*h;}zeNYoKO-Le0U>pQsSV;B4imWSbGP z8X-j7wcZSICkQFN9wR$TF;D#oW)(orDr|M&f0}UyAI2H|C`gMQ1!+vM5gDPBK@<%UfWQ7Qk9Pk&+TBQSb^IiH^sL8+}AjUwg z=;`Z^g49UYs0R@rLim{SyWw$OKpaiZjQjTKj%|@F;myQ)4_z512sAC|GQNCqt)nJm z`x7{zfO0|iB~hPFzVrKg%^_imu33b3#vhHnLz3tFfpIw`pN*#z6yc9xBT2NVM=Re1 z%Fk)ingz@*c$}4;$Ctwj4xUX8>%|9mImX+EfF>F#p93JErU=x14Y?c7osJ z%=k!n$3;6!0Af0U;obK?ROC)nKt+xp!qX(hNEq8-murI)+vd+WJC7{n64pJ*oMWyM z84?jh=HfJgvx-P6y6#t!kdtBwC1Bt%@)fD$&l2hcv!e%eZPWhkEHy!Wx;_|FF47j` zABvQitor??f2q8|u>MR5dY8<5CyGhs?VFV!hC-Kd*ZR7Mdx0ppFM)EPUCyR}H%@g~ z*fy6j;~diqY0sL)$VuBZA58gb9<^xS$@1vC;_aADJFoCayoeMWNpoho{y0)oNv2KG z^7uC&Z7s+`J5io7J?WX)8Qv24z1$(QA5VwjwvuS`by zpKT3ZcYhx4reni5sE+T$9|^vV%OOS`;cLi{G@JB_qjth+He$(-f0Ku|5WG?IN(jxD z{Ksa>OPFUeRi-Yz0pxGnVfHfVsJf~v8ZoD3KFLu@b;iK@K%CfoIY*0V86sDKjYpk9 z8s{1LV3KK?_BpdO zkEvBN^TlweiUo4NGP6Ts6)wJQv@ObNypsMNf%>;LO2L$b+Jx}_c^Wtdf;hTbmaFcE zz%zB@IX{J_FT}0n>GcB=VMj@qJ6oqs7d_v;?qd@0>%&Ns;6Ik&1M`fU+#hzY@ky!~ zk_IkiX|CA6e(mwek{tI&MnjP@5PF#P1ddtA-VGs|ur>e%0m3-idx*+d+cjwy@ScW) zx6+e|hAoYZp;?)@P|9rH<(qzk5keIuCzey*>|SJcN+J!FpKGnFhh8*x!;Z_v(0@W_#^_@S>1^)S{(5N6%5t8Xixuy*UbQWD@B!XPBNC<+niY6 z$c{DF+;xuLIH?|Ck(u_xART!isQxniE@D491W zCH7ons_N+p85WV-c^B9Ze0PNlcJRww!MKWOI$IM7$myu9C9k;Vy{~TlTXh*ESi9L@ z2C8x@SZUL-M-cDam;_5ts65M+v55VyBU-X zcY?&yKJ67_UKBHudnxBT?TEftNiGLACDh)#_F7`|F2HsSfnWh1Z3RpH?(V;uCct7O`we`Y@94`N7m( z?W5(;=9Gw~+eTBHM!ifAj|6TGr%6#_o*S+>%=ockeJWCbU7b$(AJWfb_?3Uwmh0ZO z?TD%Rw%MYfDYY`R>UPTlq5J2w#r{%Gihmry3Z@i0XmM3N+n(g9qwL#l0yVyM2h9nnEfF;a-ssa{MAyKqp zPZOEIVY+GMDgo)2PvcTa7$}Ul83+-<3~kjcz5q4d{ciR-LN>X1w1(lqnrCBZRUF6h z$FfBE_oGEf6Yg8EeNrHV;ESsPN1^y0;j=@f&pCC`i$-=Q;AV=V^x`X3huKxC8=aN{b;gp7U&WY_hR|uzUm0S!kQPe*)WTu_GGSxfwpFkbx!1)*NtT z$~6R-(v=3mgToC)>Y{K5R zUj@UFfowOYe8m|jP5e$aXTofQ8Ao}TPZ{wFwa+Ban*tJaJAkHz0-0nmd=i2y zHRgdj$@faZ&n&}^t{^ErR6fRsRu(Du#A16(n&X5#T9#5#QjwSMkRc@2Em1Se#26)* zw@?H29_RzicAob_TmPwXZ0cy*SVA>)bbP)BgKX+8$g|6APx{(s_>iJca9Kh6$F$+2Z=6#~OY2`g$H7?agj+}rfi$&boN&x4_v$G&C`%JufDT=XeH6JPU+^-j! zM>~ij`a4y{jcWd}>DTXOo^G%A3`KO_+}5&gM>#ox=baTiK7smYuFFIntSb z6rQr@Yqyn8Cn;mZenhaAaQ+jWw-TObBGgEXB(V^)b|1jKtsGErej8%ZH)r0Q0se37 z-Ba8?$c0C44SBv-Eo+ZX9uA(NWHvjEN+%i8uF4}n985fm_J`|UjeEY+ws+&JHJ<4> zv8Ujn*-Ewxos6jm!{0bdq}Cj7BkoZb11j!-;wV$or3Miem= zuqLNG@04GLpt>=uE`I*xQCaaqB%UP2FMp>-ILiOyUc(#UkOWEmt)F1lqXE{4y=~|E z-N5G9mMc-g6lMR^V$^7dk33(Mpo;V=v1?uj%u#cOwv8Jh3he4UJB{6$+ne;Pd1zhA zd?BFWm61W3TkymM+5}U#%lR(j#w;&c1*JCxl9+=I(sp_cu0MQQ6-s~Gab7O}0MgaK z+9#4XFh7J3Uj)A!8!TdRUB~~OyXf`r^fkESh$HRPY5vZ&e})N z5GT=7^(WX5jjin1wL>WV7Y{l{Cb_pxJ)nu`gX;aAQ#x#$hG^HqXmz)lt^cC3A^uX? zNVscQh#Pb5TdlD015XQAST%&IE0)Wix5pWDD(#b`DSSGMu6Eh(r~Pk`rHCq-4r;7I z3pQAT#|)D5_s$~~DUdF5CVSFs7grh7M#FRK zk_*T{F)X>s_JGJ_)8hPy+Gp?^EYS#;Un5iAz~AZZjKN0u9B^dcqQK51T5QxGXM)P4ktk$PB|8BK|H-tf0YP?~LOMg#eyU-8c^zSp z9LX6`nXDm|&T!4@Z|fqrhW?`b59$60{=|RvmV9|r7-rxB^1YhmpY9%+Wd1w$)@39$ z81 zx?}hcX;mWwNUNwPY4yic++9^A_1<_Df+escaE1xaBW?EtDc!vkF=@Q?>CcWm=dCPO zOvNVp?IG|VS1tdNXoqa#@yRo?>CMaEU`ji+IC%ds-%qgQv5;cz<>4PtF~dxZ6de%6O3qcB(p9z^pH)isie zoTSb_VUl-Zl*B2x%3Lnqgdx3;yOtqDV=&m{i$`E;VjTY4y z&qth5jQd%9W-bp`W+XlE_ZIXx;IX%l{Ct=U7`fKo0`96vX|9yF<&Wz%28AHkoB#RIMC6w7f) zN+?|KTL`DZzgDI4KZ$t$dLGiBKi&4K(m>C%U&2X3+6c14v~N5Bmo*xbil@>_CT#5*;b^T~iE>EV~P27ymBb)Nb4qfrqXSlQ4T8UZK@8itc&eucKjtQBU*EV*|S}Wn9 zCZ{_!$G`0rrUV%=d-4zk&Nj2aGy^?*7&fel-z|{4dNL};5#!yrBu{9qD&$x{RVk`i zi*ovWH)Yd?a(Nz=j)c5V`Rb*xl67R!)i*4#W!GVao@cYvZsK3Y`j>5i6Owp`;wKLH zPmPi3p7nhc>tk8wc&1r{{+0QJxC-7g&i+$F0PY80v}PhcOU&9Wf}Z=atA3IZ|Gs4L zw6;SqW6^yY9`Nsmt$E%W68V9I6*OD9zdRZHMpcV_sg-4k!pnAhCJ71`BAayn zjv^rvu1OERH3^^ZWYjgJ_1@Zd z!hZ$sjq3Y-SCl=t_v@3=22+at0*u17K+(UhVgO?z*y9}(KBRU(eATsK_Z7G?a55Ma z?ObnBYl=gHwbgXr$|F?UAC<-`pcI=t61)`1j~=fDtu(MF3oP6}!4_V-CgrSOJwTnx z3`u9HMN^O(BzXzmKrvLsw*uP?GxSuy!avc$aQDv261Bi1PH=(icanNDEOkH*Q?|QB z9ZsWcAXp-Lc}sUb6mEd?I#TF%Kf#*!dXUiB+Jaz?k$9Yd!r&bN*8pa3JWc81bGf!h z=j_~$bYhWTyL3KjG;e!sELc1$)h>DRAKEby2)r(~gMs3cQ*@jjM)j*rFwtfttP^$1 zAf@Y%?@6c^J0vQA+I37O8O|Ldxl|zWcPfm}A~t~h_bH{`j7qAFne+gP8Ny_?C^?gY z_%FZd3Z=!afVzu6N;u}!>+TbjmfnabI0x&OcB7G7pu*B38Wlat~2cn`R(ON}zE?a_ZC72~pe3c%(F}4N4 z6V?;pR?GvXwoye@$Vg2mV!0hdsVZWsPhAH^<(f5DQ!rVUZG)foc0;a*=K+o!xhd|# zW)wK34_UmXR$~guHt-McsFfp^3QR^x0#^<-+m;cUK3mh9Yzt>I&=zw%Qemau)d$|K z-++y5c-M2p-jiZ%2^W)!-gYWD2hHZ4O@eohRuNA?`y?Rux?H6pI|K6yuv3*Jx|`fb;yPLc!()F zS3j`m$P?MK^F9DQ_P#^qT;%r_tMRzp88^tZxb%n2KnX&FClaVHJzQ{13moP@PsX(S zN_{S|(0dSUl#PTTCokk?lEfim5P8ghCzJ%jJTGgmhwYuK;`NekM;N<=^;<m#qo zG5wm1RV_Nq&~C$V=$iV$8^>68^brRr18{)#m1m>bfl@q&zDy{j0&80tLb0_%Qkz&l za36fmg!iQLKXAp06{f9J!wf}K!3TX%0K^a#M{5^7;W665O7OPPTG5}4?uj#k7LQi& z5X@zemmfS=;2o~+6J-%X&pFu0lokNXZd>{$kjC5FhK$^B`pcDA=0k%H*>d(1OlUE* z*S{}O+bgqg7A+}j&vfoBPfy07ZKKEcB(6 zR|kcc=96b}kcgk9ywK|w;!}Bj?pyeOD>o{(vRt1w>$zj|ONdWmi>30APV&m0EaD+@ z^xrDn!2eR=)@MAAbXr3smhae|{qwgU!dsB~G#uOP?2n&$c~t>14^$u)Q3)f!Dj~-P z5LvT`XE-Fj?-)G$+Cvd{&REsC&s$oF{S7MO1YWoXtdLvcF7|eCeN+s^A`L0&BVb8M zSiooVFv2j`YA;?|Pom~SioN^OXLb^bDr8S57DePp8Mqd$4KN~zx(<{>w4gW;a|V2pJ2dt36H<~dQg2kKw2PoT^m}3#aguf zBMm^i{_Fmz-n!v2-BE*&C8zrS)#_iQf8Kq!PO~n*TC{Fxcv$I3Sg?*99|uYTj1Qns z|M=BwQ>59x2Wr$<9{L_tF~=&<<0CiOBpfyn#}1s-Xk8DoS$wRnbqrX?*>o1~b!iYW z1{9svho^_Ba`j&xZ>-P+G)z1rDj)MaJW+rgi6b0~)<7KDw!jo|DEn(k$zF`@!8{b zf8X%<+`1l!^9KGt@uGg{EwEw6BNgD_)#}kB0l47RQ;9iP1a}}U0fW^R;KSnkUxhwV z7p;L7mv1U(jUCeZl-@V|1nYQM{3GjA0YldHVYc``!9q6)ZY=nz$eSB3p?e*SDHF(OcjtE)%ed)de4dbx?3fEt)=6 z4zlzF8xM~t6|yZ&=67v=R~mQ?(56}YVtKQxI@QSOrtm7lw7W)_8Ki=0b z($~n4CxXahpNeV#9suopm@im(q?rJW1+JRMzrerNE!OQ%%&$Lcf9%PjN8%sXBfa2r zYzlN?obJaffclH12_!Iym3djeXan$3I><9G)*9$klgDh2w5LlO$ZVm1+g{;OmS+2d zQN|<|i5zX{Z`6_W)Fvtn%g~pg_K{59Y20=gptdvn@}&Lm6K?p)i%OcJ93nv(yF|+C*hr@EK1$V_wqvz zrT}(xj0QPnSLqWjfBDz}bo}%&y7l||Bp{xhA{H?}ZXKwW*EO>3>wLsv1%f0k^6_HC z_lcG<{0@+E)`UTWjI#f1BF=EfJ_pfNRM6c)Pca{Tac??1!jphKz+>hc*#)IXg{WJ z{lWTC;|p5a2yCABtNbT8!qsG9V# zQcTg0Zv+Z|31qBaG`JJL9Xhxy3Tn^EvJP0@a`=_~so45O4_IrTp)*@L6ReMOqeRd) zXhRghe1FOa$hU=6FP9>maPSRwIGr zf0y}~ror91W084w(Q4=E;nm+r0JXXa5J1DSRB4Y%|MR+9%r_bS=X33U{`=o@{YN=} zG~*wMa?KkL&xaMbQY{^|?V<-(TP1w#hObB~sdJB*VJ^5;oHz3CT#s4K zK-w|+d9TL)eAnM@fZ$hh)W^e>ejl^+`ve^@5@r-2`juFb$v_A=Kj-PJipN8iP#!XK zR=#u2=k9io(%EHcZ*zS~X~xU7s|-N8o^Kx|`*N~gm{oAaB*N2t8B;e&;X<&CUX1VyxX4m0vv-CCOy1diD9GL^ zh#O_&nV6V7+aR!M%|A!)+6YfUw#53({H^0}z8@D@KV@R^nN=-aDG;Bg0#CF5NZ(@X}(E7*D zk8K?z62Q=e@x6S$iuvW3nr* z{@M{AJwsIm-l4|8&8+bWm$_XjQTGKOE$iS)YXsws?LT;zUA_{I&sn~h4pvL3y*A#1 zg5E{Dq>=K`rC`6pZ98sJh*fA@AmtFjbS3lolIlt_bQ1G+8?pwMTvwV;qw2WX*W=`) z#3~URXlyW9fZuZ~$-K;z+`B8n+Ra4k0N=n4Mcbg+*|Pk3CJuY?{Z_q^s-U%Sk&|%= zwg|rwe!KMMIZu+O1Urh3O$FAPoo_;v7Fx=q=RkAv9N*wep7rg4BiwQgFduxW%?A*+A!8!aop6@>G>+0*T zJ3-NTEXrI0A3-s#hW1Umc)Ggg!zU7q9R!YtWMYF?77^FEmOdcgO}}0?!4Ez6^?wZ5 zF+hoS{$VRyi+D!F`*qa}_3#dojqk$;i{QO-XW6i>52L6aa>#p}DV3}@LQ^+G&97fbV;EI7+4$eu7QB;LcH_E?2u=9m1*ch$M7bmw{vd znq?5DLmBBx@=*)k0~)&LD3IoB*^_uP%D zVVvA#qc#!T-4c9zKh}U#lQsk*WAtMN%_P?X+xXL3vF3)1_juFvJsv^HW9c@5Tv0Gr z25xp78!Q|Tn6nXPWiV@!8Op$egF`a&7zY}B^jUE`oeUP`Hw$|UCVCCBBpc0>trk?X zTb$6d#e5W$p^^}m>x83WoOHhLPD9Vy5#p2abd_x^4E!(fWW#19gOs`Obl~KmTN5dZ zB16BZcDE`HS(!8Y8OIg z?Yyc-W@FYgrM+tE_NjyV+^z$#N%($n(=JoLALU+RNY`i7f^ucv&h?u$sN&wVWA908 zdWC8$`*u0ve3cPQo$q|;;;b1b7bcz+jEaf@hN{b^CJG#xly;>jB8FWXZ}sl!^1ukYdzHt%GDns4yn+Gd969 zhdOEBb{4to&ir zj)@cd;-Yo@#h0|vd%8{+7okzd@mA_C*c)ihmk0zKe(TexYc^LW;-gRo{KlD{>to|2 zIh1^^Z#m7RWlL@V!ZJc9K4*{5i+6a@W?<;wYOaBC4h;DpB+>56d(Ka-KOE(qaJuyU zJ?M$Ao6ivZRzm~%Q?WVB113y zv}Nny>K?-Lmx$nE;Yca=HLxDmNx)AkgSCO&l$dsKJ&jLpzXZjUO>R;l_7U3Svdf~R z(6CuMB=J>9h>lvHE>U?an^>}$bOu5#yP*~Uhssk%R36;*ybp$`Tx$5TK{)E@^duu9 zND%bhYuDRb1Fr9p^u94cYZ2`6-GTugbf>Z(jI6_-eu8Cle%avJ3Sg3qfYrNhcx&7_ z3PyS30JoGR<-En+WYskvinL!Dcy~YdG?Lf|kWtw9yq!02pxai0G$6{6euCNkO)gCB z#sgym_A+-D>E#gqn*XV~)&-|9Fz?$NMrPeanM*kt%=R5H?9u5NTF2q{dLWP`odJ=e z6sQR>ka1t>Yv=izb+3u_4_et5Y}k}AlHGJSG!7i8IhP+|`mTe!oJ_h6VM~B1D=q8% zN#KV>t&>2NENnj+*?ekR44d_Pt=iMpaXQ`y?^XTa!&r<12p`iEB11*j$Vc0Q-S6qCnu{Q$)T zzW5-sDDl&?^G;gCT0UD=;k5h%8KTJeI5)I*pFfwWhdRIbp1{bINOcnCa}0bpY#tM4E0q)3G1Ex6 zUbQ4Fi`{zW1FHW>50I3o>Fbn&C1u}~LKt<(vKL#ngtjDp7M5cY@6$b}RvfW8*3xEH z8#&Oh*{Hb1-T2YFMh5Y6spkJ{?X9DtY`eGNA%+-gfEl`T=x)TJyBR@38foc}a7c#~ zkOmP!I;2aK2I&+j=`JZjM7-bO*5~=Xf4uMdzO}yZ&sof{uIoJa-p4+Uz0ZAq?X#%+ zY>aJXU+B+&#p}UUY{nPsmR{b%l>(CRG=8Q(BB6=_l_4-!b%5HdeocptF51+iU{WOP`T7y_D(g%W2 zeuoGN<(Vbob*!>1J0Petxnck?wGHAMTFfOE#r0NBZENrD%fsru&^7*c*J9TY7?8DzW zKbM0=ooJ-XT2(|+=&sIw3v*8MVb$4Ie%&wNS^dVM%Nd0s2aQ|Bexy$m2>J3OkSV;3 z#f=Zcxye|X4)ojebh`6P+oX`4aCO09E#3bxgA!f%EXSVY|F?_ z*ZdfiSB#g8^VzV2xJc}QUyPtL$W*3!ebb*e!nl;}n|iz|Z|YmTKQ| z?6*VYl5>B&FM3WPdgiPl+uc44Bg`Q%tGFwu_n3XNKh)oAjui{rDPsw-gjnRO2+y`( z4lrzb$h}&e$;!&NgjivH$g=2Z>~(4mayH$HeQ)1r3>4#g&t7d((PUb_osYJ?tuj4B zY*^5i&b)I)couuJr!j9+xoP=oaTZ#0iT=B_aZp%(0cJmD(a40u-qW10CAcp^I$W<( zil*Izp5KaPsj9V01nw`|YF04imH8~+jBgWzTA#d3Ua4s1J&L;%6AnM^tDApv0jjmpyHD;twlPDY0~Q;2 z^~>fnSo8#m4~Tmvnjd_so@WsD(>2w@nm5x0|z^ORIO=xF^@>QZ?=fcS>~7LjITd28BL*L8;l(zvXE+G zHV%{*-P!41`()6o3xI`EoC8INgNK9r1na)8NXH%yJFgb!zfW(qslHob(NT|zCXh48 znMk~^X_>Dg1H@je?gI7eqM~Be)34+Hw#O6UV26nlf7;%u`2kJ@4w$EazcERD(jsLp zJyP}gJp3~jLC)t+L?!PxvN#QvN*gR*pc!6|9d9MC20uqH0R=lQaF_}9z@(kD5f535 zl=AL0vNuywHaUr`L5+4Evs4CPX>`^UAbSz<%lX{1>}5Obl;k!KpP=xHyxQ7N_)2S( zDT)pDSKp7+&1G^*_7&i=^{Fzi0pb-^m2i@qUBS;+>pk#D44OW!lNHKaXlxy%yhl#i zwXhh1P4>c>*`v$=q_`f~PzgwlCXEm<{#4mrvb6<-TZVX|Rhl+a(>=0mZqmZU2Is^! zxYvcjA^J*_Pyi2j8XpdWQdis5R0YpXqIj*MUTL6SDRZNuLb~d}UQtRqML@FV&|~Iw zJbY-_5E-bwpD$Uz6JmlQc^xBrjK^m#>heP7NB07XCIJ2r5cewvegbN|#^bdz=@{Jc zkk|0|xoKJyyMZ}J32N_c^iIKB?S3?j_A{n+p1|@CwLokOzQ7k)$LzrLwSzNDT4xiB z@$pSayP1Gj7ieTy*_ro2QEBYRbYi`V|D6>O0JVVfUjuVA&-xIIcV^&NN7-FLCdm`> zrq*BUfy72MREVi~j#r`&=@I)4j6&#N{|d(jBl<=y-$F=z8n8mI;T)T#6hQYKF#6R%mkvIsrFojgob z*XX;Fc*t$td@vK~F>L0}e#mhC^co7YN zt3*Oif)~&M554lno;q8N9x9n!rOoMlcaEz=u{2Bva>i?^Nawl>u!-#GsO!}y6Pn(7 z4hiFSti@SEyCPp;uQyf;mOe(hb2C?6t)OZctTn$&8{SlalS%L#=@z2hdCba;UKvnh zPJUBy92vD0_bg={D`ZL0Zos7c2NBIW+VcoXzr##faH>uJ0UR5;5X^Ju3t<>fFc=gV z>uce)(sz+%C9lI;TOvyVrJ`)|wo8f#($v~QdeBLg=Sk`i8utw4Ru^@OsKmuPneHju z$M!&K;6M!x6KauL&5Vdx+6nsBzaED22H$@Z>uiQLKj&&ZSfJsPUsHURpuRKE zqp%$lOJ^4v6#@T^f@s@8$LmMErhXHXp){0%^Rryz{VNxgZ$#fIQ5qFer2lF zzdvB~;3{jWXfnG4rF3-sY#&ZP=KdDvyqX|rXt3B)(dt}r6XEm#j=vFz%<#l0O#7fn zVC{l}t`pVEaLY!?*Xx`9fZJyiewEc|LWLCvEL(BUTcaLPN8^$|jj1IAg}G@%SD`Mv z`huzv{pXmP1X>d^DA30=V7z`|DZYLO+7?d+J#pNjmY!xUef5!`9{_HA_>kFJIy37U z?PV`@y2ZYE>|*MBbUfNjFy~LihCj`Dm-(*cxlo5IoK={EdC44iHqN-aHaNWD8T`-k z$>D-vXy7oj`mB@e6v0L zWC}|b)ayEdzp1r4o%C4yS2sid{XY2i=4fwaa%JybNm3LLG?a5CBpBu4wJo`9eh6kC zXvD@E-VJ}3$I6m_2H;+Q(ww>0^cDlA@&w1J&t+(VnhQ_N8c5z%6#DDX#dVwHA3&(* zEP1`}hj5(0(wrzwAQ7f0XIk47y`I`PY8stn|Mv=Bu3zM`V{Dme^n|F7t#+YOl3tk! z9#6cBTn~N22!MGjO<$iy6y3lKk6usT&;6bCYgOFwE$7Zu34~lv(Q+@tLA|S$XN(Nu)mnnF}U!j{;Jb{D^Fg#_)Vm*I_I6vJ7NYs z7yZ@8&)$VL%lu^gEZXzR3!_1MwzW5omwGT|x@YzjJn}LyKYSmU#6*qnSs<3KL+Ktj z`$(%`3?-veC%-I_#%9Cq6|3(3HT_c-6jGJ{G*tf``^&zi^Kiwaw-zWdq~EgE`t$4X zhi~WhJS#P&i@Mq7ax2`r_`RT78o9Bfp1G>yZq|>mR2r=1VoqCi`P@{EtYbTh)bYwy zWExZabNZ`Fuc$QX{mkv5;vD-#<$MX~33M-ZkazKVNz zewNG=q<9wel$sbb*^g-!qj4F(!2pag*-16K=d4Pu7Bjd(tJNiujmM9^*%t%#U@mPg zin{@wuOU0RC~b(WXe~CLQ`|>!)T`G{?U;T%G>ED<{ ziIC%qCyoNWJda!TOX^nH3;ZDzdBlcmZRH&s$DE^Rl9atbB-ptjOJ_Zl>aCfPGUXhG zschhk%OHdCgZ$|9^g)$yilZ~O;O$h%Hy{L8M`q6JZdi22cNmB(`kP~}x;43;vh z+f~kntY-r{k(1rPAOR?DTe#7mr@Z`0KKamcyu81yK)v!N+T3y0D1n4^1yj&i8dQzn zW1K<8;}-qUrZX2CJgVTTw%=oiXOBIx%zNsJIA zDJg&@jLz@JK#6*%d<7gpk1Yi443mIbl&6#-LMTK(V`@KM5_}pZn@8?8`9Am48=6K( z8P>{WJBN@cz4Gzk_sW`^C3M~voWnToR@svRNF%h2J)fU?fp~8@b z8tzW!4Bm1~tX`w*0cJqT1cheZ+&5W*wJ#itLFmIhCDCn?&RjcKJILKt#$ST^Wa2yQ zDsne=NFH$Z zlc{0{?^CHP64a8Nur29U2GAhQ>00nKJ!^{AP#`kFusdb5t&TC%FDMF#}m0ZVk4tQ?~@W>RPk3`6Wm@NTS7Fe%R3tL za|f6d1ot{L{sFuQ2^@Q^^Fi((faY(U`olN6P0zG`d!s``#C$>0>(}y<`PoxRcIDc~ z!V+6wB31G~H$JSm+SQwk#3>u6-}pa;9<4v2XI*MI_bUSy+aO!4Zu zUFgn%G3jnU_m_l0UsZ_)z|PT=`0($rDA^g=hP! zuYQW4w|cMC5LAeu=7ooWe8xQ4O!DC1wVjLzsiEDnZ{jIQ(`r0O=VpGC`*Ms+@c(UD;%)#BPi4l7zhlI{d(sG02r}e4PJw%LJkH!aA zmPk5Z?L4H4v+zB#4%z%B0WRG|r9*2V)D7ha$8!|LfS2kkR93ox!(Iu;(b#gQ3(K;i z#F0yU3~>y^KtX?fm;0R4aQxfOjnO2Kb-&q1L7X>$5_yy^zfk8cjrv57ObJ~G@n)|G zARx-}<%EH3z71G%b%WhUlTU{RQy)&^I?cjm`p@3RE9>a1^9R+>W3d#Z zHIV2IiEY*eywCG^t@LB5B)zGyT(FDH^6d+H3=FFcx^j))v`Dv)I;WyHFkD$tIg0ij z_B{1mI!>%b*nuAlwnBtL(+J?QhM^mdSf^rEI}_qbij>(PJGS0=gUzc`sK_J2Xo`J8 zoH14xlsNi~0#CU(bHlx`@gb1^;53J3hM81p!)IH|w2~i-GPOv$mao z@to1B!qBaRivLPMsg(5ilb@~3bbhQ200r<^8*|awND2eznTwpo1oGzb@4VJoBlh@h zwZXt$NATq(Ab0zXyhYtQgN*$vDl?n@US`o4m&b!pciT}hieQ@D@Sw;j6Knm z#VWrCXedKP0=Jjq4s%zHUOZC^?Ub7%bJhnxR*R6SXKmgpDkO5+FeQkYoumH)^Hxof zkwB9Gss0q#$<7^YZ01B#6gTn_QBa=A&!bFw_Jk&>WmYpQe;Y{olzvy(wv5;>6K+1y z0Rd0?ftBAhD=sI*;UP6PWqt{e{7dh^+q+w^$ zb%vR0@aEg!krxk)&-cqU3l*J58!&4LATyW%pi#>wH>2|?-e=)x$j0b0wPF)Nvi@;1X;@Xsq{_wA zoE(^8R)`2#%lAwxibi$pa3KzFEOhUc4fKI=k72{Y;u@i}R-#R}&z6Pp(%CRVf!X3B!c!<*utW$|9M2#szS^Q*R8H zHB|lpTmOH6?c35{U;`>?7Mk$?3v6=wk;{L8&Gdar>>)NLvqE>T^@YCGw0Na;C4(zS z{j<2C=Q3lp2pZUo;r4v&8~?8{E&g9jdy$^BJICr&*T0a5 z7jDC~;uIgy5Fpqt*qp~{QIZn8-mQpe(5r90>#Rp#%0IUDjy`mY-;aOsvVosNCdeF zPZ#!Y_E;N9k@JXQHj1*>Fw?qY6GgF~O%A4*9zaJji#zHv8)sr_v<%ZQ7u*NDpcIsXdj!?I$RFLglrX zd!QVu%6xY6dR!KdtO0o0CzrwE@Q3tC@wOeV&q8NIQn@bgAQ+uIdIX6YpnFy$^u{F~ zpD3NkY>-4_Ae&IyFWvOppQ}sROcw;|;w@I|6504HRtVnK&H(bFWC>+l?6Ql4nadAH zBsi@^?2Te-zJ3Xf{F)DDyyL+;m*YEPIrgBC(`TV#_V6;QXzrQI7I}o^8aHNj!NI_k zsHsEuu*?u{?fkPy>D-Trdf;|wrrf9zg3JFNCZ}!l^5ZxQiATv*j5*wGVy~9f!|$2I z*z(@u?_LhS_2qV_yRE(H5n;l8?at@opuolBXzn46lj0*dwl_e<#h@ohZfx=5amTE? zp3lEz^r!z}=j@bdoH*q>L$8R!ma%=`olD1Fdz4;6@t3$nI~s6>1BK{rt>BA$+a8-t zc)I*lVQyhxQCx_(g%Ny>Q#Cp>WFV(ajuC*nnD`@H0ZG`j4wGJ0L$$?a_s6+kG1y<^ z(etKw4>&WAzMNV`nmQ%27v#MW5G4{xR@JcjAqV^&{#hRLfJSEu?ni+uX~)4th+Bwh z0;i(lm1g%bIjHN%^AY>HS({6w}}C5E&cKEM=Us_>|g8net%b0XLA z9@=3W#l`Qu>`&==QG+OJnvQ?Th3zAPfIVRv2=c8Ee521W`o8(nCFnNm9IQL$sC;qT zNB=^C-YIh(jSu!qvTm)#ujAK3kV;6SY3rKsU(+3-cud6md?94 z@TDc5Ftb#_jr~d!N|$TN0-Ot@*Yg~nxbnR}%eg$Jd-{4yf;Lh+Cb2=wlaV{5CfPhT z0UHl2&b1uGyw}NLINz%@#c&m6B6 zHDelIAH6*LNh6S?pC67*6ziV?R25eeiaKjLjUaJ_v>(%7wHq`P0mX7^ryB#u2Zu#Q zEV2SBhi+{4lQW&FvXu3}3@jyMJX(fl*R=j0R?$~7H0S|^3$790E-6Aq$4`h_k`r@* z0K^2f=MR}*Jhtb%8VL}PNexEs=;|`Hbo4L|#es@2+bfoDevzB=)pk=J@lpc0M=q`) zBRUpXSq%}rE1n}xLei(QqIxRfd`7%~08~s&lS`}7dmoQ=vKJ(K@vS0-`Y6uuS=2G$ zi+jnA_CMiNGB?~tI3wK~gvI$qt}(-4mgrZ*)NbtWyQmw{bit5Yp&;K*_|uiJOybYX zDq7`3Qfo-ux5##u;?p#yN?`bGL&n&`Wn)LNd5d6vXhfYDYMkpe}yOOdW)ycmb zp8H%n<&ns`A$hstdO#b<@oAg7D}exXK&AehKE3yg^|m5Yin$gI^SCB2z{^Q8)(Az|$Cy(kN2>{8ECxaa%kuiaA7 z9<~bIZT)1=p4-=o&G~JexXIa)N&%qWK0Q==b7f-nC>AM$Pd3!z`FjPfozRZIY&BXR zUl*xOZ`$-d8gTom4CARBf#*om4_32}ZzltZtdCX6$?&d-=c=&U5lL%TfNWtseN(sSNRAl zJdD)=#kIAOpX^_E4<=r{CGe^_S4N%C!_a9zzv~6D;_a&#KQU5x6RN~E=+KYUTI|GL2nv^VQvx}aMRAcjwyCA6 z)i3F3^|t8n10^^6ehgf=C-^KriTYAnz8-RE{qE)I7zy3mbei2b`M1;_#Zmc7ADvNnA`A*qy|~H^&R0+T}3NRoJ(AMYKeBLlua+0h0`t?#VaC) z$r5B4V&vznBogcTC@|Q>)I(mx2RGKxGo3o%7h7?;l+ooj(i&vXYcoi09eqRQM9T~=5@W?={cjK~v$`ecu- z^l3k9*7&+9F{BH9;T>5oEO6=?eWG(gUVp5ctGtE`>ns(?SFN%M?tVFg+E(%mav`t@ zwO}RXW}Bu@!NuYiL$Vj)4ns?NAW0PNfFKW8!hX@5bW;Igiz5A`W-YF3g$?&m--NPT zBWUz!@)FOD3bBL{VpQBD-#kYT;jamhs5arQOCFb`EP6``i3IHE)Vb>&wK8D#sTJW|XeE86Z5$F+#XRl;|r!s0C(&d<+p zS-9MVe^OJS^eKI3Ytzq(e?*dQU0QFvoOIodSILA~Dn2j*p(lt{>AFNh4HL=ox%c_o zLs*rIU3u%<^JVBWekhg=?;Zi7x*modeOA@iY(>vYr2lv>Yr5BV@yhYnJYur)_)H75 zg-T@zjry5)?UPuf-SfIhz|p}DaDLQR<@!mP-Hjj-7e1vEP5o7J1IRjrgXw%wdw4a> z*bqJ|r@>hK-&Dm^1e{Yq12MF4VbU+row{9pzUbtq{$?Yfrtxu@|7cP0eU)YJR{6l^ zAEo$#>iF7nN}+}Mlj0N)m(A%Dhkr58Y>nz9RaqATH2|<(Dvl|U2v zK6>)>>lRRma0oK^XFn^iLZq8`ySX>pOq7{mZHaPBWZ*mULtes^bsj-GL)hDkfxNKN#KXb@MyVXf(fb*nCoXvzW-A*- zPdp8!V;&7$G61uX4@Vn2h&SM`(hIz@Pt*CU2q3|jnV!hIl<7q>4cEUZQ$62TFEYDQ zW`F|K+XCtaF*0&9`C=hR)af%#-ay|C;Zy*HnakHi!5In%ejepOjRT4U0%YG7mrhya ze-r`1AO!`?N?9u6LM@$)p{;4K>e;*2z1e<)S~q4j@OwR~lO@ktP^0*QXE;3}&!g1_ z67J8!h8ilzRsiHi?Xq`vLv|{-R@#)1)QbH0@J8UPXMSnVVcMPU&oLxVPxx7b_LI=^ zSLYF!k!rpB(M2fLJRYraRf+L&7ebz*s-4C0byn1u2ISs=6 zpaRdI@;vS~#{ktvB$qcovx9kfqDAe-g1N%}3<~!xaN*fZd zod{e|l;iW)V(!7` z$O%cK?~(Wza`KDBJrQTp-<8A3c!Z}qtiQ+h{1v$E=tT?T?D9VVhu~iF^K5X)eNR=} z(E(Ej$rovI^Zq2X?oK5el9pDaOiR{ z8{to!kfN%|A2w3@{i-Jgu=;eoY|yi3Vc4?+1fTO;HzEG#QM_;UB6}Prl-D!JgYGYK zZe6VXLMr8hCyA*4ribBh2LHMb zcSeWV(SYU9f$hN}VO?LCBp&lFEQU5HrI3+IjhI!-?Sta5)%K8g=0tUBO^&hH_;SgG z?coKo^~OB~egY%xJxsz|HPL8j)ZrrI<1uP{;^IL@m&FKG?f;)7`Q?9;UP)mXG zWb>bF3Gla(cbnrQUn+O_tYEv)zACfnflL=Z)DiQ2qmrA-_t=*`2s#PKxrj@k+e+$+ zU#{fn?3Z-3N^49cPuF3x7Qa3ZnO+3(BVqjF0{Qouuy<{w@Jsg$4RH;TV7Qu%0HOM>-NI*k4A`qoDYcWV z0Sxk&FF0b!c6Swo5^p~+uK2!2Q{{i_yJ-D@VH6Vi>nChRWOV+QzAW*No^zxP8vg7S zjh--rbmrbm<{5U2AG|EC(aG_3U~G~`lF#U35ifk@sOZ3RE*`lde_2CaSFSWCe$b)l zI)O(rW_I)X+x_BaQq_wv!*Bb)(RJ^PJvsE?W7M-#o`dnpF|Cdt(37*I{5KB`L$^7= z)x^QoL|bE%nC3zHOo1WykAfr$wGRVc`ZEe~hmoqtycx;do$`tMut5IOq4lMDx8UYS z@5ZVcDLp+@L2;+3T}_P@yH5%76675>h1o?sLX{(wZ1+qow(;e{`rr%NKwC(JE2 z!7oatL`*|(aafXatqWbm$3jI-cAS{eLXMDE7?k|SClwT;iK!?YcQ8Xl=>wmVqrsT2 zk}i>+z$@EZ+uu7E<2x9!q7agmt2Krh0xag(#(UM^&pMJC$coao(+kfit77+&`t}+b zI@+xhntiVwOI3kF#nCk+CaH-cf=C~-;<-E?db`YL7u$TZAR{L1)$ zDgeRthALu3F~p#EMue5C6UiTMT!=J3;tc%s1@CCLw71B+pn7xM{QQ@gy zal6FD){m9wKMLWt(+;NyL0S6PT5K&4ZmD0R=5VD7jPPg#vt?c6Jp+aDt0O?*%7?At zs?@9N#72eL=1h(OF{EW=gZR(6MlImXZ^%0(SSjuQD)%>hAAXNqqd0e9VbY=Ni32%_ z4I|!Y*`RLB(#?EYx8LKV2Me6rsdzkJ(fmJ}_}8OQPQ1_`m3Ui^^dm#B`=dyxD#_pa z=8VMa)|5jZe%sw}_{0QL=oF~UyD-rnLR?wu-&k==qEEsSp7u|0m5QFn<)-krA1*F0 zO-hTlua<9GFx08(iJoQzoHlJ|hl=<%{>Wv%S=DDl47@7*eSW)wO#~gwXRy;t2VmtB zidr%i%46fvLcaY6A#&*H-!?7G9<_8Q!Jda+YUsMQ5XuF28aj7Y0G^j5iic^-m*jdd zuc`$PR~3Hr;927-A-#}6C20gM=ja$Z+LmuiMFM{*5$?$6q-B^IUoFFAJb^gQQ{^jNrU z%W{G6BqKD<*K~p{I2ZM5xxeidN#ip1g-qb;yGx7yl_)CjDX9LX`Q9%g<45T6^>%4{ z@v>YslE4V+8uP6o%v@1Xw=e3kTPFNN7UjolbP%kBUP(Cav`iHelY6bU8buY! z0+_xIR|s*(3C9@cZv|5a;7rauAk$7q*1 z##5~&KF-fd)QUe%eNG~#K`39azb-i;$Q0c`jxp?=p;`cDm%t}UQcCYW`CRoXM=K)t-Z%mfU1~t-a(+@ywG}xe&$C#b z8ZcfI0k&%5=3^e*AV59nvT_1W`rr_N5S|#=UR2hWjoTr%(U&H-4?s87plUCxGi1Mn z-Ue;7YO^wf!G$i6K>%)qq7#OYX2Oe6C|@bEEI8i!-dow_Gt#I{u^Rx*rF2~n%d{P* zKWzxUC5*`fe6;+Rx#pQBBp!=3k*QecuH(ltdMWgQ=@F0y?iy@s0ORyyM|>j9nrV%2IqRD zWwm`jdVJgye9>Hf)*#1xlcRXoRMc8{hRbk!dd<^tayANYhKo;T(7Iu+Sfhl|49M#DU;hUKst@ zNmJe68c8r0%bH_^Heeq3C@L}P549~6o>|yo7L!nzEW}378CQ&seFR*g+6@qWV}XrX zPB7=~kJjk#S}yE}&N<&3C+86=01K_tcAtkY*zXc|$Iy}cn?$|6%FSN1Alg7nB0&3`h{kLtB5#`c_k4^J1mOHx~33~-%airzd6E1QB= ziiS>WJAzze$u!?NoQ4Ogu)!?3*XAKU-^>Xnhi`qV$s4i5vjg!(o^s4#G7!5wF-a>v5=mm_D7aY<>qL;QhEVIM39E7O zy?@hRQ&2Y96OG6Dm>)=1>gmHG?%u)pLwEp{Q>_v28NgBonRVmT`MiS`fw%8O;N`y} z@ZTgf4+-JLLg#IVT6C-KcPj5=^jsapT%o>ViDS>su0IEJCRQ)^ue>7L5Z4409%o)H z>&-1RpQ{e0&fl~V(8W4G9DVnWixAb&46Hdu&in~GIA1K z^o{FE()Oj&i?SIV!Jd$l;M%Gx-H#WxiBdy%ye6IZJH7UyfNw?Uju&~MrSv?Esq0xG zqrWWEPO(;QNCBM9qPIcnSE!=KKX9&Ccu5Ik!QuV(rUiY}FFtn3Z)O`<^ z1?6R=JU$a_nag;g#-~;TG0t!L&qc_OU`04VwnN^+jT#llt=0bD?-w#UfL_SLu>k+F zlE2}fR+F)RyoFuV;}PT$?uA|20X8ol1m{;*@lpRyST(T=1y~} z`LUapwHLh?wjkhghUMtEO~@ajvwh%lDyeuiV5N*Yi)XzW^MRRkcS);CWKqAiHsY>d z!8$^gu2TNGAHZo7i_2>Q7;~WCN-lM+v375;EmWW4zjD+;-wDf>6KOpdyyIeKbE|YQY|@cnTWy{wNil5u49hfqgSS9&4j5)F&l_Y#S!bY-(b7#%WvI0rD9!aE&N(| z#ylhB%hn)&r}CpKv#X0ci8=dUiP<~z{_XXEiO%Tc2fhs)WOBr413~Q37tbyaDa@b) z0e~rYY`1#dBw>mU>2vn;S5)#$P zm9h`*ES9_#tw}Cfr_X*ZpI2IQ{_ry+w|MT3`Grje(ABJ)0oo^TVh$kW`~3bTY!RVC zH96E4K(jMB-X5-&0@oA&X`AC5G%^Fwn<^7r;*7_Nf#5Nd@%2SL%}^HLZrV)oApE24 z>&7xGBCyzOyQ29X>gRNpvC!6-TedwL4l_+sLN!zAI*~|p7!rJPxinBr;v4FzsUl(P zYw-0bt9ELr54m|!*dj8*R;Hq`Fm-+s!&A? z0r$uvYC50L+ut+6PIt#F59iD7L|d({(;S&wo2zfp7ZbfnY@B!eGQ;1AvjZ}ydLTJY znP)*x2GiaOlYW2_rikJF`MU^PmSxUh!WUB#QY^GSx!NB=`k2=!yi|EU8{eoQU(;#O z|0pu7wWIuzGp3lhloGP{0omPS)xxH`c2nu2u>uYxT#_)`nW=k%v%9DGDNv#lL`jJF z)T4DHb*T2Kg0)e4Z@x^&qKNMUON!0JEQ$+hu+Plq^NNr;{t}Z zDVMsAvWn=14tm!7+#yT1%W9_{qB3NY8PzZf-q_XX4>sN88+8(JEOTj8F?QEy5`so< zK*Z}^TN7Iy9z*HLbsR^wZ^Is^2vKBODsV_LMdWYtY>2Bk>J&~%`K_)T#UARHzL{>Y zKEIH82_C^%hlc7J)P8JI>BS~Tw*lSyC;Q@^aZ<&{iaoMi+`aR41$EzM+J1b7F>_9Z zYh&WM0-1{z%KYHGh?Z&+uncKQ%_lF?8^S63ByD?f^&zd#==nQ}1Z%qnNi!7fX_Vf; zmxA>L#4A)-;r43cgwP1vT5{M^Z<6Qrfy&I;T=1|${KEbQO3H}8DqKz`etxjK;jo$h zXDYisjU)uzmU$*ls-k#wfr5*p=tXe@P+VDxeRk;y6 The script will automatically install the required packages for alpine and debian-based distributions +> when run by root, or you can install them by yourself. + +- curl +- [jq](https://github.com/jqlang/jq) +- [jo](https://github.com/jpmens/jo) + +## Environment variables + +- `LLDAP_URL` or `LLDAP_URL_FILE` - URL to your lldap instance or path to file that contains URL (**MANDATORY**) +- `LLDAP_ADMIN_USERNAME` or `LLDAP_ADMIN_USERNAME_FILE` - admin username or path to file that contains username (**MANDATORY**) +- `LLDAP_ADMIN_PASSWORD` or `LLDAP_ADMIN_PASSWORD_FILE` - admin password or path to file that contains password (**MANDATORY**) +- `USER_CONFIGS_DIR` (default value: `/user-configs`) - directory where the user JSON configs could be found +- `GROUP_CONFIGS_DIR` (default value: `/group-configs`) - directory where the group JSON configs could be found +- `LLDAP_SET_PASSWORD_PATH` - path to the `lldap_set_password` utility (default value: `/app/lldap_set_password`) +- `DO_CLEANUP` (default value: `false`) - delete groups and users not specified in config files, also remove users from groups that they do not belong to + +## Config files + +There are two types of config files: [group](#group-config-file-example) and [user](#user-config-file-example) configs. +Each config file can be as one JSON file with nested JSON top-level values as several JSON files. + +### Group config file example + +Group configs are used to define groups that will be created by the script + +Fields description: + +* `name`: name of the group (**MANDATORY**) + +```json +{ + "name": "group-1" +} +{ + "name": "group-2" +} +``` + +### User config file example + +User config defines all the lldap user structures, +if the non-mandatory field is omitted, the script will clean this field in lldap as well. + +Fields description: + +* `id`: it's just username (**MANDATORY**) +* `email`: self-explanatory (**MANDATORY**) +* `password`: would be used to set the password using `lldap_set_password` utility +* `displayName`: self-explanatory +* `firstName`: self-explanatory +* `lastName`: self-explanatory +* `avatar_file`: must be a valid path to jpeg file (ignored if `avatar_url` specified) +* `avatar_url`: must be a valid URL to jpeg file (ignored if `gravatar_avatar` specified) +* `gravatar_avatar` (`false` by default): the script will try to get an avatar from [gravatar](https://gravatar.com/) by previously specified `email` (has the highest priority) +* `weserv_avatar` (`false` by default): avatar file from `avatar_url` or `gravatar_avatar` would be converted to jpeg using [wsrv.nl](https://wsrv.nl) (useful when your avatar is png) +* `groups`: an array of groups the user would be a member of (all the groups must be specified in group config files) + +```json +{ + "id": "username", + "email": "username@example.com", + "password": "changeme", + "displayName": "Display Name", + "firstName": "First", + "lastName": "Last", + "avatar_file": "/path/to/avatar.jpg", + "avatar_url": "https://i.imgur.com/nbCxk3z.jpg", + "gravatar_avatar": "false", + "weserv_avatar": "false", + "groups": [ + "group-1", + "group-2" + ] +} + +``` + +## Usage example + +### Manually + +The script can be run manually in the terminal for initial bootstrapping of your lldap instance. +You should make sure that the [required packages](#required-packages) are installed +and the [environment variables](#environment-variables) are configured properly. + +```bash +export LLDAP_URL=http://localhost:8080 +export LLDAP_ADMIN_USERNAME=admin +export LLDAP_ADMIN_PASSWORD=changeme +export USER_CONFIGS_DIR="$(realpath ./configs/user)" +export GROUP_CONFIGS_DIR="$(realpath ./configs/group)" +export LLDAP_SET_PASSWORD_PATH="$(realpath ./lldap_set_password)" +export DO_CLEANUP=false +./bootstrap.sh +``` + +### Docker compose + +Let's suppose you have the next file structure: + +```text +./ +├─ docker-compose.yaml +└─ bootstrap + ├─ bootstrap.sh + └─ user-configs + │ ├─ user-1.json + │ ├─ ... + │ └─ user-n.json + └─ group-configs + ├─ group-1.json + ├─ ... + └─ group-n.json + +``` + +You should mount `bootstrap` dir to lldap container and set the corresponding `env` variables: + +```yaml +version: "3" + +services: + lldap: + image: lldap/lldap:v0.5.0 + volumes: + - ./bootstrap:/bootstrap + ports: + - "3890:3890" # For LDAP + - "17170:17170" # For the web front-end + environment: + # envs required for lldap + - LLDAP_LDAP_USER_EMAIL=admin@example.com + - LLDAP_LDAP_USER_PASS=changeme + - LLDAP_LDAP_BASE_DN=dc=example,dc=com + + # envs required for bootstrap.sh + - LLDAP_URL=http://localhost:17170 + - LLDAP_ADMIN_USERNAME=admin + - LLDAP_ADMIN_PASSWORD=changeme # same as LLDAP_LDAP_USER_PASS + - USER_CONFIGS_DIR=/bootstrap/user-configs + - GROUP_CONFIGS_DIR=/bootstrap/group-configs + - DO_CLEANUP=false +``` + +Then, to bootstrap your lldap just run `docker compose exec lldap /bootstrap/bootstrap.sh`. +If config files were changed, re-run the `bootstrap.sh` with the same command. + +### Kubernetes job + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: lldap-bootstrap + # Next annotations are required if the job managed by Argo CD, + # so Argo CD can relaunch the job on every app sync action + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: lldap-bootstrap + image: lldap/lldap:v0.5.0 + + command: + - /bootstrap/bootstrap.sh + + env: + - name: LLDAP_URL + value: "http://lldap:8080" + + - name: LLDAP_ADMIN_USERNAME + valueFrom: { secretKeyRef: { name: lldap-admin-user, key: username } } + + - name: LLDAP_ADMIN_PASSWORD + valueFrom: { secretKeyRef: { name: lldap-admin-user, key: password } } + + - name: DO_CLEANUP + value: "true" + + volumeMounts: + - name: bootstrap + mountPath: /bootstrap/bootstrap.sh + subPath: bootstrap.sh + + - name: user-configs + mountPath: /user-configs + readOnly: true + + - name: group-configs + mountPath: /group-configs + readOnly: true + + volumes: + - name: bootstrap + configMap: + name: bootstrap + defaultMode: 0555 + items: + - key: bootstrap.sh + path: bootstrap.sh + + - name: user-configs + projected: + sources: + - secret: + name: lldap-admin-user + items: + - key: user-config.json + path: admin-config.json + - secret: + name: lldap-password-manager-user + items: + - key: user-config.json + path: password-manager-config.json + - secret: + name: lldap-bootstrap-configs + items: + - key: user-configs.json + path: user-configs.json + + - name: group-configs + projected: + sources: + - secret: + name: lldap-bootstrap-configs + items: + - key: group-configs.json + path: group-configs.json +``` diff --git a/example_configs/bootstrap/bootstrap.sh b/example_configs/bootstrap/bootstrap.sh new file mode 100755 index 0000000..94e2cc3 --- /dev/null +++ b/example_configs/bootstrap/bootstrap.sh @@ -0,0 +1,465 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +LLDAP_URL="${LLDAP_URL}" +LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME}" +LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD}" +USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/user-configs}" +GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/group-configs}" +LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}" +DO_CLEANUP="${DO_CLEANUP:-false}" + +check_install_dependencies() { + local commands=('curl' 'jq' 'jo') + local commands_not_found='false' + + if ! hash "${commands[@]}" 2>/dev/null; then + if hash 'apk' 2>/dev/null && [[ $EUID -eq 0 ]]; then + apk add "${commands[@]}" + elif hash 'apt' 2>/dev/null && [[ $EUID -eq 0 ]]; then + apt update -yqq + apt install -yqq "${commands[@]}" + else + local command='' + for command in "${commands[@]}"; do + if ! hash "$command" 2>/dev/null; then + printf 'Command not found "%s"\n' "$command" + fi + done + commands_not_found='true' + fi + fi + + if [[ "$commands_not_found" == 'true' ]]; then + return 1 + fi +} + +check_required_env_vars() { + local env_var_not_specified='false' + local dual_env_vars_list=( + 'LLDAP_URL' + 'LLDAP_ADMIN_USERNAME' + 'LLDAP_ADMIN_PASSWORD' + ) + + local dual_env_var_name='' + for dual_env_var_name in "${dual_env_vars_list[@]}"; do + local dual_env_var_file_name="${dual_env_var_name}_FILE" + + if [[ -z "${!dual_env_var_name}" ]] && [[ -z "${!dual_env_var_file_name}" ]]; then + printf 'Please specify "%s" or "%s" variable!\n' "$dual_env_var_name" "$dual_env_var_file_name" >&2 + env_var_not_specified='true' + else + if [[ -n "${!dual_env_var_file_name}" ]]; then + declare -g "$dual_env_var_name"="$(cat "${!dual_env_var_file_name}")" + fi + fi + done + + if [[ "$env_var_not_specified" == 'true' ]]; then + return 1 + fi +} + +check_configs_validity() { + local config_file='' config_invalid='false' + for config_file in "$@"; do + local error='' + if ! error="$(jq '.' -- "$config_file" 2>&1 >/dev/null)"; then + printf '%s: %s\n' "$config_file" "$error" + config_invalid='true' + fi + done + + if [[ "$config_invalid" == 'true' ]]; then + return 1 + fi +} + +auth() { + local url="$1" admin_username="$2" admin_password="$3" + + local response + response="$(curl --silent --request POST \ + --url "$url/auth/simple/login" \ + --header 'Content-Type: application/json' \ + --data "$(jo -- username="$admin_username" password="$admin_password")")" + + TOKEN="$(printf '%s' "$response" | jq --raw-output .token)" +} + +make_query() { + local query_file="$1" variables_file="$2" + + curl --silent --request POST \ + --url "$LLDAP_URL/api/graphql" \ + --header "Authorization: Bearer $TOKEN" \ + --header 'Content-Type: application/json' \ + --data @<(jq --slurpfile variables "$variables_file" '. + {"variables": $variables[0]}' "$query_file") +} + +get_group_list() { + local query='{"query":"query GetGroupList {groups {id displayName}}","operationName":"GetGroupList"}' + make_query <(printf '%s' "$query") <(printf '{}') +} + +get_group_array() { + get_group_list | jq --raw-output '.data.groups[].displayName' +} + +group_exists() { + if [[ "$(get_group_list | jq --raw-output --arg displayName "$1" '.data.groups | any(.[]; contains({"displayName": $displayName}))')" == 'true' ]]; then + return 0 + else + return 1 + fi +} + +get_group_id() { + get_group_list | jq --raw-output --arg displayName "$1" '.data.groups[] | if .displayName == $displayName then .id else empty end' +} + +create_group() { + local group_name="$1" + + if group_exists "$group_name"; then + printf 'Group "%s" (%s) already exists\n' "$group_name" "$(get_group_id "$group_name")" + return + fi + + # shellcheck disable=SC2016 + local query='{"query":"mutation CreateGroup($name: String!) {createGroup(name: $name) {id displayName}}","operationName":"CreateGroup"}' + + local response='' error='' + response="$(make_query <(printf '%s' "$query") <(jo -- name="$group_name"))" + error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')" + if [[ -n "$error" ]]; then + printf '%s\n' "$error" + else + printf 'Group "%s" (%s) successfully created\n' "$group_name" "$(printf '%s' "$response" | jq --raw-output '.data.createGroup.id')" + fi +} + +delete_group() { + local group_name="$1" id='' + + if ! group_exists "$group_name"; then + printf '[WARNING] Group "%s" does not exist\n' "$group_name" + return + fi + + id="$(get_group_id "$group_name")" + + # shellcheck disable=SC2016 + local query='{"query":"mutation DeleteGroupQuery($groupId: Int!) {deleteGroup(groupId: $groupId) {ok}}","operationName":"DeleteGroupQuery"}' + + local response='' error='' + response="$(make_query <(printf '%s' "$query") <(jo -- groupId="$id"))" + error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')" + if [[ -n "$error" ]]; then + printf '%s\n' "$error" + else + printf 'Group "%s" (%s) successfully deleted\n' "$group_name" "$id" + fi +} + +add_user_to_group() { + local user_id="$1" group_name="$2" group_id='' + + if ! group_exists "$group_name"; then + printf '[WARNING] Group "%s" does not exist\n' "$group_name" + return + fi + + group_id="$(get_group_id "$group_name")" + + # shellcheck disable=SC2016 + local query='{"query":"mutation AddUserToGroup($user: String!, $group: Int!) {addUserToGroup(userId: $user, groupId: $group) {ok}}","operationName":"AddUserToGroup"}' + + local response='' error='' + response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))" + error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')" + if [[ -n "$error" ]]; then + printf '%s\n' "$error" + else + printf 'User "%s" successfully added to the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id" + fi +} + +remove_user_from_group() { + local user_id="$1" group_name="$2" group_id='' + + if ! group_exists "$group_name"; then + printf '[WARNING] Group "%s" does not exist\n' "$group_name" + return + fi + + group_id="$(get_group_id "$group_name")" + + # shellcheck disable=SC2016 + local query='{"operationName":"RemoveUserFromGroup","query":"mutation RemoveUserFromGroup($user: String!, $group: Int!) {removeUserFromGroup(userId: $user, groupId: $group) {ok}}"}' + + local response='' error='' + response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))" + error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')" + if [[ -n "$error" ]]; then + printf '%s\n' "$error" + else + printf 'User "%s" successfully removed from the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id" + fi +} + +get_users_list() { + # shellcheck disable=SC2016 + local query='{"query": "query ListUsersQuery($filters: RequestFilter) {users(filters: $filters) {id email displayName firstName lastName creationDate}}","operationName": "ListUsersQuery"}' + make_query <(printf '%s' "$query") <(jo -- filters=null) +} + +user_exists() { + if [[ "$(get_users_list | jq --raw-output --arg id "$1" '.data.users | any(.[]; contains({"id": $id}))')" == 'true' ]]; then + return 0 + else + return 1 + fi +} + +get_user_details() { + local id="$1" + + # shellcheck disable=SC2016 + local query='{"query":"query GetUserDetails($id: String!) {user(userId: $id) {id email displayName firstName lastName creationDate uuid groups {id displayName}}}","operationName":"GetUserDetails"}' + make_query <(printf '%s' "$query") <(jo -- id="$id") +} + +delete_user() { + local id="$1" + + if ! user_exists "$id"; then + printf 'User "%s" is not exists\n' "$id" + return + fi + + # shellcheck disable=SC2016 + local query='{"query": "mutation DeleteUserQuery($user: String!) {deleteUser(userId: $user) {ok}}","operationName": "DeleteUserQuery"}' + + local response='' error='' + response="$(make_query <(printf '%s' "$query") <(jo -- user="$id"))" + error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')" + if [[ -n "$error" ]]; then + printf '%s\n' "$error" + else + printf 'User "%s" successfully deleted\n' "$id" + fi +} + +__common_user_mutation_query() { + local \ + query="$1" \ + id="${2:-null}" \ + email="${3:-null}" \ + displayName="${4:-null}" \ + firstName="${5:-null}" \ + lastName="${6:-null}" \ + avatar_file="${7:-null}" \ + avatar_url="${8:-null}" \ + gravatar_avatar="${9:-false}" \ + weserv_avatar="${10:-false}" + + local variables_arr=( + '-s' "id=$id" + '-s' "email=$email" + '-s' "displayName=$displayName" + '-s' "firstName=$firstName" + '-s' "lastName=$lastName" + ) + + local temp_avatar_file='' + + if [[ "$gravatar_avatar" == 'true' ]]; then + avatar_url="https://gravatar.com/avatar/$(printf '%s' "$email" | sha256sum | cut -d ' ' -f 1)?size=512" + fi + + if [[ "$avatar_url" != 'null' ]]; then + temp_avatar_file="${TMP_AVATAR_DIR}/$(printf '%s' "$avatar_url" | md5sum | cut -d ' ' -f 1)" + + if ! [[ -f "$temp_avatar_file" ]]; then + if [[ "$weserv_avatar" == 'true' ]]; then + avatar_url="https://wsrv.nl/?url=$avatar_url&output=jpg" + fi + curl --silent --location --output "$temp_avatar_file" "$avatar_url" + fi + + avatar_file="$temp_avatar_file" + fi + + if [[ "$avatar_file" == 'null' ]]; then + variables_arr+=('-s' 'avatar=null') + else + variables_arr+=("avatar=%$avatar_file") + fi + + make_query <(printf '%s' "$query") <(jo -- user=:<(jo -- "${variables_arr[@]}")) +} + +create_user() { + local id="$1" + + if user_exists "$id"; then + printf 'User "%s" already exists\n' "$id" + return + fi + + # shellcheck disable=SC2016 + local query='{"query":"mutation CreateUser($user: CreateUserInput!) {createUser(user: $user) {id creationDate}}","operationName":"CreateUser"}' + + local response='' error='' + response="$(__common_user_mutation_query "$query" "$@")" + error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')" + if [[ -n "$error" ]]; then + printf '%s\n' "$error" + else + printf 'User "%s" successfully created\n' "$id" + fi +} + +update_user() { + local id="$1" + + if ! user_exists "$id"; then + printf 'User "%s" is not exists\n' "$id" + return + fi + + # shellcheck disable=SC2016 + local query='{"query":"mutation UpdateUser($user: UpdateUserInput!) {updateUser(user: $user) {ok}}","operationName":"UpdateUser"}' + + local response='' error='' + response="$(__common_user_mutation_query "$query" "$@")" + error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')" + if [[ -n "$error" ]]; then + printf '%s\n' "$error" + else + printf 'User "%s" successfully updated\n' "$id" + fi +} + +create_update_user() { + local id="$1" + + if user_exists "$id"; then + update_user "$@" + else + create_user "$@" + fi +} + +main() { + check_install_dependencies + check_required_env_vars + + local user_config_files=("${USER_CONFIGS_DIR}"/*.json) + local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json) + + if ! check_configs_validity "${group_config_files[@]}" "${user_config_files[@]}"; then + exit 1 + fi + + until curl --silent -o /dev/null "$LLDAP_URL"; do + printf 'Waiting lldap to start...\n' + sleep 10 + done + + auth "$LLDAP_URL" "$LLDAP_ADMIN_USERNAME" "$LLDAP_ADMIN_PASSWORD" + + local redundant_groups='' + redundant_groups="$(get_group_list | jq '[ .data.groups[].displayName ]' | jq --compact-output '. - ["lldap_admin","lldap_password_manager","lldap_strict_readonly"]')" + + printf -- '\n--- groups ---\n' + local group_config='' + while read -r group_config; do + local group_name='' + group_name="$(printf '%s' "$group_config" | jq --raw-output '.name')" + create_group "$group_name" + redundant_groups="$(printf '%s' "$redundant_groups" | jq --compact-output --arg name "$group_name" '. - [$name]')" + done < <(jq --compact-output '.' -- "${group_config_files[@]}") + printf -- '--- groups ---\n' + + printf -- '\n--- redundant groups ---\n' + if [[ "$redundant_groups" == '[]' ]]; then + printf 'There are no redundant groups\n' + else + local group_name='' + while read -r group_name; do + if [[ "$DO_CLEANUP" == 'true' ]]; then + delete_group "$group_name" + else + printf '[WARNING] Group "%s" is not declared in config files\n' "$group_name" + fi + done < <(printf '%s' "$redundant_groups" | jq --raw-output '.[]') + fi + printf -- '--- redundant groups ---\n' + + local redundant_users='' + redundant_users="$(get_users_list | jq '[ .data.users[].id ]' | jq --compact-output --arg admin_id "$LLDAP_ADMIN_USERNAME" '. - [$admin_id]')" + + TMP_AVATAR_DIR="$(mktemp -d)" + + local user_config='' + while read -r user_config; do + local field='' id='' email='' displayName='' firstName='' lastName='' avatar_file='' avatar_url='' gravatar_avatar='' weserv_avatar='' password='' + for field in 'id' 'email' 'displayName' 'firstName' 'lastName' 'avatar_file' 'avatar_url' 'gravatar_avatar' 'weserv_avatar' 'password'; do + declare "$field"="$(printf '%s' "$user_config" | jq --raw-output --arg field "$field" '.[$field]')" + done + printf -- '\n--- %s ---\n' "$id" + + create_update_user "$id" "$email" "$displayName" "$firstName" "$lastName" "$avatar_file" "$avatar_url" "$gravatar_avatar" "$weserv_avatar" + redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')" + + if [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then + "$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password" + fi + + local redundant_user_groups='' + redundant_user_groups="$(get_user_details "$id" | jq '[ .data.user.groups[].displayName ]')" + + local group='' + while read -r group; do + if [[ -n "$group" ]]; then + add_user_to_group "$id" "$group" + redundant_user_groups="$(printf '%s' "$redundant_user_groups" | jq --compact-output --arg group "$group" '. - [$group]')" + fi + done < <(printf '%s' "$user_config" | jq --raw-output '.groups | if . == null then "" else .[] end') + + local user_group_name='' + while read -r user_group_name; do + if [[ "$DO_CLEANUP" == 'true' ]]; then + remove_user_from_group "$id" "$user_group_name" + else + printf '[WARNING] User "%s" is not declared as member of the "%s" group in the config files\n' "$id" "$user_group_name" + fi + done < <(printf '%s' "$redundant_user_groups" | jq --raw-output '.[]') + printf -- '--- %s ---\n' "$id" + done < <(jq --compact-output '.' -- "${user_config_files[@]}") + + rm -r "$TMP_AVATAR_DIR" + + printf -- '\n--- redundant users ---\n' + if [[ "$redundant_users" == '[]' ]]; then + printf 'There are no redundant users\n' + else + local id='' + while read -r id; do + if [[ "$DO_CLEANUP" == 'true' ]]; then + delete_user "$id" + else + printf '[WARNING] User "%s" is not declared in config files\n' "$id" + fi + done < <(printf '%s' "$redundant_users" | jq --raw-output '.[]') + fi + printf -- '--- redundant users ---\n' +} + +main "$@"